Merge pull request #9280 from supabase/master

Prod deploy Studio
This commit is contained in:
Alaister Young authored and GitHub committed 2022-10-02 21:43:52 +11:00
commit a7e9cf50b2
38 files changed
+781 -472

No files matched your search

@@ -5,25 +5,39 @@ description: Convenience helpers for implementing user authentication in SvelteK
sidebar_label: SvelteKit
---
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
This submodule provides convenience helpers for implementing user authentication in [SvelteKit](https://kit.svelte.dev/) applications.
## Installation
Using [npm](https://npmjs.org):
This library supports Node.js `^16.15.0`.
<Tabs
groupId="install"
defaultValue="npm"
values={[
{label: 'npm', value: 'npm'},
{label: 'Yarn', value: 'yarn'},
]}>
<TabItem value="npm">
```sh
npm install @supabase/auth-helpers-sveltekit
```
Using [yarn](https://yarnpkg.com/):
</TabItem>
<TabItem value="yarn">
```sh
yarn add @supabase/auth-helpers-sveltekit
```
This library supports the following tooling versions:
- Node.js: `^16.15.0`
</TabItem>
</Tabs>
## Getting Started
@@ -37,12 +51,11 @@ PUBLIC_SUPABASE_URL=https://your-project.supabase.co
PUBLIC_SUPABASE_ANON_KEY=your-anon-key
```
### SupabaseClient setup
### Set up the Supabase client
We will start off by creating a `db.ts` file inside of our `src/lib` directory. Now lets instantiate our `supabaseClient`.
Start off by creating a `db.ts` file inside of the `src/lib` directory and instantiate the `supabaseClient`.
```ts
// src/lib/db.ts
```ts title="src/lib/db.ts"
import { createClient } from '@supabase/supabase-js';
import { setupSupabaseHelpers } from '@supabase/auth-helpers-sveltekit';
import { dev } from '$app/environment';
@@ -65,12 +78,11 @@ setupSupabaseHelpers({
### Initialize the client
Edit your `+layout.svelte` file and set up the client side.
Edit your `+layout.svelte` file and set up the client-side.
```html
<!-- src/routes/+layout.svelte -->
```html title="src/routes/+layout.svelte"
<script lang="ts">
// we need to make sure the supabase instance is initialized on the client
// make sure the supabase instance is initialized on the client
import '$lib/db';
import { startSupabaseSessionSync } from '@supabase/auth-helpers-sveltekit';
import { page } from '$app/stores';
@@ -86,14 +98,12 @@ Edit your `+layout.svelte` file and set up the client side.
<slot />
```
### Hooks setup
### Set up hooks
Our `hooks.ts` file is where the heavy lifting of this library happens:
The `hooks.ts` file is where the heavy lifting of this library happens:
```ts
// src/hooks.server.ts
// we need to make sure the supabase instance is initialized on the server
```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';
@@ -109,18 +119,17 @@ export const handle = sequence(auth(), yourHandler);
There are three handle methods available:
- `callback()`:
This will create 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.
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 will parse the session from the cookie and populate it in locals
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 (pages, layouts) we need to pass the session in the root layout load function:
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
// src/routes/+layout.server.ts
```ts title="src/routes/+layout.server.ts"
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals }) => {
@@ -132,10 +141,9 @@ export const load: LayoutServerLoad = async ({ locals }) => {
### 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.
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.
```ts
// src/app.d.ts
```ts title="src/app.d.ts"
/// <reference types="@sveltejs/kit" />
// See https://kit.svelte.dev/docs/types#app
@@ -159,8 +167,7 @@ This library has provided a method to delete the session cookie.
Create a server only page with an action to delete the cookie
```ts
// src/routes/logout/+page.server.ts
```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';
@@ -184,12 +191,11 @@ Then make a **POST** request with a form to the server only page above.
</form>
```
### Basic Setup
### Basic setup
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
<!-- src/routes/+page.svelte -->
```html title="src/routes/+page.svelte"
<script>
import { page } from '$app/stores';
</script>
@@ -230,8 +236,7 @@ For [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row
## Server-side data fetching with RLS
```html
<!-- src/routes/profile/+page.svelte -->
```html title="src/routes/profile/+page.svelte"
<script>
/** @type {import('./$types').PageData} */
export let data;
@@ -245,8 +250,7 @@ For [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row
For [row level security](https://supabase.com/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
// src/routes/profile/+page.ts
```ts title="src/routes/profile/+page.ts"
import type { PageLoad } from './$types';
import { withAuth } from '@supabase/auth-helpers-sveltekit';
import { redirect } from '@sveltejs/kit';
@@ -291,8 +295,7 @@ await getSupabaseClient().from('table2').select();
Wrap an API Route to check that the user has a valid session. If they're not logged in the session is `null`.
```ts
// src/routes/api/protected-route/+server.ts
```ts title="src/routes/api/protected-route/+server.ts"
import type { RequestHandler } from './$types';
import { withAuth } from '@supabase/auth-helpers-sveltekit';
import { json, redirect } from '@sveltejs/kit';
@@ -318,10 +321,9 @@ If you visit `/api/protected-route` without a valid session cookie, you will get
## Protecting Actions
Wrap an Action to check that the user has a valid session. If they're not logged in the session is `null`.
Wrap an Action to check that the user has a valid session. If they're not logged in, the session is `null`.
```ts
// src/routes/posts/+page.server.ts
```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';
@@ -419,10 +421,9 @@ export const actions: Actions = {
## Custom session namespace
If you wan´t to use something else than `locals.session` and `$page.data.session` you can do so by updating the types and creating three helper functions:
If you want to use something other than `locals.session` and `$page.data.session`, update the types and create three helper functions:
```ts
// src/app.d.ts
```ts title="src/app.d.ts"
declare namespace App {
interface Locals {
mySupabaseSession: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
@@ -431,8 +432,9 @@ declare namespace App {
mySupabaseSession: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
}
}
```
// src/hooks.server.ts
```ts title="src/hooks.server.ts"
setupSupabaseServer({
supabaseClient,
cookieOptions: {
@@ -442,8 +444,9 @@ setupSupabaseServer({
getSessionFromLocals: (locals) => locals.mySupabaseSession,
setSessionToLocals: (locals, session) => (locals.mySupabaseSession = session)
});
```
// src/lib/db.ts
```ts title="src/lib/db.ts"
setupSupabaseClient({
supabaseClient,
// --- change location within pageData ---
@@ -451,23 +454,26 @@ setupSupabaseClient({
});
```
## Migration from 0.6.11 and below to 0.7.0
## Migrate from 0.6.11 and below to 0.7.0 {#migration}
There are quite a lot of breaking changes in the latest 0.7.0 version of this library.
There are numerous breaking changes in the latest 0.7.0 version of this library.
### Environment variable prefix has changed
### Environment variable prefix
0.6.11 and below: `VITE_`
The environment variable prefix is now `PUBLIC_` instead of `VITE_` (e.g., `VITE_SUPABASE_URL` is now `PUBLIC_SUPABASE_URL`).
0.7.0: `PUBLIC_`
### Set up the Supabase client {#migration-set-up-supabase-client}
e.g. `VITE_SUPABASE_URL` is now `PUBLIC_SUPABASE_URL`
<Tabs
groupId="version"
values={[
{label: '0.6.11 and below', value: 'older'},
{label: '0.7.0', value: 'latest'},
]}>
### Setup Supabase client
<TabItem value="older">
0.6.11 and below:
```js
// src/lib/db.ts
```js title="src/lib/db.ts"
import { createSupabaseClient } from '@supabase/auth-helpers-sveltekit';
const { supabaseClient } = createSupabaseClient(
@@ -478,9 +484,11 @@ const { supabaseClient } = createSupabaseClient(
export { supabaseClient };
```
0.7.0:
```js
// src/lib/db.ts
</TabItem>
<TabItem value="latest">
```js title="src/lib/db.ts"
import { createClient } from '@supabase/supabase-js';
import { setupSupabaseHelpers } from '@supabase/auth-helpers-sveltekit';
import { dev } from '$app/environment';
@@ -502,11 +510,21 @@ setupSupabaseHelpers({
});
```
### Initialize client side code
</TabItem>
</Tabs>
0.6.11 and below:
```html
<!-- src/routes/__layout.svelte -->
### Initialize the client {#migration-initialize-client}
<Tabs
groupId="version"
values={[
{label: '0.6.11 and below', value: 'older'},
{label: '0.7.0', value: 'latest'},
]}>
<TabItem value="older">
```html title="src/routes/__layout.svelte"
<script>
import { session } from '$app/stores';
import { supabaseClient } from '$lib/db';
@@ -518,11 +536,15 @@ import { SupaAuthHelper } from '@supabase/auth-helpers-svelte';
</SupaAuthHelper>
```
0.7.0:
```html
<!-- src/routes/+layout.svelte -->
</TabItem>
<TabItem value="latest">
The `@supabase/auth-helpers-svelte` library is no longer required as the `@supabase/auth-helpers-sveltekit` library handles all the client-side code.
```html title="src/routes/+layout.svelte"
<script lang="ts">
// we need to make sure the supabase instance is initialized on the client
// make sure the supabase instance is initialized on the client
import '$lib/db';
import { startSupabaseSessionSync } from '@supabase/auth-helpers-sveltekit';
import { page } from '$app/stores';
@@ -538,13 +560,21 @@ import { SupaAuthHelper } from '@supabase/auth-helpers-svelte';
<slot />
```
> We no longer require the @supabase/auth-helpers-svelte library as the @supabase/auth-helpers-sveltekit library handles all the client side code too
</TabItem>
</Tabs>
### Hooks setup
### Set up hooks {#migration-set-up-hooks}
0.6.11 and below:
```ts
// src/hooks.ts
<Tabs
groupId="version"
values={[
{label: '0.6.11 and below', value: 'older'},
{label: '0.7.0', value: 'latest'},
]}>
<TabItem value="older">
```ts title="src/hooks.ts"
import { handleAuth } from '@supabase/auth-helpers-sveltekit';
import type { GetSession, Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
@@ -561,10 +591,12 @@ export const getSession: GetSession = async (event) => {
}
```
0.7.0:
```ts
// src/hooks.server.ts
// we need to make sure the supabase instance is initialized on the server
</TabItem>
<TabItem value="latest">
```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';
@@ -573,9 +605,9 @@ export const handle = auth();
```
**Optional** *if using additional handle methods*
```ts
// src/hooks.server.ts
// we need to make sure the supabase instance is initialized on the server
```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';
@@ -584,10 +616,21 @@ import { sequence } from '@sveltejs/kit/hooks';
export const handle = sequence(auth(), yourHandler);
```
### Typings
0.6.11 and below:
```ts
// src/app.d.ts
</TabItem>
</Tabs>
### Typings {#migration-typings}
<Tabs
groupId="version"
values={[
{label: '0.6.11 and below', value: 'older'},
{label: '0.7.0', value: 'latest'},
]}>
<TabItem value="older">
```ts title="src/app.d.ts"
/// <reference types="@sveltejs/kit" />
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
@@ -608,9 +651,11 @@ declare namespace App {
}
```
0.7.0:
```ts
// src/app.d.ts
</TabItem>
<TabItem value="latest">
```ts title="src/app.d.ts"
/// <reference types="@sveltejs/kit" />
// See https://kit.svelte.dev/docs/types#app
@@ -630,11 +675,21 @@ declare namespace App {
}
```
### Checking user on the client
</TabItem>
</Tabs>
0.6.11 and below:
```html
<!-- src/routes/index.svelte -->
### Check the user on the client
<Tabs
groupId="version"
values={[
{label: '0.6.11 and below', value: 'older'},
{label: '0.7.0', value: 'latest'},
]}>
<TabItem value="older">
```html title="src/routes/index.svelte"
<script>
import { session } from '$app/stores';
</script>
@@ -647,9 +702,11 @@ declare namespace App {
{/if}
```
0.7.0:
```html
<!-- src/routes/+page.svelte -->
</TabItem>
<TabItem value="latest">
```html title="src/routes/+page.svelte"
<script>
import { page } from '$app/stores';
</script>
@@ -662,11 +719,21 @@ declare namespace App {
{/if}
```
</TabItem>
</Tabs>
### withPageAuth
0.6.11 and below:
```html
<!-- // src/routes/protected-route.svelte -->
<Tabs
groupId="version"
values={[
{label: '0.6.11 and below', value: 'older'},
{label: '0.7.0', value: 'latest'},
]}>
<TabItem value="older">
```html title="src/routes/protected-route.svelte"
<script lang="ts" context="module">
import { supabaseServerClient, withPageAuth } from '@supabase/auth-helpers-sveltekit';
import type { Load } from './__types/protected-page';
@@ -696,9 +763,11 @@ export let user;
<pre>{JSON.stringify(user, null, 2)}</pre>
```
0.7.0:
```html
<!-- src/routes/protected-route/+page.svelte -->
</TabItem>
<TabItem value="latest">
```html title="src/routes/protected-route/+page.svelte"
<script lang="ts">
import type { PageData } from './$types';
@@ -714,8 +783,7 @@ export let user;
<pre>{JSON.stringify(user, null, 2)}</pre>
```
```ts
// src/routes/protected-route/+page.ts
```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';
@@ -730,11 +798,21 @@ export const load: PageLoad = withAuth(async ({ session, getSupabaseClient }) =>
});
```
</TabItem>
</Tabs>
### withApiAuth
0.6.11 and below:
```ts
// src/routes/api/protected-route.ts
<Tabs
groupId="version"
values={[
{label: '0.6.11 and below', value: 'older'},
{label: '0.7.0', value: 'latest'},
]}>
<TabItem value="older">
```ts title="src/routes/api/protected-route.ts"
import { supabaseServerClient, withApiAuth } from "@supabase/auth-helpers-sveltekit";
import type { RequestHandler } from "./__types/protected-route";
@@ -759,10 +837,11 @@ export const GET: RequestHandler<GetOutput> = async ({ locals, request }) =>
});
```
</TabItem>
0.7.0:
```ts
// src/routes/api/protected-route/+server.ts
<TabItem value="latest">
```ts title="src/routes/api/protected-route/+server.ts"
import type { RequestHandler } from './$types';
import { withAuth } from '@supabase/auth-helpers-sveltekit';
import { json, redirect } from '@sveltejs/kit';
@@ -785,6 +864,9 @@ export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient
);
```
</TabItem>
</Tabs>
## Additional Links
- [Auth Helpers Source code](https://github.com/supabase/auth-helpers)
+3
View File
@@ -70,5 +70,8 @@ ENABLE_PHONE_AUTOCONFIRM=true
# Studio - Configuration for the Dashboard
############
STUDIO_ORGANIZATION_NAME=Default Organization
STUDIO_PROJECT_NAME=Default Project
STUDIO_PORT=3000
PUBLIC_REST_URL=http://localhost:8000/rest/v1/ # replace if you intend to use Studio outside of localhost
+3
View File
@@ -17,6 +17,9 @@ services:
STUDIO_PG_META_URL: http://meta:8080
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
DEFAULT_ORGANIZATION: ${STUDIO_DEFAULT_ORGANIZATION}
DEFAULT_PROJECT: ${STUDIO_DEFAULT_PROJECT}
SUPABASE_URL: http://kong:8000
SUPABASE_REST_URL: ${PUBLIC_REST_URL}
SUPABASE_ANON_KEY: ${ANON_KEY}
+3
View File
@@ -1,6 +1,9 @@
STUDIO_PG_META_URL=http://localhost:8000/pg
POSTGRES_PASSWORD=your-super-secret-and-long-postgres-password
DEFAULT_ORGANIZATION_NAME=Default Organization
DEFAULT_PROJECT_NAME=Default Project
SUPABASE_URL=http://localhost:8000
SUPABASE_REST_URL=http://localhost:8000/rest/v1/
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
+7
View File
@@ -67,3 +67,10 @@ Then run the following commands to install dependencies and start the dashboard.
npm install
npm run dev
```
If you would like to configure different defaults for "Default Organization" and "Default Project", you will need to update the `.env` in the studio folder with the corresponding values.
```
DEFAULT_ORGANIZATION_NAME=
DEFAULT_PROJECT_NAME=
```
+13 -3
View File
@@ -79,10 +79,20 @@ export function initTable(
? onLoadStorage(props.storageRef, table.name, table.schema)
: undefined
// Load sort and filters via URL param only if given
// Check for saved state on initial load and also, load sort and filters via URL param only if given
// Otherwise load from local storage to resume user session
if (sort === undefined && filter === undefined && (savedState?.sorts || savedState?.filters)) {
return { savedState: { sorts: savedState.sorts, filters: savedState.filters } }
if (
!state.isInitialComplete &&
sort === undefined &&
filter === undefined &&
(savedState?.sorts || savedState?.filters)
) {
return {
savedState: {
sorts: savedState.sorts,
filters: savedState.filters,
},
}
}
const gridColumns = getGridColumns(table, {
@@ -1,9 +1,10 @@
import * as React from 'react'
import { FC, useCallback, KeyboardEvent, ReactNode } from 'react'
type BlockKeysProps = {
interface BlockKeysProps {
value: string | null
children: ReactNode
onEscape?: (value: string | null) => void
onEnter?: (value: string | null) => void
value: string | null
}
/**
@@ -11,9 +12,9 @@ type BlockKeysProps = {
* We use this with cell editor to allow editor component to handle keys.
* Example: press enter to add newline on textEditor
*/
export const BlockKeys: React.FC<BlockKeysProps> = ({ onEscape, onEnter, value, children }) => {
const handleKeyDown = React.useCallback(
(ev: React.KeyboardEvent<HTMLDivElement>) => {
export const BlockKeys: FC<BlockKeysProps> = ({ value, children, onEscape, onEnter }) => {
const handleKeyDown = useCallback(
(ev: KeyboardEvent<HTMLDivElement>) => {
switch (ev.key) {
case 'Escape':
ev.stopPropagation()
@@ -21,7 +22,10 @@ export const BlockKeys: React.FC<BlockKeysProps> = ({ onEscape, onEnter, value,
break
case 'Enter':
ev.stopPropagation()
if (onEnter) onEnter(value)
if (!ev.shiftKey && onEnter) {
ev.preventDefault()
onEnter(value)
}
break
}
},
@@ -29,7 +33,7 @@ export const BlockKeys: React.FC<BlockKeysProps> = ({ onEscape, onEnter, value,
)
function onBlur() {
if (onEscape) onEscape(value)
if (onEnter) onEnter(value)
}
return (
@@ -1,4 +1,4 @@
import * as React from 'react'
import { useState, useCallback } from 'react'
import { EditorProps } from '@supabase/react-data-grid'
import { useTrackedState } from '../../store'
@@ -11,18 +11,22 @@ export function JsonEditor<TRow, TSummaryRow = unknown>({
onRowChange,
}: EditorProps<TRow, TSummaryRow>) {
const state = useTrackedState()
const [isPopoverOpen, setIsPopoverOpen] = React.useState(true)
const [isPopoverOpen, setIsPopoverOpen] = useState(true)
const gridColumn = state.gridColumns.find((x) => x.name == column.key)
const initialValue = row[column.key as keyof TRow] as unknown
const jsonString = initialValue ? JSON.stringify(initialValue) : ''
const prettyJsonValue = prettifyJSON(jsonString)
const [value, setValue] = React.useState<string | null>(prettyJsonValue)
const [value, setValue] = useState<string | null>(prettyJsonValue)
const onEscape = React.useCallback((newValue: string | null) => {
commitChange(newValue)
const cancelChanges = useCallback(() => {
onRowChange(row, true)
setIsPopoverOpen(false)
}, [])
const saveChanges = useCallback((newValue: string | null) => {
commitChange(newValue)
}, [])
function onChange(_value: string | undefined) {
if (!_value || _value == '') setValue(null)
else setValue(_value)
@@ -31,12 +35,14 @@ export function JsonEditor<TRow, TSummaryRow = unknown>({
function commitChange(newValue: string | null) {
if (!newValue) {
onRowChange({ ...row, [column.key]: null }, true)
setIsPopoverOpen(false)
} else if (verifyJSON(newValue)) {
const jsonValue = JSON.parse(newValue)
onRowChange({ ...row, [column.key]: jsonValue }, true)
setIsPopoverOpen(false)
} else {
const { onError } = state
if (onError) onError(Error('invalid input'))
if (onError) onError(Error('Please enter a valid JSON'))
}
}
@@ -48,13 +54,17 @@ export function JsonEditor<TRow, TSummaryRow = unknown>({
sideOffset={-35}
className="rounded-none"
overlay={
<BlockKeys value={value} onEscape={onEscape}>
<BlockKeys value={value} onEscape={cancelChanges} onEnter={saveChanges}>
<MonacoEditor
width={`${gridColumn?.width || column.width}px`}
value={value ?? ''}
language="json"
onChange={onChange}
/>
<div className="flex items-center justify-end p-2 bg-scale-400 space-x-2">
<p className="text-xs text-scale-1100">Save changes</p>
<code className="text-xs">⏎</code>
</div>
</BlockKeys>
}
>
@@ -1,4 +1,4 @@
import * as React from 'react'
import { useState, useCallback } from 'react'
import { EditorProps } from '@supabase/react-data-grid'
import { useTrackedState } from '../../store'
import { BlockKeys, MonacoEditor, NullValue, EmptyValue } from '../common'
@@ -10,12 +10,17 @@ export function TextEditor<TRow, TSummaryRow = unknown>({
onRowChange,
}: EditorProps<TRow, TSummaryRow>) {
const state = useTrackedState()
const [isPopoverOpen, setIsPopoverOpen] = React.useState(true)
const [isPopoverOpen, setIsPopoverOpen] = useState(true)
const gridColumn = state.gridColumns.find((x) => x.name == column.key)
const initialValue = row[column.key as keyof TRow] as unknown as string
const [value, setValue] = React.useState<string | null>(initialValue)
const [value, setValue] = useState<string | null>(initialValue)
const onEscape = React.useCallback((newValue: string | null) => {
const cancelChanges = useCallback(() => {
onRowChange(row, true)
setIsPopoverOpen(false)
}, [])
const saveChanges = useCallback((newValue: string | null) => {
onRowChange({ ...row, [column.key]: newValue }, true)
setIsPopoverOpen(false)
}, [])
@@ -33,12 +38,16 @@ export function TextEditor<TRow, TSummaryRow = unknown>({
sideOffset={-35}
className="rounded-none"
overlay={
<BlockKeys value={value} onEscape={onEscape}>
<BlockKeys value={value} onEscape={cancelChanges} onEnter={saveChanges}>
<MonacoEditor
width={`${gridColumn?.width || column.width}px`}
value={value ?? ''}
onChange={onChange}
/>
<div className="flex items-center justify-end p-2 bg-scale-400 space-x-2">
<p className="text-xs text-scale-1100">Save changes</p>
<code className="text-xs">⏎</code>
</div>
</BlockKeys>
}
>
@@ -1,4 +1,4 @@
import { FC, ReactNode, useState } from 'react'
import { FC, useState } from 'react'
import { observer } from 'mobx-react-lite'
import {
Input,
@@ -10,66 +10,40 @@ import {
IconTrash,
} from '@supabase/ui'
import Table from '../../../to-be-cleaned/Table'
import { useStore } from 'hooks'
import Table from 'components/to-be-cleaned/Table'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
import { PostgresTable } from '@supabase/postgres-meta'
const Header: FC<{
filterString: string
filterPlaceholder: string
leftComponents: ReactNode
rightComponents: ReactNode
setFilterString: (value: string) => void
}> = ({
filterString,
setFilterString,
filterPlaceholder = 'Filter',
leftComponents,
rightComponents,
}) => (
<div className="flex items-center justify-between">
<div className="flex items-center">
<div>{leftComponents}</div>
<div>
<Input
size="small"
placeholder={filterPlaceholder}
value={filterString}
onChange={(e: any) => setFilterString(e.target.value)}
icon={<IconSearch size="tiny" />}
/>
</div>
</div>
<div className="">{rightComponents}</div>
</div>
)
const ColumnList: FC<{
selectedTable: any
interface Props {
selectedTable: PostgresTable
onSelectBack: () => void
onAddColumn: () => void
onEditColumn: (column: any) => void
onDeleteColumn: (column: any) => void
}> = ({
}
const ColumnList: FC<Props> = ({
selectedTable,
onSelectBack = () => {},
onAddColumn = () => {},
onEditColumn = () => {},
onDeleteColumn = () => {},
}) => {
const { meta } = useStore()
const [filterString, setFilterString] = useState<string>('')
const columns =
filterString.length === 0
? selectedTable.columns
: selectedTable.columns.filter((column: any) => column.name.includes(filterString))
const isLocked = meta.excludedSchemas.includes(selectedTable?.schema ?? '')
return (
<>
<div className="mb-4">
<Header
filterPlaceholder="Filter columns"
filterString={filterString}
setFilterString={setFilterString}
leftComponents={
<div className="flex items-center justify-between">
<div className="flex items-center">
<div className="flex items-center mr-4">
<Button
type="outline"
@@ -80,13 +54,24 @@ const ColumnList: FC<{
/>
<code>{selectedTable.name}</code>
</div>
}
rightComponents={
<Button icon={<IconPlus />} onClick={() => onAddColumn()}>
New
</Button>
}
/>
<div>
<Input
size="small"
placeholder="Filter columns"
value={filterString}
onChange={(e: any) => setFilterString(e.target.value)}
icon={<IconSearch size="tiny" />}
/>
</div>
</div>
{!isLocked && (
<div>
<Button icon={<IconPlus />} onClick={() => onAddColumn()}>
New column
</Button>
</div>
)}
</div>
</div>
{columns.length === 0 ? (
<NoSearchResults />
@@ -111,8 +96,12 @@ const ColumnList: FC<{
<Table.td>
<p>{x.name}</p>
</Table.td>
<Table.td>
<p>{x.comment}</p>
<Table.td className="break-all whitespace-normal">
{x.comment !== null ? (
<p title={x.comment}>{x.comment}</p>
) : (
<p className="text-scale-800">No description</p>
)}
</Table.td>
<Table.td>
<code className="text-sm">{x.data_type}</code>
@@ -127,12 +116,14 @@ const ColumnList: FC<{
icon={<IconEdit3 />}
style={{ padding: 5 }}
type="text"
disabled={isLocked}
/>
<Button
onClick={() => onDeleteColumn(x)}
icon={<IconTrash />}
style={{ padding: 5 }}
type="text"
disabled={isLocked}
/>
</div>
</Table.td>
@@ -1,155 +1,195 @@
import { FC, ReactNode, useState } from 'react'
import { FC, useState } from 'react'
import { observer } from 'mobx-react-lite'
import {
Button,
IconPlus,
Input,
Listbox,
IconSearch,
IconTrash,
IconEdit3,
IconColumns,
IconLock,
} from '@supabase/ui'
import { partition } from 'lodash'
import { useStore } from 'hooks'
import Table from 'components/to-be-cleaned/Table'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
import { PostgresTable, PostgresSchema } from '@supabase/postgres-meta'
const Header: FC<{
filterString: string
filterPlaceholder: string
leftComponents: ReactNode
rightComponents: ReactNode
setFilterString: (value: string) => void
}> = ({
filterString,
setFilterString,
filterPlaceholder = 'Filter',
leftComponents,
rightComponents,
}) => (
<div className="flex items-center justify-between">
<div className="flex items-center">
<div>{leftComponents}</div>
<div>
<Input
size="small"
placeholder={filterPlaceholder}
value={filterString}
onChange={(e: any) => setFilterString(e.target.value)}
icon={<IconSearch size="tiny" />}
/>
</div>
</div>
<div className="">{rightComponents}</div>
</div>
)
const TableList: FC<{
interface Props {
selectedSchema: string
onSelectSchema: (schema: string) => void
onAddTable: () => void
onEditTable: (table: any) => void
onDeleteTable: (table: any) => void
onOpenTable: (table: any) => void
}> = observer(
({
onAddTable = () => {},
onEditTable = () => {},
onDeleteTable = () => {},
onOpenTable = () => {},
}) => {
const { meta } = useStore()
const [filterString, setFilterString] = useState<string>('')
const tables =
filterString.length === 0
? meta.tables.list((table: any) => table.schema === 'public')
: meta.tables.list(
(table: any) => table.schema === 'public' && table.name.includes(filterString)
)
}
return (
<>
<div>
{/* @ts-ignore */}
<Header
filterString={filterString}
setFilterString={setFilterString}
rightComponents={
<Button icon={<IconPlus />} onClick={() => onAddTable()}>
New
</Button>
}
/>
</div>
{tables.length === 0 ? (
<NoSearchResults />
) : (
<div className="my-4 w-full">
<Table
head={[
<Table.th key="name">Name</Table.th>,
<Table.th key="schema">Schema</Table.th>,
<Table.th key="description" className="hidden lg:table-cell">
Description
</Table.th>,
<Table.th key="rows" className="hidden xl:table-cell">
Rows (Estimated)
</Table.th>,
<Table.th key="size" className="hidden xl:table-cell">
Size (Estimated)
</Table.th>,
<Table.th key="buttons"></Table.th>,
]}
body={tables.map((x: any, i: any) => (
<Table.tr key={x.id} hoverable>
<Table.td>
<p>{x.name}</p>
</Table.td>
<Table.td>
<p>{x.schema}</p>
</Table.td>
<Table.td className=" hidden max-w-sm truncate lg:table-cell">
<p>{x.comment}</p>
</Table.td>
<Table.td className=" hidden xl:table-cell">
<code className="text-sm">{x.live_rows_estimate ?? x.live_row_count}</code>
</Table.td>
<Table.td className=" hidden xl:table-cell">
<code className="text-sm">{x.size}</code>
</Table.td>
<Table.td>
<div className="flex justify-end gap-2">
<Button
iconRight={<IconColumns />}
type="default"
className="whitespace-nowrap hover:border-gray-500"
style={{ paddingTop: 3, paddingBottom: 3 }}
onClick={() => {
onOpenTable(x)
}}
>
{x.columns.length} columns
</Button>
<Button
onClick={() => onEditTable(x)}
icon={<IconEdit3 />}
style={{ padding: 5 }}
type="text"
/>
<Button
onClick={() => onDeleteTable(x)}
icon={<IconTrash />}
style={{ padding: 5 }}
type="text"
/>
</div>
</Table.td>
</Table.tr>
const TableList: FC<Props> = ({
selectedSchema,
onSelectSchema = () => {},
onAddTable = () => {},
onEditTable = () => {},
onDeleteTable = () => {},
onOpenTable = () => {},
}) => {
const { meta } = useStore()
const [filterString, setFilterString] = useState<string>('')
const schemas: PostgresSchema[] = meta.schemas.list()
const [protectedSchemas, openSchemas] = partition(schemas, (schema) =>
meta.excludedSchemas.includes(schema?.name ?? '')
)
const allTables: PostgresTable[] = meta.tables.list(
(table: PostgresTable) => table.schema === selectedSchema
)
const tables =
filterString.length === 0
? allTables
: // @ts-ignore
allTables.filter((table: PostgresTable) => table.name.includes(filterString))
// @ts-ignore
const schema = schemas.find((schema) => schema.name === selectedSchema)
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
return (
<>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="w-[230px]">
<Listbox
size="small"
value={selectedSchema}
onChange={onSelectSchema}
icon={isLocked && <IconLock size={16} strokeWidth={2} />}
>
<Listbox.Option disabled key="normal-schemas" value="normal-schemas" label="Schemas">
<p className="text-sm">Schemas</p>
</Listbox.Option>
{/* @ts-ignore */}
{openSchemas.map((schema) => (
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <span className="text-scale-900">schema</span>}
>
<span className="text-scale-1200 text-sm">{schema.name}</span>
</Listbox.Option>
))}
<Listbox.Option
disabled
key="protected-schemas"
value="protected-schemas"
label="Protected schemas"
>
<p className="text-sm">Protected schemas</p>
</Listbox.Option>
{protectedSchemas.map((schema) => (
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <span className="text-scale-900">schema</span>}
>
<span className="text-scale-1200 text-sm">{schema.name}</span>
</Listbox.Option>
))}
</Listbox>
</div>
<div>
<Input
size="small"
placeholder="Filter tables"
value={filterString}
onChange={(e: any) => setFilterString(e.target.value)}
icon={<IconSearch size="tiny" />}
/>
</div>
</div>
{!isLocked && (
<div>
<Button icon={<IconPlus />} onClick={() => onAddTable()}>
New table
</Button>
</div>
)}
</>
)
}
)
</div>
{tables.length === 0 ? (
<NoSearchResults />
) : (
<div className="my-4 w-full">
<Table
head={[
<Table.th key="name">Name</Table.th>,
<Table.th key="description" className="hidden lg:table-cell">
Description
</Table.th>,
<Table.th key="rows" className="hidden xl:table-cell">
Rows (Estimated)
</Table.th>,
<Table.th key="size" className="hidden xl:table-cell">
Size (Estimated)
</Table.th>,
<Table.th key="buttons"></Table.th>,
]}
body={tables.map((x: any, i: any) => (
<Table.tr key={x.id}>
<Table.td>
<p title={x.name}>{x.name}</p>
</Table.td>
<Table.td className="hidden max-w-sm truncate lg:table-cell break-all whitespace-normal">
{x.comment !== null ? (
<p title={x.comment}>{x.comment}</p>
) : (
<p className="text-scale-800">No description</p>
)}
</Table.td>
<Table.td className="hidden xl:table-cell">
<code className="text-sm">{x.live_rows_estimate ?? x.live_row_count}</code>
</Table.td>
<Table.td className="hidden xl:table-cell">
<code className="text-sm">{x.size}</code>
</Table.td>
<Table.td>
<div className="flex justify-end gap-2">
<Button
type="default"
iconRight={<IconColumns />}
className="whitespace-nowrap hover:border-gray-500"
style={{ paddingTop: 3, paddingBottom: 3 }}
onClick={() => onOpenTable(x)}
>
{x.columns.length} columns
</Button>
<Button
type="text"
icon={<IconEdit3 />}
style={{ padding: 5 }}
disabled={isLocked}
onClick={() => onEditTable(x)}
/>
<Button
type="text"
icon={<IconTrash />}
style={{ padding: 5 }}
disabled={isLocked}
onClick={() => onDeleteTable(x)}
/>
</div>
</Table.td>
</Table.tr>
))}
/>
</div>
)}
</>
)
}
export default TableList
export default observer(TableList)
@@ -30,7 +30,7 @@ const ExampleProject: FC<Props> = ({ framework, title, description, url }) => {
<img
className="transition-all group-hover:scale-110"
src={`/img/libraries/${framework.toLowerCase()}${
framework.toLowerCase() == 'nextjs' ? (isDarkTheme ? '-dark' : '') : ''
['expo', 'nextjs'].includes(framework.toLowerCase()) ? (isDarkTheme ? '-dark' : '') : ''
}-icon.svg`}
alt={`${framework} logo`}
width={26}
@@ -17,7 +17,6 @@ const APIKeys = () => {
const availableLanguages = [
{ name: 'Javascript', key: 'js' },
{ name: 'Python', key: 'python' },
{ name: 'Dart', key: 'dart' },
]
const [selectedLanguage, setSelectedLanguage] = useState(availableLanguages[0])
@@ -2,7 +2,14 @@ import Link from 'next/link'
import { FC } from 'react'
import SVG from 'react-inlinesvg'
import { useRouter } from 'next/router'
import { Button, IconKey, IconArchive, IconExternalLink, IconCode } from '@supabase/ui'
import {
Button,
IconKey,
IconArchive,
IconExternalLink,
IconCode,
IconActivity,
} from '@supabase/ui'
import Panel from 'components/ui/Panel'
import APIKeys from './APIKeys'
@@ -18,7 +25,7 @@ const NewProjectPanel: FC<Props> = ({}) => {
<div className="grid grid-cols-12 gap-4 lg:gap-20">
<div className="col-span-12">
<div className="flex flex-col space-y-20">
<div className="flex flex-col justify-between h-full">
<div className="flex h-full flex-col justify-between">
<div className="space-y-2">
<h3 className="text-xl text-scale-1200">Welcome to your new project</h3>
<p className="text-base text-scale-1100">
@@ -29,7 +36,7 @@ const NewProjectPanel: FC<Props> = ({}) => {
</div>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-12 lg:col-span-7 space-y-8 flex flex-col justify-center">
<div className="col-span-12 flex flex-col justify-center space-y-8 lg:col-span-7">
<div className="space-y-2">
<h3 className="text-xl text-scale-1200">
Get started by building out your database
@@ -40,7 +47,7 @@ const NewProjectPanel: FC<Props> = ({}) => {
you need something more.
</p>
</div>
<div className="flex items-center gap-2 flex-wrap">
<div className="flex flex-wrap items-center gap-2">
<Link href={`/project/${ref}/editor`}>
<a>
<Button
@@ -91,7 +98,7 @@ const NewProjectPanel: FC<Props> = ({}) => {
</div>
</div>
<div className="flex flex-col justify-between h-full space-y-6">
<div className="flex h-full flex-col justify-between space-y-6">
<div className="max-w-2xl space-y-2">
<h3 className="text-xl text-scale-1200">Explore our other products</h3>
<p className="text-base text-scale-1100">
@@ -99,21 +106,21 @@ const NewProjectPanel: FC<Props> = ({}) => {
it completely, or just the features you need.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 lg:gap-8">
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-4 md:gap-y-0 xl:grid-cols-4">
<Panel>
<Panel.Content className="flex flex-col space-y-4 lg:h-40">
<Panel.Content className="flex flex-col space-y-4 md:px-3">
<div className="flex items-center space-x-3">
<div className="bg-scale-600 text-scale-1000 shadow-sm rounded p-1.5">
<div className="rounded bg-scale-600 p-1.5 text-scale-1000 shadow-sm">
<IconKey strokeWidth={2} size={16} />
</div>
<h5>Authentication</h5>
</div>
<div className="flex flex-grow">
<p className="text-scale-1000 text-sm">
<div className="flex flex-grow md:min-h-[50px] xl:min-h-[75px]">
<p className="text-sm text-scale-1000">
A complete user management system that works without any additional tools.
</p>
</div>
<div className="space-x-2 flex items-center">
<div className="flex items-center space-x-2">
<Link href={`/project/${ref}/auth/users`}>
<a>
<Button type="default">Explore Auth</Button>
@@ -135,19 +142,19 @@ const NewProjectPanel: FC<Props> = ({}) => {
</Panel>
<Panel>
<Panel.Content className="flex flex-col space-y-4 lg:h-40">
<Panel.Content className="flex flex-col space-y-4 md:px-3">
<div className="flex items-center space-x-3">
<div className="bg-scale-600 text-scale-1000 shadow-sm rounded p-1.5">
<div className="rounded bg-scale-600 p-1.5 text-scale-1000 shadow-sm">
<IconArchive strokeWidth={2} size={16} />
</div>
<h5>Storage</h5>
</div>
<div className="flex flex-grow">
<p className="text-scale-1000 text-sm">
<div className="flex md:min-h-[50px] xl:min-h-[75px]">
<p className="text-sm text-scale-1000">
Store, organize, and serve any file types of any size from multiple buckets.
</p>
</div>
<div className="space-x-2 flex items-center">
<div className="flex items-center space-x-2">
<Link href={`/project/${ref}/storage/buckets`}>
<a>
<Button type="default">Explore Storage</Button>
@@ -169,20 +176,20 @@ const NewProjectPanel: FC<Props> = ({}) => {
</Panel>
<Panel>
<Panel.Content className="flex flex-col space-y-4 lg:h-40">
<Panel.Content className="flex flex-col space-y-4 md:px-3">
<div className="flex items-center space-x-3">
<div className="bg-scale-600 text-scale-1000 shadow-sm rounded p-1.5">
<div className="rounded bg-scale-600 p-1.5 text-scale-1000 shadow-sm">
<IconCode strokeWidth={2} size={16} />
</div>
<h5>Edge Functions</h5>
</div>
<div className="flex flex-grow">
<p className="text-scale-1000 text-sm">
<div className="flex md:min-h-[50px] xl:min-h-[75px]">
<p className="text-sm text-scale-1000">
Write custom code without deploying or scaling servers, with fast deploy times
and low latency.
</p>
</div>
<div className="space-x-2 flex items-center">
<div className="flex items-center space-x-2">
<Link href={`/project/${ref}/functions`}>
<a>
<Button type="default">Explore Functions</Button>
@@ -202,6 +209,34 @@ const NewProjectPanel: FC<Props> = ({}) => {
</div>
</Panel.Content>
</Panel>
<Panel>
<Panel.Content className="flex flex-col space-y-4 md:px-3">
<div className="flex items-center space-x-4">
<div className="rounded bg-scale-600 p-1.5 text-scale-1000 shadow-sm">
<IconActivity strokeWidth={2} size={16} />
</div>
<h5>Realtime</h5>
</div>
<div className="flex md:min-h-[50px] xl:min-h-[75px]">
<p className="text-sm text-scale-1000">
Listen to your to PostgreSQL database in realtime via websockets.
</p>
</div>
<div className="flex items-center space-x-2">
<Link href="https://supabase.com/docs/guides/realtime">
<a>
<Button
className="translate-y-[1px]"
icon={<IconExternalLink size={14} />}
type="default"
>
About Realtime
</Button>
</a>
</Link>
</div>
</Panel.Content>
</Panel>
</div>
</div>
</div>
@@ -211,14 +246,14 @@ const NewProjectPanel: FC<Props> = ({}) => {
<div className="space-y-6">
<div className="space-y-2">
<h3 className="text-xl text-scale-1200">Connecting to your new project</h3>
<p className="text-base lg:max-w-sm text-scale-1100">
<p className="text-base text-scale-1100 lg:max-w-sm">
Interact with your database through the{' '}
<Link href="https://supabase.com/docs/reference">
<a className="text-brand-900">Supabase client libraries</a>
</Link>{' '}
with your API keys.
</p>
<p className="text-base lg:max-w-sm text-scale-1100">
<p className="text-base text-scale-1100 lg:max-w-sm">
More information about your project's keys can be found in your project's API
settings.
</p>
@@ -122,7 +122,7 @@ const ColumnEditor: FC<Props> = ({
if (isEmpty(errors)) {
const payload = isNewRecord
? generateCreateColumnPayload(selectedTable.id, columnFields)
: generateUpdateColumnPayload(column!, columnFields)
: generateUpdateColumnPayload(column!, selectedTable, columnFields)
const foreignKey = columnFields.foreignKey
? { ...columnFields.foreignKey, source_column_name: columnFields.name }
: undefined
@@ -177,6 +177,16 @@ const ColumnEditor: FC<Props> = ({
value={columnFields?.comment ?? ''}
onChange={(event: any) => onUpdateField({ comment: event.target.value })}
/>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-12 md:col-span-8 md:col-start-5">
<Checkbox
label="Is Primary Key"
description="A primary key indicates that a column or group of columns can be used as a unique identifier for rows in the table."
checked={columnFields?.isPrimaryKey ?? false}
onChange={() => onUpdateField({ isPrimaryKey: !columnFields?.isPrimaryKey })}
/>
</div>
</div>
</div>
</SidePanel.Content>
@@ -184,18 +194,6 @@ const ColumnEditor: FC<Props> = ({
<SidePanel.Content>
<div className="space-y-10 py-6">
{isNewRecord && !hasPrimaryKey && (
<div className="grid grid-cols-12 gap-4">
<div className="col-span-12 md:col-span-8 md:col-start-5">
<Checkbox
label={hasPrimaryKey ? 'Add to composite primary key' : 'Is Primary Key'}
description="A primary key indicates that a column or group of columns can be used as a unique identifier for rows in the table."
checked={columnFields?.isPrimaryKey ?? false}
onChange={() => onUpdateField({ isPrimaryKey: !columnFields?.isPrimaryKey })}
/>
</div>
</div>
)}
<ColumnForeignKey
column={columnFields}
originalForeignKey={originalForeignKey}
@@ -216,8 +214,12 @@ const ColumnEditor: FC<Props> = ({
/>
<div className="text-sm grid md:grid-cols-12 md:gap-x-4 ">
<p className='text-scale-1100 col-span-7 col-start-5 -mt-4'>
<a href="https://supabase.com/docs/guides/database/tables#data-types" target="_blank" className="underline inline-block gap-1 p-2 rounded-sm">
<p className="text-scale-1100 col-span-7 col-start-5 -mt-4">
<a
href="https://supabase.com/docs/guides/database/tables#data-types"
target="_blank"
className="underline inline-block gap-1 p-2 rounded-sm"
>
Learn more about data types
</a>
</p>
@@ -103,8 +103,13 @@ export const generateCreateColumnPayload = (
export const generateUpdateColumnPayload = (
originalColumn: PostgresColumn,
table: PostgresTable,
field: ColumnField
): Partial<UpdateColumnPayload> => {
// @ts-ignore
const primaryKeyColumns = table.primary_keys.map((key) => key.name)
const isOriginallyPrimaryKey = primaryKeyColumns.includes(originalColumn.name)
// Only append the properties which are getting updated
const defaultValue = field.defaultValue
const type = field.isArray ? `${field.format}[]` : field.format
@@ -136,6 +141,9 @@ export const generateUpdateColumnPayload = (
if (!isEqual(originalColumn.is_unique, field.isUnique)) {
payload.isUnique = field.isUnique
}
if (!isEqual(isOriginallyPrimaryKey, field.isPrimaryKey)) {
payload.isPrimaryKey = field.isPrimaryKey
}
return payload
}
@@ -28,7 +28,7 @@ const InputField: FC<Props> = ({
<Select
size="medium"
layout="horizontal"
value={field.value}
value={field.value ?? ''}
label={field.name}
labelOptional={field.format}
descriptionText={field.comment}
@@ -51,7 +51,7 @@ const InputField: FC<Props> = ({
<Input
layout="horizontal"
label={field.name}
value={field.value}
value={field.value ?? ''}
// @ts-ignore This is creating some validateDOMNesting errors
// because descriptionText is a <p> element as a parent
descriptionText={
@@ -73,7 +73,7 @@ const InputField: FC<Props> = ({
onChange={(event: any) => onUpdateField({ [field.name]: event.target.value })}
actions={
<Button
disabled={field.value.length === 0}
disabled={field.value === null || field.value?.length === 0}
type="default"
htmlType="button"
onClick={onViewForeignKey}
@@ -98,7 +98,7 @@ const InputField: FC<Props> = ({
disabled={!isEditable}
error={errors[field.name]}
rows={5}
value={field.value}
value={field.value ?? ''}
placeholder={
typeof field.defaultValue === 'string' && field.defaultValue.length === 0
? 'Default: Empty string'
@@ -114,7 +114,7 @@ const InputField: FC<Props> = ({
return (
<Input
layout="horizontal"
value={field.value}
value={field.value ?? ''}
label={field.name}
descriptionText={field.comment}
labelOptional={field.format}
@@ -141,7 +141,7 @@ const InputField: FC<Props> = ({
<DateTimeInput
name={field.name}
format={field.format}
value={field.value}
value={field.value ?? ''}
description={field.comment}
onChange={(value: any) => onUpdateField({ [field.name]: value })}
/>
@@ -155,7 +155,7 @@ const InputField: FC<Props> = ({
descriptionText={field.comment}
labelOptional={field.format}
error={errors[field.name]}
value={field.value}
value={field.value ?? ''}
placeholder={field.isIdentity ? 'Automatically generated as identity' : field.defaultValue}
disabled={!isEditable}
onChange={(event: any) => onUpdateField({ [field.name]: event.target.value })}
@@ -17,7 +17,7 @@ export interface RowField {
comment: string
format: string
enums: string[]
value: string
value: string | null
defaultValue: string
foreignKey?: PostgresRelationship
isNullable: boolean
@@ -60,16 +60,16 @@ export const generateRowFields = (
export const validateFields = (fields: RowField[]) => {
const errors = {} as any
fields.forEach((field) => {
if (field.format.startsWith('_') && field.value?.length > 0) {
if (field.format.startsWith('_') && (field.value?.length ?? 0) > 0) {
try {
minifyJSON(field.value)
minifyJSON(field.value ?? '')
} catch {
errors[field.name] = 'Invalid array'
}
}
if (field.format.includes('json') && field.value?.length > 0) {
if (field.format.includes('json') && (field.value?.length ?? 0) > 0) {
try {
minifyJSON(field.value)
minifyJSON(field.value ?? '')
} catch {
errors[field.name] = 'Invalid JSON'
}
@@ -120,7 +120,11 @@ const SidePanelEditor: FC<Props> = ({
resolve: any
) => {
const response = isNewRecord
? await meta.createColumn(payload as CreateColumnPayload, foreignKey)
? await meta.createColumn(
payload as CreateColumnPayload,
selectedTable as PostgresTable,
foreignKey
)
: await meta.updateColumn(
configuration.columnId as string,
payload as UpdateColumnPayload,
@@ -25,6 +25,7 @@ export interface UpdateColumnPayload {
isIdentity?: boolean
isNullable?: boolean
isUnique?: boolean
isPrimaryKey?: boolean
identityGeneration?: 'BY DEFAULT' | 'ALWAYS'
}
@@ -231,9 +231,9 @@ const Column: FC<Props> = ({
</div>
{!hasImportContent && (
<div className="flex w-[5%] justify-end">
<div className="cursor-pointer" onClick={() => onRemoveColumn()}>
<button className="cursor-pointer" onClick={() => onRemoveColumn()}>
<IconX strokeWidth={1} />
</div>
</button>
</div>
)}
</div>
@@ -1,6 +1,15 @@
import Link from 'next/link'
import React, { FC, useState } from 'react'
import { partition, isEmpty, isUndefined } from 'lodash'
import { Alert, Button, IconEdit, IconHelpCircle, IconKey, IconTrash } from '@supabase/ui'
import {
Alert,
Button,
IconEdit,
IconExternalLink,
IconHelpCircle,
IconKey,
IconTrash,
} from '@supabase/ui'
import {
PostgresTable,
PostgresColumn,
@@ -41,7 +50,7 @@ const ColumnManagement: FC<Props> = ({
tables = [],
columns = [],
enumTypes = [],
importContent = {},
importContent,
isNewRecord,
onColumnsUpdated = () => {},
onSelectImportData = () => {},
@@ -316,11 +325,17 @@ const ColumnManagement: FC<Props> = ({
Add column
</Button>
)}
<p className='text-sm text-scale-1100'>
<a href="https://supabase.com/docs/guides/database/tables#data-types" target="_blank" className="underline flex items-center gap-1">
Learn more about data types
<Link href="https://supabase.com/docs/guides/database/tables#data-types">
<a>
<Button
type="text"
className="text-scale-1000 hover:text-scale-1200"
icon={<IconExternalLink size={12} strokeWidth={2} />}
>
Learn more about data types
</Button>
</a>
</p>
</Link>
</div>
</div>
<ForeignKeySelector
@@ -39,76 +39,75 @@ export const generateDatabaseMenu = (project?: Project): ProductMenuGroup[] => {
},
...(IS_PLATFORM
? [
{
title: 'Logs and Usage',
items: [
{
name: 'API logs',
key: 'api-logs',
url: `/project/${ref}/database/api-logs`,
items: [],
},
...(reportsOverview
? [
{
name: 'API usage',
key: 'api-usage',
url: `/project/${ref}/database/api-usage`,
items: [],
},
]
: []),
{
name: 'Postgres logs',
key: 'postgres-logs',
url: `/project/${ref}/database/postgres-logs`,
items: [],
},
...(logsRealtime
? [
{
name: 'Realtime logs',
key: 'realtime-logs',
url: `/project/${ref}/database/realtime-logs`,
items: [],
},
]
: []),
],
},
]
{
title: 'Logs and Usage',
items: [
{
name: 'API logs',
key: 'api-logs',
url: `/project/${ref}/database/api-logs`,
items: [],
},
...(reportsOverview
? [
{
name: 'API usage',
key: 'api-usage',
url: `/project/${ref}/database/api-usage`,
items: [],
},
]
: []),
{
name: 'Postgres logs',
key: 'postgres-logs',
url: `/project/${ref}/database/postgres-logs`,
items: [],
},
...(logsRealtime
? [
{
name: 'Realtime logs',
key: 'realtime-logs',
url: `/project/${ref}/database/realtime-logs`,
items: [],
},
]
: []),
],
},
]
: []),
...(IS_PLATFORM
? [
{
title: 'Alpha Preview',
isPreview: true,
items: [
{
name: 'Triggers',
key: 'triggers',
url: `/project/${ref}/database/triggers`,
items: [],
},
{
name: 'Functions',
key: 'functions',
url: `/project/${ref}/database/functions`,
items: [],
},
...(showHooksRoute
? [
{
name: 'Database Webhooks',
key: 'hooks',
url: `/project/${ref}/database/hooks`,
items: [],
},
]
: []),
],
},
]
{
title: 'Alpha Preview',
items: [
{
name: 'Database Triggers',
key: 'triggers',
url: `/project/${ref}/database/triggers`,
items: [],
},
{
name: 'Database Functions',
key: 'functions',
url: `/project/${ref}/database/functions`,
items: [],
},
...(showHooksRoute
? [
{
name: 'Database Webhooks',
key: 'hooks',
url: `/project/${ref}/database/hooks`,
items: [],
},
]
: []),
],
},
]
: []),
]
}
@@ -104,8 +104,8 @@ const TableEditorMenu: FC<Props> = ({
<div className="my-6 mx-4 flex flex-grow flex-col space-y-6">
{/* Schema selection dropdown */}
<div className="px-3">
{meta.schemas.isLoading ? (
<div className="flex h-[30px] items-center space-x-3 rounded border border-gray-500 px-3">
{!meta.schemas.isInitialized ? (
<div className="flex h-[26px] items-center space-x-3 rounded border border-gray-500 px-3">
<IconLoader className="animate-spin" size={12} />
<span className="text-scale-900 text-xs">Loading schemas...</span>
</div>
@@ -121,7 +121,7 @@ const TableEditorMenu: FC<Props> = ({
}}
>
<Listbox.Option disabled key="normal-schemas" value="normal-schemas" label="Schemas">
<p className="text-sm">Schemas</p>
<p className="text-xs text-scale-1100">Schemas</p>
</Listbox.Option>
{/* @ts-ignore */}
{openSchemas.map((schema) => (
@@ -129,9 +129,9 @@ const TableEditorMenu: FC<Props> = ({
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <span className="text-scale-900">schema</span>}
addOnBefore={() => <span className="text-scale-900 text-xs">schema</span>}
>
<span className="text-scale-1200 text-sm">{schema.name}</span>
<span className="text-scale-1200 text-xs">{schema.name}</span>
</Listbox.Option>
))}
<Listbox.Option
@@ -140,16 +140,16 @@ const TableEditorMenu: FC<Props> = ({
value="protected-schemas"
label="Protected schemas"
>
<p className="text-sm">Protected schemas</p>
<p className="text-xs text-scale-1100">Protected schemas</p>
</Listbox.Option>
{protectedSchemas.map((schema) => (
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <span className="text-scale-900">schema</span>}
addOnBefore={() => <span className="text-scale-900 text-xs">schema</span>}
>
<span className="text-scale-1200 text-sm">{schema.name}</span>
<span className="text-scale-1200 text-xs">{schema.name}</span>
</Listbox.Option>
))}
</Listbox>
@@ -290,7 +290,7 @@ const UtilityTabResults = observer(() => {
<div className="bg-table-header-light dark:bg-table-header-dark">
<p className="m-0 border-0 px-6 py-4 text-sm text-scale-1000">
Click <code>RUN</code> or hit{' '}
<code>{window.navigator.platform.match(/^Mac/) ? 'cmd' : 'ctrl'} + ⏎</code> to execute
<code>{window.navigator.platform.match(/^Mac/) ? '⌘' : 'Ctrl'} + Enter</code> to execute
your query.
</p>
</div>
+4 -2
View File
@@ -16,11 +16,13 @@ export function usePermissions(profile?: any, returning?: 'minimal') {
data: data,
error,
mutate,
} = useSWR<any>(profile !== undefined ? url : null, get, { loadingTimeout: 10000 })
} = useSWR<any>(profile !== undefined && IS_PLATFORM ? url : null, get, {
loadingTimeout: 10000,
})
const anyError = data?.error || error
return {
permissions: anyError ? undefined : data,
permissions: IS_PLATFORM ? (anyError ? undefined : data) : [],
isLoading: !anyError && !data,
isError: !!anyError,
mutate,
+3 -2
View File
@@ -1,6 +1,7 @@
import apiWrapper from 'lib/api/apiWrapper'
import { NextApiRequest, NextApiResponse } from 'next'
import apiWrapper from 'lib/api/apiWrapper'
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -20,7 +21,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
const response = [
{
id: 1,
name: 'Default Organization',
name: process.env.DEFAULT_ORGANIZATION_NAME || 'Default Organization',
slug: 'default-org-slug',
billing_email: 'billing@supabase.co',
},
+4 -3
View File
@@ -1,7 +1,8 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { IS_PLATFORM } from 'lib/constants'
import { createEncryptedDbConnectionString } from 'lib/api/apiHelpers'
import apiWrapper from 'lib/api/apiWrapper'
import { createEncryptedDbConnectionString } from 'lib/api/apiHelpers'
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
@@ -28,14 +29,14 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
organizations: [
{
id: 1,
name: 'Default Organization',
name: process.env.DEFAULT_ORGANIZATION_NAME || 'Default Organization',
slug: 'default-org-slug',
billing_email: 'billing@supabase.co',
projects: [
{
id: 1,
ref: 'default',
name: 'Default Project',
name: process.env.DEFAULT_PROJECT_NAME || 'Default Project',
organization_id: 1,
cloud_provider: 'localhost',
status: 'ACTIVE_HEALTHY',
+3 -2
View File
@@ -1,6 +1,7 @@
import apiWrapper from 'lib/api/apiWrapper'
import { NextApiRequest, NextApiResponse } from 'next'
import apiWrapper from 'lib/api/apiWrapper'
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -20,7 +21,7 @@ const handleGet = async (req: NextApiRequest, res: NextApiResponse) => {
const response = {
id: 1,
ref: 'default',
name: 'Default Project',
name: process.env.DEFAULT_PROJECT_NAME || 'Default Project',
organization_id: 1,
cloud_provider: 'localhost',
status: 'ACTIVE_HEALTHY',
+3 -2
View File
@@ -1,6 +1,7 @@
import apiWrapper from 'lib/api/apiWrapper'
import { NextApiRequest, NextApiResponse } from 'next'
import apiWrapper from 'lib/api/apiWrapper'
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -21,7 +22,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
{
id: 1,
ref: 'default',
name: 'Default Project',
name: process.env.DEFAULT_PROJECT_NAME || 'Default Project',
organization_id: 1,
cloud_provider: 'localhost',
status: 'ACTIVE_HEALTHY',
+2 -1
View File
@@ -1,4 +1,5 @@
import { NextApiRequest, NextApiResponse } from 'next'
import apiWrapper from 'lib/api/apiWrapper'
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
@@ -27,7 +28,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
db_user: 'postgres',
id: 1,
inserted_at: undefined,
name: 'Default Project',
name: process.env.DEFAULT_PROJECT_NAME || 'Default Project',
ref: 'default',
status: 'ACTIVE_HEALTHY',
services: [
@@ -1,4 +1,5 @@
import { NextApiRequest, NextApiResponse } from 'next'
import apiWrapper from 'lib/api/apiWrapper'
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
@@ -21,7 +22,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
project: {
id: 1,
ref: 'default',
name: 'Default Project',
name: process.env.DEFAULT_PROJECT_NAME || 'Default Project',
status: 'ACTIVE_HEALTHY',
organization_id: 1,
inserted_at: '2021-08-02T06:40:40.646362',
@@ -1,4 +1,5 @@
import { NextApiRequest, NextApiResponse } from 'next'
import apiWrapper from 'lib/api/apiWrapper'
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
@@ -29,7 +30,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
id: 1,
inserted_at: undefined,
jwt_secret: '-',
name: 'Default Project',
name: process.env.DEFAULT_PROJECT_NAME || 'Default Project',
ref: 'default',
region: 'ap-southeast-1',
status: 'ACTIVE_HEALTHY',
@@ -16,8 +16,10 @@ const DatabaseTables: NextPageWithLayout = () => {
const { meta, ui } = useStore()
const [isDeleting, setIsDeleting] = useState<boolean>(false)
const [selectedSchema, setSelectedSchema] = useState('public')
const [selectedTable, setSelectedTable] = useState<any>()
const [sidePanelKey, setSidePanelKey] = useState<'column' | 'table'>()
const [selectedColumnToEdit, setSelectedColumnToEdit] = useState<PostgresColumn>()
const [selectedTableToEdit, setSelectedTableToEdit] = useState<PostgresTable>()
@@ -121,6 +123,8 @@ const DatabaseTables: NextPageWithLayout = () => {
<div className="p-4">
{isUndefined(selectedTable) ? (
<TableList
selectedSchema={selectedSchema}
onSelectSchema={setSelectedSchema}
onAddTable={onAddTable}
onEditTable={onEditTable}
onDeleteTable={onDeleteTable}
+23 -1
View File
@@ -8,7 +8,7 @@ import { PostgresTable, PostgresColumn } from '@supabase/postgres-meta'
import Base64 from 'lib/base64'
import { tryParseJson } from 'lib/helpers'
import { useStore, withAuth } from 'hooks'
import { useStore, withAuth, useUrlState } from 'hooks'
import { Dictionary } from 'components/grid'
import { TableEditorLayout } from 'components/layouts'
import { TableGridEditor } from 'components/interfaces'
@@ -17,6 +17,7 @@ import ConfirmationModal from 'components/ui/ConfirmationModal'
const TableEditorPage: NextPage = () => {
const router = useRouter()
const { id }: any = router.query
const [_, setParams] = useUrlState({ arrayKeys: ['filter', 'sort'] })
const { meta, ui } = useStore()
const [selectedSchema, setSelectedSchema] = useState<string>()
@@ -98,11 +99,32 @@ const TableEditorPage: NextPage = () => {
setSidePanelKey(undefined)
}
const removeDeletedColumnFromFiltersAndSorts = (columnName: string) => {
setParams((prevParams) => {
const existingFilters = (prevParams?.filter ?? []) as string[]
const existingSorts = (prevParams?.sort ?? []) as string[]
return {
...prevParams,
filter: existingFilters.filter((filter: string) => {
const [column] = filter.split(':')
if (column !== columnName) return filter
}),
sort: existingSorts.filter((sort: string) => {
const [column] = sort.split(':')
if (column !== columnName) return sort
}),
}
})
}
const onConfirmDeleteColumn = async () => {
try {
const response: any = await meta.columns.del(selectedColumnToDelete!.id)
if (response.error) throw response.error
removeDeletedColumnFromFiltersAndSorts(selectedColumnToDelete!.name)
await meta.tables.loadById(selectedColumnToDelete!.table_id)
} catch (error: any) {
ui.setNotification({
+4 -2
View File
@@ -3,7 +3,7 @@ import { Project } from 'types'
import { IRootStore } from '../RootStore'
import { constructHeaders } from 'lib/api/apiHelpers'
import { get } from 'lib/common/fetch'
import { PROJECT_STATUS } from 'lib/constants'
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
import PostgresMetaInterface, { IPostgresMetaInterface } from '../common/PostgresMetaInterface'
import pingPostgrest from 'lib/pingPostgrest'
@@ -57,7 +57,9 @@ export default class ProjectStore extends PostgresMetaInterface<Project> {
this.data[project.id] = project
// lazy fetches
this.fetchSubscriptionTier(project)
if (IS_PLATFORM) {
this.fetchSubscriptionTier(project)
}
}
}
+56 -9
View File
@@ -84,12 +84,17 @@ export interface IMetaStore {
/** The methods below involve several contexts due to the UI flow of the
* dashboard and hence do not sit within their own stores */
createColumn: (payload: CreateColumnPayload, foreignKey?: Partial<PostgresRelationship>) => any
createColumn: (
payload: CreateColumnPayload,
selectedTable: PostgresTable,
foreignKey?: Partial<PostgresRelationship>
) => any
updateColumn: (
id: string,
payload: UpdateColumnPayload,
selectedTable: PostgresTable,
foreignKey?: Partial<PostgresRelationship>
foreignKey?: Partial<PostgresRelationship>,
skipPKCreation?: boolean
) => any
duplicateTable: (
payload: any,
@@ -314,11 +319,34 @@ export default class MetaStore implements IMetaStore {
}
}
async createColumn(payload: CreateColumnPayload, foreignKey?: Partial<PostgresRelationship>) {
async createColumn(
payload: CreateColumnPayload,
selectedTable: PostgresTable,
foreignKey?: Partial<PostgresRelationship>
) {
try {
const column: any = await this.columns.create(payload)
// Once pg-meta supports composite keys, we can remove this logic
const { isPrimaryKey, ...formattedPayload } = payload
const column: any = await this.columns.create(formattedPayload)
if (column.error) throw column.error
// Firing createColumn in createTable will bypass this block
if (isPrimaryKey) {
// Same logic in createTable: Remove any primary key constraints first (we'll add it back later)
// @ts-ignore
const existingPrimaryKeys = selectedTable.primary_keys.map((x) => x.name)
if (existingPrimaryKeys.length > 0) {
const removePK = await this.removePrimaryKey(column.schema, column.table)
if (removePK.error) throw removePK.error
}
const primaryKeyColumns = existingPrimaryKeys.concat([column.name])
const addPK = await this.addPrimaryKey(column.schema, column.table, primaryKeyColumns)
if (addPK.error) throw addPK.error
}
if (!isUndefined(foreignKey)) {
const relation: any = await this.addForeignKey(foreignKey)
if (relation.error) throw relation.error
@@ -332,10 +360,12 @@ export default class MetaStore implements IMetaStore {
id: string,
payload: UpdateColumnPayload,
selectedTable: PostgresTable,
foreignKey?: Partial<PostgresRelationship>
foreignKey?: Partial<PostgresRelationship>,
skipPKCreation?: boolean
) {
try {
const column: any = await this.columns.update(id, payload)
const { isPrimaryKey, ...formattedPayload } = payload
const column: any = await this.columns.update(id, formattedPayload)
if (column.error) throw column.error
const originalColumn = find(selectedTable.columns, { id })
@@ -343,6 +373,21 @@ export default class MetaStore implements IMetaStore {
source_column_name: originalColumn!.name,
})
if (!skipPKCreation && isPrimaryKey !== undefined) {
const existingPrimaryKeys = selectedTable.primary_keys.map((x) => x.name)
// Primary key is getting updated for the column
if (existingPrimaryKeys.length > 0) {
const removePK = await this.removePrimaryKey(column.schema, column.table)
if (removePK.error) throw removePK.error
}
const primaryKeyColumns = isPrimaryKey
? existingPrimaryKeys.concat([column.name])
: existingPrimaryKeys.filter((x) => x !== column.name)
const addPK = await this.addPrimaryKey(column.schema, column.table, primaryKeyColumns)
if (addPK.error) throw addPK.error
}
// For updating of foreign key relationship, we remove the original one by default
// Then just add whatever was in foreignKey - simplicity over trying to derive whether to update or not
if (!isUndefined(existingForeignKey)) {
@@ -623,11 +668,11 @@ export default class MetaStore implements IMetaStore {
...column,
isPrimaryKey: false,
})
await this.createColumn(columnPayload, column.foreignKey)
await this.createColumn(columnPayload, updatedTable, column.foreignKey)
} else {
const originalColumn = find(originalColumns, { id: column.id })
if (originalColumn) {
const columnPayload = generateUpdateColumnPayload(originalColumn, column)
const columnPayload = generateUpdateColumnPayload(originalColumn, updatedTable, column)
const originalForeignKey = find(table.relationships, {
source_schema: originalColumn.schema,
source_table_name: originalColumn.table,
@@ -640,11 +685,13 @@ export default class MetaStore implements IMetaStore {
category: 'loading',
message: `Updating column ${column.name} from ${updatedTable.name}`,
})
const skipPKCreation = true
const res: any = await this.updateColumn(
column.id,
columnPayload,
updatedTable,
column.foreignKey
column.foreignKey,
skipPKCreation
)
if (res?.error) {
hasError = true