mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 18:05:11 +03:00
48 files changed
+52236
-491
No files matched your search
@@ -12,7 +12,7 @@ jobs:
|
||||
with:
|
||||
ref: master
|
||||
- uses: sobolevn/misspell-fixer-action@master
|
||||
- uses: peter-evans/create-pull-request@v2.4.4
|
||||
- uses: peter-evans/create-pull-request@v4
|
||||
env:
|
||||
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
|
||||
with:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// [Terry] Also need to look at getPageType() in ./lib/helpers
|
||||
// to set a menu to a page
|
||||
import SupabaseJsV1Nav from 'data/nav/supabase-js/v1'
|
||||
import SupabaseJsV2Nav from 'data/nav/supabase-js/v2'
|
||||
import SupabaseDartV0Nav from 'data/nav/supabase-dart/v0'
|
||||
@@ -5,6 +7,7 @@ import SupabaseDartV1Nav from 'data/nav/supabase-dart/v1'
|
||||
import SupabaseCLINav from 'data/nav/supabase-cli'
|
||||
import SupabaseAPINav from 'data/nav/supabase-api'
|
||||
import AuthServerNav from 'data/nav/auth-server'
|
||||
import RealtimeServerNav from 'data/nav/realtime-server'
|
||||
import StorageServerNav from 'data/nav/storage-server'
|
||||
|
||||
import { NavMenu, References } from './Navigation.types'
|
||||
@@ -178,11 +181,6 @@ export const menuItems: NavMenu = {
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Overview', url: '/guides/database/extensions', items: [] },
|
||||
{
|
||||
name: 'plv8: Javascript Language',
|
||||
url: '/guides/database/extensions/plv8',
|
||||
items: [],
|
||||
},
|
||||
{ name: 'http: RESTful Client', url: '/guides/database/extensions/http', items: [] },
|
||||
{
|
||||
name: 'pg_cron: Job Scheduling',
|
||||
@@ -195,6 +193,11 @@ export const menuItems: NavMenu = {
|
||||
items: [],
|
||||
},
|
||||
{ name: 'pgTAP: Unit Testing', url: '/guides/database/extensions/pgtap', items: [] },
|
||||
{
|
||||
name: 'plv8: Javascript Language',
|
||||
url: '/guides/database/extensions/plv8',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'uuid-ossp: Unique Identifiers',
|
||||
url: '/guides/database/extensions/uuid-ossp',
|
||||
@@ -335,6 +338,7 @@ export const menuItems: NavMenu = {
|
||||
label: 'Self-hosting',
|
||||
items: [
|
||||
{ name: 'Auth Server', url: '/reference/auth', items: [] },
|
||||
{ name: 'Realtime Server', url: '/reference/realtime', items: [] },
|
||||
{ name: 'Storage Server', url: '/reference/storage', items: [] },
|
||||
],
|
||||
},
|
||||
@@ -346,5 +350,6 @@ export const menuItems: NavMenu = {
|
||||
'reference/cli': SupabaseCLINav,
|
||||
'reference/api': SupabaseAPINav,
|
||||
'reference/auth': AuthServerNav,
|
||||
'reference/realtime': RealtimeServerNav,
|
||||
'reference/storage': StorageServerNav,
|
||||
}
|
||||
@@ -86,7 +86,7 @@
|
||||
},
|
||||
{
|
||||
"text": "Open Source",
|
||||
"url": "/oss"
|
||||
"url": "/docs/oss"
|
||||
},
|
||||
{
|
||||
"text": "SupaSquad",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
const Nav = [
|
||||
{
|
||||
label: 'Realtime',
|
||||
items: [
|
||||
{ name: 'Realtime Server', url: '/reference/realtime', items: [] },
|
||||
{ name: 'Configuration', url: '/reference/realtime/config', items: [] },
|
||||
{ name: 'Release Notes', url: '/reference/realtime/release-notes', items: [] },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export default Nav
|
||||
@@ -21,6 +21,8 @@ const nonGeneratedReferencePages = [
|
||||
'docs/reference/auth/release-notes',
|
||||
'docs/reference/storage',
|
||||
'docs/reference/storage/release-notes',
|
||||
'docs/reference/realtime',
|
||||
'docs/reference/realtime/release-notes',
|
||||
]
|
||||
|
||||
export default nonGeneratedReferencePages
|
||||
@@ -199,13 +199,6 @@ JS Reference: [select()](../reference/javascript/select),
|
||||
|
||||
### GraphQL API
|
||||
|
||||
<Admonition type='note'>
|
||||
|
||||
To rebuild your GraphQL schema from the SQL schema, call `select graphql.rebuild_schema();`.
|
||||
Be sure to rebuild the GraphQL schema after altering the SQL schema.
|
||||
|
||||
</Admonition>
|
||||
|
||||
You can use any GraphQL client with the Supabase GraphQL API. For our GraphQL example we will use [urql](https://formidable.com/open-source/urql/docs/).
|
||||
|
||||
<Tabs
|
||||
@@ -356,6 +349,12 @@ alter table todos enable row level security;
|
||||
|
||||
Never expose the `service_role` key in a browser or anywhere where a user can see it. This Key is designed to bypass Row Level Security - so it should only be used on a private server.
|
||||
|
||||
A common use case for the `service_role` key is to run data analytics jobs on the backend. To support joins on user id, it is often useful to grant the service role read access to `auth.users` table.
|
||||
|
||||
```sql
|
||||
grant select on table auth.users to service_role;
|
||||
```
|
||||
|
||||
We have [partnered with GitHub](https://github.blog/changelog/2022-03-28-supabase-is-now-a-github-secret-scanning-partner/) to scan for Supabase `service_role` keys pushed to public repositories.
|
||||
If they detect any keys with service_role privileges being pushed to GitHub, they will forward the API key to us, so that we can automatically revoke the detected secrets and notify you, protecting your data against malicious actors.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ description: How to generate types for your API and Supabase libraries.
|
||||
|
||||
Supabase APIs are generated from your database, which means that we can use database introspection to generate type-safe API definitions.
|
||||
|
||||
### Generating types using Supabase CLI
|
||||
## Generating types using Supabase CLI
|
||||
|
||||
The Supabase CLI is a single binary Go application that provides everything you need to setup a local development environment.
|
||||
|
||||
@@ -16,13 +16,13 @@ You can [install the CLI](https://www.npmjs.com/package/supabase) via npm or oth
|
||||
npm i supabase@">=1.8.1" --save-dev
|
||||
```
|
||||
|
||||
Login with your Personal Access Token
|
||||
Login with your Personal Access Token:
|
||||
|
||||
```bash
|
||||
npx supabase login
|
||||
```
|
||||
|
||||
Generate types for your project to produce the file `types/supabase.ts`
|
||||
Generate types for your project to produce the `types/supabase.ts` file:
|
||||
|
||||
```bash
|
||||
npx supabase gen types typescript --project-id "$PROJECT_ID" --schema public > types/supabase.ts
|
||||
|
||||
@@ -7,11 +7,9 @@ description: Add Captcha Protection to your Supabase project
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
## hCaptcha guide
|
||||
|
||||
Supabase provides you with the option of adding captcha to your sign-in, sign-up, and password reset forms. This keeps your website safe from bots and malicious scripts. Supabase authentication has support for [hCaptcha](https://www.hcaptcha.com/).
|
||||
|
||||
### Sign up for hCaptcha
|
||||
## Sign up for hCaptcha
|
||||
|
||||
Go to the [hCaptcha](https://www.hcaptcha.com/) website and sign up for an account. On the welcome page, copy the **Sitekey** and **Secret key**.
|
||||
|
||||
@@ -27,7 +25,7 @@ In the Settings page, look for the **Sitekey** section and copy the key.
|
||||
|
||||

|
||||
|
||||
### Enable hCaptcha protection for your Supabase project
|
||||
## Enable hCaptcha protection for your Supabase project
|
||||
|
||||
Navigate to the **[Authentication](https://app.supabase.com/project/_/settings/auth)** page in the Supabase Dashboard and find the **Enable hCaptcha protection** toggle under the **Security and Protection** section.
|
||||
|
||||
@@ -35,7 +33,7 @@ Navigate to the **[Authentication](https://app.supabase.com/project/_/settings/a
|
||||
|
||||
Enter your hCaptcha **Secret key** and click **Save**.
|
||||
|
||||
### Add the hCaptcha frontend component
|
||||
## Add the hCaptcha frontend component
|
||||
|
||||
The frontend requires some changes to provide the captcha on-screen for the user. This example uses React and the hCaptcha React component, but hCaptcha can be used with any JavaScript framework.
|
||||
|
||||
|
||||
@@ -53,11 +53,351 @@ PUBLIC_SUPABASE_ANON_KEY=your-anon-key
|
||||
Start off by creating a `db.ts` file inside of the `src/lib` directory and instantiate the `supabaseClient`.
|
||||
|
||||
```ts title=src/lib/db.ts
|
||||
import { createClient } from '@supabase/auth-helpers-sveltekit';
|
||||
import { env } from '$env/dynamic/public';
|
||||
// or use the static env
|
||||
// import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
|
||||
|
||||
export const supabaseClient = createClient(
|
||||
env.PUBLIC_SUPABASE_URL,
|
||||
env.PUBLIC_SUPABASE_ANON_KEY
|
||||
);
|
||||
```
|
||||
|
||||
To make sure the client is initialized on the server and the client, include this file in `src/hooks.server.js` and `src/hooks.client.js`:
|
||||
|
||||
```ts
|
||||
import '$lib/db';
|
||||
```
|
||||
|
||||
### Synchronizing the page store
|
||||
|
||||
Edit your `+layout.svelte` file and set up the client side.
|
||||
|
||||
```html title=src/routes/+layout.svelte
|
||||
<script>
|
||||
import { supabaseClient } from '$lib/db';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(() => {
|
||||
const {
|
||||
data: { subscription }
|
||||
} = supabaseClient.auth.onAuthStateChange(() => {
|
||||
invalidate('supabase:auth');
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
```
|
||||
|
||||
Every `PageLoad` or `LayoutLoad` using `getSupabase()` will update when `invalidate('supabase:auth')` is called.
|
||||
|
||||
If some data is not updated on signin/signout you can fall back to `invalidateAll()`.
|
||||
|
||||
### Send session to client
|
||||
|
||||
To make the session available to the UI (pages, layouts), pass the session in the root layout server load function:
|
||||
|
||||
```ts title=src/routes/+layout.server.ts
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
import { getServerSession } from '@supabase/auth-helpers-sveltekit';
|
||||
|
||||
export const load: LayoutServerLoad = async (event) => {
|
||||
return {
|
||||
session: await getServerSession(event)
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
In addition you can create a layout load function if you are using `invalidate('supabase:auth')`:
|
||||
|
||||
```ts title=src/routes/+layout.ts
|
||||
import type { LayoutLoad } from './$types';
|
||||
import { getSupabase } from '@supabase/auth-helpers-sveltekit';
|
||||
|
||||
export const load: LayoutLoad = async (event) => {
|
||||
const { session } = await getSupabase(event);
|
||||
return { session };
|
||||
};
|
||||
```
|
||||
|
||||
This results in fewer server calls as the client manages the session on its own.
|
||||
|
||||
### Typings
|
||||
|
||||
In order to get the most out of TypeScript and it´s intellisense, you should import our types into the `app.d.ts` type definition file that comes with your SvelteKit project.
|
||||
|
||||
```ts title=src/app.d.ts
|
||||
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
declare namespace App {
|
||||
interface Supabase {
|
||||
Database: import('./DatabaseDefinitions').Database;
|
||||
SchemaName: 'public';
|
||||
}
|
||||
|
||||
// interface Locals {}
|
||||
interface PageData {
|
||||
session: import('@supabase/supabase-js').Session | null;
|
||||
}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
}
|
||||
```
|
||||
|
||||
### Basic Setup
|
||||
|
||||
You can now determine if a user is authenticated on the client-side by checking that the `session` object in `$page.data` is defined.
|
||||
|
||||
```html title=src/routes/+page.svelte
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
|
||||
{#if !$page.data.session}
|
||||
<h1>I am not logged in</h1>
|
||||
{:else}
|
||||
<h1>Welcome {$page.data.session.user.email}</h1>
|
||||
<p>I am logged in!</p>
|
||||
{/if}
|
||||
```
|
||||
|
||||
## Client-side data fetching with RLS
|
||||
|
||||
For [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) to work properly when fetching data client-side, you need to make sure to import the `{ supabaseClient }` from `$lib/db` and only run your query once the session is defined client-side in `$page.data`:
|
||||
|
||||
```html
|
||||
<script>
|
||||
import { supabaseClient } from '$lib/db';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
let loadedData = [];
|
||||
async function loadData() {
|
||||
const { data } = await supabaseClient.from('test').select('*').limit(20);
|
||||
loadedData = data;
|
||||
}
|
||||
|
||||
$: if ($page.data.session) {
|
||||
loadData();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $page.data.session}
|
||||
<p>client-side data fetching with RLS</p>
|
||||
<pre>{JSON.stringify(loadedData, null, 2)}</pre>
|
||||
{/if}
|
||||
```
|
||||
|
||||
## Server-side data fetching with RLS
|
||||
|
||||
```html title=src/routes/profile/+page.svelte
|
||||
<script>
|
||||
/** @type {import('./$types').PageData} */
|
||||
export let data;
|
||||
$: ({ user, tableData } = data);
|
||||
</script>
|
||||
|
||||
<div>Protected content for {user.email}</div>
|
||||
<pre>{JSON.stringify(tableData, null, 2)}</pre>
|
||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
```
|
||||
|
||||
For [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) to work in a server environment, you need to use the `getSupabase` helper to check if the user is authenticated. The helper requires the `event` and returns `session` and `supabaseClient`:
|
||||
|
||||
```ts title=src/routes/profile/+page.ts
|
||||
import type { PageLoad } from './$types';
|
||||
import { getSupabase } from '@supabase/auth-helpers-sveltekit';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export const load: PageLoad = async (event) => {
|
||||
const { session, supabaseClient } = await getSupabase(event);
|
||||
if (!session) {
|
||||
throw redirect(303, '/');
|
||||
}
|
||||
const { data: tableData } = await supabaseClient.from('test').select('*');
|
||||
|
||||
return {
|
||||
user: session.user,
|
||||
tableData
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Protecting API routes
|
||||
|
||||
Wrap an API Route to check that the user has a valid session. If they're not logged in the session is `null`.
|
||||
|
||||
```ts title=src/routes/api/protected-route/+server.ts
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getSupabase } from '@supabase/auth-helpers-sveltekit';
|
||||
import { json, redirect } from '@sveltejs/kit';
|
||||
|
||||
export const GET: RequestHandler = async (event) => {
|
||||
const { session, supabaseClient } = await getSupabase(event);
|
||||
if (!session) {
|
||||
throw redirect(303, '/');
|
||||
}
|
||||
const { data } = await supabaseClient.from('test').select('*');
|
||||
|
||||
return json({ data });
|
||||
};
|
||||
```
|
||||
|
||||
If you visit `/api/protected-route` without a valid session cookie, you will get a 303 response.
|
||||
|
||||
## Protecting Actions
|
||||
|
||||
Wrap an Action to check that the user has a valid session. If they're not logged in the session is `null`.
|
||||
|
||||
```ts title=src/routes/posts/+page.server.ts
|
||||
import type { Actions } from './$types';
|
||||
import { getSupabase } from '@supabase/auth-helpers-sveltekit';
|
||||
import { error, invalid } from '@sveltejs/kit';
|
||||
|
||||
export const actions: Actions = {
|
||||
createPost: async (event) => {
|
||||
const { request } = event;
|
||||
const { session, supabaseClient } = await getSupabase(event);
|
||||
if (!session) {
|
||||
// the user is not signed in
|
||||
throw error(403, { message: 'Unauthorized' });
|
||||
}
|
||||
// we are save, let the user create the post
|
||||
const formData = await request.formData();
|
||||
const content = formData.get('content');
|
||||
|
||||
const { error: createPostError, data: newPost } = await supabaseClient
|
||||
.from('posts')
|
||||
.insert({ content });
|
||||
|
||||
if (createPostError) {
|
||||
return invalid(500, {
|
||||
supabaseErrorMessage: createPostError.message
|
||||
});
|
||||
}
|
||||
return {
|
||||
newPost
|
||||
};
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
If you try to submit a form with the action `?/createPost` without a valid session cookie, you will get a 403 error response.
|
||||
|
||||
## Saving and deleting the session
|
||||
|
||||
```ts
|
||||
import type { Actions } from './$types';
|
||||
import { invalid, redirect } from '@sveltejs/kit';
|
||||
import { getSupabase } from '@supabase/auth-helpers-sveltekit';
|
||||
|
||||
export const actions: Actions = {
|
||||
signin: async (event) => {
|
||||
const { request, cookies, url } = event;
|
||||
const { session, supabaseClient } = await getSupabase(event);
|
||||
const formData = await request.formData();
|
||||
|
||||
const email = formData.get('email') as string;
|
||||
const password = formData.get('password') as string;
|
||||
|
||||
const { error } = await supabaseClient.auth.signInWithPassword({
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
if (error) {
|
||||
if (error instanceof AuthApiError && error.status === 400) {
|
||||
return invalid(400, {
|
||||
error: 'Invalid credentials.',
|
||||
values: {
|
||||
email
|
||||
}
|
||||
});
|
||||
}
|
||||
return invalid(500, {
|
||||
error: 'Server error. Try again later.',
|
||||
values: {
|
||||
email
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
throw redirect(303, '/dashboard');
|
||||
},
|
||||
|
||||
signout: async (event) => {
|
||||
const { supabaseClient } = await getSupabase(event);
|
||||
await supabaseClient.auth.signOut();
|
||||
throw redirect(303, '/');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Protecting multiple routes
|
||||
|
||||
To avoid writing the same auth logic in every single route you can use the handle hook to
|
||||
protect multiple routes at once.
|
||||
|
||||
```ts title=src/hooks.server.ts
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getSupabase } from '@supabase/auth-helpers-sveltekit';
|
||||
import { redirect, error } from '@sveltejs/kit';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// protect requests to all routes that start with /protected-routes
|
||||
if (event.url.pathname.startsWith('/protected-routes')) {
|
||||
const { session, supabaseClient } = await getSupabase(event);
|
||||
|
||||
if (!session) {
|
||||
throw redirect(303, '/');
|
||||
}
|
||||
}
|
||||
|
||||
// protect POST requests to all routes that start with /protected-posts
|
||||
if (
|
||||
event.url.pathname.startsWith('/protected-posts') &&
|
||||
event.request.method === 'POST'
|
||||
) {
|
||||
const { session, supabaseClient } = await getSupabase(event);
|
||||
|
||||
if (!session) {
|
||||
throw error(303, '/');
|
||||
}
|
||||
}
|
||||
|
||||
return resolve(event);
|
||||
};
|
||||
```
|
||||
|
||||
## Migrate from 0.7.x to 0.8 {#migration}
|
||||
|
||||
### Set up the Supabase client {#migration-set-up-supabase-client}
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="older-0.7"
|
||||
>
|
||||
<TabPanel id="older-0.7" label="0.7.x">
|
||||
|
||||
```js title=src/lib/db.ts
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { setupSupabaseHelpers } from '@supabase/auth-helpers-sveltekit'
|
||||
import { dev } from '$app/environment'
|
||||
import { env } from '$env/dynamic/public'
|
||||
// or use the static env
|
||||
|
||||
// import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
|
||||
|
||||
export const supabaseClient = createClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_ANON_KEY, {
|
||||
@@ -73,9 +413,34 @@ setupSupabaseHelpers({
|
||||
})
|
||||
```
|
||||
|
||||
### Initialize the client
|
||||
</TabPanel>
|
||||
<TabPanel id="0.8.0" label="0.8.0">
|
||||
|
||||
Edit your `+layout.svelte` file and set up the client-side.
|
||||
```js title=src/lib/db.ts
|
||||
import { createClient } from '@supabase/auth-helpers-sveltekit'
|
||||
import { env } from '$env/dynamic/public'
|
||||
// or use the static env
|
||||
|
||||
// import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
|
||||
|
||||
export const supabaseClient = createClient(
|
||||
env.PUBLIC_SUPABASE_URL,
|
||||
env.PUBLIC_SUPABASE_ANON_KEY
|
||||
);
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Initialize the client {#migration-initialize-client}
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="older-0.7"
|
||||
>
|
||||
<TabPanel id="older-0.7" label="0.7.x">
|
||||
|
||||
```html title=src/routes/+layout.svelte
|
||||
<script lang="ts">
|
||||
@@ -95,9 +460,43 @@ Edit your `+layout.svelte` file and set up the client-side.
|
||||
<slot />
|
||||
```
|
||||
|
||||
### Set up hooks
|
||||
</TabPanel>
|
||||
<TabPanel id="0.8.0" label="0.8.0">
|
||||
|
||||
The `hooks.server.ts` file is where the heavy lifting of this library happens:
|
||||
```html title=src/routes/+layout.svelte
|
||||
<script>
|
||||
import { supabaseClient } from '$lib/db';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(() => {
|
||||
const {
|
||||
data: { subscription }
|
||||
} = supabaseClient.auth.onAuthStateChange(() => {
|
||||
invalidate('supabase:auth');
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Set up hooks {#migration-set-up-hooks}
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="older-0-7"
|
||||
>
|
||||
<TabPanel id="older-0-7" label="0.7.x">
|
||||
|
||||
```ts title=src/hooks.server.ts
|
||||
// make sure the supabase instance is initialized on the server
|
||||
@@ -106,39 +505,45 @@ import { dev } from '$app/environment'
|
||||
import { auth } from '@supabase/auth-helpers-sveltekit/server'
|
||||
|
||||
export const handle = auth()
|
||||
```
|
||||
|
||||
// use the sequence helper if you have additional Handle methods
|
||||
**Optional** _if using additional handle methods_
|
||||
|
||||
```ts title=src/hooks.server.ts
|
||||
// make sure the supabase instance is initialized on the server
|
||||
import '$lib/db'
|
||||
import { dev } from '$app/environment'
|
||||
import { auth } from '@supabase/auth-helpers-sveltekit/server'
|
||||
import { sequence } from '@sveltejs/kit/hooks'
|
||||
|
||||
export const handle = sequence(auth(), yourHandler)
|
||||
```
|
||||
|
||||
There are three handle methods available:
|
||||
</TabPanel>
|
||||
<TabPanel id="0.8.0" label="0.8.0">
|
||||
|
||||
- `callback()`:
|
||||
This creates a handler for `/api/auth/callback`. The `client` forwards the session details here every time `onAuthStateChange` fires on the client-side. This is needed to set up the cookies for your application so that SSR works seamlessly.
|
||||
- `session()`:
|
||||
This parses the session from the cookie and populate it in locals
|
||||
- `auth()`:
|
||||
a shorthand for `sequence(callback(), session())` that uses both handlers
|
||||
|
||||
### Send session to client
|
||||
|
||||
In order to make the session available to the UI (e.g., pages and layouts), pass the session in the root layout load function:
|
||||
|
||||
```ts title=src/routes/+layout.server.ts
|
||||
import type { LayoutServerLoad } from './$types'
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||
return {
|
||||
session: locals.session,
|
||||
}
|
||||
}
|
||||
```ts title=src/hooks.server.ts
|
||||
// make sure the supabase instance is initialized on the server
|
||||
import '$lib/db'
|
||||
```
|
||||
|
||||
### Typings
|
||||
```ts title=src/hooks.client.ts
|
||||
// make sure the supabase instance is initialized on the client
|
||||
import '$lib/db'
|
||||
```
|
||||
|
||||
In order to get the most out of TypeScript and IntelliSense, import the types into the `app.d.ts` type definition file that comes with your SvelteKit project.
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Typings {#migration-typings}
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="older-0-7"
|
||||
>
|
||||
<TabPanel id="older-0-7" label="0.7.x">
|
||||
|
||||
```ts title=src/app.d.ts
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
@@ -150,95 +555,91 @@ declare namespace App {
|
||||
interface Locals {
|
||||
session: import('@supabase/auth-helpers-sveltekit').SupabaseSession
|
||||
}
|
||||
|
||||
interface PageData {
|
||||
session: import('@supabase/auth-helpers-sveltekit').SupabaseSession
|
||||
}
|
||||
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="0.8.0" label="0.8.0">
|
||||
|
||||
```ts title=src/app.d.ts
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
declare namespace App {
|
||||
interface Supabase {
|
||||
Database: import('./DatabaseDefinitions').Database;
|
||||
SchemaName: 'public';
|
||||
}
|
||||
|
||||
// interface Locals {}
|
||||
interface PageData {
|
||||
session: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
|
||||
}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
}
|
||||
```
|
||||
|
||||
### Signing out
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
This library has provided a method to delete the session cookie.
|
||||
### withPageAuth {#migration-with-page-auth}
|
||||
|
||||
Create a server only page with an action to delete the cookie
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="older-0-7"
|
||||
>
|
||||
<TabPanel id="older-0-7" label="0.7.x">
|
||||
|
||||
```ts title=src/routes/logout/+page.server.ts
|
||||
import { deleteSession } from '@supabase/auth-helpers-sveltekit/server'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
import type { Actions } from './$types'
|
||||
|
||||
export const actions: Actions = {
|
||||
async default({ cookies }) {
|
||||
deleteSession(cookies)
|
||||
throw redirect(303, '/')
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Then make a **POST** request with a form to the server only page above.
|
||||
|
||||
```html
|
||||
```html title=src/routes/protected-route/+page.svelte
|
||||
<script lang="ts">
|
||||
import { enhanceAndInvalidate } from '@supabase/auth-helpers-sveltekit'
|
||||
import type { PageData } from './$types'
|
||||
|
||||
export let data: PageData
|
||||
$: ({ tableData, user } = data)
|
||||
</script>
|
||||
|
||||
<form action="/logout" method="post" use:enhanceAndInvalidate>
|
||||
<button type="submit">Sign out</button>
|
||||
</form>
|
||||
<div>Protected content for {user.email}</div>
|
||||
<p>server-side fetched data with RLS:</p>
|
||||
<pre>{JSON.stringify(tableData, null, 2)}</pre>
|
||||
<p>user:</p>
|
||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
```
|
||||
|
||||
### Basic setup
|
||||
```ts title=src/routes/protected-route/+page.ts
|
||||
import { withAuth } from '@supabase/auth-helpers-sveltekit'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
import type { PageLoad } from './$types'
|
||||
|
||||
You can now determine if a user is authenticated on the client-side by checking that the `user` object in `$page.data.session` is defined.
|
||||
|
||||
```html title=src/routes/+page.svelte
|
||||
<script>
|
||||
import { page } from '$app/stores'
|
||||
</script>
|
||||
|
||||
{#if !$page.data.session.user}
|
||||
<h1>I am not logged in</h1>
|
||||
{:else}
|
||||
<h1>Welcome {$page.data.session.user.email}</h1>
|
||||
<p>I am logged in!</p>
|
||||
{/if}
|
||||
```
|
||||
|
||||
## Client-side data fetching with RLS
|
||||
|
||||
For [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) to work properly when fetching data client-side, you need to make sure to import the `{ supabaseClient }` from `$lib/db` and only run your query once the user is defined client-side in `$page.data.session`:
|
||||
|
||||
```html
|
||||
<script>
|
||||
import { supabaseClient } from '$lib/db'
|
||||
import { page } from '$app/stores'
|
||||
|
||||
let loadedData = []
|
||||
async function loadData() {
|
||||
const { data } = await supabaseClient.from('test').select('*').limit(20)
|
||||
loadedData = data
|
||||
export const load: PageLoad = withAuth(async ({ session, getSupabaseClient }) => {
|
||||
if (!session.user) {
|
||||
throw redirect(303, '/')
|
||||
}
|
||||
|
||||
$: if ($page.data.session.user) {
|
||||
loadData()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $page.data.session.user}
|
||||
<p>client-side data fetching with RLS</p>
|
||||
<pre>{JSON.stringify(loadedData, null, 2)}</pre>
|
||||
{/if}
|
||||
const { data: tableData } = await getSupabaseClient().from('test').select('*')
|
||||
return { tableData, user: session.user }
|
||||
})
|
||||
```
|
||||
|
||||
## Server-side data fetching with RLS
|
||||
</TabPanel>
|
||||
<TabPanel id="0.8.0" label="0.8.0">
|
||||
|
||||
```html title=src/routes/profile/+page.svelte
|
||||
```html title=src/routes/protected-route/+page.svelte
|
||||
<script>
|
||||
/** @type {import('./$types').PageData} */
|
||||
export let data
|
||||
$: ({ user, tableData } = data)
|
||||
export let data;
|
||||
$: ({ user, tableData } = data);
|
||||
</script>
|
||||
|
||||
<div>Protected content for {user.email}</div>
|
||||
@@ -246,52 +647,38 @@ For [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) to
|
||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
```
|
||||
|
||||
For [row level security](/docs/learn/auth-deep-dive/auth-row-level-security) to work in a server environment, you need to use the `withAuth` helper to check if the user is authenticated. The helper extends the event with `session` and `getSupabaseClient()`:
|
||||
|
||||
```ts title=src/routes/profile/+page.ts
|
||||
```ts title=src/routes/protected-route/+page.ts
|
||||
// src/routes/profile/+page.ts
|
||||
import type { PageLoad } from './$types';
|
||||
import { withAuth } from '@supabase/auth-helpers-sveltekit';
|
||||
import { getSupabase } from '@supabase/auth-helpers-sveltekit';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
interface TestTable {
|
||||
id: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export const load: PageLoad = withAuth(async ({ getSupabaseClient, session }) => {
|
||||
if (!session.user) {
|
||||
export const load: PageLoad = async (event) => {
|
||||
const { session, supabaseClient } = await getSupabase(event);
|
||||
if (!session) {
|
||||
throw redirect(303, '/');
|
||||
}
|
||||
const { data: tableData } = await getSupabaseClient()
|
||||
.from<TestTable>('test')
|
||||
.select('*');
|
||||
const { data: tableData } = await supabaseClient.from('test').select('*');
|
||||
|
||||
return {
|
||||
user: session.user,
|
||||
tableData
|
||||
};
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Caution:**
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
Always use the instance returned by `getSupabaseClient()` directly!
|
||||
### withApiAuth {#migration-with-api-auth}
|
||||
|
||||
```ts
|
||||
// Bad
|
||||
const supabaseClient = getSupabaseClient()
|
||||
|
||||
await supabaseClient.from('table1').select()
|
||||
await supabaseClient.from('table2').select()
|
||||
|
||||
// Good
|
||||
await getSupabaseClient().from('table1').select()
|
||||
await getSupabaseClient().from('table2').select()
|
||||
```
|
||||
|
||||
## Protecting API routes
|
||||
|
||||
Wrap an API Route to check that the user has a valid session. If they're not logged in the session is `null`.
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="older-0-7"
|
||||
>
|
||||
<TabPanel id="older-0-7" label="0.7.x">
|
||||
|
||||
```ts title=src/routes/api/protected-route/+server.ts
|
||||
import type { RequestHandler } from './$types';
|
||||
@@ -307,148 +694,38 @@ export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient
|
||||
if (!session.user) {
|
||||
throw redirect(303, '/');
|
||||
}
|
||||
|
||||
const { data } = await getSupabaseClient()
|
||||
.from<TestTable>('test')
|
||||
.select('*');
|
||||
|
||||
return json({ data });
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
If you visit `/api/protected-route` without a valid session cookie, you will get a 303 response.
|
||||
</TabPanel>
|
||||
<TabPanel id="0.8.0" label="0.8.0">
|
||||
|
||||
## Protecting Actions
|
||||
```ts title=src/routes/api/protected-route/+server.ts
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getSupabase } from '@supabase/auth-helpers-sveltekit';
|
||||
import { json, redirect } from '@sveltejs/kit';
|
||||
|
||||
Wrap an Action to check that the user has a valid session. If they're not logged in, the session is `null`.
|
||||
|
||||
```ts title=src/routes/posts/+page.server.ts
|
||||
import type { Actions } from './$types'
|
||||
import { withAuth } from '@supabase/auth-helpers-sveltekit'
|
||||
import { error, invalid } from '@sveltejs/kit'
|
||||
|
||||
export const actions: Actions = {
|
||||
createPost: withAuth(async ({ session, getSupabaseClient, request }) => {
|
||||
if (!session.user) {
|
||||
// the user is not signed in
|
||||
throw error(403, { message: 'Unauthorized' })
|
||||
}
|
||||
// we are save, let the user create the post
|
||||
const formData = await request.formData()
|
||||
const content = formData.get('content')
|
||||
|
||||
const { error: createPostError, data: newPost } = await getSupabaseClient()
|
||||
.from('posts')
|
||||
.insert({ content })
|
||||
|
||||
if (createPostError) {
|
||||
return invalid(500, {
|
||||
supabaseErrorMessage: createPostError.message,
|
||||
})
|
||||
}
|
||||
return {
|
||||
newPost,
|
||||
}
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
If you try to submit a form with the action `?/createPost` without a valid session cookie, you will get a 403 error response.
|
||||
|
||||
## Saving and deleting the session
|
||||
|
||||
Use `saveSession` to save the session cookies:
|
||||
|
||||
```ts
|
||||
import type { Actions } from './$types'
|
||||
import { supabaseClient } from '$lib/db'
|
||||
import { invalid, redirect } from '@sveltejs/kit'
|
||||
import { saveSession } from '@supabase/auth-helpers-sveltekit/server'
|
||||
|
||||
export const actions: Actions = {
|
||||
async signin({ request, cookies, url }) {
|
||||
const formData = await request.formData()
|
||||
|
||||
const email = formData.get('email') as string
|
||||
const password = formData.get('password') as string
|
||||
|
||||
const { data, error } = await supabaseClient.auth.api.signInWithEmail(email, password, {
|
||||
redirectTo: `${url.origin}/logging-in`,
|
||||
})
|
||||
|
||||
if (error || !data) {
|
||||
if (error?.status === 400) {
|
||||
return invalid(400, {
|
||||
error: 'Invalid credentials',
|
||||
values: {
|
||||
email,
|
||||
},
|
||||
})
|
||||
}
|
||||
return invalid(500, {
|
||||
error: 'Server error. Try again later.',
|
||||
values: {
|
||||
email,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
saveSession(cookies, data)
|
||||
throw redirect(303, '/dashboard')
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Use `deleteSession` to delete the session cookies:
|
||||
|
||||
```ts
|
||||
import type { Actions } from './$types'
|
||||
import { deleteSession } from '@supabase/auth-helpers-sveltekit/server'
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
|
||||
export const actions: Actions = {
|
||||
async logout({ cookies }) {
|
||||
deleteSession(cookies)
|
||||
throw redirect(303, '/')
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Custom session namespace
|
||||
|
||||
If you want to use something other than `locals.session` and `$page.data.session`, update the types and create three helper functions:
|
||||
|
||||
```ts title=src/app.d.ts
|
||||
declare namespace App {
|
||||
interface Locals {
|
||||
mySupabaseSession: import('@supabase/auth-helpers-sveltekit').SupabaseSession
|
||||
export const GET: RequestHandler = async (event) => {
|
||||
const { session, supabaseClient } = await getSupabase(event);
|
||||
if (!session) {
|
||||
throw redirect(303, '/');
|
||||
}
|
||||
interface PageData {
|
||||
mySupabaseSession: import('@supabase/auth-helpers-sveltekit').SupabaseSession
|
||||
}
|
||||
}
|
||||
const { data } = await supabaseClient.from('test').select('*');
|
||||
|
||||
return json({ data });
|
||||
};
|
||||
```
|
||||
|
||||
```ts title=src/hooks.server.ts
|
||||
setupSupabaseServer({
|
||||
supabaseClient,
|
||||
cookieOptions: {
|
||||
secure: !dev,
|
||||
},
|
||||
// --- change location within locals ---
|
||||
getSessionFromLocals: (locals) => locals.mySupabaseSession,
|
||||
setSessionToLocals: (locals, session) => (locals.mySupabaseSession = session),
|
||||
})
|
||||
```
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
```ts title=src/lib/db.ts
|
||||
setupSupabaseClient({
|
||||
supabaseClient,
|
||||
// --- change location within pageData ---
|
||||
getSessionFromPageData: (data) => data.mySupabaseSession,
|
||||
})
|
||||
```
|
||||
|
||||
## Migrate from 0.6.11 and below to 0.7.0 {#migration}
|
||||
## Migrate from 0.6.11 and below to 0.7.0 {#migration-0-7}
|
||||
|
||||
There are numerous breaking changes in the latest 0.7.0 version of this library.
|
||||
|
||||
@@ -456,7 +733,7 @@ There are numerous breaking changes in the latest 0.7.0 version of this library.
|
||||
|
||||
The environment variable prefix is now `PUBLIC_` instead of `VITE_` (e.g., `VITE_SUPABASE_URL` is now `PUBLIC_SUPABASE_URL`).
|
||||
|
||||
### Set up the Supabase client {#migration-set-up-supabase-client}
|
||||
### Set up the Supabase client {#migration-set-up-supabase-client-0-7}
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -478,7 +755,7 @@ export { supabaseClient };
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="latest" label="0.7.0">
|
||||
<TabPanel id="0.7.0" label="0.7.0">
|
||||
|
||||
```js title=src/lib/db.ts
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
@@ -505,7 +782,7 @@ setupSupabaseHelpers({
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Initialize the client {#migration-initialize-client}
|
||||
### Initialize the client {#migration-initialize-client-0-7}
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -528,7 +805,7 @@ setupSupabaseHelpers({
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="latest" label="0.7.0">
|
||||
<TabPanel id="0.7.0" label="0.7.0">
|
||||
|
||||
The `@supabase/auth-helpers-svelte` library is no longer required as the `@supabase/auth-helpers-sveltekit` library handles all the client-side code.
|
||||
|
||||
@@ -553,7 +830,7 @@ The `@supabase/auth-helpers-svelte` library is no longer required as the `@supab
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Set up hooks {#migration-set-up-hooks}
|
||||
### Set up hooks {#migration-set-up-hooks-0-7}
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -581,7 +858,7 @@ export const getSession: GetSession = async (event) => {
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="latest" label="0.7.0">
|
||||
<TabPanel id="0.7.0" label="0.7.0">
|
||||
|
||||
```ts title=src/hooks.server.ts
|
||||
// make sure the supabase instance is initialized on the server
|
||||
@@ -607,7 +884,7 @@ export const handle = sequence(auth(), yourHandler)
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Typings {#migration-typings}
|
||||
### Typings {#migration-typings-0-7}
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -639,7 +916,7 @@ declare namespace App {
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="latest" label="0.7.0">
|
||||
<TabPanel id="0.7.0" label="0.7.0">
|
||||
|
||||
```ts title=src/app.d.ts
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
@@ -688,7 +965,7 @@ declare namespace App {
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="latest" label="0.7.0">
|
||||
<TabPanel id="0.7.0" label="0.7.0">
|
||||
|
||||
```html title=src/routes/+page.svelte
|
||||
<script>
|
||||
@@ -747,7 +1024,7 @@ declare namespace App {
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="latest" label="0.7.0">
|
||||
<TabPanel id="0.7.0" label="0.7.0">
|
||||
|
||||
```html title=src/routes/protected-route/+page.svelte
|
||||
<script lang="ts">
|
||||
@@ -818,7 +1095,7 @@ export const GET: RequestHandler<GetOutput> = async ({ locals, request }) =>
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="latest" label="0.7.0">
|
||||
<TabPanel id="0.7.0" label="0.7.0">
|
||||
|
||||
```ts title=src/routes/api/protected-route/+server.ts
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
@@ -48,13 +48,13 @@ scoop install supabase
|
||||
|
||||
The CLI is available through [Homebrew](https://brew.sh) and Linux packages.
|
||||
|
||||
### Homebrew
|
||||
#### Homebrew
|
||||
|
||||
```sh
|
||||
brew install supabase/tap/supabase
|
||||
```
|
||||
|
||||
### Linux packages
|
||||
#### Linux packages
|
||||
|
||||
Linux packages are provided in [Releases](https://github.com/supabase/cli/releases).
|
||||
To install, download the `.apk`/`.deb`/`.rpm` file depending on your package manager
|
||||
|
||||
@@ -56,7 +56,7 @@ values
|
||||
|
||||
The functions we'll cover in this guide are:
|
||||
|
||||
### `to_tsvector()`
|
||||
### `to_tsvector()` {#to-tsvector}
|
||||
|
||||
Converts your data into searchable "tokens". `to_tsvector()` stands for "to text search vector". For example:
|
||||
|
||||
@@ -68,14 +68,14 @@ select to_tsvector('green eggs and ham')
|
||||
|
||||
Collectively these tokens are called a "document" which Postgres can use for comparisons.
|
||||
|
||||
### `to_tsquery()`
|
||||
### `to_tsquery()` {#to-tsquery}
|
||||
|
||||
Converts a query string into "tokens" to match. `to_tsquery()` stands for "to text search query".
|
||||
|
||||
This conversion step is important because we will want to "fuzzy match" on keywords.
|
||||
For example if a user searches for "eggs", and a column has the value "egg", we probably still want to return a match.
|
||||
|
||||
### Match: `@@`
|
||||
### Match: `@@` {#match}
|
||||
|
||||
The `@@` symbol is the "match" symbol for Full Text Search. It returns any matches between a `to_tsvector` result and a `to_tsquery` result.
|
||||
|
||||
@@ -450,7 +450,7 @@ final result = await client
|
||||
Visit [PostgreSQL: Text Search Functions and Operators](https://www.postgresql.org/docs/current/functions-textsearch.html)
|
||||
to learn about additional query operators you can use to do more advanced `full text queries`, such as:
|
||||
|
||||
### Proximity: `<->`
|
||||
### Proximity: `<->` {#proximity}
|
||||
|
||||
The proximity symbol is useful for searching for terms that are a certain "distance" apart.
|
||||
For example, to find the phrase `big dreams`, where the a match for "big" is followed immediately by a match for "dreams":
|
||||
@@ -539,7 +539,7 @@ final result = await client
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Negation: `!`
|
||||
### Negation: `!` {#negation}
|
||||
|
||||
The negation symbol can be used to find phrases which _don't_ contain a search term.
|
||||
For example, to find records that have the word `big` but not `little`:
|
||||
|
||||
@@ -11,7 +11,7 @@ Your PostgreSQL database is the core of your Supabase project, so it's important
|
||||
|
||||
If you use special symbols in your postgres password, you must remember to [percent-encode](https://en.wikipedia.org/wiki/Percent-encoding) your password later if using the postgres connection string e.g. `postgresql://postgres:p%3Dword@db.cvwawazfelidkloqmbma.supabase.co:5432/postgres`
|
||||
|
||||
### Changing your project password
|
||||
## Changing your project password
|
||||
|
||||
When you created your project you were also asked to enter a password. This is actually the password for your database, specifically for the `postgres` user.
|
||||
You can update this from the Dashboard under the [database settings](https://app.supabase.com/project/_/settings/database) page.
|
||||
|
||||
@@ -55,14 +55,6 @@ Use `psql` to import the Heroku database file to your Supabase project.
|
||||
psql -h $SUPABASE_HOST -U postgres -f heroku_dump.sql
|
||||
```
|
||||
|
||||
## (Optional) Post processing {#post-processing}
|
||||
|
||||
If you're using GraphQL with your Supabase project, you may need to run the following SQL command in the Supabase Dashboard (SQL Editor) to make sure the new tables show up:
|
||||
|
||||
```sql
|
||||
select graphql.rebuild_schema();
|
||||
```
|
||||
|
||||
## Additional options
|
||||
|
||||
- To only migrate a single database schema, add the `--schema=PATTERN` parameter to your `pg_dump` command.
|
||||
|
||||
@@ -10,17 +10,17 @@ import TabItem from '@theme/TabItem'
|
||||
|
||||
Supabase Storage makes it simple to store and serve large files.
|
||||
|
||||
### Files
|
||||
## Files
|
||||
|
||||
Files can be any sort of media file. This includes images, GIFs, and videos. It is best practice to store files outside of your database because of their sizes. For security, HTML files are returned as plain text.
|
||||
|
||||
### Folders
|
||||
## Folders
|
||||
|
||||
Folders are a way to organize your files (just like on your computer).
|
||||
There is no right or wrong way to
|
||||
organize your files. You can store them in whichever folder structure suits your project.
|
||||
|
||||
### Buckets
|
||||
## Buckets
|
||||
|
||||
Buckets are distinct containers for files and folders. You can think of them like "super folders".
|
||||
Generally you would create distinct buckets for different Security and Access Rules. For example, you might
|
||||
|
||||
@@ -50,18 +50,24 @@ Reference documentation for self-hosting Supabase features.
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="w-1/2">
|
||||
<ButtonCard
|
||||
to='/reference/auth'
|
||||
title='Auth Server'
|
||||
to="/reference/auth"
|
||||
title="Auth Server"
|
||||
description=" JSON Web Token (JWT)-based API for managing users and issuing access tokens."
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<ButtonCard
|
||||
to='/reference/storage'
|
||||
title='Storage'
|
||||
to="/reference/realtime"
|
||||
title="Realtime"
|
||||
description="Build multiplayer applications and listen to your database changes via websockets."
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<ButtonCard
|
||||
to="/reference/storage"
|
||||
title="Storage"
|
||||
description="S3-compatible object storage service that integrates with Postgres."
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
slug: /
|
||||
sidebar_position: 1
|
||||
id: realtime
|
||||
title: Supabase Realtime Server
|
||||
sidebar_label: Supabase Realtime Server
|
||||
---
|
||||
|
||||
Supabase Realtime is a server built with Elixir using the [Phoenix Framework](https://www.phoenixframework.org) that allows you to listen to changes in your PostgreSQL database via logical replication and then broadcast those changes via WebSockets.
|
||||
|
||||
There are two versions of this server: `Realtime` and `Realtime RLS`.
|
||||
|
||||
`Realtime` server works by:
|
||||
|
||||
1. listening to PostgreSQL's replication functionality (using PostgreSQL's logical decoding)
|
||||
2. converting the byte stream into JSON
|
||||
3. broadcasting to all connected clients over WebSockets
|
||||
|
||||
`Realtime RLS` server works by:
|
||||
|
||||
1. polling PostgreSQL's replication functionality (using PostgreSQL's logical decoding and [wal2json](https://github.com/eulerto/wal2json) output plugin)
|
||||
2. passing database changes to a [Write Ahead Log Realtime Unified Security (WALRUS)](https://github.com/supabase/walrus) PostgresSQL function and receiving a list of authorized subscribers depending on Row Level Security (RLS) policies
|
||||
3. converting the changes into JSON
|
||||
4. broadcasting to authorized subscribers over WebSockets
|
||||
|
||||
## Why not just use PostgreSQL's `NOTIFY`?
|
||||
|
||||
A few reasons:
|
||||
|
||||
1. You don't have to set up triggers on every table.
|
||||
2. `NOTIFY` has a payload limit of 8000 bytes and will fail for anything larger. The usual solution is to send an ID and then fetch the record, but that's heavy on the database.
|
||||
3. `Realtime` server consumes two connections to the database, then you can connect many clients to this server. Easier on your database, and to scale up you just add additional `Realtime` servers.
|
||||
|
||||
## Benefits
|
||||
|
||||
1. The beauty of listening to the replication functionality is that you can make changes to your database from anywhere - your API, directly in the DB, via a console, etc. - and you will still receive the changes via WebSockets.
|
||||
2. Decoupling. For example, if you want to send a new slack message every time someone makes a new purchase you might build that functionality directly into your API. This allows you to decouple your async functionality from your API.
|
||||
3. This is built with Phoenix, an [extremely scalable Elixir framework](https://www.phoenixframework.org/blog/the-road-to-2-million-websocket-connections).
|
||||
|
||||
## Does this server guarantee delivery of every data change?
|
||||
|
||||
Not yet! Due to the following limitations:
|
||||
|
||||
1. Postgres database runs out of disk space due to Write-Ahead Logging (WAL) buildup, which can crash the database and prevent Realtime server from receiving and broadcasting changes. This can be mitigated in the Realtime RLS version of this server by setting the Postgres config `max_slot_wal_keep_size` to a reasonable size.
|
||||
2. Realtime server can crash due to a larger replication lag than available memory, forcing the creation of a new replication slot and resetting replication to read from the latest WAL data.
|
||||
3. When Realtime server falls too far behind for any reason, for example disconnecting from database as WAL continues to build up, then database can delete WAL segments the server still needs to read from, for example after reconnecting.
|
||||
|
||||
## Client libraries
|
||||
|
||||
- [JavaScript](https://github.com/supabase/realtime-js)
|
||||
- [Dart](https://github.com/supabase/realtime-dart)
|
||||
|
||||
## Additional Links
|
||||
|
||||
- [Source Code](https://github.com/supabase/realtime)
|
||||
- [Known bugs and issues](https://github.com/supabase/realtime/issues)
|
||||
- [Realtime Guides](https://supabase.com/docs/guides/realtime)
|
||||
Whitespace-only changes.
@@ -0,0 +1,7 @@
|
||||
---
|
||||
id: release-notes
|
||||
---
|
||||
|
||||
# Release Notes
|
||||
|
||||
All release notes can be found in the [GitHub Releases](https://github.com/supabase/realtime/releases) page.
|
||||
@@ -22,11 +22,11 @@ toc_max_heading_level: 3
|
||||
|
||||
<%- parameter.description %>
|
||||
|
||||
<ul>
|
||||
<li>Required: <code><%= parameter.required %></code></li>
|
||||
<li>Default: <code><%- parameter?.default ? parameter?.default : 'None' %></code></li><% if(parameter?.links?.length){ %>
|
||||
<ul class="method-list-group">
|
||||
<li class="method-list-item-label flex items-center gap-4">Required: <code><%= parameter.required %></code></li>
|
||||
<li class="method-list-item-label flex items-center gap-4">Default: <code><%- parameter?.default ? parameter?.default : 'None' %></code></li><% if(parameter?.links?.length){ %>
|
||||
<li>
|
||||
<span>See also:</span>
|
||||
<h3>See also:</h3>
|
||||
<ul><% parameter.links?.forEach(function(link){ %><li><a href="<%- link.link %>"><%- link.name %></a></li><% }); %></ul>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
@@ -52,7 +52,11 @@ const Layout: FC<Props> = ({ meta, children, toc, menuItems, currentPage }) => {
|
||||
: 'col-span-12 lg:col-span-9'
|
||||
} py-2 lg:py-4 px-2 lg:px-8 mx-auto`}
|
||||
>
|
||||
<article className="doc-content-container prose dark:prose-dark dark:bg-scale-200 width-full mt-8 2xl:max-w-[880px] ">
|
||||
<article
|
||||
className={`${
|
||||
meta?.hide_table_of_contents || !hasTableOfContents ? 'xl:min-w-[880px]' : ''
|
||||
} doc-content-container prose dark:prose-dark dark:bg-scale-200 width-full mt-8 2xl:max-w-[880px]`}
|
||||
>
|
||||
{children}
|
||||
</article>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ export function getDocsBySlug(slug: string) {
|
||||
(realSlug.includes('reference/cli/') && !nonGeneratedReferencePages.includes(realSlug)) ||
|
||||
(realSlug.includes('reference/api/') && !nonGeneratedReferencePages.includes(realSlug)) ||
|
||||
(realSlug.includes('reference/auth/') && !nonGeneratedReferencePages.includes(realSlug)) ||
|
||||
(realSlug.includes('reference/realtime/') && !nonGeneratedReferencePages.includes(realSlug)) ||
|
||||
(realSlug.includes('reference/storage/') && !nonGeneratedReferencePages.includes(realSlug))
|
||||
? getPathToGeneratedDoc(realSlug)
|
||||
: realSlug
|
||||
|
||||
@@ -20,6 +20,8 @@ export function getPageType(asPath: string) {
|
||||
page = 'reference/cli'
|
||||
} else if (asPath.includes('/reference/auth')) {
|
||||
page = 'reference/auth'
|
||||
} else if (asPath.includes('/reference/realtime')) {
|
||||
page = 'reference/realtime'
|
||||
} else if (asPath.includes('/reference/storage')) {
|
||||
page = 'reference/storage'
|
||||
} else if (asPath.includes('/reference')) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"postbuild": "node ./scripts/build-search.js",
|
||||
"generate:all": "npm-run-all --parallel gen:api gen:cli gen:gotrue gen:storage gen:supabase-dart:v0 gen:supabase-dart:v1 gen:supabase-js:v1 gen:supabase-js:v2",
|
||||
"generate:all": "npm-run-all --parallel gen:api gen:cli gen:gotrue gen:storage gen:supabase-dart:v0 gen:supabase-dart:v1 gen:supabase-js:v1 gen:supabase-js:v2 gen:realtime",
|
||||
"gen:api": "npm-run-all gen:api:usage",
|
||||
"gen:api:usage": "ts-node ./generator/index.ts gen --type api --url https://api.supabase.com --input ../../spec/transforms/api_v0_openapi_deparsed.json --output ./docs/reference/api/generated/usage.mdx",
|
||||
"gen:cli": "npm-run-all gen:cli:commands gen:cli:config",
|
||||
@@ -27,7 +27,9 @@
|
||||
"gen:supabase-js:v1": "npm-run-all gen:supabase-js:v1:ref",
|
||||
"gen:supabase-js:v1:ref": "ts-node ./generator/index.ts gen --type legacy --input ../../spec/supabase_js_v1.yml --output ./docs/reference/javascript/v1/generated",
|
||||
"gen:supabase-js:v2": "npm-run-all gen:supabase-js:v2:ref",
|
||||
"gen:supabase-js:v2:ref": "ts-node ./generator/index.ts gen --type legacy --input ../../spec/supabase_js_v2_temp.yml --output ./docs/reference/javascript/generated"
|
||||
"gen:supabase-js:v2:ref": "ts-node ./generator/index.ts gen --type legacy --input ../../spec/supabase_js_v2_temp.yml --output ./docs/reference/javascript/generated",
|
||||
"gen:realtime": "npm-run-all gen:realtime:config",
|
||||
"gen:realtime:config": "ts-node ./generator/index.ts gen --type config --input ../../spec/realtime_v0_config.yaml --output ./docs/reference/realtime/generated/config.mdx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@algolia/autocomplete-js": "^1.7.2",
|
||||
|
||||
@@ -63,8 +63,8 @@ export default function Oss({ meta }) {
|
||||
<div className="">
|
||||
<h2>Community Maintainers</h2>
|
||||
|
||||
<div className="overflow-auto max-w-[300px] md:max-w-none">
|
||||
<ul className="flex gap-4 items-center p-0">
|
||||
<div className="overflow-auto md:max-w-none hidden sm:block">
|
||||
<ul className="flex 2xl:gap-4 items-center p-0">
|
||||
{maintainerPills.map((x) => (
|
||||
<li
|
||||
key={x}
|
||||
|
||||
@@ -106,10 +106,6 @@ pre[class*='language-'] {
|
||||
}
|
||||
}
|
||||
|
||||
.method-list-group li:last-child {
|
||||
border-bottom: 1px solid var(--colors-scale3);
|
||||
}
|
||||
|
||||
.method-list-title-isChild {
|
||||
font-family: var(--custom-font-size-xs);
|
||||
& h5 {
|
||||
@@ -125,7 +121,7 @@ pre[class*='language-'] {
|
||||
}
|
||||
|
||||
.method-list-group {
|
||||
@apply p-0 m-0 list-none;
|
||||
@apply p-0 m-0 list-none border-b;
|
||||
}
|
||||
|
||||
.method-list-group .method-list-group {
|
||||
@@ -225,3 +221,9 @@ a:has(code) {
|
||||
.doc-content-container ul li a {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
// fix ToC links when they have <code> inside
|
||||
.toc-menu li a code {
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
@@ -221,13 +221,6 @@ JS Reference: [select()](../reference/javascript/select),
|
||||
|
||||
### GraphQL API
|
||||
|
||||
:::note
|
||||
|
||||
To rebuild your GraphQL schema from the SQL schema, call `select graphql.rebuild_schema();`.
|
||||
Be sure to rebuild the GraphQL schema after altering the SQL schema.
|
||||
|
||||
:::
|
||||
|
||||
You can use any GraphQL client with the Supabase GraphQL API. For our GraphQL example we will use [urql](https://formidable.com/open-source/urql/docs/).
|
||||
|
||||
<Tabs
|
||||
@@ -392,6 +385,12 @@ alter table todos enable row level security;
|
||||
|
||||
Never expose the `service_role` key in a browser or anywhere where a user can see it. This Key is designed to bypass Row Level Security - so it should only be used on a private server.
|
||||
|
||||
A common use case for the `service_role` key is to run data analytics jobs on the backend. To support joins on user id, it is often useful to grant the service role read access to `auth.users` table.
|
||||
|
||||
```sql
|
||||
grant select on table auth.users to service_role;
|
||||
```
|
||||
|
||||
We have [partnered with GitHub](https://github.blog/changelog/2022-03-28-supabase-is-now-a-github-secret-scanning-partner/) to scan for Supabase `service_role` keys pushed to public repositories.
|
||||
If they detect any keys with service_role privileges being pushed to GitHub, they will forward the API key to us, so that we can automatically revoke the detected secrets and notify you, protecting your data against malicious actors.
|
||||
|
||||
|
||||
@@ -55,14 +55,6 @@ Use `psql` to import the Heroku database file to your Supabase project.
|
||||
psql -h $SUPABASE_HOST -U postgres -f heroku_dump.sql
|
||||
```
|
||||
|
||||
## (Optional) Post processing {#post-processing}
|
||||
|
||||
If you're using GraphQL with your Supabase project, you may need to run the following SQL command in the Supabase Dashboard (SQL Editor) to make sure the new tables show up:
|
||||
|
||||
```sql
|
||||
select graphql.rebuild_schema();
|
||||
```
|
||||
|
||||
## Additional options
|
||||
|
||||
- To only migrate a single database schema, add the `--schema=PATTERN` parameter to your `pg_dump` command.
|
||||
|
||||
@@ -67,9 +67,6 @@ create table "Account"(
|
||||
"createdAt" timestamp not null,
|
||||
"updatedAt" timestamp not null
|
||||
);
|
||||
|
||||
-- Rebuild the GraphQL Schema Cache
|
||||
select graphql.rebuild_schema();
|
||||
```
|
||||
|
||||
Translates to the GraphQL base type
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"realtime": {
|
||||
"name": "Realtime",
|
||||
"icon": "M13 10V3L4 14h7v7l9-11h-7z",
|
||||
"description": "Create multiplayer experiences by sharing, broadcasting, and listenning to changes from other clients or the Database.",
|
||||
"description": "Create multiplayer experiences by sharing, broadcasting, and listening to changes from other clients or the Database.",
|
||||
"description_short": "",
|
||||
"label": "",
|
||||
"url": "/realtime"
|
||||
|
||||
Generated
+55
-1
@@ -10415,6 +10415,31 @@
|
||||
"@types/react-window": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/recharts": {
|
||||
"version": "1.8.23",
|
||||
"resolved": "https://registry.npmjs.org/@types/recharts/-/recharts-1.8.23.tgz",
|
||||
"integrity": "sha512-O/mIPm9f6dwRWfenOI3GQwsGta3x1YWjwqXOCZqC0MATQ6C+A+Jc8VxFnSUr4N3uYv64zkq90RwXFaMNbhJKvg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/d3-shape": "^1",
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/recharts/node_modules/@types/d3-path": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz",
|
||||
"integrity": "sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/recharts/node_modules/@types/d3-shape": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.8.tgz",
|
||||
"integrity": "sha512-gqfnMz6Fd5H6GOLYixOZP/xlrMtJms9BaS+6oWxTKHNqPGZ93BkWWupQSCYm6YHqx6h9wjRupuJb90bun6ZaYg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/d3-path": "^1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resize-observer-browser": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz",
|
||||
@@ -37190,6 +37215,7 @@
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/react-window-infinite-loader": "^1.0.5",
|
||||
"@types/recharts": "^1.8.23",
|
||||
"@types/semver": "^7.3.9",
|
||||
"@types/sqlstring": "^2.3.0",
|
||||
"@types/uuid": "^8.3.4",
|
||||
@@ -44752,6 +44778,33 @@
|
||||
"@types/react-window": "*"
|
||||
}
|
||||
},
|
||||
"@types/recharts": {
|
||||
"version": "1.8.23",
|
||||
"resolved": "https://registry.npmjs.org/@types/recharts/-/recharts-1.8.23.tgz",
|
||||
"integrity": "sha512-O/mIPm9f6dwRWfenOI3GQwsGta3x1YWjwqXOCZqC0MATQ6C+A+Jc8VxFnSUr4N3uYv64zkq90RwXFaMNbhJKvg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/d3-shape": "^1",
|
||||
"@types/react": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/d3-path": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz",
|
||||
"integrity": "sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/d3-shape": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.8.tgz",
|
||||
"integrity": "sha512-gqfnMz6Fd5H6GOLYixOZP/xlrMtJms9BaS+6oWxTKHNqPGZ93BkWWupQSCYm6YHqx6h9wjRupuJb90bun6ZaYg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/d3-path": "^1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/resize-observer-browser": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz",
|
||||
@@ -48613,7 +48666,7 @@
|
||||
"next-themes": "0.0.15",
|
||||
"next-transpile-modules": "9.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"octokit": "*",
|
||||
"octokit": "^2.0.10",
|
||||
"openapi-types": "^12.0.2",
|
||||
"react": "17.0.2",
|
||||
"react-copy-to-clipboard": "^5.0.2",
|
||||
@@ -62566,6 +62619,7 @@
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/react-window-infinite-loader": "^1.0.5",
|
||||
"@types/recharts": "^1.8.23",
|
||||
"@types/semver": "^7.3.9",
|
||||
"@types/sqlstring": "^2.3.0",
|
||||
"@types/uuid": "^8.3.4",
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ run: download transform format
|
||||
# Init - prepare the environment
|
||||
###############################################################################
|
||||
|
||||
init:
|
||||
init:
|
||||
cd $(REPO_DIR)/parser && npm install
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ download: download.api.v1 download.auth.v1 download.storage.v1 download.tsdoc.v2
|
||||
|
||||
download.api.v1:
|
||||
curl -sS https://api.supabase.com/api/v1-json > $(REPO_DIR)/api_v0_openapi.json
|
||||
|
||||
|
||||
download.auth.v1:
|
||||
curl -sS https://supabase.github.io/gotrue/swagger.json > $(REPO_DIR)/auth_v1_openapi.json
|
||||
|
||||
@@ -95,7 +95,7 @@ combine.tsdoc.v2:
|
||||
> $(REPO_DIR)/enrichments/tsdoc_v2/combined.json
|
||||
|
||||
###############################################################################
|
||||
# Format everything - easier for git to track changes
|
||||
# Format everything - easier for git to track changes.
|
||||
###############################################################################
|
||||
format:
|
||||
npx prettier --write .
|
||||
|
||||
@@ -32,7 +32,7 @@ const PolicyRoles: FC<Props> = ({ roles, selectedRoles, onUpdateSelectedRoles })
|
||||
<MultiSelect
|
||||
options={formattedRoles}
|
||||
value={selectedRoles}
|
||||
placeholder="Defaults to all roles if none selected"
|
||||
placeholder="Defaults to all (public) roles if none selected"
|
||||
searchPlaceholder="Search for a role"
|
||||
onChange={onUpdateSelectedRoles}
|
||||
/>
|
||||
|
||||
@@ -52,7 +52,6 @@ const PAYGUsage: FC<Props> = ({ dateRange }) => {
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
interval="1d"
|
||||
highlight="maximum"
|
||||
defaultChartStyle="bar"
|
||||
customDateFormat={'MMM D, YYYY'}
|
||||
/>
|
||||
|
||||
@@ -42,14 +42,18 @@ const HelpPopover: FC<Props> = () => {
|
||||
</Button>
|
||||
</Link>
|
||||
<Link passHref href="https://supabase.com/docs/">
|
||||
<Button type="text" size="tiny" icon={<IconBookOpen />} as="a">
|
||||
Docs
|
||||
</Button>
|
||||
<a target="_blank" rel="noreferrer">
|
||||
<Button type="text" size="tiny" icon={<IconBookOpen />}>
|
||||
Docs
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
<Link passHref href="https://status.supabase.com/">
|
||||
<Button type="text" size="tiny" icon={<IconActivity />} as="a">
|
||||
Supabase Status
|
||||
</Button>
|
||||
<a target="_blank" rel="noreferrer">
|
||||
<Button type="text" size="tiny" icon={<IconActivity />}>
|
||||
Supabase Status
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<p className="text-sm text-scale-900">
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getWithTimeout } from 'lib/common/fetch'
|
||||
interface Props {}
|
||||
|
||||
const RestoringState: FC<Props> = ({}) => {
|
||||
const { app, ui } = useStore()
|
||||
const { app, ui, meta } = useStore()
|
||||
const project = ui.selectedProject
|
||||
const checkServerInterval = useRef<number>()
|
||||
|
||||
@@ -37,7 +37,7 @@ const RestoringState: FC<Props> = ({}) => {
|
||||
|
||||
const onConfirm = async () => {
|
||||
setLoading(true)
|
||||
await app.projects.fetchDetail(project?.ref ?? '')
|
||||
await app.projects.fetchDetail(project?.ref ?? '', (project) => meta.setProjectDetails(project))
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,9 +6,9 @@ import { Dictionary } from 'components/grid'
|
||||
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { BarChart, AreaChart } from './ChartRenderer'
|
||||
import { BarChart } from './ChartRenderer'
|
||||
import AreaChart from 'components/ui/Charts/AreaChart'
|
||||
import { ChartData } from './ChartHandler.types'
|
||||
import { AreaProps } from 'recharts'
|
||||
|
||||
interface Props {
|
||||
label: string
|
||||
@@ -19,7 +19,6 @@ interface Props {
|
||||
interval: string
|
||||
customDateFormat?: string
|
||||
children?: ReactNode
|
||||
highlight?: 'total' | 'average' | 'maximum'
|
||||
defaultChartStyle?: 'bar' | 'line'
|
||||
hideChartType?: boolean
|
||||
data?: ChartData
|
||||
@@ -27,7 +26,6 @@ interface Props {
|
||||
format?: string
|
||||
highlightedValue?: string | number
|
||||
onBarClick?: (v: any) => void
|
||||
areaType?: AreaProps['type']
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,7 +46,6 @@ const ChartHandler: FC<Props> = ({
|
||||
interval,
|
||||
customDateFormat,
|
||||
children = null,
|
||||
highlight,
|
||||
defaultChartStyle = 'bar',
|
||||
hideChartType = false,
|
||||
data,
|
||||
@@ -56,7 +53,6 @@ const ChartHandler: FC<Props> = ({
|
||||
format,
|
||||
highlightedValue,
|
||||
onBarClick,
|
||||
areaType,
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
@@ -172,10 +168,10 @@ const ChartHandler: FC<Props> = ({
|
||||
<AreaChart
|
||||
data={chartData?.data ?? []}
|
||||
format={format || chartData?.format}
|
||||
attribute={attribute}
|
||||
yAxisLimit={chartData?.yAxisLimit}
|
||||
xAxisKey="period_start"
|
||||
yAxisKey={attribute}
|
||||
highlightedValue={highlightedValue}
|
||||
label={label}
|
||||
title={label}
|
||||
customDateFormat={customDateFormat}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Loading } from 'ui'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
BarChart as RechartBarChart,
|
||||
AreaChart as RechartAreaChart,
|
||||
Area,
|
||||
Bar,
|
||||
XAxis,
|
||||
@@ -268,125 +267,4 @@ export function BarChart({
|
||||
</div>
|
||||
</Loading>
|
||||
)
|
||||
}
|
||||
|
||||
export function AreaChart({
|
||||
data,
|
||||
attribute,
|
||||
yAxisLimit,
|
||||
format,
|
||||
highlightedValue,
|
||||
customDateFormat,
|
||||
label,
|
||||
}: any) {
|
||||
const hasData = dataCheck(data, attribute)
|
||||
|
||||
const [focusBar, setFocusBar] = useState<any>(null)
|
||||
const [mouseLeave, setMouseLeave] = useState<any>(true)
|
||||
|
||||
const onMouseMove = (state: any) => {
|
||||
if (state?.activeTooltipIndex) {
|
||||
setFocusBar(state.activeTooltipIndex)
|
||||
setMouseLeave(false)
|
||||
} else {
|
||||
setFocusBar(null)
|
||||
setMouseLeave(true)
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseLeave = () => {
|
||||
setFocusBar(false)
|
||||
setMouseLeave(true)
|
||||
}
|
||||
|
||||
// For future reference: https://github.com/supabase/supabase/pull/5311#discussion_r800852828
|
||||
const chartHeight = 160
|
||||
|
||||
return (
|
||||
<Loading active={!data}>
|
||||
<Header
|
||||
label={label}
|
||||
attribute={attribute}
|
||||
focus={focusBar}
|
||||
highlightedValue={highlightedValue}
|
||||
data={data}
|
||||
format={format}
|
||||
customDateFormat={customDateFormat}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: `${chartHeight}px`,
|
||||
}}
|
||||
>
|
||||
{hasData ? (
|
||||
<>
|
||||
<ResponsiveContainer width="100%" height={chartHeight}>
|
||||
<RechartAreaChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
className="overflow-visible"
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={CHART_COLORS.GREEN_1} stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor={CHART_COLORS.GREEN_1} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis
|
||||
dataKey="period_start"
|
||||
//interval={size === 'small' ? 5 : 1}
|
||||
interval={data ? data.length - 2 : 0}
|
||||
angle={0}
|
||||
// stroke="#4B5563"
|
||||
tick={{
|
||||
fontSize: '0px',
|
||||
color: CHART_COLORS.TICK,
|
||||
}}
|
||||
axisLine={{
|
||||
stroke: CHART_COLORS.AXIS,
|
||||
}}
|
||||
tickLine={{
|
||||
stroke: CHART_COLORS.AXIS,
|
||||
}}
|
||||
/>
|
||||
{yAxisLimit && <YAxis type="number" domain={[0, yAxisLimit]} hide />}
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey={attribute}
|
||||
stroke={CHART_COLORS.GREEN_1}
|
||||
fillOpacity={1}
|
||||
fill="url(#colorUv)"
|
||||
/>
|
||||
</RechartAreaChart>
|
||||
</ResponsiveContainer>
|
||||
{data && (
|
||||
<div className="-mt-5 flex items-center justify-between text-xs text-scale-900">
|
||||
<span>
|
||||
{dayjs(data[0].period_start).format(
|
||||
customDateFormat ? customDateFormat : DATE_FORMAT__WITH_TIME
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
{dayjs(data[data?.length - 1]?.period_start).format(
|
||||
customDateFormat ? customDateFormat : DATE_FORMAT__WITH_TIME
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<EmptyState />
|
||||
)}
|
||||
</div>
|
||||
</Loading>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -40,9 +40,9 @@ export default function Introduction({ autoApiService, selectedLang }) {
|
||||
<h2 className="doc-heading">Client Libraries</h2>
|
||||
<div className="doc-section">
|
||||
<article className="text ">
|
||||
<p>Your API consists of both a RESTful interface and a realtime interface.</p>
|
||||
<p>Your API consists of both a RESTful interface and a Realtime interface.</p>
|
||||
<p>
|
||||
For interacting with the realtime streams, we provide client libraries that handle the
|
||||
For interacting with the Realtime streams, we provide client libraries that handle the
|
||||
websockets.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react'
|
||||
import { ComponentMeta } from '@storybook/react'
|
||||
import Chart, { AreaChartProps } from './AreaChart'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export default {
|
||||
title: 'Charts',
|
||||
} as ComponentMeta<typeof Chart>
|
||||
|
||||
const DATA = [290, 430, 649, 422, 321, 893, 111].map((value, index) => ({
|
||||
ram_usage: value,
|
||||
timestamp: dayjs().subtract(index, 'day').toISOString(),
|
||||
}))
|
||||
|
||||
const PROPS: AreaChartProps<typeof DATA[number]> = {
|
||||
title: 'Memory usage',
|
||||
xAxisKey: 'timestamp',
|
||||
yAxisKey: 'ram_usage',
|
||||
data: DATA,
|
||||
}
|
||||
|
||||
export const AreaChart = () => (
|
||||
<div className="flex flex-row gap-4 flex-wrap p-10">
|
||||
{[
|
||||
{ title: 'Normal', props: {} },
|
||||
{ title: 'No title', props: { title: undefined } },
|
||||
{ title: 'Minimal Header', props: { minimalHeader: true } },
|
||||
{ title: 'Minimal Highlighted Value', props: { minimalHeader: true, highlightedValue: 123 } },
|
||||
{ title: 'Highlighted Value', props: { highlightedValue: 123, highlightedLabel: "some custom label" } },
|
||||
{ title: 'Formatter - Auto 2 Precision for floats', props: { data: DATA.map(d=> ({...d, ram_usage: d.ram_usage + 0.1231233})) } },
|
||||
{ title: 'UTC Dates', props: { displayDateInUtc: true } },
|
||||
{ title: 'Custom date format', props: { customDateFormat: 'HH:mm' } },
|
||||
{ title: 'Value format', props: { format: 'c' } },
|
||||
{ title: 'Minimal Value format', props: { minimalHeader: true, format: 'c' } },
|
||||
{ title: 'No data', props: { data: [] } },
|
||||
].map(({ title, props }) => (
|
||||
<div className="w-72 h-72">
|
||||
<ExampleName>{title}</ExampleName>
|
||||
<div className="w-full h-full mt-4">
|
||||
<Chart {...PROPS} {...props} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
const ExampleName: React.FC = ({ children }) => (
|
||||
<h1 className="font-bold text-scale-1100">{children}</h1>
|
||||
)
|
||||
@@ -0,0 +1,125 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
AreaChart as RechartAreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts'
|
||||
import dayjs from 'dayjs'
|
||||
import { CHART_COLORS, DateTimeFormats } from 'components/ui/Charts/Charts.constants'
|
||||
import ChartHeader from './ChartHeader'
|
||||
import { Datum, CommonChartProps } from './Charts.types'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import ChartNoData from './NoDataPlaceholder'
|
||||
import { numberFormatter } from './Charts.utils'
|
||||
dayjs.extend(utc)
|
||||
|
||||
export interface AreaChartProps<D = Datum> extends CommonChartProps<D> {
|
||||
yAxisKey: string
|
||||
xAxisKey: string
|
||||
format?: string
|
||||
customDateFormat?: string
|
||||
displayDateInUtc?: boolean
|
||||
}
|
||||
|
||||
const AreaChart: React.FC<AreaChartProps> = ({
|
||||
data,
|
||||
yAxisKey,
|
||||
xAxisKey,
|
||||
format,
|
||||
customDateFormat = DateTimeFormats.FULL,
|
||||
title,
|
||||
highlightedValue,
|
||||
highlightedLabel,
|
||||
displayDateInUtc,
|
||||
minimalHeader,
|
||||
className = '',
|
||||
}) => {
|
||||
const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
|
||||
|
||||
// For future reference: https://github.com/supabase/supabase/pull/5311#discussion_r800852828
|
||||
const chartHeight = 160
|
||||
|
||||
if (data.length === 0) return <ChartNoData />
|
||||
|
||||
const day = (value: number | string) => (displayDateInUtc ? dayjs(value).utc() : dayjs(value))
|
||||
const resolvedHighlightedLabel =
|
||||
(focusDataIndex !== null &&
|
||||
data &&
|
||||
data[focusDataIndex] &&
|
||||
day(data[focusDataIndex][xAxisKey]).format(customDateFormat)) ||
|
||||
highlightedLabel
|
||||
|
||||
const resolvedHighlightedValue =
|
||||
(focusDataIndex !== null ? data[focusDataIndex]?.[yAxisKey] : null) || highlightedValue
|
||||
|
||||
return (
|
||||
<div className={['flex flex-col gap-3', className].join(' ')}>
|
||||
<ChartHeader
|
||||
title={title}
|
||||
format={format}
|
||||
customDateFormat={customDateFormat}
|
||||
highlightedValue={
|
||||
typeof resolvedHighlightedValue === 'number'
|
||||
? numberFormatter(resolvedHighlightedValue)
|
||||
: resolvedHighlightedValue
|
||||
}
|
||||
highlightedLabel={resolvedHighlightedLabel}
|
||||
minimalHeader={minimalHeader}
|
||||
/>
|
||||
<ResponsiveContainer width="100%" height={chartHeight}>
|
||||
<RechartAreaChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
className="overflow-visible"
|
||||
// mouse hover focusing logic
|
||||
onMouseMove={(e: any) => {
|
||||
if (e.activeTooltipIndex !== focusDataIndex) {
|
||||
setFocusDataIndex(e.activeTooltipIndex)
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => setFocusDataIndex(null)}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={CHART_COLORS.GREEN_1} stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor={CHART_COLORS.GREEN_1} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis
|
||||
dataKey={xAxisKey}
|
||||
interval={data.length - 2}
|
||||
angle={0}
|
||||
// hide the tick
|
||||
tick={{ fontSize: '0px' }}
|
||||
// color the axis
|
||||
axisLine={{ stroke: CHART_COLORS.AXIS }}
|
||||
tickLine={{ stroke: CHART_COLORS.AXIS }}
|
||||
/>
|
||||
<Tooltip content={() => null} />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey={yAxisKey}
|
||||
stroke={CHART_COLORS.GREEN_1}
|
||||
fillOpacity={1}
|
||||
fill="url(#colorUv)"
|
||||
/>
|
||||
</RechartAreaChart>
|
||||
</ResponsiveContainer>
|
||||
{data && (
|
||||
<div className="text-scale-900 -mt-5 flex items-center justify-between text-xs">
|
||||
<span>{dayjs(data[0][xAxisKey]).format(customDateFormat)}</span>
|
||||
<span>{dayjs(data[data?.length - 1]?.[xAxisKey]).format(customDateFormat)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default AreaChart
|
||||
@@ -0,0 +1,54 @@
|
||||
import { DateTimeFormats } from './Charts.constants'
|
||||
|
||||
export interface ChartHeaderProps {
|
||||
title?: string
|
||||
format?: string
|
||||
customDateFormat?: string
|
||||
minimalHeader?: boolean
|
||||
displayDateInUtc?: boolean
|
||||
highlightedLabel?: number | string | null
|
||||
highlightedValue?: number | string | null
|
||||
}
|
||||
const ChartHeader: React.FC<ChartHeaderProps> = ({
|
||||
format,
|
||||
highlightedValue,
|
||||
highlightedLabel,
|
||||
title,
|
||||
minimalHeader = false,
|
||||
}) => {
|
||||
const chartTitle = (
|
||||
<h3 className={'text-scale-900 ' + (minimalHeader ? 'text-xs' : 'text-sm')}>{title}</h3>
|
||||
)
|
||||
|
||||
const highlighted = (
|
||||
<h5
|
||||
className={`text-scale-1200 text-xl font-normal ${minimalHeader ? 'text-base' : 'text-2xl'}`}
|
||||
>
|
||||
{highlightedValue}
|
||||
<span className="text-lg">{format}</span>
|
||||
</h5>
|
||||
)
|
||||
const label = <h5 className="text-scale-900 text-xs">{highlightedLabel}</h5>
|
||||
|
||||
if (minimalHeader) {
|
||||
return (
|
||||
<div className="flex flex-row items-center gap-x-4" style={{ minHeight: '1.8rem' }}>
|
||||
{chartTitle}
|
||||
<div className="flex flex-row items-baseline gap-x-2">
|
||||
{highlightedValue && highlighted}
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-16">
|
||||
{chartTitle}
|
||||
{highlightedValue && highlighted}
|
||||
{label}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChartHeader
|
||||
@@ -17,3 +17,8 @@ export const USAGE_COLORS = {
|
||||
404: 'var(--colors-amber7)',
|
||||
500: 'var(--colors-red9)',
|
||||
}
|
||||
|
||||
export enum DateTimeFormats {
|
||||
FULL = 'MMM D, YYYY, hh:mma',
|
||||
DATE_ONLY = 'MMM D, YYYY',
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
export interface CommonChartProps<D>
|
||||
extends Pick<
|
||||
HeaderType<D>,
|
||||
| 'highlightedValue'
|
||||
| 'highlightedLabel'
|
||||
| 'customDateFormat'
|
||||
| 'data'
|
||||
| 'format'
|
||||
| 'minimalHeader'
|
||||
| 'displayDateInUtc'
|
||||
> {
|
||||
title?: string;
|
||||
className?: string
|
||||
}
|
||||
|
||||
export type HeaderType<D> = {
|
||||
attribute: string
|
||||
focus: number | null
|
||||
format?: string
|
||||
highlightedValue?: number | string
|
||||
highlightedLabel?: string
|
||||
data: D[]
|
||||
customDateFormat?: string
|
||||
label: string
|
||||
minimalHeader?: boolean
|
||||
displayDateInUtc?: boolean
|
||||
}
|
||||
|
||||
export interface Datum {
|
||||
[attribute: string]: number | string
|
||||
}
|
||||
|
||||
export interface TimeseriesDatum extends Datum {
|
||||
timestamp: string
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
/**
|
||||
* Auto formats a number to a default precision if it is a float
|
||||
*
|
||||
* @example
|
||||
* numberFormatter(123) // "123"
|
||||
* numberFormatter(123.123) // "123.12"
|
||||
* numberFormatter(123, 2) // "123.00"
|
||||
*/
|
||||
export const numberFormatter = (num: number, precision = 2) => isFloat(num) ? precisionFormatter(num, precision) : String(num);
|
||||
|
||||
|
||||
/**
|
||||
* Tests if a number is a float.
|
||||
*
|
||||
* @example
|
||||
* isFloat(123) // false
|
||||
* isFloat(123.123) // true
|
||||
*/
|
||||
export const isFloat = (num: number) => String(num).includes(".")
|
||||
|
||||
|
||||
/**
|
||||
* Formats a number to a particular precision.
|
||||
*
|
||||
* @example
|
||||
* precisionFormatter(123, 2) // "123.00"
|
||||
* precisionFormatter(123.123, 2) // "123.12"
|
||||
*/
|
||||
export const precisionFormatter = (num: number, precision: number): string => {
|
||||
if (isFloat(num)) {
|
||||
const [head, tail] = String(num).split(".")
|
||||
return head + "." + tail.slice(0, precision)
|
||||
} else {
|
||||
// pad int with 0
|
||||
return String(num) + "." + ("0".repeat(precision))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { IconBarChart2 } from '@supabase/ui'
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
message?: string
|
||||
className?: string
|
||||
}
|
||||
const NoDataPlaceholder: React.FC<Props> = ({
|
||||
title = 'No data to show',
|
||||
message,
|
||||
className = '',
|
||||
}) => (
|
||||
<div
|
||||
className={
|
||||
'border-scale-600 flex h-full w-full flex-col items-center justify-center space-y-2 border border-dashed text-center ' +
|
||||
className
|
||||
}
|
||||
>
|
||||
<IconBarChart2 className="text-scale-800" />
|
||||
<div>
|
||||
<p className="text-scale-1100 text-xs">{title}</p>
|
||||
{message && <p className="text-scale-900 text-xs">{message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
export default NoDataPlaceholder
|
||||
Generated
+51111
File diff suppressed because it is too large.
Load diff
@@ -113,6 +113,7 @@
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/react-window-infinite-loader": "^1.0.5",
|
||||
"@types/recharts": "^1.8.23",
|
||||
"@types/semver": "^7.3.9",
|
||||
"@types/sqlstring": "^2.3.0",
|
||||
"@types/uuid": "^8.3.4",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { isFloat, numberFormatter, precisionFormatter } from "components/ui/Charts/Charts.utils"
|
||||
|
||||
|
||||
test("isFloat", () => {
|
||||
expect(isFloat(123)).toBe(false)
|
||||
expect(isFloat(123.123)).toBe(true)
|
||||
})
|
||||
test("numberFormatter", () => {
|
||||
expect(numberFormatter(123)).toBe("123")
|
||||
expect(numberFormatter(123.123)).toBe("123.12")
|
||||
})
|
||||
|
||||
test("precisionFormatter", () => {
|
||||
expect(precisionFormatter(123, 1)).toBe("123.0")
|
||||
expect(precisionFormatter(123.12345, 4)).toBe("123.1234")
|
||||
})
|
||||
Reference in new issue
Block a user