Merge branch 'master' of github.com:supabase/supabase
No files matched your search
@@ -2,17 +2,18 @@ name: Publish to Image Registry
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- studio
|
||||
paths:
|
||||
- 'studio/**'
|
||||
tags:
|
||||
- 'v*'
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Image tag'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
@@ -21,9 +22,10 @@ jobs:
|
||||
supabase/studio
|
||||
public.ecr.aws/t3w2s2c9/studio
|
||||
flavor: |
|
||||
latest=true
|
||||
latest=false
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=raw,value=${{ inputs.version }},enable=${{ github.event_name != 'push' }}
|
||||
|
||||
- uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
@@ -47,7 +49,7 @@ jobs:
|
||||
- uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
context: studio
|
||||
context: '{{defaultContext}}:studio'
|
||||
target: production
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
@@ -110,6 +110,16 @@ You can use it completely, or just the features you need.
|
||||
style={{ height: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
{/* Realtime */}
|
||||
<div class="col col--4">
|
||||
<ButtonCard
|
||||
class="card"
|
||||
to={useBaseUrl('/guides/realtime')}
|
||||
title={'Realtime'}
|
||||
description={'Build with Broadcast, Presence, and Postgres Changes.'}
|
||||
style={{ height: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
{/* File Storage */}
|
||||
<div class="col col--4">
|
||||
<ButtonCard
|
||||
@@ -130,16 +140,6 @@ You can use it completely, or just the features you need.
|
||||
style={{ height: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
{/* Realtime */}
|
||||
<div class="col col--4">
|
||||
<ButtonCard
|
||||
class="card"
|
||||
to={useBaseUrl('/guides/realtime')}
|
||||
title={'Realtime'}
|
||||
description={'Build with Broadcast, Presence, and Postgres Changes.'}
|
||||
style={{ height: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -163,14 +163,14 @@ Let's start building the SolidJS app from scratch.
|
||||
We can use [Degit](https://github.com/Rich-Harris/degit) to initialize an app called `supabase-solid`:
|
||||
|
||||
```bash
|
||||
npx degit solidjs/templates/js supabase-solid
|
||||
npx degit solidjs/templates/ts supabase-solid
|
||||
cd supabase-solid
|
||||
```
|
||||
|
||||
Then let's install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js)
|
||||
|
||||
```bash
|
||||
npm install @supabase/supabase-js
|
||||
npm install @supabase/supabase-js@rc
|
||||
```
|
||||
|
||||
And finally we want to save the environment variables in a `.env`.
|
||||
@@ -200,7 +200,7 @@ You can find the full contents of this file [here](https://raw.githubusercontent
|
||||
|
||||
Let's set up a SolidJS component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords.
|
||||
|
||||
```jsx title="/src/Auth.jsx"
|
||||
```jsx title="/src/Auth.tsx"
|
||||
import { createSignal } from 'solid-js'
|
||||
import { supabase } from './supabaseClient'
|
||||
|
||||
@@ -208,46 +208,46 @@ export default function Auth() {
|
||||
const [loading, setLoading] = createSignal(false)
|
||||
const [email, setEmail] = createSignal('')
|
||||
|
||||
const handleLogin = async (e) => {
|
||||
const handleLogin = async (e: SubmitEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const { error } = await supabase.auth.signIn({ email: email() })
|
||||
const { error } = await supabase.auth.signInWithOtp({ email: email() })
|
||||
if (error) throw error
|
||||
alert('Check your email for the login link!')
|
||||
} catch (error) {
|
||||
alert(error.error_description || error.message)
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row flex-center flex">
|
||||
<div className="col-6 form-widget" aria-live="polite">
|
||||
<h1 className="header">Supabase + SolidJS</h1>
|
||||
<p className="description">
|
||||
Sign in via magic link with your email below
|
||||
</p>
|
||||
{loading() ? (
|
||||
'Sending magic link...'
|
||||
) : (
|
||||
<form onSubmit={handleLogin}>
|
||||
<label htmlFor="email">Email</label>
|
||||
<div class="row flex-center flex">
|
||||
<div class="col-6 form-widget" aria-live="polite">
|
||||
<h1 class="header">Supabase + SolidJS</h1>
|
||||
<p class="description">Sign in via magic link with your email below</p>
|
||||
<form class="form-widget" onSubmit={handleLogin}>
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
className="inputField"
|
||||
class="inputField"
|
||||
type="email"
|
||||
placeholder="Your email"
|
||||
value={email()}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
onChange={(e) => setEmail(e.currentTarget.value)}
|
||||
/>
|
||||
<button type="submit" className="button block" aria-live="polite">
|
||||
Send magic link
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="button block" aria-live="polite">
|
||||
{loading() ? <span>Loading</span> : <span>Send magic link</span>}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -258,27 +258,31 @@ export default function Auth() {
|
||||
|
||||
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.js`.
|
||||
Let's create a new component for that called `Account.tsx`.
|
||||
|
||||
```jsx title="src/Account.jsx"
|
||||
import { createSignal, createEffect } from 'solid-js'
|
||||
```jsx title="src/Account.tsx"
|
||||
import { AuthSession } from '@supabase/supabase-js'
|
||||
import { Component, createEffect, createSignal } from 'solid-js'
|
||||
import { supabase } from './supabaseClient'
|
||||
|
||||
const Account = (props) => {
|
||||
interface Props {
|
||||
session: AuthSession
|
||||
}
|
||||
|
||||
const Account: Component<Props> = ({ session }) => {
|
||||
const [loading, setLoading] = createSignal(true)
|
||||
const [username, setUsername] = createSignal(null)
|
||||
const [website, setWebsite] = createSignal(null)
|
||||
const [avatar_url, setAvatarUrl] = createSignal(null)
|
||||
const [username, setUsername] = createSignal<string | null>(null)
|
||||
const [website, setWebsite] = createSignal<string | null>(null)
|
||||
const [avatarUrl, setAvatarUrl] = createSignal<string | null>(null)
|
||||
|
||||
createEffect(() => {
|
||||
props.session
|
||||
getProfile()
|
||||
})
|
||||
|
||||
const getProfile = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const user = supabase.auth.user()
|
||||
const { user } = session
|
||||
|
||||
let { data, error, status } = await supabase
|
||||
.from('profiles')
|
||||
@@ -296,36 +300,38 @@ const Account = (props) => {
|
||||
setAvatarUrl(data.avatar_url)
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const updateProfile = async (e) => {
|
||||
const updateProfile = async (e: Event) => {
|
||||
e.preventDefault()
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const user = supabase.auth.user()
|
||||
const { user } = session
|
||||
|
||||
const updates = {
|
||||
id: user.id,
|
||||
username: username(),
|
||||
website: website(),
|
||||
avatar_url: avatar_url(),
|
||||
updated_at: new Date(),
|
||||
avatar_url: avatarUrl(),
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
|
||||
let { error } = await supabase.from('profiles').upsert(updates, {
|
||||
returning: 'minimal', // Don't return the value after inserting
|
||||
})
|
||||
let { error } = await supabase.from('profiles').upsert(updates)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -333,47 +339,35 @@ const Account = (props) => {
|
||||
|
||||
return (
|
||||
<div aria-live="polite">
|
||||
{loading() ? (
|
||||
'Saving ...'
|
||||
) : (
|
||||
<form onSubmit={updateProfile} className="form-widget">
|
||||
<div>Email: {props.session.user.email}</div>
|
||||
<div>
|
||||
<label htmlFor="username">Name</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
value={username() || ''}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="website">Website</label>
|
||||
<input
|
||||
id="website"
|
||||
type="url"
|
||||
value={website() || ''}
|
||||
onChange={(e) => setWebsite(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
className="button primary block"
|
||||
disabled={loading()}
|
||||
>
|
||||
Update profile
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="button block"
|
||||
onClick={() => supabase.auth.signOut()}
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
<form onSubmit={updateProfile} class="form-widget">
|
||||
<div>Email: {session.user.email}</div>
|
||||
<div>
|
||||
<label for="username">Name</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
value={username() || ''}
|
||||
onChange={(e) => setUsername(e.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="website">Website</label>
|
||||
<input
|
||||
id="website"
|
||||
type="text"
|
||||
value={website() || ''}
|
||||
onChange={(e) => setWebsite(e.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="button primary block" disabled={loading()}>
|
||||
{loading() ? 'Saving ...' : 'Update profile'}
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="button block" onClick={() => supabase.auth.signOut()}>
|
||||
Sign Out
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -383,20 +377,22 @@ export default Account
|
||||
|
||||
### Launch!
|
||||
|
||||
Now that we have all the components in place, let's update `App.js`:
|
||||
Now that we have all the components in place, let's update `App.tsx`:
|
||||
|
||||
```jsx title="src/App.jsx"
|
||||
import './index.css'
|
||||
import { createSignal, createEffect } from 'solid-js'
|
||||
```jsx title="src/App.tsx"
|
||||
import { Component, createEffect, createSignal } from 'solid-js'
|
||||
import { supabase } from './supabaseClient'
|
||||
import Auth from './Auth'
|
||||
import { AuthSession } from '@supabase/supabase-js'
|
||||
import Account from './Account'
|
||||
import Auth from './Auth'
|
||||
|
||||
export default () => {
|
||||
const [session, setSession] = createSignal(null)
|
||||
const App: Component = () => {
|
||||
const [session, setSession] = createSignal<AuthSession | null>(null)
|
||||
|
||||
createEffect(() => {
|
||||
setSession(supabase.auth.session())
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setSession(session)
|
||||
})
|
||||
|
||||
supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session)
|
||||
@@ -404,15 +400,13 @@ export default () => {
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="container" style={{ padding: '50px 0 100px 0' }}>
|
||||
{!session() ? (
|
||||
<Auth />
|
||||
) : (
|
||||
<Account key={session().user.id} session={session()} />
|
||||
)}
|
||||
<div class="container" style={{ padding: '50px 0 100px 0' }}>
|
||||
{!session() ? <Auth /> : <Account session={session()!} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
```
|
||||
|
||||
Once that's done, run this in a terminal window:
|
||||
@@ -433,49 +427,54 @@ 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:
|
||||
|
||||
```jsx title="src/Avatar.jsx"
|
||||
import { createEffect, createSignal } from 'solid-js'
|
||||
```jsx title="src/Avatar.tsx"
|
||||
import { Component, createEffect, createSignal, JSX } from 'solid-js'
|
||||
import { supabase } from './supabaseClient'
|
||||
|
||||
export default (props) => {
|
||||
const [avatarUrl, setAvatarUrl] = createSignal(null)
|
||||
interface Props {
|
||||
size: number
|
||||
url: string | null
|
||||
onUpload: (event: Event, filePath: string) => void
|
||||
}
|
||||
|
||||
const Avatar: Component<Props> = (props) => {
|
||||
const [avatarUrl, setAvatarUrl] = createSignal<string | null>(null)
|
||||
const [uploading, setUploading] = createSignal(false)
|
||||
|
||||
createEffect(() => {
|
||||
if (props.url()) downloadImage(props.url())
|
||||
if (props.url) downloadImage(props.url)
|
||||
})
|
||||
|
||||
const downloadImage = async (path) => {
|
||||
const downloadImage = async (path: string) => {
|
||||
try {
|
||||
const { data, error } = await supabase.storage
|
||||
.from('avatars')
|
||||
.download(path)
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path)
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
const url = URL.createObjectURL(data)
|
||||
setAvatarUrl(url)
|
||||
} catch (error) {
|
||||
console.log('Error downloading image: ', error.message)
|
||||
if (error instanceof Error) {
|
||||
console.log('Error downloading image: ', error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const uploadAvatar = async (event) => {
|
||||
const uploadAvatar: JSX.EventHandler<HTMLInputElement, Event> = async (event) => {
|
||||
try {
|
||||
setUploading(true)
|
||||
|
||||
if (!event.target.files || event.target.files.length === 0) {
|
||||
const target = event.currentTarget
|
||||
if (!target?.files || target.files.length === 0) {
|
||||
throw new Error('You must select an image to upload.')
|
||||
}
|
||||
|
||||
const file = event.target.files[0]
|
||||
const file = target.files[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)
|
||||
let { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)
|
||||
|
||||
if (uploadError) {
|
||||
throw uploadError
|
||||
@@ -483,7 +482,9 @@ export default (props) => {
|
||||
|
||||
props.onUpload(event, filePath)
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
@@ -491,56 +492,57 @@ export default (props) => {
|
||||
|
||||
return (
|
||||
<div style={{ width: props.size }} aria-live="polite">
|
||||
<img
|
||||
src={
|
||||
avatarUrl()
|
||||
? avatarUrl()
|
||||
: `https://place-hold.it/${props.size}x${props.size}`
|
||||
}
|
||||
alt={avatarUrl() ? 'Avatar' : 'No image'}
|
||||
className="avatar image"
|
||||
style={{ height: props.size, width: props.size }}
|
||||
/>
|
||||
{uploading() ? (
|
||||
'Uploading...'
|
||||
{avatarUrl() ? (
|
||||
<img
|
||||
src={avatarUrl()!}
|
||||
alt={avatarUrl() ? 'Avatar' : 'No image'}
|
||||
class="avatar image"
|
||||
style={{ height: `${props.size}px`, width: `${props.size}px` }}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<label className="button primary block" htmlFor="single">
|
||||
Upload an avatar
|
||||
</label>
|
||||
<span style="display:none">
|
||||
<input
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
onChange={uploadAvatar}
|
||||
disabled={uploading()}
|
||||
/>
|
||||
</span>
|
||||
</>
|
||||
<div
|
||||
class="avatar no-image"
|
||||
style={{ height: `${props.size}px`, width: `${props.size}px` }}
|
||||
/>
|
||||
)}
|
||||
<div style={{ width: `${props.size}px` }}>
|
||||
<label class="button primary block" for="single">
|
||||
{uploading() ? 'Uploading ...' : 'Upload avatar'}
|
||||
</label>
|
||||
<span style="display:none">
|
||||
<input
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
onChange={uploadAvatar}
|
||||
disabled={uploading()}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Avatar
|
||||
```
|
||||
|
||||
### Add the new widget
|
||||
|
||||
And then we can add the widget to the Account page:
|
||||
|
||||
```jsx title="src/Account.jsx"
|
||||
```jsx title="src/Account.tsx"
|
||||
// Import the new component
|
||||
import Avatar from './Avatar'
|
||||
|
||||
// ...
|
||||
|
||||
return (
|
||||
<div className="form-widget">
|
||||
<form onSubmit={updateProfile} class="form-widget">
|
||||
{/* Add to the body */}
|
||||
<Avatar
|
||||
url={avatar_url}
|
||||
url={avatarUrl()}
|
||||
size={150}
|
||||
onUpload={(e, url) => {
|
||||
onUpload={(e: Event, url: string) => {
|
||||
setAvatarUrl(url)
|
||||
updateProfile(e)
|
||||
}}
|
||||
|
||||
@@ -156,6 +156,17 @@ const sidebars = {
|
||||
collapsed: true,
|
||||
items: ['guides/functions'],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Realtime',
|
||||
collapsed: true,
|
||||
items: [
|
||||
'guides/realtime',
|
||||
'guides/realtime/broadcast',
|
||||
'guides/realtime/presence',
|
||||
'guides/realtime/postgres-changes',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Storage',
|
||||
@@ -174,17 +185,6 @@ const sidebars = {
|
||||
'guides/platform/permissions',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Realtime',
|
||||
collapsed: true,
|
||||
items: [
|
||||
'guides/realtime',
|
||||
'guides/realtime/broadcast',
|
||||
'guides/realtime/presence',
|
||||
'guides/realtime/postgres-changes',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Self Hosting',
|
||||
|
||||
@@ -205,5 +205,13 @@
|
||||
"position": "Engineering",
|
||||
"author_url": "https://github.com/j0",
|
||||
"author_image_url": "https://github.com/j0.png"
|
||||
},
|
||||
{
|
||||
"author_id": "simon_grimm",
|
||||
"author": "Simon Grimm",
|
||||
"username": "schlimmson",
|
||||
"position": "Guest Author",
|
||||
"author_url": "https://twitter.com/schlimmson",
|
||||
"author_image_url": "https://github.com/saimon24.png"
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 619 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 234 KiB |
|
After Width: | Height: | Size: 129 KiB |
@@ -5,833 +5,840 @@
|
||||
<link>https://supabase.com</link>
|
||||
<description>Latest news from Supabase</description>
|
||||
<language>en</language>
|
||||
<lastBuildDate>Fri, 19 Aug 2022 05:05:00 +0000</lastBuildDate>
|
||||
<lastBuildDate>Tue, 23 Aug 2022 23:11:00 +0000</lastBuildDate>
|
||||
<atom:link href="https://supabase.com/rss.xml" rel="self" type="application/rss+xml"/>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/building-a-realtime-trello-board-with-supabase-and-angular</guid>
|
||||
<title>Building a Realtime Trello Board with Supabase and Angular</title>
|
||||
<link>https://supabase.com/blog/building-a-realtime-trello-board-with-supabase-and-angular</link>
|
||||
<description>Go beyond the hello world example with this real world project.</description>
|
||||
<pubDate>Tue, 23 Aug 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-vault</guid>
|
||||
<title>Supabase Vault</title>
|
||||
<link>https://supabase.com/blog/supabase-vault</link>
|
||||
<description>Today we're announcing Vault, a Postgres extension for managing secrets and encryption inside your database.</description>
|
||||
<pubDate>Fri, 19 Aug 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Thu, 18 Aug 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/postgrest-v10</guid>
|
||||
<title>PostgREST v10: EXPLAIN and Improved Relationship Detection</title>
|
||||
<link>https://supabase.com/blog/postgrest-v10</link>
|
||||
<description>Today, PostgREST 10 was released. Let's take a look at some of the new features that go hand in hand with supabase-js v2.</description>
|
||||
<pubDate>Fri, 19 Aug 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Thu, 18 Aug 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/pg-jsonschema-a-postgres-extension-for-json-validation</guid>
|
||||
<title>pg_jsonschema: JSON Schema support for Postgres</title>
|
||||
<link>https://supabase.com/blog/pg-jsonschema-a-postgres-extension-for-json-validation</link>
|
||||
<description>Today we're releasing pg_jsonschema, a Postgres extension for JSON validation.</description>
|
||||
<pubDate>Fri, 19 Aug 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Thu, 18 Aug 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/launch-week-5-one-more-thing</guid>
|
||||
<title>One more thing</title>
|
||||
<link>https://supabase.com/blog/launch-week-5-one-more-thing</link>
|
||||
<description>Let's be honest, it's never just one more thing.</description>
|
||||
<pubDate>Fri, 19 Aug 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Thu, 18 Aug 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/launch-week-5-community-day</guid>
|
||||
<title>Community Day</title>
|
||||
<link>https://supabase.com/blog/launch-week-5-community-day</link>
|
||||
<description>Wrapping up Launch Week 5 with contributors, partners, and friends.</description>
|
||||
<pubDate>Fri, 19 Aug 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Thu, 18 Aug 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-realtime-multiplayer-general-availability copy</guid>
|
||||
<guid>https://supabase.com/blog/supabase-realtime-multiplayer-general-availability</guid>
|
||||
<title>Realtime: Multiplayer Edition</title>
|
||||
<link>https://supabase.com/blog/supabase-realtime-multiplayer-general-availability copy</link>
|
||||
<link>https://supabase.com/blog/supabase-realtime-multiplayer-general-availability</link>
|
||||
<description>Announcing the general availability of Realtime's Broadcast and Presence.</description>
|
||||
<pubDate>Wed, 17 Aug 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Wed, 17 Aug 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-soc2</guid>
|
||||
<title>Supabase is SOC2 compliant</title>
|
||||
<link>https://supabase.com/blog/supabase-soc2</link>
|
||||
<description>Supabase is now SOC2 compliant. Learn how we got here and what it means for our customers.</description>
|
||||
<pubDate>Tue, 16 Aug 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Tue, 16 Aug 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-js-v2</guid>
|
||||
<title>supabase-js v2 Release Candidate</title>
|
||||
<link>https://supabase.com/blog/supabase-js-v2</link>
|
||||
<description>First peek of supabase-js v2, which focuses on quality-of-life improvements for developers.</description>
|
||||
<pubDate>Mon, 15 Aug 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Mon, 15 Aug 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-cli-v1-and-admin-api-beta</guid>
|
||||
<title>Supabase CLI v1 and Admin API Beta</title>
|
||||
<link>https://supabase.com/blog/supabase-cli-v1-and-admin-api-beta</link>
|
||||
<description>We are moving Supabase CLI v1 out of beta, and releasing Admin API beta.</description>
|
||||
<pubDate>Sun, 14 Aug 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Sun, 14 Aug 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-series-b</guid>
|
||||
<title>Supabase Series B</title>
|
||||
<link>https://supabase.com/blog/supabase-series-b</link>
|
||||
<description>Supabase raised $80M in May, bringing our total funding to $116M.</description>
|
||||
<pubDate>Thu, 11 Aug 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Thu, 11 Aug 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/launch-week-5-hackathon</guid>
|
||||
<title>Launch Week 5 Hackathon</title>
|
||||
<link>https://supabase.com/blog/launch-week-5-hackathon</link>
|
||||
<description>Build to win $1500 - Friday 12th to Monday 21st August 2022</description>
|
||||
<pubDate>Tue, 09 Aug 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Tue, 09 Aug 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/slack-consolidate-slackbot-to-consolidate-messages</guid>
|
||||
<title>Slack Consolidate: a slackbot built with Python and Supabase</title>
|
||||
<link>https://supabase.com/blog/slack-consolidate-slackbot-to-consolidate-messages</link>
|
||||
<description>A slackbot to consolidate messages from different channels using Supabase, Slack SDK and Python</description>
|
||||
<pubDate>Mon, 08 Aug 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Mon, 08 Aug 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-beta-update-july-2022</guid>
|
||||
<title>Supabase Beta July 2022</title>
|
||||
<link>https://supabase.com/blog/supabase-beta-update-july-2022</link>
|
||||
<description>Launch Week Golden Tickets, Flutter SDK 1.0 Developer Preview and more...</description>
|
||||
<pubDate>Tue, 02 Aug 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Tue, 02 Aug 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-flutter-sdk-1-developer-preview</guid>
|
||||
<title>Supabase Flutter SDK 1.0 Developer Preview</title>
|
||||
<link>https://supabase.com/blog/supabase-flutter-sdk-1-developer-preview</link>
|
||||
<description>Supabase Flutter SDK is getting a major update and we need your help making it better.</description>
|
||||
<pubDate>Mon, 01 Aug 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Mon, 01 Aug 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/seen-by-in-postgresql</guid>
|
||||
<title>Implementing "seen by" functionality with Postgres</title>
|
||||
<link>https://supabase.com/blog/seen-by-in-postgresql</link>
|
||||
<description>Different approaches for tracking visitor counts with PostgreSQL.</description>
|
||||
<pubDate>Sun, 17 Jul 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Sun, 17 Jul 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-auth-helpers-with-sveltekit-support</guid>
|
||||
<title>Revamped Auth Helpers for Supabase (with SvelteKit support)</title>
|
||||
<link>https://supabase.com/blog/supabase-auth-helpers-with-sveltekit-support</link>
|
||||
<description>Supabase Auth Helpers now have improved developer experience, Sveltekit support, and more.</description>
|
||||
<pubDate>Tue, 12 Jul 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Tue, 12 Jul 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/beta-update-june-2022</guid>
|
||||
<title>Supabase Beta June 2022</title>
|
||||
<link>https://supabase.com/blog/beta-update-june-2022</link>
|
||||
<description>Auth Helpers, Unlimited Free Projects, CLI and more...</description>
|
||||
<pubDate>Tue, 05 Jul 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Tue, 05 Jul 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/flutter-tutorial-building-a-chat-app</guid>
|
||||
<title>Flutter Tutorial: building a Flutter chat app</title>
|
||||
<link>https://supabase.com/blog/flutter-tutorial-building-a-chat-app</link>
|
||||
<description>How to create a real time chat app using Flutter.</description>
|
||||
<pubDate>Wed, 29 Jun 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Wed, 29 Jun 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/visualizing-supabase-data-using-metabase</guid>
|
||||
<title>Visualizing Supabase Data using Metabase</title>
|
||||
<link>https://supabase.com/blog/visualizing-supabase-data-using-metabase</link>
|
||||
<description>How to create different kinds of charts out of data stored in Supabase using Metabase.</description>
|
||||
<pubDate>Tue, 28 Jun 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Tue, 28 Jun 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/partial-postgresql-data-dumps-with-rls</guid>
|
||||
<title>Partial data dumps using Postgres Row Level Security</title>
|
||||
<link>https://supabase.com/blog/partial-postgresql-data-dumps-with-rls</link>
|
||||
<description>Using RLS to create seed files for local PostgreSQL testing.</description>
|
||||
<pubDate>Mon, 27 Jun 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Mon, 27 Jun 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/loading-data-supabase-python</guid>
|
||||
<title>Python data loading with Supabase</title>
|
||||
<link>https://supabase.com/blog/loading-data-supabase-python</link>
|
||||
<description>An example of how to load data into Supabase using supabase-py</description>
|
||||
<pubDate>Thu, 16 Jun 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Thu, 16 Jun 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/beta-update-may-2022</guid>
|
||||
<title>Supabase Beta May 2022</title>
|
||||
<link>https://supabase.com/blog/beta-update-may-2022</link>
|
||||
<description>Product and community updates including wildcard auth redirects, edge functions with webhooks, and Prometheus endpoints for everybody.</description>
|
||||
<pubDate>Tue, 31 May 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Tue, 31 May 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/how-supabase-accelerates-development-of-all-pull-together</guid>
|
||||
<title>How Mike Lyndon is using Supabase to accelerate development of AllPullTogether</title>
|
||||
<link>https://supabase.com/blog/how-supabase-accelerates-development-of-all-pull-together</link>
|
||||
<description>Mike Lyndon is learning web development as he builds AllPullTogether, and Supabase is helping him accelerate what he can accomplish.</description>
|
||||
<pubDate>Wed, 25 May 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Wed, 25 May 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/partner-gallery-works-with-supabase</guid>
|
||||
<title>Works With Supabase - announcing our Partner Gallery</title>
|
||||
<link>https://supabase.com/blog/partner-gallery-works-with-supabase</link>
|
||||
<description>Introducing our Partner Gallery - open source and made with Supabase.</description>
|
||||
<pubDate>Tue, 19 Apr 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Tue, 19 Apr 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/bring-the-func-hackathon-winners</guid>
|
||||
<title>Bring the Func Hackathon Winners 2022</title>
|
||||
<link>https://supabase.com/blog/bring-the-func-hackathon-winners</link>
|
||||
<description>Celebrating many amazing OSS Hackathon projects using GraphQL and Edge Functions.</description>
|
||||
<pubDate>Sun, 17 Apr 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Sun, 17 Apr 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/beta-update-march-2022</guid>
|
||||
<title>Supabase Beta March 2022</title>
|
||||
<link>https://supabase.com/blog/beta-update-march-2022</link>
|
||||
<description>Functions, GraphQL, and much more.</description>
|
||||
<pubDate>Thu, 14 Apr 2022 05:05:00 +0000</pubDate>
|
||||
<pubDate>Thu, 14 Apr 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabrew</guid>
|
||||
<title>Supabrew - Never Code Thirsty</title>
|
||||
<link>https://supabase.com/blog/supabrew</link>
|
||||
<description>A light and refreshing non-alcoholic beer for devs.</description>
|
||||
<pubDate>Fri, 01 Apr 2022 06:06:00 +0000</pubDate>
|
||||
<pubDate>Thu, 31 Mar 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-realtime-with-multiplayer-features</guid>
|
||||
<title>Supabase Realtime, with Multiplayer Features</title>
|
||||
<link>https://supabase.com/blog/supabase-realtime-with-multiplayer-features</link>
|
||||
<description>Today we're announced Realtime, with multiplayer features. Realtime enables broadcast, presence, and listening to database changes delivered over WebSockets.</description>
|
||||
<pubDate>Fri, 01 Apr 2022 06:06:00 +0000</pubDate>
|
||||
<pubDate>Thu, 31 Mar 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/hackathon-bring-the-func</guid>
|
||||
<title>Hackathon: Bring the Func(🕺)</title>
|
||||
<link>https://supabase.com/blog/hackathon-bring-the-func</link>
|
||||
<description>Build open-source projects with our latest features, win limited edition swag and plant a tree!</description>
|
||||
<pubDate>Fri, 01 Apr 2022 06:06:00 +0000</pubDate>
|
||||
<pubDate>Thu, 31 Mar 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-edge-functions</guid>
|
||||
<title>Edge Functions are now available in Supabase</title>
|
||||
<link>https://supabase.com/blog/supabase-edge-functions</link>
|
||||
<description>Today we're launching Edge Functions. Edge Functions let you execute Typescript code close to your users, no matter where they're located.</description>
|
||||
<pubDate>Wed, 30 Mar 2022 06:06:00 +0000</pubDate>
|
||||
<pubDate>Wed, 30 Mar 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-enterprise</guid>
|
||||
<title>Introducing Supabase Enterprise</title>
|
||||
<link>https://supabase.com/blog/supabase-enterprise</link>
|
||||
<description>Today we are releasing Supabase Enterprise, a suite of features to scale your project.</description>
|
||||
<pubDate>Tue, 29 Mar 2022 06:06:00 +0000</pubDate>
|
||||
<pubDate>Tue, 29 Mar 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/graphql-now-available</guid>
|
||||
<title>GraphQL is now available in Supabase</title>
|
||||
<link>https://supabase.com/blog/graphql-now-available</link>
|
||||
<description>GraphQL support is now in general availability on the Supabase platform via our open source PostgreSQL extension, pg_graphql (beta).</description>
|
||||
<pubDate>Mon, 28 Mar 2022 06:06:00 +0000</pubDate>
|
||||
<pubDate>Mon, 28 Mar 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/community-day-lw4</guid>
|
||||
<title>Community Day</title>
|
||||
<link>https://supabase.com/blog/community-day-lw4</link>
|
||||
<description>Kicking off Launch Week 4 with contributors, partners, and friends.</description>
|
||||
<pubDate>Sun, 27 Mar 2022 06:06:00 +0000</pubDate>
|
||||
<pubDate>Sun, 27 Mar 2022 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-launch-week-four</guid>
|
||||
<title>Supabase Launch Week 4</title>
|
||||
<link>https://supabase.com/blog/supabase-launch-week-four</link>
|
||||
<description>Launch Week 4: One new feature every day for an entire week. Starting Monday 28th March.</description>
|
||||
<pubDate>Thu, 24 Mar 2022 06:06:00 +0000</pubDate>
|
||||
<pubDate>Fri, 25 Mar 2022 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/should-i-open-source-my-company</guid>
|
||||
<title>Should I Open Source my Company?</title>
|
||||
<link>https://supabase.com/blog/should-i-open-source-my-company</link>
|
||||
<description>The unexpected upsides of building in public</description>
|
||||
<pubDate>Thu, 24 Mar 2022 06:06:00 +0000</pubDate>
|
||||
<pubDate>Fri, 25 Mar 2022 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/audit</guid>
|
||||
<title>Postgres Auditing in 150 lines of SQL</title>
|
||||
<link>https://supabase.com/blog/audit</link>
|
||||
<description>PostgreSQL has a robust set of features which we can leverage to create a generic auditing solution in 150 lines of SQL.</description>
|
||||
<pubDate>Mon, 07 Mar 2022 06:06:00 +0000</pubDate>
|
||||
<pubDate>Tue, 08 Mar 2022 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-beta-january-2022</guid>
|
||||
<title>Supabase Beta January 2022</title>
|
||||
<link>https://supabase.com/blog/supabase-beta-january-2022</link>
|
||||
<description>New auth providers, SMS providers, and new videos.</description>
|
||||
<pubDate>Mon, 21 Feb 2022 06:06:00 +0000</pubDate>
|
||||
<pubDate>Tue, 22 Feb 2022 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-beta-december-2021</guid>
|
||||
<title>Supabase Beta December 2021</title>
|
||||
<link>https://supabase.com/blog/supabase-beta-december-2021</link>
|
||||
<description>New crypto extension, Postgres videos, and a bunch of cool integrations.</description>
|
||||
<pubDate>Wed, 19 Jan 2022 06:06:00 +0000</pubDate>
|
||||
<pubDate>Thu, 20 Jan 2022 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/product-hunt-golden-kitty-awards-2021</guid>
|
||||
<title>Golden Kitty Awards Ceremony Watch Party with Supabase</title>
|
||||
<link>https://supabase.com/blog/product-hunt-golden-kitty-awards-2021</link>
|
||||
<description>Hang out with us while watching the Product Hunt Golden Kitty Awards Ceremony</description>
|
||||
<pubDate>Wed, 19 Jan 2022 06:06:00 +0000</pubDate>
|
||||
<pubDate>Thu, 20 Jan 2022 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/holiday-hackdays-winners-2021</guid>
|
||||
<title>Holiday Hackdays Winners 2021</title>
|
||||
<link>https://supabase.com/blog/holiday-hackdays-winners-2021</link>
|
||||
<description>Celebrating many amazing projects submitted to our Holiday Hackdays Hackathon.</description>
|
||||
<pubDate>Thu, 16 Dec 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Fri, 17 Dec 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/beta-november-2021-launch-week-recap</guid>
|
||||
<title>Supabase Beta November 2021: Launch Week Recap</title>
|
||||
<link>https://supabase.com/blog/beta-november-2021-launch-week-recap</link>
|
||||
<description>We wrapped up November with Supabase's third Launch Week. Here's all the awesome stuff that got shipped ...</description>
|
||||
<pubDate>Tue, 14 Dec 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Wed, 15 Dec 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-holiday-hackdays-hackathon</guid>
|
||||
<title>Kicking off the Holiday Hackdays</title>
|
||||
<link>https://supabase.com/blog/supabase-holiday-hackdays-hackathon</link>
|
||||
<description>Build cool stuff and celebrate open-source software with us during the Holiday Hackdays!</description>
|
||||
<pubDate>Thu, 02 Dec 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Fri, 03 Dec 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/pg-graphql</guid>
|
||||
<title>pg_graphql: A GraphQL extension for PostgreSQL</title>
|
||||
<link>https://supabase.com/blog/pg-graphql</link>
|
||||
<description>GraphQL support is in development for PostgreSQL + Supabase.</description>
|
||||
<pubDate>Thu, 02 Dec 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Fri, 03 Dec 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/launch-week-three-friday-five-more-things</guid>
|
||||
<title>Five more things</title>
|
||||
<link>https://supabase.com/blog/launch-week-three-friday-five-more-things</link>
|
||||
<description>It's never just one more thing!</description>
|
||||
<pubDate>Thu, 02 Dec 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Fri, 03 Dec 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-acquires-logflare</guid>
|
||||
<title>Supabase acquires Logflare</title>
|
||||
<link>https://supabase.com/blog/supabase-acquires-logflare</link>
|
||||
<description>Today, we're ecstatic to announce that Logflare is joining Supabase.</description>
|
||||
<pubDate>Wed, 01 Dec 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Thu, 02 Dec 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/realtime-row-level-security-in-postgresql</guid>
|
||||
<title>Realtime Postgres RLS now available on Supabase</title>
|
||||
<link>https://supabase.com/blog/realtime-row-level-security-in-postgresql</link>
|
||||
<description>Realtime database changes are now broadcast to authenticated users, respecting the same PostgreSQL policies that you use for Row Level Security.</description>
|
||||
<pubDate>Tue, 30 Nov 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Wed, 01 Dec 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-studio</guid>
|
||||
<title>Supabase Studio</title>
|
||||
<link>https://supabase.com/blog/supabase-studio</link>
|
||||
<description>The same Dashboard that you're using on our Platform is now available for local development and Self-Hosting.</description>
|
||||
<pubDate>Mon, 29 Nov 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Tue, 30 Nov 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/community-day-lw3</guid>
|
||||
<title>Community Day</title>
|
||||
<link>https://supabase.com/blog/community-day-lw3</link>
|
||||
<description>Kicking off launch week by highlighting the communities around Supabase.</description>
|
||||
<pubDate>Sun, 28 Nov 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Mon, 29 Nov 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/whats-new-in-postgres-14</guid>
|
||||
<title>New in PostgreSQL 14: What every developer should know</title>
|
||||
<link>https://supabase.com/blog/whats-new-in-postgres-14</link>
|
||||
<description>A quick look at some new features and functionality in PostgreSQL 14.</description>
|
||||
<pubDate>Sat, 27 Nov 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Sun, 28 Nov 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/postgrest-9</guid>
|
||||
<title>PostgREST 9</title>
|
||||
<link>https://supabase.com/blog/postgrest-9</link>
|
||||
<description>New features and updates in PostgREST version 9.</description>
|
||||
<pubDate>Fri, 26 Nov 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Sat, 27 Nov 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-launch-week-the-trilogy</guid>
|
||||
<title>Supabase Launch Week III: Holiday Special</title>
|
||||
<link>https://supabase.com/blog/supabase-launch-week-the-trilogy</link>
|
||||
<description>Tis the season to be shipping.</description>
|
||||
<pubDate>Thu, 25 Nov 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Fri, 26 Nov 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-how-we-launch</guid>
|
||||
<title>How we launch at Supabase</title>
|
||||
<link>https://supabase.com/blog/supabase-how-we-launch</link>
|
||||
<description>The history and methodology of Supabase Launch Week.</description>
|
||||
<pubDate>Thu, 25 Nov 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Fri, 26 Nov 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-beta-october-2021</guid>
|
||||
<title>Supabase Beta October 2021</title>
|
||||
<link>https://supabase.com/blog/supabase-beta-october-2021</link>
|
||||
<description>Three new Auth providers, multi-schema support, and we're gearing up for another Launch Week.</description>
|
||||
<pubDate>Sat, 06 Nov 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Sun, 07 Nov 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-series-a</guid>
|
||||
<title>Supabase $30m Series A</title>
|
||||
<link>https://supabase.com/blog/supabase-series-a</link>
|
||||
<description>Supabase just raised $30M, bringing our total funding to $36M.</description>
|
||||
<pubDate>Wed, 27 Oct 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Wed, 27 Oct 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/replenysh-time-to-value-in-less-than-24-hours</guid>
|
||||
<title>Replenysh uses Supabase to implement OTP in less than 24-hours</title>
|
||||
<link>https://supabase.com/blog/replenysh-time-to-value-in-less-than-24-hours</link>
|
||||
<description>Learn how Replenysh uses Supabase to power the circular economy, redefining how brands interact with their customers and products.</description>
|
||||
<pubDate>Mon, 18 Oct 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Mon, 18 Oct 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/hacktoberfest-hackathon-winners-2021</guid>
|
||||
<title>Hacktoberfest Hackathon Winners 2021</title>
|
||||
<link>https://supabase.com/blog/hacktoberfest-hackathon-winners-2021</link>
|
||||
<description>Celebrating many amazing projects submitted to our Hacktoberfest Hackathon.</description>
|
||||
<pubDate>Wed, 13 Oct 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Wed, 13 Oct 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-beta-sept-2021</guid>
|
||||
<title>Supabase Beta Sept 2021</title>
|
||||
<link>https://supabase.com/blog/supabase-beta-sept-2021</link>
|
||||
<description>Hackathon, Aborting request, UI updates, and now Hiring.</description>
|
||||
<pubDate>Sun, 03 Oct 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Sun, 03 Oct 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-hacktoberfest-hackathon-2021</guid>
|
||||
<title>Supabase Hacktoberfest Hackathon 2021</title>
|
||||
<link>https://supabase.com/blog/supabase-hacktoberfest-hackathon-2021</link>
|
||||
<description>We're running another Supabase Hackathon during Hacktoberfest!</description>
|
||||
<pubDate>Mon, 27 Sep 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Mon, 27 Sep 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-beta-august-2021</guid>
|
||||
<title>Supabase Beta August 2021</title>
|
||||
<link>https://supabase.com/blog/supabase-beta-august-2021</link>
|
||||
<description>Fundraising, Realtime Security, custom SMS templates, and deployments in South Korea.</description>
|
||||
<pubDate>Thu, 09 Sep 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Thu, 09 Sep 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-beta-july-2021</guid>
|
||||
<title>Supabase Beta July 2021</title>
|
||||
<link>https://supabase.com/blog/supabase-beta-july-2021</link>
|
||||
<description>Discord Logins, Vercel Integration, Full text search, and OAuth guides.</description>
|
||||
<pubDate>Wed, 11 Aug 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Wed, 11 Aug 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/hackathon-winners</guid>
|
||||
<title>Open Source Hackathon Winners</title>
|
||||
<link>https://supabase.com/blog/hackathon-winners</link>
|
||||
<description>Let the medal ceremony begin for the best projects submitted during the Supabase Hackathon.</description>
|
||||
<pubDate>Sun, 08 Aug 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Sun, 08 Aug 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-swag-store</guid>
|
||||
<title>Supabase Swag Store</title>
|
||||
<link>https://supabase.com/blog/supabase-swag-store</link>
|
||||
<description>Today we are officially launching the Supabase Swag Store.</description>
|
||||
<pubDate>Thu, 29 Jul 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Thu, 29 Jul 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-functions-updates</guid>
|
||||
<title>Updates for Supabase Functions</title>
|
||||
<link>https://supabase.com/blog/supabase-functions-updates</link>
|
||||
<description>The question on everyone's mind - are we launching Supabase Functions? Well, it's complicated.</description>
|
||||
<pubDate>Thu, 29 Jul 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Thu, 29 Jul 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/1-the-supabase-hackathon</guid>
|
||||
<title>The Supabase Hackathon</title>
|
||||
<link>https://supabase.com/blog/1-the-supabase-hackathon</link>
|
||||
<description>A whole week of Hacking for Fun and Prizes.</description>
|
||||
<pubDate>Thu, 29 Jul 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Thu, 29 Jul 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-reports-and-metrics</guid>
|
||||
<title>Supabase Reports and Metrics</title>
|
||||
<link>https://supabase.com/blog/supabase-reports-and-metrics</link>
|
||||
<description>We're exposing a full set of metrics in your projects, so that you can build better (and faster) products for your users.</description>
|
||||
<pubDate>Wed, 28 Jul 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Wed, 28 Jul 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-auth-passwordless-sms-login</guid>
|
||||
<title>Supabase Auth v2: Phone Auth now available</title>
|
||||
<link>https://supabase.com/blog/supabase-auth-passwordless-sms-login</link>
|
||||
<description>Phone Auth is available today on all new and existing Supabase projects.</description>
|
||||
<pubDate>Tue, 27 Jul 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Tue, 27 Jul 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/mobbin-supabase-200000-users</guid>
|
||||
<title>Mobbin uses Supabase to authenticate 200,000 users</title>
|
||||
<link>https://supabase.com/blog/mobbin-supabase-200000-users</link>
|
||||
<description>Learn how Mobbin migrated 200,000 users from Firebase for a better authentication experience.</description>
|
||||
<pubDate>Tue, 27 Jul 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Tue, 27 Jul 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/storage-beta</guid>
|
||||
<title>Supabase Storage now in Beta</title>
|
||||
<link>https://supabase.com/blog/storage-beta</link>
|
||||
<description>Supabase Storage moves into Beta.</description>
|
||||
<pubDate>Mon, 26 Jul 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Mon, 26 Jul 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/spot-flutter-with-postgres</guid>
|
||||
<title>Spot: a video sharing app built with Flutter</title>
|
||||
<link>https://supabase.com/blog/spot-flutter-with-postgres</link>
|
||||
<description>Spot is a geolocation-based video-sharing app with some social networking features.</description>
|
||||
<pubDate>Mon, 26 Jul 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Mon, 26 Jul 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-postgres-13</guid>
|
||||
<title>Supabase is now on Postgres 13.3</title>
|
||||
<link>https://supabase.com/blog/supabase-postgres-13</link>
|
||||
<description>From today, new Supabase projects will be on a version of Supabase Postgres that runs on Postgres 13.3.</description>
|
||||
<pubDate>Sun, 25 Jul 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Sun, 25 Jul 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-community-day</guid>
|
||||
<title>Supabase Community Day</title>
|
||||
<link>https://supabase.com/blog/supabase-community-day</link>
|
||||
<description>Community Day</description>
|
||||
<pubDate>Sun, 25 Jul 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Sun, 25 Jul 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/epsilon3-self-hosting</guid>
|
||||
<title>Epsilon3 Self-Host Supabase To Revolutionize Space Operations </title>
|
||||
<link>https://supabase.com/blog/epsilon3-self-hosting</link>
|
||||
<description>Learn how the team at Epsilon3 use Supabase to help teams execute secure and reliable operations in an industry that project spend runs into the billions.</description>
|
||||
<pubDate>Sun, 25 Jul 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Sun, 25 Jul 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-launch-week-sql</guid>
|
||||
<title>Supabase Launch Week II: The SQL</title>
|
||||
<link>https://supabase.com/blog/supabase-launch-week-sql</link>
|
||||
<description>Five days of Supabase. Again.</description>
|
||||
<pubDate>Wed, 21 Jul 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Wed, 21 Jul 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/roles-postgres-hooks</guid>
|
||||
<title>Protecting reserved roles with PostgreSQL Hooks</title>
|
||||
<link>https://supabase.com/blog/roles-postgres-hooks</link>
|
||||
<description>Using Postgres Hooks to protect functionality in your Postgres database.</description>
|
||||
<pubDate>Thu, 01 Jul 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Thu, 01 Jul 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-beta-june-2021</guid>
|
||||
<title>Supabase Beta June 2021</title>
|
||||
<link>https://supabase.com/blog/supabase-beta-june-2021</link>
|
||||
<description>Discord Logins, Vercel Integration, Full text search, and OAuth guides.</description>
|
||||
<pubDate>Tue, 01 Jun 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Tue, 01 Jun 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-beta-may-2021</guid>
|
||||
<title>Supabase Beta May 2021</title>
|
||||
<link>https://supabase.com/blog/supabase-beta-may-2021</link>
|
||||
<description>Apple & Twitter Logins, Supabase Grid, Go & Swift Libraries.</description>
|
||||
<pubDate>Tue, 01 Jun 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Tue, 01 Jun 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-beta-april-2021</guid>
|
||||
<title>Supabase Beta April 2021</title>
|
||||
<link>https://supabase.com/blog/supabase-beta-april-2021</link>
|
||||
<description>Supabase "gardening" - stability, security, and community support.</description>
|
||||
<pubDate>Tue, 04 May 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Tue, 04 May 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-beta-march-2021</guid>
|
||||
<title>Supabase Beta March 2021</title>
|
||||
<link>https://supabase.com/blog/supabase-beta-march-2021</link>
|
||||
<description>Launch week, Storage, Supabase CLI, Connection Pooling, Supabase UI, and Pricing.</description>
|
||||
<pubDate>Mon, 05 Apr 2021 05:05:00 +0000</pubDate>
|
||||
<pubDate>Mon, 05 Apr 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-workflows</guid>
|
||||
<title>Workflows are coming to Supabase</title>
|
||||
<link>https://supabase.com/blog/supabase-workflows</link>
|
||||
<description>Functions are great, but you know what's better?</description>
|
||||
<pubDate>Thu, 01 Apr 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Thu, 01 Apr 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-pgbouncer</guid>
|
||||
<title>PgBouncer is now available in Supabase</title>
|
||||
<link>https://supabase.com/blog/supabase-pgbouncer</link>
|
||||
<description>Better support for Serverless and Postgres.</description>
|
||||
<pubDate>Thu, 01 Apr 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Thu, 01 Apr 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-dot-com</guid>
|
||||
<title>Supabase Dot Com</title>
|
||||
<link>https://supabase.com/blog/supabase-dot-com</link>
|
||||
<description>The Supabase Domain name is changing.</description>
|
||||
<pubDate>Thu, 01 Apr 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Thu, 01 Apr 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-nft-marketplace</guid>
|
||||
<title>Supabase Launches NFT Marketplace</title>
|
||||
<link>https://supabase.com/blog/supabase-nft-marketplace</link>
|
||||
<description>A fully encrypted NFT platform to protect and transact your digital assets</description>
|
||||
<pubDate>Thu, 01 Apr 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Wed, 31 Mar 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-cli</guid>
|
||||
<title>Supabase CLI</title>
|
||||
<link>https://supabase.com/blog/supabase-cli</link>
|
||||
<description>Local development, database migrations, and self-hosting.</description>
|
||||
<pubDate>Wed, 31 Mar 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Tue, 30 Mar 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-storage</guid>
|
||||
<title>Storage is now available in Supabase</title>
|
||||
<link>https://supabase.com/blog/supabase-storage</link>
|
||||
<description>Launching Supabase Storage and how you can use it in your apps</description>
|
||||
<pubDate>Tue, 30 Mar 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Mon, 29 Mar 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/pricing</guid>
|
||||
<title>Supabase Beta Pricing</title>
|
||||
<link>https://supabase.com/blog/pricing</link>
|
||||
<description>Supabase launches Beta pricing structure</description>
|
||||
<pubDate>Mon, 29 Mar 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Sun, 28 Mar 2021 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/launch-week</guid>
|
||||
<title>Launch week</title>
|
||||
<link>https://supabase.com/blog/launch-week</link>
|
||||
<description>Five days of Supabase.</description>
|
||||
<pubDate>Wed, 24 Mar 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Thu, 25 Mar 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/angels-of-supabase</guid>
|
||||
<title>Angels of Supabase</title>
|
||||
<link>https://supabase.com/blog/angels-of-supabase</link>
|
||||
<description>Meet the investors of Supabase.</description>
|
||||
<pubDate>Wed, 24 Mar 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Thu, 25 Mar 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/In-The-Loop</guid>
|
||||
<title>Developers stay up to date with intheloop.dev</title>
|
||||
<link>https://supabase.com/blog/In-The-Loop</link>
|
||||
<description>Learn why Kevin is building intheloop.dev with Supabase</description>
|
||||
<pubDate>Mon, 22 Mar 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Mon, 22 Mar 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/using-supabase-replit</guid>
|
||||
<title>Using Supabase in Replit</title>
|
||||
<link>https://supabase.com/blog/using-supabase-replit</link>
|
||||
<description>Free hosted relational database from within your node.js repl</description>
|
||||
<pubDate>Thu, 11 Mar 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Thu, 11 Mar 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/toad-a-link-shortener-with-simple-apis-for-low-coders</guid>
|
||||
<title>Toad, a link shortener with simple APIs for low-coders</title>
|
||||
<link>https://supabase.com/blog/toad-a-link-shortener-with-simple-apis-for-low-coders</link>
|
||||
<description>An easy-to-use link shortening tool with simple APIs</description>
|
||||
<pubDate>Mon, 08 Mar 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Mon, 08 Mar 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/postgres-as-a-cron-server</guid>
|
||||
<title>Postgres as a CRON Server</title>
|
||||
<link>https://supabase.com/blog/postgres-as-a-cron-server</link>
|
||||
<description>Running repetitive tasks with your Postgres database.</description>
|
||||
<pubDate>Fri, 05 Mar 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Fri, 05 Mar 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-beta-february-2021</guid>
|
||||
<title>Supabase Beta February 2021</title>
|
||||
<link>https://supabase.com/blog/supabase-beta-february-2021</link>
|
||||
<description>One year of building.</description>
|
||||
<pubDate>Tue, 02 Mar 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Tue, 02 Mar 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/cracking-postgres-interview</guid>
|
||||
<title>Cracking PostgreSQL Interview Questions</title>
|
||||
<link>https://supabase.com/blog/cracking-postgres-interview</link>
|
||||
<description>Understand the top PostgreSQL Interview Questions</description>
|
||||
<pubDate>Sat, 27 Feb 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Sat, 27 Feb 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/case-study-roboflow</guid>
|
||||
<title>Roboflow.com choose Supabase to power Paint.wtf leaderboard</title>
|
||||
<link>https://supabase.com/blog/case-study-roboflow</link>
|
||||
<description>Learn how Roboflow.com used Supabase to build their Paint.wtf leaderboard</description>
|
||||
<pubDate>Tue, 09 Feb 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Tue, 09 Feb 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-beta-january-2021</guid>
|
||||
<title>Supabase Beta January 2021</title>
|
||||
<link>https://supabase.com/blog/supabase-beta-january-2021</link>
|
||||
<description>Eleven months of building.</description>
|
||||
<pubDate>Tue, 02 Feb 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Tue, 02 Feb 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-beta-december-2020</guid>
|
||||
<title>Supabase Beta December 2020</title>
|
||||
<link>https://supabase.com/blog/supabase-beta-december-2020</link>
|
||||
<description>Ten months of building.</description>
|
||||
<pubDate>Sat, 02 Jan 2021 06:06:00 +0000</pubDate>
|
||||
<pubDate>Sat, 02 Jan 2021 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-dashboard-performance</guid>
|
||||
<title>Making the Supabase Dashboard Supa-fast</title>
|
||||
<link>https://supabase.com/blog/supabase-dashboard-performance</link>
|
||||
<description>Improving the performance of the Supabase dashboard</description>
|
||||
<pubDate>Sun, 13 Dec 2020 06:06:00 +0000</pubDate>
|
||||
<pubDate>Sun, 13 Dec 2020 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-striveschool</guid>
|
||||
<title>Supabase Partners With Strive School To Help Teach Open Source</title>
|
||||
<link>https://supabase.com/blog/supabase-striveschool</link>
|
||||
<description>Supabase Partners With Strive School To Help Teach Open Source To The Next Generation Of Developers</description>
|
||||
<pubDate>Wed, 02 Dec 2020 06:06:00 +0000</pubDate>
|
||||
<pubDate>Wed, 02 Dec 2020 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/case-study-xendit</guid>
|
||||
<title>Xendit Built a Counter-Fraud Watchlist for the Fintech Industry</title>
|
||||
<link>https://supabase.com/blog/case-study-xendit</link>
|
||||
<description>See how Xendit use Supabase to build a full-text search engine.</description>
|
||||
<pubDate>Wed, 02 Dec 2020 06:06:00 +0000</pubDate>
|
||||
<pubDate>Wed, 02 Dec 2020 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/case-study-tayfa</guid>
|
||||
<title>TAYFA Built a No-Code Website Builder in Seven Days</title>
|
||||
<link>https://supabase.com/blog/case-study-tayfa</link>
|
||||
<description>See how Tayfa went from idea to paying customer in less than 30 days.</description>
|
||||
<pubDate>Wed, 02 Dec 2020 06:06:00 +0000</pubDate>
|
||||
<pubDate>Wed, 02 Dec 2020 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/case-study-monitoro</guid>
|
||||
<title>Monitoro Built a Web Crawler Handling Millions of API Requests</title>
|
||||
<link>https://supabase.com/blog/case-study-monitoro</link>
|
||||
<description>See how Monitoro built an automated scraping platform using Supabase.</description>
|
||||
<pubDate>Wed, 02 Dec 2020 06:06:00 +0000</pubDate>
|
||||
<pubDate>Wed, 02 Dec 2020 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-alpha-november-2020</guid>
|
||||
<title>Supabase Alpha November 2020</title>
|
||||
<link>https://supabase.com/blog/supabase-alpha-november-2020</link>
|
||||
<description>Nine months of building.</description>
|
||||
<pubDate>Tue, 01 Dec 2020 06:06:00 +0000</pubDate>
|
||||
<pubDate>Tue, 01 Dec 2020 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/postgresql-views</guid>
|
||||
<title>Postgres Views</title>
|
||||
<link>https://supabase.com/blog/postgresql-views</link>
|
||||
<description>Creating and using a view in PostgreSQL.</description>
|
||||
<pubDate>Wed, 18 Nov 2020 06:06:00 +0000</pubDate>
|
||||
<pubDate>Wed, 18 Nov 2020 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-alpha-october-2020</guid>
|
||||
<title>Supabase Alpha October 2020</title>
|
||||
<link>https://supabase.com/blog/supabase-alpha-october-2020</link>
|
||||
<description>Eight months of building.</description>
|
||||
<pubDate>Mon, 02 Nov 2020 06:06:00 +0000</pubDate>
|
||||
<pubDate>Mon, 02 Nov 2020 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/improved-dx</guid>
|
||||
<title>Supabase.js 1.0</title>
|
||||
<link>https://supabase.com/blog/improved-dx</link>
|
||||
<description>We're releasing a new version of our Supabase client with some awesome new improvements.</description>
|
||||
<pubDate>Fri, 30 Oct 2020 06:06:00 +0000</pubDate>
|
||||
<pubDate>Fri, 30 Oct 2020 00:12:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-alpha-september-2020</guid>
|
||||
<title>Supabase Alpha September 2020</title>
|
||||
<link>https://supabase.com/blog/supabase-alpha-september-2020</link>
|
||||
<description>Seven months of building.</description>
|
||||
<pubDate>Sat, 03 Oct 2020 05:05:00 +0000</pubDate>
|
||||
<pubDate>Fri, 02 Oct 2020 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-hacktoberfest-2020</guid>
|
||||
<title>Supabase Hacktoberfest 2020</title>
|
||||
<link>https://supabase.com/blog/supabase-hacktoberfest-2020</link>
|
||||
<description>Join us for a celebration of open source software and learn how to contribute to Supabase.</description>
|
||||
<pubDate>Fri, 11 Sep 2020 05:05:00 +0000</pubDate>
|
||||
<pubDate>Thu, 10 Sep 2020 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-alpha-august-2020</guid>
|
||||
<title>Supabase Alpha August 2020</title>
|
||||
<link>https://supabase.com/blog/supabase-alpha-august-2020</link>
|
||||
<description>Six months of building</description>
|
||||
<pubDate>Thu, 03 Sep 2020 05:05:00 +0000</pubDate>
|
||||
<pubDate>Wed, 02 Sep 2020 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-auth</guid>
|
||||
<title>Supabase Auth</title>
|
||||
<link>https://supabase.com/blog/supabase-auth</link>
|
||||
<description>Authenticate and authorize your users with Supabase Auth</description>
|
||||
<pubDate>Wed, 05 Aug 2020 05:05:00 +0000</pubDate>
|
||||
<pubDate>Tue, 04 Aug 2020 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-alpha-july-2020</guid>
|
||||
<title>Supabase Alpha July 2020</title>
|
||||
<link>https://supabase.com/blog/supabase-alpha-july-2020</link>
|
||||
<description>Five months of building</description>
|
||||
<pubDate>Sun, 02 Aug 2020 05:05:00 +0000</pubDate>
|
||||
<pubDate>Sat, 01 Aug 2020 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/continuous-postgresql-backup-walg</guid>
|
||||
<title>Continuous PostgreSQL Backups using WAL-G</title>
|
||||
<link>https://supabase.com/blog/continuous-postgresql-backup-walg</link>
|
||||
<description>Have you ever wanted to restore your database's state to a particular moment in time? This post explains how, using WAL-G.</description>
|
||||
<pubDate>Sun, 02 Aug 2020 05:05:00 +0000</pubDate>
|
||||
<pubDate>Sat, 01 Aug 2020 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/alpha-launch-postmortem</guid>
|
||||
<title>Alpha Launch Postmortem</title>
|
||||
<link>https://supabase.com/blog/alpha-launch-postmortem</link>
|
||||
<description>Everything that went wrong with Supabase's launch</description>
|
||||
<pubDate>Fri, 10 Jul 2020 05:05:00 +0000</pubDate>
|
||||
<pubDate>Thu, 09 Jul 2020 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/postgresql-templates</guid>
|
||||
<title>What are PostgreSQL Templates?</title>
|
||||
<link>https://supabase.com/blog/postgresql-templates</link>
|
||||
<description>What are PostgreSQL templates and what are they used for?</description>
|
||||
<pubDate>Thu, 09 Jul 2020 05:05:00 +0000</pubDate>
|
||||
<pubDate>Wed, 08 Jul 2020 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/postgresql-physical-logical-backups</guid>
|
||||
<title>Physical vs Logical Backups in PostgreSQL</title>
|
||||
<link>https://supabase.com/blog/postgresql-physical-logical-backups</link>
|
||||
<description>What are physical and logical backups in Postgres?</description>
|
||||
<pubDate>Tue, 07 Jul 2020 05:05:00 +0000</pubDate>
|
||||
<pubDate>Mon, 06 Jul 2020 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-alpha-june-2020</guid>
|
||||
<title>Supabase Alpha June 2020</title>
|
||||
<link>https://supabase.com/blog/supabase-alpha-june-2020</link>
|
||||
<description>Four months of building</description>
|
||||
<pubDate>Wed, 01 Jul 2020 05:05:00 +0000</pubDate>
|
||||
<pubDate>Tue, 30 Jun 2020 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-steve-chavez</guid>
|
||||
<title>Steve Chavez has joined Supabase</title>
|
||||
<link>https://supabase.com/blog/supabase-steve-chavez</link>
|
||||
<description>Steve joins Supabase to help build Auth.</description>
|
||||
<pubDate>Mon, 15 Jun 2020 05:05:00 +0000</pubDate>
|
||||
<pubDate>Sun, 14 Jun 2020 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-alpha-may-2020</guid>
|
||||
<title>Supabase Alpha May 2020</title>
|
||||
<link>https://supabase.com/blog/supabase-alpha-may-2020</link>
|
||||
<description>Three months of building</description>
|
||||
<pubDate>Mon, 01 Jun 2020 05:05:00 +0000</pubDate>
|
||||
<pubDate>Sun, 31 May 2020 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<guid>https://supabase.com/blog/supabase-alpha-april-2020</guid>
|
||||
<title>Supabase Alpha April 2020</title>
|
||||
<link>https://supabase.com/blog/supabase-alpha-april-2020</link>
|
||||
<description>Two months of building</description>
|
||||
<pubDate>Mon, 01 Jun 2020 05:05:00 +0000</pubDate>
|
||||
<pubDate>Sun, 31 May 2020 23:11:00 +0000</pubDate>
|
||||
</item>
|
||||
|
||||
</channel>
|
||||
|
||||
@@ -7,9 +7,10 @@ interface Props {
|
||||
name: string
|
||||
properties: any
|
||||
formValues: any
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const FormField: FC<Props> = ({ name, properties, formValues }) => {
|
||||
const FormField: FC<Props> = ({ name, properties, formValues, disabled = false }) => {
|
||||
if (properties.show && formValues[properties.show.key] !== properties.show.matches) return <></>
|
||||
|
||||
const [hidden, setHidden] = useState(!!properties.isSecret)
|
||||
@@ -22,6 +23,7 @@ const FormField: FC<Props> = ({ name, properties, formValues }) => {
|
||||
layout="vertical"
|
||||
id={name}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
type={hidden ? 'password' : 'text'}
|
||||
label={properties.title}
|
||||
labelOptional={
|
||||
@@ -64,6 +66,7 @@ const FormField: FC<Props> = ({ name, properties, formValues }) => {
|
||||
layout="vertical"
|
||||
id={name}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
label={properties.title}
|
||||
labelOptional={
|
||||
properties.descriptionOptional ? (
|
||||
@@ -97,6 +100,7 @@ const FormField: FC<Props> = ({ name, properties, formValues }) => {
|
||||
size="small"
|
||||
id={name}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
label={properties.title}
|
||||
descriptionText={properties.description}
|
||||
/>
|
||||
@@ -107,6 +111,7 @@ const FormField: FC<Props> = ({ name, properties, formValues }) => {
|
||||
<Listbox
|
||||
size="small"
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
label={properties.title}
|
||||
descriptionText={properties.description}
|
||||
defaultValue={properties.enum[0]}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { FC, useState } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { Alert, Button, Collapsible, Form, IconCheck, IconChevronUp, Input } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { useStore, checkPermissions } from 'hooks'
|
||||
import { Provider } from './AuthProvidersForm.types'
|
||||
import { ProviderCollapsibleClasses } from './AuthProvidersForm.constants'
|
||||
import FormField from './FormField'
|
||||
@@ -16,6 +18,7 @@ const ProviderForm: FC<Props> = ({ provider }) => {
|
||||
const { authConfig, ui } = useStore()
|
||||
|
||||
const doubleNegativeKeys = ['MAILER_AUTOCONFIRM', 'SMS_AUTOCONFIRM']
|
||||
const canUpdateConfig = checkPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
|
||||
|
||||
const generateInitialValues = () => {
|
||||
const initialValues: { [x: string]: string } = {}
|
||||
@@ -103,8 +106,8 @@ const ProviderForm: FC<Props> = ({ provider }) => {
|
||||
validationSchema={provider.validationSchema}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({ isSubmitting, handleReset, values }: any) => {
|
||||
const noChanges = JSON.stringify(INITIAL_VALUES) === JSON.stringify(values)
|
||||
{({ isSubmitting, handleReset, initialValues, values }: any) => {
|
||||
const noChanges = JSON.stringify(initialValues) === JSON.stringify(values)
|
||||
return (
|
||||
<Collapsible.Content>
|
||||
<div
|
||||
@@ -113,13 +116,14 @@ const ProviderForm: FC<Props> = ({ provider }) => {
|
||||
border-scale-500 bg-scale-100 py-6 px-6 text-scale-1200 dark:bg-scale-300
|
||||
"
|
||||
>
|
||||
<div className="mx-auto my-6 max-w-md space-y-6">
|
||||
<div className="mx-auto my-6 max-w-lg space-y-6">
|
||||
{Object.keys(provider.properties).map((x: string) => (
|
||||
<FormField
|
||||
key={x}
|
||||
name={x}
|
||||
properties={provider.properties[x]}
|
||||
formValues={values}
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
))}
|
||||
{provider?.misc?.alert && (
|
||||
@@ -149,12 +153,36 @@ const ProviderForm: FC<Props> = ({ provider }) => {
|
||||
handleReset()
|
||||
setOpen(false)
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button htmlType="submit" loading={isSubmitting} disabled={noChanges}>
|
||||
Save
|
||||
</Button>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger type="button">
|
||||
<Button
|
||||
htmlType="submit"
|
||||
loading={isSubmitting}
|
||||
disabled={!canUpdateConfig || noChanges}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canUpdateConfig && (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'bg-scale-100 rounded py-1 px-2 leading-none shadow',
|
||||
'border-scale-200 border',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-scale-1200 text-xs">
|
||||
You need additional permissions to update provider settings
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { boolean, number, object, string } from 'yup'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { Button, Form, Input, IconEye, IconEyeOff, InputNumber, Toggle } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { useStore, checkPermissions } from 'hooks'
|
||||
import {
|
||||
FormActions,
|
||||
FormHeader,
|
||||
@@ -20,6 +21,8 @@ const AutoSchemaForm = observer(() => {
|
||||
const formId = 'auth-config-general-form'
|
||||
const [hidden, setHidden] = useState(true)
|
||||
|
||||
const canUpdateConfig = checkPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
|
||||
|
||||
const INITIAL_VALUES = {
|
||||
DISABLE_SIGNUP: !authConfig.config.DISABLE_SIGNUP,
|
||||
JWT_EXP: authConfig.config.JWT_EXP,
|
||||
@@ -90,12 +93,18 @@ const AutoSchemaForm = observer(() => {
|
||||
<FormPanel
|
||||
disabled={true}
|
||||
footer={
|
||||
<div className="flex justify-between py-4 px-8">
|
||||
<div className="flex py-4 px-8">
|
||||
<FormActions
|
||||
form={formId}
|
||||
isSubmitting={isSubmitting}
|
||||
hasChanges={hasChanges}
|
||||
handleReset={handleReset}
|
||||
disabled={!canUpdateConfig}
|
||||
helper={
|
||||
!canUpdateConfig
|
||||
? 'You need additional permissions to update authentication settings'
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
@@ -108,6 +117,7 @@ const AutoSchemaForm = observer(() => {
|
||||
label="Allow new users to sign up"
|
||||
layout="flex"
|
||||
descriptionText="If this is disabled, new users will not be able to sign up to your application."
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
@@ -121,6 +131,7 @@ const AutoSchemaForm = observer(() => {
|
||||
size="small"
|
||||
label="Site URL"
|
||||
descriptionText="The base URL of your website. Used as an allow-list for redirects and for constructing URLs used in emails."
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
<InputNumber
|
||||
id="JWT_EXP"
|
||||
@@ -128,6 +139,7 @@ const AutoSchemaForm = observer(() => {
|
||||
label="JWT expiry limit"
|
||||
descriptionText="How long tokens are valid for. Defaults to 3600 (1 hour), maximum 604,800 seconds (one week)."
|
||||
actions={<span className="text-scale-900 mr-3">seconds</span>}
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
@@ -139,6 +151,7 @@ const AutoSchemaForm = observer(() => {
|
||||
label="Enable hCaptcha protection"
|
||||
layout="flex"
|
||||
descriptionText="Protect authentication endpoints from abuse."
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
{values.SECURITY_CAPTCHA_ENABLED && (
|
||||
<Input
|
||||
@@ -146,6 +159,7 @@ const AutoSchemaForm = observer(() => {
|
||||
type={hidden ? 'password' : 'text'}
|
||||
size="small"
|
||||
label="hCaptcha secret"
|
||||
disabled={!canUpdateConfig}
|
||||
actions={
|
||||
<Button
|
||||
icon={hidden ? <IconEye /> : <IconEyeOff />}
|
||||
@@ -161,6 +175,7 @@ const AutoSchemaForm = observer(() => {
|
||||
label="Enable automatic reuse detection"
|
||||
layout="flex"
|
||||
descriptionText="Prevent replay attacks from compromised refresh tokens."
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
{values.REFRESH_TOKEN_ROTATION_ENABLED && (
|
||||
<InputNumber
|
||||
@@ -170,6 +185,7 @@ const AutoSchemaForm = observer(() => {
|
||||
label="Reuse Interval"
|
||||
descriptionText="Time interval where the same refresh token can be used to request for an access token."
|
||||
actions={<span className="text-scale-900 mr-3">seconds</span>}
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
)}
|
||||
</FormSectionContent>
|
||||
|
||||
@@ -2,8 +2,9 @@ import ReactMarkdown from 'react-markdown'
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Input, Form } from '@supabase/ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { useStore, checkPermissions } from 'hooks'
|
||||
import { FormSchema } from 'types'
|
||||
import { FormActions, FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms'
|
||||
import CodeEditor from 'components/ui/CodeEditor'
|
||||
@@ -22,6 +23,7 @@ const TemplateEditor: FC<Props> = ({ template }) => {
|
||||
|
||||
const formId = `auth-config-email-templates-${id}`
|
||||
const INITIAL_VALUES: { [x: string]: string } = {}
|
||||
const canUpdateConfig = checkPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoaded) {
|
||||
@@ -107,6 +109,7 @@ const TemplateEditor: FC<Props> = ({ template }) => {
|
||||
</ReactMarkdown>
|
||||
) : null
|
||||
}
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -138,6 +141,7 @@ const TemplateEditor: FC<Props> = ({ template }) => {
|
||||
id="code-id"
|
||||
loading={!isLoaded}
|
||||
language="html"
|
||||
isReadOnly={!canUpdateConfig}
|
||||
className="!mb-0 h-96 overflow-hidden rounded border"
|
||||
onInputChange={(e: string | undefined) => setBodyValue(e ?? '')}
|
||||
options={{ wordWrap: 'off', contextmenu: false }}
|
||||
@@ -146,7 +150,7 @@ const TemplateEditor: FC<Props> = ({ template }) => {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="col-span-12 flex w-full justify-between">
|
||||
<div className="col-span-12 flex w-full">
|
||||
<FormActions
|
||||
handleReset={() => {
|
||||
resetForm({
|
||||
@@ -158,6 +162,12 @@ const TemplateEditor: FC<Props> = ({ template }) => {
|
||||
form={formId}
|
||||
isSubmitting={isSubmitting}
|
||||
hasChanges={hasChanges}
|
||||
disabled={!canUpdateConfig}
|
||||
helper={
|
||||
!canUpdateConfig
|
||||
? 'You need additional permissions to update authentication settings'
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</FormSectionContent>
|
||||
|
||||
@@ -8,10 +8,11 @@ import { EmptyListState } from 'components/ui/States'
|
||||
import { HorizontalShimmerWithIcon } from 'components/ui/Shimmers'
|
||||
|
||||
interface Props {
|
||||
canUpdate: boolean
|
||||
onSelectDomainToDelete: (domain: string) => void
|
||||
}
|
||||
|
||||
const DomainList: FC<Props> = ({ onSelectDomainToDelete }) => {
|
||||
const DomainList: FC<Props> = ({ canUpdate, onSelectDomainToDelete }) => {
|
||||
const { authConfig } = useStore()
|
||||
|
||||
const URI_ALLOW_LIST_ARRAY = authConfig.config.URI_ALLOW_LIST
|
||||
@@ -33,19 +34,21 @@ const DomainList: FC<Props> = ({ onSelectDomainToDelete }) => {
|
||||
URI_ALLOW_LIST_ARRAY.map((domain: string) => {
|
||||
return (
|
||||
<ValueContainer key={domain}>
|
||||
<div className="flex items-center gap-2 font-mono">
|
||||
<div className="flex items-center gap-4 font-mono">
|
||||
<span className="text-scale-900">
|
||||
<IconGlobe strokeWidth={2} size={14} />
|
||||
</span>
|
||||
{domain}
|
||||
<span className="text-sm">{domain}</span>
|
||||
</div>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<IconTrash />}
|
||||
onClick={() => onSelectDomainToDelete(domain)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
{canUpdate && (
|
||||
<Button
|
||||
type="default"
|
||||
icon={<IconTrash />}
|
||||
onClick={() => onSelectDomainToDelete(domain)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
)}
|
||||
</ValueContainer>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useState } from 'react'
|
||||
import { object, string } from 'yup'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { Button, Form, Input, Modal } from '@supabase/ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { checkPermissions, useStore } from 'hooks'
|
||||
import { FormHeader } from 'components/ui/Forms'
|
||||
import { domainRegex } from '../Auth.constants'
|
||||
import DomainList from './DomainList'
|
||||
@@ -19,6 +21,8 @@ const RedirectDomains = () => {
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [selectedDomainToDelete, setSelectedDomainToDelete] = useState<string>()
|
||||
|
||||
const canUpdateConfig = checkPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
|
||||
|
||||
const newDomainSchema = object({
|
||||
domain: string().matches(domainRegex, 'URL is not valid').required(),
|
||||
})
|
||||
@@ -86,9 +90,30 @@ const RedirectDomains = () => {
|
||||
title="Redirect URLs"
|
||||
description="URLs that auth providers are permitted to redirect to post authentication"
|
||||
/>
|
||||
<Button onClick={() => setOpen(true)}>Add domain</Button>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Button disabled={!canUpdateConfig} onClick={() => setOpen(true)}>
|
||||
Add domain
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canUpdateConfig && (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'bg-scale-100 rounded py-1 px-2 leading-none shadow',
|
||||
'border-scale-200 border',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-scale-1200 text-xs">
|
||||
You need additional permissions to update redirect URLs
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
<DomainList onSelectDomainToDelete={setSelectedDomainToDelete} />
|
||||
<DomainList canUpdate={canUpdateConfig} onSelectDomainToDelete={setSelectedDomainToDelete} />
|
||||
<Modal
|
||||
hideFooter
|
||||
size="small"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { number, object, string } from 'yup'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { Alert, Button, Form, Input, InputNumber, Toggle, IconEye, IconEyeOff } from '@supabase/ui'
|
||||
|
||||
import {
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
FormSectionContent,
|
||||
FormSectionLabel,
|
||||
} from 'components/ui/Forms'
|
||||
import { useStore } from 'hooks'
|
||||
import { useStore, checkPermissions } from 'hooks'
|
||||
import { domainRegex } from './../Auth.constants'
|
||||
import { defaultDisabledSmtpFormValues } from './SmtpForm.constants'
|
||||
import { generateFormValues, isSmtpEnabled } from './SmtpForm.utils'
|
||||
@@ -25,6 +26,7 @@ const SmtpForm = () => {
|
||||
|
||||
const formId = 'auth-config-smtp-form'
|
||||
const initialValues = generateFormValues(authConfig.config)
|
||||
const canUpdateConfig = checkPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoaded && isSmtpEnabled(config)) {
|
||||
@@ -143,12 +145,18 @@ const SmtpForm = () => {
|
||||
/>
|
||||
<FormPanel
|
||||
footer={
|
||||
<div className="flex justify-between py-4 px-8">
|
||||
<div className="flex py-4 px-8">
|
||||
<FormActions
|
||||
form={formId}
|
||||
isSubmitting={isSubmitting}
|
||||
hasChanges={hasChanges}
|
||||
handleReset={onResetForm}
|
||||
disabled={!canUpdateConfig}
|
||||
helper={
|
||||
!canUpdateConfig
|
||||
? 'You need additional permissions to update authentication settings'
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
@@ -161,6 +169,7 @@ const SmtpForm = () => {
|
||||
label="Enable Custom SMTP"
|
||||
layout="flex"
|
||||
checked={enableSmtp}
|
||||
disabled={!canUpdateConfig}
|
||||
// @ts-ignore
|
||||
onChange={(value: boolean) => setEnableSmtp(value)}
|
||||
descriptionText="Emails will be sent using your custom SMTP provider"
|
||||
@@ -188,6 +197,7 @@ const SmtpForm = () => {
|
||||
label="Sender email"
|
||||
descriptionText="This is the email address the emails are sent from"
|
||||
placeholder="noreply@yourdomain.com"
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
<Input
|
||||
name="SMTP_SENDER_NAME"
|
||||
@@ -195,6 +205,7 @@ const SmtpForm = () => {
|
||||
label="Sender name"
|
||||
descriptionText="Name displayed in the recipient's inbox"
|
||||
placeholder="The name shown on the email"
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
@@ -218,6 +229,7 @@ const SmtpForm = () => {
|
||||
id="SMTP_HOST"
|
||||
label="Host"
|
||||
descriptionText="Hostname or IP address of your SMTP server."
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
<InputNumber
|
||||
name="SMTP_PORT"
|
||||
@@ -236,6 +248,7 @@ const SmtpForm = () => {
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
<InputNumber
|
||||
id="SMTP_MAX_FREQUENCY"
|
||||
@@ -243,6 +256,7 @@ const SmtpForm = () => {
|
||||
label="Minimum interval between emails being sent"
|
||||
descriptionText="How long between each email can a new email be sent via your SMTP server."
|
||||
actions={<span className="text-scale-900 mr-3">seconds</span>}
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
<InputNumber
|
||||
name="RATE_LIMIT_EMAIL_SENT"
|
||||
@@ -251,12 +265,14 @@ const SmtpForm = () => {
|
||||
label="Rate limit for sending emails"
|
||||
descriptionText="How many emails can be sent per hour."
|
||||
actions={<span className="text-scale-900 mr-3">emails per hour</span>}
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
<Input
|
||||
name="SMTP_USER"
|
||||
id="SMTP_USER"
|
||||
label="Username"
|
||||
placeholder="SMTP Username"
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
<Input
|
||||
name="SMTP_PASS"
|
||||
@@ -271,6 +287,7 @@ const SmtpForm = () => {
|
||||
onClick={() => setHidden(!hidden)}
|
||||
/>
|
||||
}
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useContext, useState } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { Button, Modal, Input, IconPlus, IconMail, Form } from '@supabase/ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { useStore, checkPermissions } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { PageContext } from 'pages/project/[ref]/auth/users'
|
||||
@@ -13,6 +15,7 @@ const InviteUserModal = () => {
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
const handleToggle = () => setVisible(!visible)
|
||||
const canInviteUsers = checkPermissions(PermissionAction.AUTH_EXECUTE, 'invite_user')
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {}
|
||||
@@ -53,9 +56,28 @@ const InviteUserModal = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={handleToggle} icon={<IconPlus />}>
|
||||
Invite
|
||||
</Button>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Button as="span" onClick={handleToggle} icon={<IconPlus />} disabled={!canInviteUsers}>
|
||||
Invite
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canInviteUsers && (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'bg-scale-100 rounded py-1 px-2 leading-none shadow',
|
||||
'border-scale-200 border',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-scale-1200 text-xs">
|
||||
You need additional permissions to invite users
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
<Modal
|
||||
closable
|
||||
hideFooter
|
||||
@@ -91,7 +113,7 @@ const InviteUserModal = () => {
|
||||
size="small"
|
||||
htmlType="submit"
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
disabled={!canInviteUsers || isSubmitting}
|
||||
>
|
||||
Invite user
|
||||
</Button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FC, useContext, useState } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { Button, Dropdown, Divider, IconTrash, IconMail, IconMoreHorizontal } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
@@ -8,8 +9,14 @@ import { post, delete_ } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { PageContext } from 'pages/project/[ref]/auth/users'
|
||||
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
|
||||
import { User } from './Users.types'
|
||||
|
||||
const UserDropdown: FC<{ user: any }> = ({ user }) => {
|
||||
interface Props {
|
||||
user: User
|
||||
canRemoveUser: boolean
|
||||
}
|
||||
|
||||
const UserDropdown: FC<Props> = ({ user, canRemoveUser }) => {
|
||||
const PageState: any = useContext(PageContext)
|
||||
const { ui } = useStore()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
@@ -134,9 +141,32 @@ const UserDropdown: FC<{ user: any }> = ({ user }) => {
|
||||
</Dropdown.Item>
|
||||
) : null}
|
||||
<Dropdown.Seperator />
|
||||
<Dropdown.Item onClick={handleDelete} icon={<IconTrash size="tiny" />}>
|
||||
Delete user
|
||||
</Dropdown.Item>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger className="w-full">
|
||||
<Dropdown.Item
|
||||
onClick={handleDelete}
|
||||
icon={<IconTrash size="tiny" />}
|
||||
disabled={!canRemoveUser}
|
||||
>
|
||||
Delete user
|
||||
</Dropdown.Item>
|
||||
</Tooltip.Trigger>
|
||||
{!canRemoveUser && (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'bg-scale-100 rounded py-1 px-2 leading-none shadow',
|
||||
'border-scale-200 border',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-scale-1200 text-xs">
|
||||
You need additional permissions to delete users
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export interface User {
|
||||
id: string
|
||||
email: string
|
||||
phone: string
|
||||
email_confirmed_at: string
|
||||
phone_confirmed_at: string
|
||||
created_at: string
|
||||
last_sign_in_at: string
|
||||
raw_app_meta_data?: {
|
||||
provider: string
|
||||
}
|
||||
app_metadata?: {
|
||||
provider: string
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,12 @@ import { PageContext } from 'pages/project/[ref]/auth/users'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import UserListItem from './UsersListItem'
|
||||
import UsersPagination from './UsersPagination'
|
||||
import { checkPermissions } from 'hooks'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
const UsersList = ({}) => {
|
||||
const PageState: any = useContext(PageContext)
|
||||
const canRemoveUser = checkPermissions(PermissionAction.TENANT_SQL_DELETE, 'auth.users')
|
||||
|
||||
return (
|
||||
<Loading active={PageState.usersLoading}>
|
||||
@@ -56,7 +59,9 @@ const UsersList = ({}) => {
|
||||
</Table.tr>
|
||||
)}
|
||||
{PageState.users.length > 0 &&
|
||||
PageState.users.map((x: any) => <UserListItem key={x.id} user={x} />)}
|
||||
PageState.users.map((x: any) => (
|
||||
<UserListItem key={x.id} user={x} canRemoveUser={canRemoveUser} />
|
||||
))}
|
||||
<Table.tr>
|
||||
<Table.td colSpan={7}>
|
||||
<UsersPagination />
|
||||
|
||||
@@ -6,26 +6,14 @@ import Table from 'components/to-be-cleaned/Table'
|
||||
import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock'
|
||||
import UserDropdown from './UserDropdown'
|
||||
import { getDateFromIsoString } from './Users.utils'
|
||||
import { User } from './Users.types'
|
||||
|
||||
interface Props {
|
||||
user: {
|
||||
id: string
|
||||
email: string
|
||||
phone: string
|
||||
email_confirmed_at: string
|
||||
phone_confirmed_at: string
|
||||
created_at: string
|
||||
last_sign_in_at: string
|
||||
raw_app_meta_data?: {
|
||||
provider: string
|
||||
}
|
||||
app_metadata?: {
|
||||
provider: string
|
||||
}
|
||||
}
|
||||
user: User
|
||||
canRemoveUser: boolean
|
||||
}
|
||||
|
||||
const UserListItem: FC<Props> = ({ user }) => {
|
||||
const UserListItem: FC<Props> = ({ user, canRemoveUser }) => {
|
||||
const isUserConfirmed = user.email_confirmed_at || user.phone_confirmed_at
|
||||
const createdAt = getDateFromIsoString(user.created_at)
|
||||
const lastSignedIn = getDateFromIsoString(user.last_sign_in_at)
|
||||
@@ -63,7 +51,7 @@ const UserListItem: FC<Props> = ({ user }) => {
|
||||
</SimpleCodeBlock>
|
||||
</Table.td>
|
||||
<Table.td className="text-right">
|
||||
<UserDropdown user={user} />
|
||||
<UserDropdown user={user} canRemoveUser={canRemoveUser} />
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
|
||||
@@ -88,21 +88,17 @@ const GeneralSettings = observer(() => {
|
||||
return (
|
||||
<FormPanel
|
||||
footer={
|
||||
<div
|
||||
className={`flex py-4 px-8 ${
|
||||
canUpdateOrganization ? 'justify-end' : 'justify-between'
|
||||
}`}
|
||||
>
|
||||
{!canUpdateOrganization && (
|
||||
<p className="text-sm text-scale-1000">
|
||||
You need additional permissions to manage this organization's settings
|
||||
</p>
|
||||
)}
|
||||
<div className="flex py-4 px-8">
|
||||
<FormActions
|
||||
form={formId}
|
||||
isSubmitting={isSubmitting}
|
||||
hasChanges={hasChanges}
|
||||
handleReset={handleReset}
|
||||
helper={
|
||||
!canUpdateOrganization
|
||||
? "You need additional permissions to manage this organization's settings"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -59,21 +59,17 @@ const General: FC<Props> = ({}) => {
|
||||
<FormPanel
|
||||
disabled={!canUpdateProject}
|
||||
footer={
|
||||
<div
|
||||
className={`flex py-4 px-8 ${
|
||||
canUpdateProject ? 'justify-end' : 'justify-between'
|
||||
}`}
|
||||
>
|
||||
{!canUpdateProject && (
|
||||
<p className="text-sm text-scale-1000">
|
||||
You need additional permissions to manage this project's settings
|
||||
</p>
|
||||
)}
|
||||
<div className="flex py-4 px-8">
|
||||
<FormActions
|
||||
form={formId}
|
||||
isSubmitting={isSubmitting}
|
||||
hasChanges={hasChanges}
|
||||
handleReset={handleReset}
|
||||
helper={
|
||||
!canUpdateProject
|
||||
? "You need additional permissions to manage this project's settings"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -19,7 +19,12 @@ export default [
|
||||
key: 'level',
|
||||
headerRenderer: () => <HeaderFormmater value={'Level'} />,
|
||||
name: 'level',
|
||||
formatter: (data: any) => <SeverityFormatter value={data.row.level} />,
|
||||
formatter: (data: any) => {
|
||||
if (data.row.event_type === 'uncaughtException') {
|
||||
return <SeverityFormatter value={data.row.event_type} uppercase={false} />
|
||||
}
|
||||
return <SeverityFormatter value={data.row.level} />
|
||||
},
|
||||
width: 24,
|
||||
resizable: true,
|
||||
},
|
||||
|
||||
@@ -1,99 +1,116 @@
|
||||
import { BarChart } from 'components/to-be-cleaned/Charts/ChartRenderer'
|
||||
import dayjs from 'dayjs'
|
||||
import { LogData } from '.'
|
||||
import { EventChartData, isUnixMicro, LogData, unixMicroToIsoTimestamp } from '.'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
interface Props {
|
||||
data?: LogData[]
|
||||
onBarClick: (timestamp: number) => void
|
||||
data?: EventChartData[]
|
||||
onBarClick: (isoTimestamp: string) => void
|
||||
}
|
||||
|
||||
type TimestampMap = { [timestamp: number]: number }
|
||||
const LogEventChart: React.FC<Props> = ({ data, onBarClick }) => {
|
||||
if (!data) return null
|
||||
const aggregated = useAggregated(data)
|
||||
// TODO: remove once endpoint returns iso timestamp directly
|
||||
const transformedData = useMemo(() => {
|
||||
return data.map((d) => {
|
||||
const iso = isUnixMicro(d.timestamp)
|
||||
? unixMicroToIsoTimestamp(d.timestamp)
|
||||
: dayjs(d.timestamp).toISOString()
|
||||
|
||||
return {
|
||||
...d,
|
||||
timestamp: iso,
|
||||
// needed for bar chart
|
||||
// TODO: remove once bar chart is refactored
|
||||
period_start: iso,
|
||||
}
|
||||
})
|
||||
}, [JSON.stringify(data)])
|
||||
|
||||
return (
|
||||
<BarChart
|
||||
minimalHeader
|
||||
chartSize="tiny"
|
||||
data={aggregated}
|
||||
data={transformedData}
|
||||
attribute="count"
|
||||
label="Events"
|
||||
onBarClick={(v?: { activePayload?: { payload: any }[] }) => {
|
||||
if (!v || !v?.activePayload?.[0]?.payload) return
|
||||
const timestamp = v.activePayload[0].payload.timestamp
|
||||
const unixOrIsoTimestamp = v.activePayload[0].payload.timestamp
|
||||
const isoTimestamp = isUnixMicro(unixOrIsoTimestamp)
|
||||
? unixMicroToIsoTimestamp(unixOrIsoTimestamp)
|
||||
: unixOrIsoTimestamp
|
||||
// 60s before
|
||||
onBarClick((Number(timestamp) + 60) * 1000 * 1000)
|
||||
onBarClick(isoTimestamp)
|
||||
}}
|
||||
customDateFormat="MMM D, HH:mm"
|
||||
customDateFormat="MMM D, HH:mm:s"
|
||||
noDataMessage={''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const useAggregated = (data: LogData[]) => {
|
||||
const truncateToMinute = (micro: number) => Math.floor(micro / 1000 / 1000 / 60) * 60
|
||||
const truncateToHour = (micro: number) => Math.floor(micro / 1000 / 1000 / 60 / 60) * 60 * 60
|
||||
const getDiffMinute = (currentTimestamp: number, olderTimestamp: number) =>
|
||||
Math.round((currentTimestamp - olderTimestamp) / 1000 / 60)
|
||||
const getDiffHour = (currentTimestamp: number, olderTimestamp: number) =>
|
||||
Math.round((currentTimestamp - olderTimestamp) / 1000 / 60 / 60)
|
||||
const diffMultiplierMinute = (v: number) => v * 60
|
||||
const diffMultiplierHour = (v: number) => v * 60 * 60
|
||||
return useMemo(() => {
|
||||
const oldest = data[data.length - 1]
|
||||
if (!oldest) return
|
||||
const latest = data[0]
|
||||
const oldestDayjs = dayjs(oldest.timestamp / 1000)
|
||||
const latestDayjs = dayjs(latest.timestamp / 1000)
|
||||
let truncFunc = truncateToMinute
|
||||
let getDiff = getDiffMinute
|
||||
let diffMultiplier = diffMultiplierMinute
|
||||
if (Math.abs(oldestDayjs.diff(latestDayjs, 'day', true)) > 0.25) {
|
||||
truncFunc = truncateToHour
|
||||
getDiff = getDiffHour
|
||||
diffMultiplier = diffMultiplierHour
|
||||
}
|
||||
// const useAggregated = (data: LogData[]) => {
|
||||
// const truncateToMinute = (micro: number) => Math.floor(micro / 1000 / 1000 / 60) * 60
|
||||
// const truncateToHour = (micro: number) => Math.floor(micro / 1000 / 1000 / 60 / 60) * 60 * 60
|
||||
// const getDiffMinute = (currentTimestamp: number, olderTimestamp: number) =>
|
||||
// Math.round((currentTimestamp - olderTimestamp) / 1000 / 60)
|
||||
// const getDiffHour = (currentTimestamp: number, olderTimestamp: number) =>
|
||||
// Math.round((currentTimestamp - olderTimestamp) / 1000 / 60 / 60)
|
||||
// const diffMultiplierMinute = (v: number) => v * 60
|
||||
// const diffMultiplierHour = (v: number) => v * 60 * 60
|
||||
// return useMemo(() => {
|
||||
// const oldest = data[data.length - 1]
|
||||
// if (!oldest) return
|
||||
// const latest = data[0]
|
||||
// const oldestDayjs = dayjs(oldest.timestamp / 1000)
|
||||
// const latestDayjs = dayjs(latest.timestamp / 1000)
|
||||
// let truncFunc = truncateToMinute
|
||||
// let getDiff = getDiffMinute
|
||||
// let diffMultiplier = diffMultiplierMinute
|
||||
// if (Math.abs(oldestDayjs.diff(latestDayjs, 'day', true)) > 0.25) {
|
||||
// truncFunc = truncateToHour
|
||||
// getDiff = getDiffHour
|
||||
// diffMultiplier = diffMultiplierHour
|
||||
// }
|
||||
|
||||
const countMap = data
|
||||
.map((d) => {
|
||||
// truncate to per-minute
|
||||
return { ...d, timestamp: truncateToMinute(d.timestamp) }
|
||||
})
|
||||
.reduce((acc, d) => {
|
||||
if (acc[d.timestamp]) {
|
||||
acc[d.timestamp] = acc[d.timestamp] + 1
|
||||
} else {
|
||||
acc[d.timestamp] = 1
|
||||
}
|
||||
return acc
|
||||
}, {} as TimestampMap)
|
||||
// const countMap = data
|
||||
// .map((d) => {
|
||||
// // truncate to per-minute
|
||||
// return { ...d, timestamp: truncateToMinute(d.timestamp) }
|
||||
// })
|
||||
// .reduce((acc, d) => {
|
||||
// if (acc[d.timestamp]) {
|
||||
// acc[d.timestamp] = acc[d.timestamp] + 1
|
||||
// } else {
|
||||
// acc[d.timestamp] = 1
|
||||
// }
|
||||
// return acc
|
||||
// }, {} as TimestampMap)
|
||||
|
||||
// Add in additional data points for empty minutes
|
||||
const oldestEvent = data[data.length - 1]
|
||||
if (!oldestEvent) return []
|
||||
const currentTimestamp = new Date().getTime()
|
||||
const oldestTimestampMicro = oldestEvent.timestamp
|
||||
const latestTimestamp = truncFunc(data[0]['timestamp'])
|
||||
const diff = getDiff(currentTimestamp, oldestTimestampMicro / 1000)
|
||||
for (const toAdd of Array.from(Array(diff).keys())) {
|
||||
const tsToCheck = truncFunc(oldestTimestampMicro) + diffMultiplier(toAdd)
|
||||
if (!(tsToCheck in countMap) && tsToCheck <= latestTimestamp) {
|
||||
countMap[tsToCheck] = 0
|
||||
}
|
||||
}
|
||||
// // Add in additional data points for empty minutes
|
||||
// const oldestEvent = data[data.length - 1]
|
||||
// if (!oldestEvent) return []
|
||||
// const currentTimestamp = new Date().getTime()
|
||||
// const oldestTimestampMicro = oldestEvent.timestamp
|
||||
// const latestTimestamp = truncFunc(data[0]['timestamp'])
|
||||
// const diff = getDiff(currentTimestamp, oldestTimestampMicro / 1000)
|
||||
// for (const toAdd of Array.from(Array(diff).keys())) {
|
||||
// const tsToCheck = truncFunc(oldestTimestampMicro) + diffMultiplier(toAdd)
|
||||
// if (!(tsToCheck in countMap) && tsToCheck <= latestTimestamp) {
|
||||
// countMap[tsToCheck] = 0
|
||||
// }
|
||||
// }
|
||||
|
||||
let aggregated = []
|
||||
for (const [key, value] of Object.entries(countMap)) {
|
||||
const v: number = Number(key)
|
||||
aggregated.push({
|
||||
timestamp: key,
|
||||
timestampMicro: v * 1000 * 1000,
|
||||
count: value,
|
||||
})
|
||||
}
|
||||
return aggregated.sort((a, b) => a.timestampMicro - b.timestampMicro)
|
||||
}, [JSON.stringify(data)])
|
||||
}
|
||||
// let aggregated = []
|
||||
// for (const [key, value] of Object.entries(countMap)) {
|
||||
// const v: number = Number(key)
|
||||
// aggregated.push({
|
||||
// timestamp: key,
|
||||
// timestampMicro: v * 1000 * 1000,
|
||||
// count: value,
|
||||
// })
|
||||
// }
|
||||
// return aggregated.sort((a, b) => a.timestampMicro - b.timestampMicro)
|
||||
// }, [JSON.stringify(data)])
|
||||
// }
|
||||
export default LogEventChart
|
||||
@@ -1,20 +1,19 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { useEffect, useState, useMemo } from 'react'
|
||||
import { Alert, Button, IconEye, IconEyeOff, Input } from '@supabase/ui'
|
||||
import DataGrid from '@supabase/react-data-grid'
|
||||
|
||||
import LogSelection, { LogSelectionProps } from './LogSelection'
|
||||
import { LogData, QueryType } from './Logs.types'
|
||||
import { SeverityFormatter, ResponseCodeFormatter, HeaderFormmater } from './LogsFormatters'
|
||||
import { isDefaultLogPreviewFormat } from './Logs.utils'
|
||||
// column renders
|
||||
import CSVButton from 'components/ui/CSVButton'
|
||||
import DatabaseApiColumnRender from './LogColumnRenderers/DatabaseApiColumnRender'
|
||||
import DatabasePostgresColumnRender from './LogColumnRenderers/DatabasePostgresColumnRender'
|
||||
import CSVButton from 'components/ui/CSVButton'
|
||||
import DefaultPreviewColumnRenderer from './LogColumnRenderers/DefaultPreviewColumnRenderer'
|
||||
import { LogQueryError } from '.'
|
||||
import ResourcesExceededErrorRenderer from './LogsErrorRenderers/ResourcesExceededErrorRenderer'
|
||||
import DefaultErrorRenderer from './LogsErrorRenderers/DefaultErrorRenderer'
|
||||
import FunctionsLogsColumnRender from './LogColumnRenderers/FunctionsLogsColumnRender'
|
||||
import FunctionsEdgeColumnRender from './LogColumnRenderers/FunctionsEdgeColumnRender'
|
||||
|
||||
interface Props {
|
||||
data?: Array<LogData | Object>
|
||||
@@ -100,88 +99,10 @@ const LogTable = ({
|
||||
break
|
||||
|
||||
case 'fn_edge':
|
||||
columns = [
|
||||
{
|
||||
key: 'timestamp',
|
||||
headerRenderer: () => (
|
||||
<div className="flex w-full justify-end h-full">
|
||||
<HeaderFormmater value={'timestamp'} />
|
||||
</div>
|
||||
),
|
||||
name: 'timestamp',
|
||||
formatter: (data: any) => (
|
||||
<span className="flex w-full h-full items-center gap-1">
|
||||
<span className="text-xs">
|
||||
{dayjs(data?.row?.timestamp / 1000).format('DD MMM')}
|
||||
</span>
|
||||
<span className="text-xs">
|
||||
{dayjs(data?.row?.timestamp / 1000).format('HH:mm:ss')}
|
||||
</span>
|
||||
</span>
|
||||
),
|
||||
width: 128,
|
||||
},
|
||||
{
|
||||
key: 'status_code',
|
||||
headerRenderer: () => <HeaderFormmater value={'Status'} />,
|
||||
name: 'status_code',
|
||||
formatter: (data: any) => (
|
||||
<ResponseCodeFormatter row={data} value={data.row.status_code} />
|
||||
),
|
||||
width: 0,
|
||||
resizable: true,
|
||||
},
|
||||
{
|
||||
key: 'method',
|
||||
headerRenderer: () => <HeaderFormmater value={'method'} />,
|
||||
width: 0,
|
||||
resizable: true,
|
||||
},
|
||||
{
|
||||
key: 'id',
|
||||
headerRenderer: () => <HeaderFormmater value={'id'} />,
|
||||
name: 'id',
|
||||
resizable: true,
|
||||
},
|
||||
]
|
||||
columns = FunctionsEdgeColumnRender
|
||||
break
|
||||
case 'functions':
|
||||
columns = [
|
||||
{
|
||||
key: 'timestamp',
|
||||
headerRenderer: () => (
|
||||
<div className="flex w-full justify-end h-full">
|
||||
<HeaderFormmater value={'timestamp'} />
|
||||
</div>
|
||||
),
|
||||
name: 'timestamp',
|
||||
formatter: (data: any) => (
|
||||
<span className="flex w-full h-full items-center gap-1">
|
||||
<span className="text-xs !text-scale-1100">
|
||||
{dayjs(data?.row?.timestamp / 1000).format('DD MMM')}
|
||||
</span>
|
||||
<span className="text-xs !text-scale-1100">
|
||||
{dayjs(data?.row?.timestamp / 1000).format('HH:mm:ss')}
|
||||
</span>
|
||||
</span>
|
||||
),
|
||||
width: 128,
|
||||
resizable: true,
|
||||
},
|
||||
{
|
||||
key: 'level',
|
||||
headerRenderer: () => <HeaderFormmater value={'Level'} />,
|
||||
name: 'level',
|
||||
formatter: (data: any) => <SeverityFormatter value={data.row.level} />,
|
||||
width: 24,
|
||||
resizable: true,
|
||||
},
|
||||
{
|
||||
key: 'event_message',
|
||||
headerRenderer: () => <HeaderFormmater value={'Event message'} />,
|
||||
resizable: true,
|
||||
},
|
||||
]
|
||||
columns = FunctionsLogsColumnRender
|
||||
break
|
||||
|
||||
default:
|
||||
|
||||
@@ -24,8 +24,6 @@ const DatePickers: React.FC<Props> = ({ to, from, onChange, helpers }) => {
|
||||
const selectedHelper = helpers.find((helper) => {
|
||||
if (to === helper.calcTo() && from === helper.calcFrom()) {
|
||||
return true
|
||||
} else if (helper.text === helperValue) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
@@ -33,8 +31,10 @@ const DatePickers: React.FC<Props> = ({ to, from, onChange, helpers }) => {
|
||||
useEffect(() => {
|
||||
if (selectedHelper && helperValue !== selectedHelper.text) {
|
||||
setHelperValue(selectedHelper.text)
|
||||
} else if (!selectedHelper && (to || from)) {
|
||||
setHelperValue('')
|
||||
}
|
||||
}, [selectedHelper])
|
||||
}, [selectedHelper, to, from])
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
@@ -66,6 +66,7 @@ const DatePickers: React.FC<Props> = ({ to, from, onChange, helpers }) => {
|
||||
<DatePicker
|
||||
triggerButtonClassName="rounded-l-none"
|
||||
triggerButtonType={selectedHelper ? 'default' : 'secondary'}
|
||||
triggerButtonTitle="Custom"
|
||||
onChange={(value) => {
|
||||
setHelperValue('')
|
||||
if (onChange) onChange(value)
|
||||
|
||||
@@ -254,7 +254,7 @@ export const LOGS_TABLES = {
|
||||
functions: LogsTableName.FUNCTIONS,
|
||||
fn_edge: LogsTableName.FN_EDGE,
|
||||
auth: LogsTableName.AUTH,
|
||||
realtime: LogsTableName.REALTIME
|
||||
realtime: LogsTableName.REALTIME,
|
||||
}
|
||||
|
||||
export const LOGS_SOURCE_DESCRIPTION = {
|
||||
@@ -266,8 +266,6 @@ export const LOGS_SOURCE_DESCRIPTION = {
|
||||
[LogsTableName.REALTIME]: 'Realtime server for Postgres logical replication broadcasting',
|
||||
}
|
||||
|
||||
export const genCountQuery = (table: string): string => `SELECT count(*) as count FROM ${table}`
|
||||
|
||||
export const genQueryParams = (params: { [k: string]: string }) => {
|
||||
// remove keys which are empty strings, null, or undefined
|
||||
for (const k in params) {
|
||||
|
||||
@@ -7,9 +7,13 @@ interface Metadata {
|
||||
export type DatePickerToFrom = { to: string | null; from: string | null }
|
||||
|
||||
export type LogSearchCallback = (
|
||||
event:
|
||||
| 'search-input-change'
|
||||
| 'event-chart-bar-click'
|
||||
| 'datepicker-change',
|
||||
filters: {
|
||||
query: string
|
||||
} & DatePickerToFrom
|
||||
query?: string
|
||||
} & Partial<DatePickerToFrom>
|
||||
) => void
|
||||
|
||||
export interface LogsWarning {
|
||||
@@ -48,6 +52,11 @@ export interface CountData {
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface EventChartData {
|
||||
count: number
|
||||
timestamp: string | number
|
||||
}
|
||||
|
||||
type LFResponse<T> = {
|
||||
result: T[]
|
||||
error?: {
|
||||
@@ -63,9 +72,10 @@ type LFResponse<T> = {
|
||||
}
|
||||
type ApiError = string
|
||||
|
||||
export type LogQueryError = Omit<LFResponse<unknown>, "result"> | ApiError
|
||||
export type LogQueryError = Omit<LFResponse<unknown>, 'result'> | ApiError
|
||||
|
||||
export type Count = LFResponse<CountData>
|
||||
export type EventChart = LFResponse<EventChartData>
|
||||
|
||||
export type Logs = LFResponse<LogData>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Filters, LogData, LogsTableName, SQL_FILTER_TEMPLATES } from '.'
|
||||
import dayjs from 'dayjs'
|
||||
import { Filters, LogData, LogsEndpointParams, LogsTableName, SQL_FILTER_TEMPLATES } from '.'
|
||||
import dayjs, { Dayjs } from 'dayjs'
|
||||
import { get } from 'lodash'
|
||||
|
||||
/**
|
||||
@@ -164,4 +164,74 @@ export const genDefaultQuery = (table: LogsTableName, filters: Filters) => {
|
||||
/**
|
||||
* SQL query to retrieve only one log
|
||||
*/
|
||||
export const genSingleLogQuery = (table: LogsTableName, id: string) => `select id, timestamp, event_message, metadata from ${table} where id = '${id}' limit 1`
|
||||
export const genSingleLogQuery = (table: LogsTableName, id: string) =>
|
||||
`select id, timestamp, event_message, metadata from ${table} where id = '${id}' limit 1`
|
||||
|
||||
export const genCountQuery = (table: string): string => `SELECT count(*) as count FROM ${table}`
|
||||
|
||||
/** calculates how much the chart start datetime should be offset given the current datetime filter params */
|
||||
export const calcChartStart = (params: Partial<LogsEndpointParams>): [Dayjs, string] => {
|
||||
const ite = params.iso_timestamp_end ? dayjs(params.iso_timestamp_end) : dayjs()
|
||||
const its = params.iso_timestamp_start ? dayjs(params.iso_timestamp_start) : dayjs()
|
||||
let trunc = 'minute'
|
||||
let extendValue = 60 * 6
|
||||
const minuteDiff = ite.diff(its, 'minute')
|
||||
const hourDiff = ite.diff(its, 'hour')
|
||||
if (minuteDiff > (60 * 12)) {
|
||||
trunc = 'hour'
|
||||
extendValue = 24 * 3
|
||||
} else if (hourDiff > 24 * 5) {
|
||||
trunc = 'day'
|
||||
extendValue = 7
|
||||
}
|
||||
return [its.add(-extendValue, trunc), trunc]
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* generates log event chart query
|
||||
*/
|
||||
export const genChartQuery = (
|
||||
table: LogsTableName,
|
||||
params: LogsEndpointParams,
|
||||
filters: Filters
|
||||
) => {
|
||||
const [startOffset, trunc] = calcChartStart(params)
|
||||
const where = _genWhereStatement(table, filters)
|
||||
return `
|
||||
SELECT
|
||||
timestamp_trunc(t.timestamp, ${trunc}) as timestamp,
|
||||
count(timestamp) as count
|
||||
FROM
|
||||
${table} t
|
||||
${where ? where + ` and t.timestamp > '${startOffset.toISOString()}'` : ""}
|
||||
GROUP BY
|
||||
timestamp
|
||||
ORDER BY
|
||||
timestamp ASC
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
type TsPair = [string | '', string | '']
|
||||
export const ensureNoTimestampConflict = ([initialStart, initialEnd]: TsPair, [nextStart, nextEnd]: TsPair): TsPair => {
|
||||
if (initialStart && initialEnd && nextEnd && !nextStart) {
|
||||
const resolvedDiff = dayjs(nextEnd).diff(dayjs(initialStart))
|
||||
let start = dayjs(initialStart)
|
||||
|
||||
if (resolvedDiff <= 0) {
|
||||
// start ts is definitely before end ts
|
||||
const currDiff = Math.abs(
|
||||
dayjs(initialEnd).diff(start, 'minute')
|
||||
)
|
||||
// shift start ts backwards by the current ts difference
|
||||
start = dayjs(nextEnd).subtract(currDiff, 'minute')
|
||||
}
|
||||
return [start.toISOString(), nextEnd]
|
||||
} else if (!nextEnd && nextStart) {
|
||||
return [nextStart, initialEnd]
|
||||
} else {
|
||||
return [nextStart, nextEnd]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -92,7 +92,13 @@ export const ResponseCodeFormatter = ({ value }: any) => {
|
||||
* for http response codes
|
||||
*/
|
||||
|
||||
export const SeverityFormatter = ({ value }: { value: string }) => {
|
||||
export const SeverityFormatter = ({
|
||||
value,
|
||||
uppercase = true,
|
||||
}: {
|
||||
value: string
|
||||
uppercase?: boolean
|
||||
}) => {
|
||||
if (!value) {
|
||||
return (
|
||||
<div>
|
||||
@@ -101,9 +107,11 @@ export const SeverityFormatter = ({ value }: { value: string }) => {
|
||||
)
|
||||
}
|
||||
|
||||
value = value.toUpperCase()
|
||||
const uppercasedValue = value.toUpperCase()
|
||||
const text = uppercase ? uppercasedValue : value
|
||||
|
||||
switch (value) {
|
||||
switch (uppercasedValue) {
|
||||
case 'UNCAUGHTEXCEPTION':
|
||||
case 'PANIC':
|
||||
case 'FATAL':
|
||||
case 'ERROR':
|
||||
@@ -112,7 +120,7 @@ export const SeverityFormatter = ({ value }: { value: string }) => {
|
||||
<div className=" p-0.5 rounded !text-red-900">
|
||||
<IconAlertCircle size={14} strokeWidth={2} />
|
||||
</div>
|
||||
<span className="!text-red-900 !block titlecase">{value}</span>
|
||||
<span className="!text-red-900 !block titlecase">{text}</span>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
@@ -125,7 +133,7 @@ export const SeverityFormatter = ({ value }: { value: string }) => {
|
||||
<div className=" p-0.5 rounded !text-blue-900">
|
||||
<IconAlertCircle size={14} strokeWidth={2} />
|
||||
</div>
|
||||
<span className="!text-blue-900 !block titlecase">{value}</span>
|
||||
<span className="!text-blue-900 !block titlecase">{text}</span>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
@@ -136,7 +144,7 @@ export const SeverityFormatter = ({ value }: { value: string }) => {
|
||||
<div className=" p-0.5 rounded !text-blue-900">
|
||||
<IconInfo size={14} strokeWidth={2} />
|
||||
</div>
|
||||
<span className="!text-blue-900 !block titlecase">{value}</span>
|
||||
<span className="!text-blue-900 !block titlecase">{text}</span>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
@@ -147,7 +155,7 @@ export const SeverityFormatter = ({ value }: { value: string }) => {
|
||||
<div className=" p-0.5 rounded !text-amber-900">
|
||||
<IconAlertCircle size={14} strokeWidth={2} />
|
||||
</div>
|
||||
<span className="!text-amber-900 !block titlecase">{value}</span>
|
||||
<span className="!text-amber-900 !block titlecase">{text}</span>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
@@ -157,7 +165,7 @@ export const SeverityFormatter = ({ value }: { value: string }) => {
|
||||
return (
|
||||
<div className="flex items-center h-full">
|
||||
<div className="relative rounded px-2 py-1 text-center h-6 flex justify-center items-center bg-scale-300">
|
||||
<label className="block font-mono text-sm text-scale-900">{value}</label>
|
||||
<label className="block font-mono text-sm text-scale-900">{text}</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
QueryType,
|
||||
LogEventChart,
|
||||
Filters,
|
||||
unixMicroToIsoTimestamp,
|
||||
ensureNoTimestampConflict,
|
||||
} from 'components/interfaces/Settings/Logs'
|
||||
import useLogsPreview from 'hooks/analytics/useLogsPreview'
|
||||
import PreviewFilterPanel from 'components/interfaces/Settings/Logs/PreviewFilterPanel'
|
||||
@@ -52,7 +52,7 @@ export const LogsPreviewer: React.FC<Props> = ({
|
||||
const table = !tableName ? LOGS_TABLES[queryType] : tableName
|
||||
|
||||
const [
|
||||
{ error, logData, params, newCount, filters, isLoading },
|
||||
{ error, logData, params, newCount, filters, isLoading, eventChartData },
|
||||
{ loadOlder, setFilters, refresh, setParams },
|
||||
] = useLogsPreview(projectRef as string, table, filterOverride)
|
||||
|
||||
@@ -83,24 +83,48 @@ export const LogsPreviewer: React.FC<Props> = ({
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleSearch: LogSearchCallback = async ({ query = '', to, from }) => {
|
||||
setParams((prev) => ({
|
||||
...prev,
|
||||
iso_timestamp_start: from || prev.iso_timestamp_start || '',
|
||||
iso_timestamp_end: to || prev.iso_timestamp_end || '',
|
||||
}))
|
||||
setFilters((prev) => ({ ...prev, search_query: query }))
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
s: query || '',
|
||||
its: from || its || '',
|
||||
ite: to || ite || '',
|
||||
},
|
||||
})
|
||||
const handleSearch: LogSearchCallback = async (event, { query, to, from }) => {
|
||||
if (event === 'search-input-change') {
|
||||
setFilters((prev) => ({ ...prev, search_query: query }))
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...router.query, s: query },
|
||||
})
|
||||
} else if (event === 'event-chart-bar-click') {
|
||||
const [nextStart, nextEnd] = ensureNoTimestampConflict(
|
||||
[params.iso_timestamp_start || '', params.iso_timestamp_end || ''],
|
||||
[from || '', to || '']
|
||||
)
|
||||
setParams((prev) => ({
|
||||
...prev,
|
||||
iso_timestamp_start: nextStart,
|
||||
iso_timestamp_end: nextEnd,
|
||||
}))
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
its: nextStart,
|
||||
ite: nextEnd,
|
||||
},
|
||||
})
|
||||
} else if (event === 'datepicker-change') {
|
||||
setParams((prev) => ({
|
||||
...prev,
|
||||
iso_timestamp_start: from || '',
|
||||
iso_timestamp_end: to || '',
|
||||
}))
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
its: from || '',
|
||||
ite: to || '',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col flex-grow">
|
||||
<PreviewFilterPanel
|
||||
@@ -143,10 +167,13 @@ export const LogsPreviewer: React.FC<Props> = ({
|
||||
<div className={condensedLayout ? 'px-4' : ''}>
|
||||
{showChart && (
|
||||
<LogEventChart
|
||||
data={!isLoading ? logData : undefined}
|
||||
onBarClick={(timestampMicro) => {
|
||||
const to = unixMicroToIsoTimestamp(timestampMicro)
|
||||
handleSearch({ query: filters.search_query as string, to, from: null })
|
||||
data={!isLoading && eventChartData ? eventChartData : undefined}
|
||||
onBarClick={(isoTimestamp) => {
|
||||
handleSearch('event-chart-bar-click', {
|
||||
query: filters.search_query as string,
|
||||
to: isoTimestamp as string,
|
||||
from: null,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -97,8 +97,10 @@ const PreviewFilterPanel: FC<Props> = ({
|
||||
Refresh
|
||||
</Button>
|
||||
)
|
||||
const handleSearch = (partial: Partial<Parameters<LogSearchCallback>[0]>) =>
|
||||
onSearch({ query: search, to: partial?.to || null, from: partial?.from || null })
|
||||
const handleDatepickerChange = ({ to, from }: Partial<Parameters<LogSearchCallback>[1]>) => {
|
||||
onSearch('datepicker-change', { to, from })
|
||||
}
|
||||
const handleInputSearch = (query: string) => onSearch('search-input-change', { query })
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -110,7 +112,7 @@ const PreviewFilterPanel: FC<Props> = ({
|
||||
onSubmit={(e) => {
|
||||
// prevent redirection
|
||||
e.preventDefault()
|
||||
handleSearch({})
|
||||
handleInputSearch(search)
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
@@ -120,7 +122,7 @@ const PreviewFilterPanel: FC<Props> = ({
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onBlur={(e: React.FocusEvent<HTMLInputElement>) => {
|
||||
setSearch(e.target.value)
|
||||
handleSearch({ query: e.target.value })
|
||||
handleInputSearch(e.target.value)
|
||||
}}
|
||||
icon={
|
||||
<div className="text-scale-900">
|
||||
@@ -131,7 +133,7 @@ const PreviewFilterPanel: FC<Props> = ({
|
||||
actions={
|
||||
hasEdits && (
|
||||
<button
|
||||
onClick={() => handleSearch({})}
|
||||
onClick={() => handleInputSearch(search)}
|
||||
className="text-scale-1100 hover:text-scale-1200 mx-2"
|
||||
>
|
||||
{'↲'}
|
||||
@@ -142,7 +144,7 @@ const PreviewFilterPanel: FC<Props> = ({
|
||||
</form>
|
||||
|
||||
<DatePickers
|
||||
onChange={handleSearch}
|
||||
onChange={handleDatepickerChange}
|
||||
to={defaultToValue}
|
||||
from={defaultFromValue}
|
||||
helpers={PREVIEWER_DATEPICKER_HELPERS}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Dictionary, parseSupaTable, SupabaseGrid, SupabaseGridRef } from 'components/grid'
|
||||
import { PostgresColumn, PostgresTable } from '@supabase/postgres-meta'
|
||||
import { find, isUndefined } from 'lodash'
|
||||
import { FC, useRef } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FC, useRef } from 'react'
|
||||
|
||||
import { find, isUndefined } from 'lodash'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { SchemaView } from 'components/layouts/TableEditorLayout/TableEditorLayout.types'
|
||||
import { PostgresColumn, PostgresTable } from '@supabase/postgres-meta'
|
||||
|
||||
import { checkPermissions, useStore } from 'hooks'
|
||||
import GridHeaderActions from './GridHeaderActions'
|
||||
import NotFoundState from './NotFoundState'
|
||||
import SidePanelEditor from './SidePanelEditor'
|
||||
import { SchemaView } from 'components/layouts/TableEditorLayout/TableEditorLayout.types'
|
||||
import { Dictionary, parseSupaTable, SupabaseGrid, SupabaseGridRef } from 'components/grid'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
|
||||
interface Props {
|
||||
/** Theme for the editor */
|
||||
@@ -59,17 +60,28 @@ const TableGridEditor: FC<Props> = ({
|
||||
const gridRef = useRef<SupabaseGridRef>(null)
|
||||
const projectRef = ui.selectedProject?.ref
|
||||
|
||||
if (isUndefined(selectedTable)) {
|
||||
return <NotFoundState id={Number(router.query.id)} />
|
||||
}
|
||||
// Permissions scaffolding
|
||||
const canRead = false
|
||||
const canInsert = false
|
||||
const canUpdate = false
|
||||
const canDelete = false
|
||||
|
||||
const tableId = selectedTable?.id
|
||||
// [Joshen] When we get to this, double check again what are the actions
|
||||
// const canUpdate = checkPermissions(PermissionAction.TENANT_SQL_UPDATE, String(tableId))
|
||||
// const canAdminWrite = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, String(tableId))
|
||||
// const canInsert = checkPermissions(PermissionAction.CREATE, String(tableId))
|
||||
// const canEdit = canAdminWrite && canInsert && canUpdate
|
||||
|
||||
if (isUndefined(selectedTable)) {
|
||||
return <NotFoundState id={Number(router.query.id)} />
|
||||
}
|
||||
|
||||
// if (!canRead) {
|
||||
// return <NoPermission isFullPage resourceText={`access the table "${selectedTable.name}"`} />
|
||||
// }
|
||||
|
||||
const tableId = selectedTable?.id
|
||||
|
||||
// @ts-ignore
|
||||
const schema = meta.schemas.list().find((schema) => schema.name === selectedSchema)
|
||||
const isViewSelected = Object.keys(selectedTable).length === 2
|
||||
|
||||
@@ -13,7 +13,7 @@ export const generateAuthMenu = (ref: string): ProductMenuGroup[] => {
|
||||
{
|
||||
name: 'Logs',
|
||||
key: 'auth-logs',
|
||||
url: `/project/${ref}/auth/auth-logs`,
|
||||
url: `/project/${ref}/auth/logs`,
|
||||
items: [],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface DatePickerProps {
|
||||
from?: string
|
||||
triggerButtonType?: ButtonProps['type']
|
||||
triggerButtonClassName?: string
|
||||
triggerButtonTitle?: string
|
||||
renderFooter?: (args: DatePickerToFrom) => React.ReactNode | void
|
||||
}
|
||||
|
||||
@@ -36,6 +37,7 @@ function _DatePicker({
|
||||
onChange,
|
||||
triggerButtonType = 'default',
|
||||
triggerButtonClassName = '',
|
||||
triggerButtonTitle,
|
||||
renderFooter = () => null,
|
||||
}: DatePickerProps) {
|
||||
const [open, setOpen] = useState<boolean>(false)
|
||||
@@ -220,6 +222,7 @@ function _DatePicker({
|
||||
}
|
||||
>
|
||||
<Button
|
||||
title={triggerButtonTitle}
|
||||
type={triggerButtonType}
|
||||
as="span"
|
||||
icon={<IconCalendar />}
|
||||
|
||||
@@ -1,57 +1,43 @@
|
||||
import { Button } from '@supabase/ui'
|
||||
import { ButtonHTMLAttributes } from 'react'
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Handling a submitting/loading state
|
||||
*/
|
||||
isSubmitting?: boolean
|
||||
/**
|
||||
* Handling a reset/cancel of the form
|
||||
*/
|
||||
handleReset: () => void
|
||||
/**
|
||||
* Disables submit button if false
|
||||
*/
|
||||
hasChanges: boolean | undefined
|
||||
/**
|
||||
* Helper text to show alongside actions
|
||||
*/
|
||||
helper?: React.ReactNode
|
||||
form: React.HTMLProps<HTMLButtonElement>['form']
|
||||
hasChanges: boolean | undefined // Disables submit button if false
|
||||
handleReset: () => void // Handling a reset/cancel of the form
|
||||
helper?: React.ReactNode // Helper text to show alongside actions
|
||||
disabled?: boolean
|
||||
isSubmitting?: boolean
|
||||
}
|
||||
|
||||
const FormActions = ({
|
||||
isSubmitting,
|
||||
handleReset,
|
||||
hasChanges = undefined,
|
||||
helper,
|
||||
form,
|
||||
hasChanges = undefined,
|
||||
handleReset,
|
||||
helper,
|
||||
disabled = false,
|
||||
isSubmitting,
|
||||
}: Props) => {
|
||||
const isDisabled = disabled || (!hasChanges && hasChanges !== undefined)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
'flex items-center gap-2',
|
||||
'flex items-center gap-2 w-full',
|
||||
// justify actions to right if no helper text
|
||||
helper ? 'justify-between' : 'justify-end',
|
||||
].join(' ')}
|
||||
>
|
||||
{helper && <span className="text-scale-900 text-xs">{helper}</span>}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
disabled={!hasChanges && hasChanges !== undefined}
|
||||
type="default"
|
||||
htmlType="reset"
|
||||
onClick={() => handleReset()}
|
||||
>
|
||||
{helper && <span className="text-scale-900 text-sm">{helper}</span>}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button disabled={isDisabled} type="default" htmlType="reset" onClick={() => handleReset()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!hasChanges && hasChanges !== undefined}
|
||||
loading={isSubmitting}
|
||||
form={form}
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
form={form}
|
||||
disabled={isDisabled}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
|
||||
@@ -3,32 +3,47 @@ import { FC } from 'react'
|
||||
|
||||
interface Props {
|
||||
resourceText: string
|
||||
isFullPage?: boolean
|
||||
}
|
||||
|
||||
const NoPermission: FC<Props> = ({ resourceText }) => (
|
||||
<div
|
||||
className={[
|
||||
'block w-full rounded border border-opacity-20 py-4 px-6',
|
||||
'border-gray-600 bg-gray-100',
|
||||
'dark:border-gray-300 dark:bg-gray-400',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="flex space-x-3">
|
||||
<div className="mt-1">
|
||||
<IconAlertCircle size="large" />
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm">You need additional permissions to {resourceText}</p>
|
||||
<div>
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Contact your organization owner or adminstrator for assistance.
|
||||
</p>
|
||||
const NoPermission: FC<Props> = ({ resourceText, isFullPage = false }) => {
|
||||
const NoPermissionMessage = ({}) => (
|
||||
<div
|
||||
className={[
|
||||
'block w-full rounded border border-opacity-20 py-4 px-6',
|
||||
'border-gray-600 bg-gray-100',
|
||||
'dark:border-gray-300 dark:bg-gray-400',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="flex space-x-3">
|
||||
<div className="mt-1">
|
||||
<IconAlertCircle size="large" />
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm">You need additional permissions to {resourceText}</p>
|
||||
<div>
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Contact your organization owner or adminstrator for assistance.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
|
||||
if (isFullPage) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="w-[500px]">
|
||||
<NoPermissionMessage />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return <NoPermissionMessage />
|
||||
}
|
||||
}
|
||||
|
||||
export default NoPermission
|
||||
@@ -1,6 +1,9 @@
|
||||
import {
|
||||
Count,
|
||||
EventChart,
|
||||
EventChartData,
|
||||
Filters,
|
||||
genChartQuery,
|
||||
genCountQuery,
|
||||
genDefaultQuery,
|
||||
genQueryParams,
|
||||
@@ -27,6 +30,7 @@ interface Data {
|
||||
filters: Filters
|
||||
params: LogsEndpointParams
|
||||
oldestTimestamp?: string
|
||||
eventChartData: EventChartData[] | null
|
||||
}
|
||||
interface Handlers {
|
||||
loadOlder: () => void
|
||||
@@ -116,11 +120,40 @@ function useLogsPreview(
|
||||
})
|
||||
const newCount = countData?.result?.[0]?.count ?? 0
|
||||
|
||||
// chart data
|
||||
|
||||
const chartQuery = genChartQuery(table, params, filters)
|
||||
const chartUrl = () => {
|
||||
// cancel request if no sql provided
|
||||
if (!params.sql) {
|
||||
// return null to restrict unnecessary requests to api
|
||||
// https://swr.vercel.app/docs/conditional-fetching#conditional
|
||||
return null
|
||||
}
|
||||
|
||||
return `${API_URL}/projects/${projectRef}/analytics/endpoints/logs.all?${genQueryParams({
|
||||
iso_timestamp_end: params.iso_timestamp_end,
|
||||
project: params.project,
|
||||
sql: chartQuery,
|
||||
} as any)}`
|
||||
}
|
||||
|
||||
const { data: eventChartResponse, mutate: refreshEventChart } = useSWR<EventChart>(
|
||||
chartUrl,
|
||||
get,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 10000,
|
||||
refreshInterval: 0,
|
||||
}
|
||||
)
|
||||
|
||||
const refresh = async () => {
|
||||
const generatedSql = genDefaultQuery(table, filters)
|
||||
setParams((prev) => ({ ...prev, sql: generatedSql }))
|
||||
setLatestRefresh(new Date().toISOString())
|
||||
setSize(1)
|
||||
refreshEventChart()
|
||||
}
|
||||
|
||||
let error: null | string | object = swrError ? swrError.message : null
|
||||
@@ -155,6 +188,7 @@ function useLogsPreview(
|
||||
filters,
|
||||
params,
|
||||
oldestTimestamp: oldestTimestamp ? String(oldestTimestamp) : undefined,
|
||||
eventChartData: eventChartResponse?.result || null,
|
||||
},
|
||||
{
|
||||
setFilters: handleSetFilters,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRouter } from 'next/router'
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
|
||||
import { Member, NextPageWithLayout, Organization, Project, Role, User } from 'types'
|
||||
import { useOrganizationDetail, useOrganizationRoles, useStore, withAuth } from 'hooks'
|
||||
import { useFlag, useOrganizationDetail, useOrganizationRoles, useStore, withAuth } from 'hooks'
|
||||
import { AccountLayoutWithoutAuth } from 'components/layouts'
|
||||
import {
|
||||
GeneralSettings,
|
||||
@@ -21,7 +21,14 @@ const OrgSettingsLayout = withAuth(
|
||||
const router = useRouter()
|
||||
|
||||
const slug = ui.selectedOrganization?.slug || ''
|
||||
const { roles } = useOrganizationRoles(slug)
|
||||
|
||||
const { roles: allRoles } = useOrganizationRoles(slug)
|
||||
const enableBillingOnlyReadOnly = useFlag('enableBillingOnlyReadOnlyRoles')
|
||||
const roles = enableBillingOnlyReadOnly
|
||||
? allRoles
|
||||
: (allRoles ?? []).filter((role) =>
|
||||
['Owner', 'Administrator', 'Developer'].includes(role.name)
|
||||
)
|
||||
|
||||
// [Refactor] Eventually move away from useLocalObservable
|
||||
const PageState: any = useLocalObservable(() => ({
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useStore } from 'hooks'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { checkPermissions, useStore } from 'hooks'
|
||||
import { AuthLayout } from 'components/layouts'
|
||||
import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
|
||||
const LogsPage: NextPageWithLayout = () => {
|
||||
const { ui } = useStore()
|
||||
const project = ui.selectedProject
|
||||
|
||||
return (
|
||||
const canReadAuthLogs = checkPermissions(PermissionAction.ANALYTICS_READ, 'logflare')
|
||||
|
||||
return !canReadAuthLogs ? (
|
||||
<NoPermission isFullPage resourceText="access your project's authentication logs" />
|
||||
) : (
|
||||
<>{project && <LogsPreviewer condensedLayout projectRef={project!.ref} queryType="auth" />}</>
|
||||
)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import AutoSchemaForm from 'components/interfaces/Auth/AutoSchemaForm'
|
||||
import RedirectDomains from 'components/interfaces/Auth/RedirectDomains/RedirectDomains'
|
||||
import { AuthLayout } from 'components/layouts'
|
||||
import { FormsContainer } from 'components/ui/Forms'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
|
||||
const PageLayout: NextPageWithLayout = () => {
|
||||
const { ui, authConfig } = useStore()
|
||||
@@ -17,9 +18,11 @@ const PageLayout: NextPageWithLayout = () => {
|
||||
authConfig.load()
|
||||
}, [ui.selectedProjectRef])
|
||||
|
||||
// const canUpdate = checkPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
|
||||
const canReadAuthSettings = checkPermissions(PermissionAction.READ, 'custom_config_gotrue')
|
||||
|
||||
if (authConfig) {
|
||||
if (!canReadAuthSettings) {
|
||||
return <NoPermission isFullPage resourceText="access your project's authentication settings" />
|
||||
} else if (authConfig) {
|
||||
return (
|
||||
<FormsContainer>
|
||||
<AutoSchemaForm />
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { EmailTemplates, SmtpForm } from 'components/interfaces'
|
||||
import { AuthLayout } from 'components/layouts'
|
||||
import { FormsContainer } from 'components/ui/Forms'
|
||||
import { useStore } from 'hooks'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useEffect } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { useStore, checkPermissions } from 'hooks'
|
||||
import { AuthLayout } from 'components/layouts'
|
||||
import { EmailTemplates, SmtpForm } from 'components/interfaces'
|
||||
import { FormsContainer } from 'components/ui/Forms'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
|
||||
const PageLayout: NextPageWithLayout = () => {
|
||||
const { ui, authConfig } = useStore()
|
||||
|
||||
useEffect(() => {
|
||||
// temporary store loader
|
||||
authConfig.load()
|
||||
}, [ui.selectedProjectRef])
|
||||
|
||||
if (authConfig) {
|
||||
const canReadAuthSettings = checkPermissions(PermissionAction.READ, 'custom_config_gotrue')
|
||||
|
||||
if (!canReadAuthSettings) {
|
||||
return <NoPermission isFullPage resourceText="access your project's email settings" />
|
||||
} else if (authConfig) {
|
||||
return (
|
||||
<FormsContainer>
|
||||
<SmtpForm />
|
||||
|
||||
@@ -4,10 +4,12 @@ import { useRouter } from 'next/router'
|
||||
import { get } from 'lib/common/fetch'
|
||||
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useStore } from 'hooks'
|
||||
import { checkPermissions, useStore } from 'hooks'
|
||||
import { AuthLayout } from 'components/layouts'
|
||||
import { Users } from 'components/interfaces/Authentication'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
|
||||
export const PageContext = createContext(null)
|
||||
|
||||
@@ -84,7 +86,13 @@ const UsersPage: NextPageWithLayout = () => {
|
||||
PageState!.projectKpsVersion = project?.kpsVersion
|
||||
}, [project])
|
||||
|
||||
return <Users />
|
||||
const canReadUsers = checkPermissions(PermissionAction.TENANT_SQL_SELECT, 'auth.users')
|
||||
|
||||
return !canReadUsers ? (
|
||||
<NoPermission isFullPage resourceText="access your project's users" />
|
||||
) : (
|
||||
<Users />
|
||||
)
|
||||
}
|
||||
|
||||
UsersPage.getLayout = (page) => (
|
||||
|
||||
@@ -22,9 +22,7 @@ const PageLayout: NextPageWithLayout = () => {
|
||||
const { ui } = useStore()
|
||||
const project = ui.selectedProject
|
||||
|
||||
// const canCreateReport = checkPermissions(PermissionAction.CREATE, 'user_content', {
|
||||
// resource: { type: 'report' },
|
||||
// })
|
||||
// const canCreateReport = checkPermissions(PermissionAction.CR
|
||||
const contentStore = useProjectContentStore(ref)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -62,6 +62,47 @@ test('can display standard preview table columns', async () => {
|
||||
await expect(screen.findByText(/12345/)).rejects.toThrow()
|
||||
await expect(screen.findByText(fakeMicroTimestamp)).rejects.toThrow()
|
||||
})
|
||||
|
||||
test.each([
|
||||
{
|
||||
queryType: 'functions',
|
||||
data: [
|
||||
{
|
||||
event_message: 'This is a error log\n',
|
||||
event_type: 'log',
|
||||
function_id: '001b0b08-331c-403e-810c-a2004b03a019',
|
||||
level: 'error',
|
||||
timestamp: 1659545029083869,
|
||||
id: '3475cf6f-2929-4296-ab44-ce2c17069937',
|
||||
},
|
||||
],
|
||||
includes: [/ERROR/],
|
||||
excludes: ['undefined', 'null'],
|
||||
},
|
||||
{
|
||||
queryType: 'functions',
|
||||
data: [
|
||||
{
|
||||
event_message: 'This is a uncaughtExceptop\n',
|
||||
event_type: 'uncaughtException',
|
||||
function_id: '001b0b08-331c-403e-810c-a2004b03a019',
|
||||
timestamp: 1659545029083869,
|
||||
id: '4475cf6f-2929-4296-ab44-ce2c17069937',
|
||||
level: null,
|
||||
},
|
||||
],
|
||||
includes: [/uncaughtException/],
|
||||
excludes: [/ERROR/],
|
||||
},
|
||||
])('table col renderer for $queryType', async ({ queryType, data, includes, excludes }) => {
|
||||
render(<LogTable queryType={queryType} data={data} />)
|
||||
|
||||
await Promise.all([
|
||||
...includes.map((text) => screen.findByText(text)),
|
||||
...excludes.map((text) => expect(screen.findByText(text)).rejects.toThrow()),
|
||||
])
|
||||
})
|
||||
|
||||
test('can display custom columns and headers based on data input', async () => {
|
||||
render(<LogTable data={[{ some_header: 'some_data', kinda: 123456 }]} />)
|
||||
await waitFor(() => screen.getByText(/some_header/))
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import {
|
||||
ensureNoTimestampConflict,
|
||||
genChartQuery,
|
||||
genDefaultQuery,
|
||||
LogsTableName,
|
||||
SQL_FILTER_TEMPLATES,
|
||||
} from 'components/interfaces/Settings/Logs'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
describe.each(Object.values(LogsTableName))('%s', (table) => {
|
||||
const templates = SQL_FILTER_TEMPLATES[table]
|
||||
@@ -60,5 +63,38 @@ describe.each(Object.values(LogsTableName))('%s', (table) => {
|
||||
contains.forEach((str) => expect(generated).toContain(str))
|
||||
excludes.forEach((str) => expect(generated).not.toContain(str))
|
||||
})
|
||||
test('generated chart query should include filters', () => {
|
||||
const generated = genChartQuery(table, {}, { 'override.nested': 'something' })
|
||||
expect(generated).toContain('override.nested')
|
||||
expect(generated).toContain('something')
|
||||
expect(generated).toContain('timestamp_trunc')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const base = dayjs().subtract(2, 'day')
|
||||
const baseIso = base.toISOString()
|
||||
test.each([
|
||||
{
|
||||
case: 'next start is after initial start',
|
||||
initial: [base.subtract(1, 'day').toISOString(), baseIso],
|
||||
next: [base.subtract(2, 'day').toISOString(), null],
|
||||
expected: [base.subtract(2, 'day').toISOString(), baseIso],
|
||||
},
|
||||
{
|
||||
case: 'next end is before initial start',
|
||||
initial: [base.subtract(1, 'day').toISOString(), baseIso],
|
||||
next: [null, base.subtract(2, 'day').toISOString()],
|
||||
expected: [base.subtract(3, 'day').toISOString(), base.subtract(2, 'day').toISOString()],
|
||||
},
|
||||
{
|
||||
case: 'next end is not before initial start',
|
||||
initial: [base.subtract(2, 'day').toISOString(), baseIso],
|
||||
next: [null, base.subtract(1, 'day').toISOString()],
|
||||
expected: [base.subtract(2, 'day').toISOString(), base.subtract(1, 'day').toISOString()],
|
||||
},
|
||||
])('ensure no timestamp conflict: $case', ({ initial, next, expected }) => {
|
||||
const result = ensureNoTimestampConflict(initial, next)
|
||||
expect(result[0]).toEqual(expected[0])
|
||||
expect(result[1]).toEqual(expected[1])
|
||||
})
|
||||
@@ -193,6 +193,21 @@ test('poll count for new messages', async () => {
|
||||
await waitFor(() => screen.queryByText(/125/) === null)
|
||||
await screen.findByText(/some-uuid123/)
|
||||
})
|
||||
test('log event chart', async () => {
|
||||
get.mockImplementation((url) => {
|
||||
// truncate
|
||||
if (url.includes('trunc')) {
|
||||
return { result: [{ timestamp: new Date().toISOString(), count: 125 }] }
|
||||
}
|
||||
return {
|
||||
result: [logDataFixture({ id: 'some-uuid123' })],
|
||||
}
|
||||
})
|
||||
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
|
||||
|
||||
await waitFor(() => screen.queryByText(/some-uuid123/) === null)
|
||||
expect(get).toBeCalledWith(expect.stringContaining('trunc'))
|
||||
})
|
||||
|
||||
test('s= query param will populate the search bar', async () => {
|
||||
const router = defaultRouterMock()
|
||||
@@ -219,7 +234,7 @@ test('te= query param will populate the timestamp to input', async () => {
|
||||
expect.stringContaining(`iso_timestamp_end=${encodeURIComponent(iso)}`)
|
||||
)
|
||||
})
|
||||
userEvent.click(await screen.findByText('Custom'))
|
||||
userEvent.click(await screen.findByTitle('Custom'))
|
||||
})
|
||||
test('ts= query param will populate the timestamp from input', async () => {
|
||||
// get time 20 mins before
|
||||
@@ -236,7 +251,7 @@ test('ts= query param will populate the timestamp from input', async () => {
|
||||
expect.stringContaining(`iso_timestamp_start=${encodeURIComponent(iso)}`)
|
||||
)
|
||||
})
|
||||
userEvent.click(await screen.findByText('Custom'))
|
||||
userEvent.click(await screen.findByTitle('Custom'))
|
||||
await screen.findByText(new RegExp(newDate.getFullYear()))
|
||||
})
|
||||
|
||||
@@ -289,6 +304,9 @@ test('bug: load older btn does not error out when previous page is empty', async
|
||||
})
|
||||
|
||||
test('log event chart hide', async () => {
|
||||
get.mockImplementation((url) => {
|
||||
return { result: [] }
|
||||
})
|
||||
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
|
||||
await screen.findByText('Events')
|
||||
const toggle = await screen.findByText(/Chart/)
|
||||
|
||||
@@ -49,11 +49,11 @@ test('Manual refresh', async () => {
|
||||
expect(mockFn).toBeCalled()
|
||||
})
|
||||
test('Datepicker dropdown', async () => {
|
||||
render(<PreviewFilterPanel />)
|
||||
const fn = jest.fn()
|
||||
render(<PreviewFilterPanel onSearch={fn} />)
|
||||
clickDropdown(await screen.findByText(/Last hour/))
|
||||
userEvent.click(await screen.findByText(/Last 3 hours/))
|
||||
await screen.findByText(/Last 3 hours/)
|
||||
await expect(screen.findByText(/Last hour/)).rejects.toThrow()
|
||||
expect(fn).toBeCalled()
|
||||
})
|
||||
|
||||
test.todo('timestamp to/from filter default value')
|
||||
|
||||