Merge branch 'master' of github.com:supabase/supabase

This commit is contained in:
Terry Sutton committed 2022-08-24 12:53:08 -02:30
commit 147b2dc1b2
56 files changed
+2445 -660

No files matched your search

+10 -8
View File
@@ -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 }}
+10 -10
View File
@@ -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>
+145 -143
View File
@@ -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)
}}
+11 -11
View File
@@ -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',
+8
View File
@@ -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"
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 619 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

+128 -121
View File
@@ -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 &amp; Twitter Logins, Supabase Grid, Go &amp; 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 />}
+19 -33
View File
@@ -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>
+36 -21
View File
@@ -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
+34
View File
@@ -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,
+9 -2
View File
@@ -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" />}</>
)
}
+5 -2
View File
@@ -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 />
+13 -7
View File
@@ -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 />
+10 -2
View File
@@ -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) => (
+1 -3
View File
@@ -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')