mirror of
https://github.com/supabase/supabase.git
synced 2026-10-19 16:19:10 +03:00
Merge branch 'master' of github.com:supabase/supabase
This commit is contained in:
commit
990b2ff9f2
31 files changed
+749
-508
No files matched your search
@@ -30,7 +30,7 @@ const data: CommunityItem[] = [
|
||||
},
|
||||
{
|
||||
title: 'GitHub',
|
||||
stat: '32,000+',
|
||||
stat: '34,000+',
|
||||
statLabel: 'GitHub stars',
|
||||
img: 'github.png',
|
||||
invertImgDarkMode: true,
|
||||
@@ -42,7 +42,7 @@ const data: CommunityItem[] = [
|
||||
},
|
||||
{
|
||||
title: 'Twitter',
|
||||
stat: '30,000+',
|
||||
stat: '32,000+',
|
||||
statLabel: 'Followers',
|
||||
img: 'twitter.png',
|
||||
detail: () => (
|
||||
@@ -54,7 +54,7 @@ const data: CommunityItem[] = [
|
||||
{
|
||||
title: 'Discord',
|
||||
stat: '6,000+',
|
||||
statLabel: 'SuperTroopers',
|
||||
statLabel: 'SupaTroopers',
|
||||
img: 'discord.png',
|
||||
detail: () => (
|
||||
<div>
|
||||
|
||||
+39
-15
@@ -14,39 +14,63 @@ const data: PressItem[] = [
|
||||
},
|
||||
{
|
||||
type: 'article',
|
||||
href: 'https://techcrunch.com/2021/09/09/supabase-raises-30m-for-its-open-source-insta-backend',
|
||||
title: 'Open source backend-as-a-service startup Supabase raises $30M',
|
||||
publisher: 'TechCrunch',
|
||||
href: 'https://www.prnewswire.com/news-releases/supabase-announces-80-million-series-b-led-by-felicis-301544453.html',
|
||||
title: 'Supabase Announces $80 Million Series B Led by Felicis',
|
||||
publisher: 'PR Newswire',
|
||||
},
|
||||
{
|
||||
type: 'article',
|
||||
href: 'https://techcrunch.com/2020/12/15/supabase-raises-6m-for-its-open-source-firebase-alternative',
|
||||
title: 'Supabase raises $6M for its open-source Firebase alternative',
|
||||
publisher: 'TechCrunch',
|
||||
href: 'https://medium.com/lightspeedindia/our-investment-in-supabase-2f443ff55aad',
|
||||
title: 'Our investment in Supabase',
|
||||
publisher: 'Medium',
|
||||
},
|
||||
{
|
||||
type: 'podcast',
|
||||
href: 'https://softwareengineeringdaily.com/2020/10/15/supabase-open-source-firebase-with-paul-copplestone',
|
||||
title: 'Software Engineering Daily',
|
||||
publisher: 'Software Engineering Daily',
|
||||
href: 'https://podcasts.apple.com/us/podcast/73-elixir-at-supabase-with-paul-copplestone/id1516100616?i=1000542087899',
|
||||
title: 'Elixir at Supabase with Paul Copplestone',
|
||||
publisher: 'Thinking Elixir Podcast',
|
||||
},
|
||||
{
|
||||
type: 'podcast',
|
||||
href: 'https://podcasts.apple.com/us/podcast/73-elixir-at-supabase-with-paul-copplestone/id1516100616?i=1000542087899',
|
||||
title: 'Ant Wilson on Supabase ',
|
||||
publisher: 'Software Engineering Radio',
|
||||
},
|
||||
{
|
||||
type: 'podcast',
|
||||
href: 'https://podcasts.apple.com/us/podcast/78-logflare-with-chase-granberry/id1516100616?i=1000545628124',
|
||||
title: 'Logflare with Chase Granberry',
|
||||
publisher: 'Thinking Elixir Podcast',
|
||||
},
|
||||
{
|
||||
type: 'podcast',
|
||||
href: 'https://stackoverflow.blog/2022/05/17/open-source-is-winning-over-developers-and-investors-ep-442/',
|
||||
title: 'Open-source is winning over developers and investors',
|
||||
publisher: 'Stackoverflow',
|
||||
},
|
||||
{
|
||||
type: 'podcast',
|
||||
href: 'https://changelog.com/podcast/476',
|
||||
title: 'Supabase is all in on Postgres',
|
||||
publisher: 'changelog',
|
||||
},
|
||||
{
|
||||
type: 'podcast',
|
||||
href: 'https://fsjam.org/episodes/episode-33-supabase-with-paul-copplestone',
|
||||
title: 'FS Jam',
|
||||
title: 'Supabase with Paul Copplestone',
|
||||
publisher: 'FS Jam',
|
||||
},
|
||||
{
|
||||
type: 'podcast',
|
||||
href: 'https://www.heavybit.com/library/podcasts/jamstack-radio/ep-71-open-source-firebase-alternative-with-paul-copplestone-of-supabase',
|
||||
title: 'Jamstack Radio',
|
||||
publisher: 'Heavybit',
|
||||
href: 'https://www.heavybit.com/library/podcasts/jamstack-radio/ep-71-open-source-firebase-alternative-with-paul-copplestone-of-supabase/',
|
||||
title: 'Open Source Firebase Alternative with Paul Copplestone of Supabase',
|
||||
publisher: 'Jamstack',
|
||||
},
|
||||
{
|
||||
type: 'podcast',
|
||||
href: 'https://podrocket.logrocket.com/9',
|
||||
title: 'PodRocket',
|
||||
publisher: 'LogRocket',
|
||||
title: 'Supabase with Paul Copplestone',
|
||||
publisher: 'Podrocket',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
+29
-20
@@ -15,6 +15,7 @@ import InvestorData from 'data/Investors'
|
||||
import TeamData from 'data/Team'
|
||||
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -313,30 +314,38 @@ const Press = () => {
|
||||
</div>
|
||||
<div className="mx-auto mt-5 grid gap-5 lg:max-w-none lg:grid-cols-3">
|
||||
{PressData.filter((x) => x.type == 'article').map((x) => (
|
||||
<a href={x.href} key={x.href} target="_blank">
|
||||
<Card key={`press_${x.href}`} hoverable className="dark:bg-scale-400 bg-white">
|
||||
<Space className="h-40 justify-between" direction="vertical">
|
||||
<div>
|
||||
<h1 className="text-scale-1200 text-xl">{x.type.toUpperCase()}</h1>
|
||||
<p className="text-scale-1100 text-base">{x.title}</p>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</a>
|
||||
<Link href={x.href} key={x.href}>
|
||||
<a target="_blank">
|
||||
<Card key={`press_${x.href}`} hoverable className="dark:bg-scale-400 h-36 bg-white">
|
||||
<Space className="h-40 justify-between" direction="vertical">
|
||||
<div>
|
||||
<h1 className="text-scale-1200 text-xl">{x.type.toUpperCase()}</h1>
|
||||
<p className="text-scale-1100 line-clamp block h-12 overflow-hidden text-ellipsis text-base">
|
||||
{x.title}
|
||||
</p>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="mx-auto mt-5 grid gap-5 sm:grid-cols-2 lg:max-w-none lg:grid-cols-4">
|
||||
{PressData.filter((x) => x.type == 'podcast').map((x) => (
|
||||
<a href={x.href} key={x.href} target="_blank">
|
||||
<Card key={`press_${x.href}`} hoverable className="dark:bg-scale-400 bg-white">
|
||||
<Space className="h-40 justify-between" direction="vertical">
|
||||
<div>
|
||||
<h1 className="text-scale-1200 text-xl">{x.type.toUpperCase()}</h1>
|
||||
<p className="text-scale-1100 text-base">{x.title}</p>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</a>
|
||||
<Link href={x.href} key={x.href}>
|
||||
<a target="_blank">
|
||||
<Card key={`press_${x.href}`} hoverable className="dark:bg-scale-400 h-36 bg-white">
|
||||
<Space className="h-40 justify-between" direction="vertical">
|
||||
<div>
|
||||
<h1 className="text-scale-1200 text-xl">{x.type.toUpperCase()}</h1>
|
||||
<p className="text-scale-1100 line-clamp block h-12 overflow-hidden text-ellipsis text-base">
|
||||
{x.title}
|
||||
</p>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</SectionContainer>
|
||||
|
||||
@@ -242,3 +242,10 @@ table {
|
||||
position: relative !important;
|
||||
height: unset !important;
|
||||
}
|
||||
|
||||
.line-clamp {
|
||||
display: -webkit-box;
|
||||
margin: 0 auto;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
@@ -1,40 +1,33 @@
|
||||
import React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useMonaco } from '@monaco-editor/react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { Dictionary, SupabaseGridProps, SupabaseGridRef } from './types';
|
||||
import { DataGridHandle } from '@supabase/react-data-grid';
|
||||
import { RowContextMenu } from './components/menu';
|
||||
import { StoreProvider, useDispatch, useTrackedState } from './store';
|
||||
import { fetchCount, fetchPage, refreshPageDebounced } from './utils';
|
||||
import { REFRESH_PAGE_IMMEDIATELY, TOTAL_ROWS_RESET } from './constants';
|
||||
import { Grid } from './components/grid';
|
||||
import { Shortcuts } from './components/common';
|
||||
import Header from './components/header';
|
||||
import Footer from './components/footer';
|
||||
import {
|
||||
cleanupProps,
|
||||
initTable,
|
||||
saveStorageDebounced,
|
||||
} from './SupabaseGrid.utils';
|
||||
import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useMonaco } from '@monaco-editor/react'
|
||||
import { DndProvider } from 'react-dnd'
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend'
|
||||
import { Dictionary, SupabaseGridProps, SupabaseGridRef } from './types'
|
||||
import { DataGridHandle } from '@supabase/react-data-grid'
|
||||
import { RowContextMenu } from './components/menu'
|
||||
import { StoreProvider, useDispatch, useTrackedState } from './store'
|
||||
import { fetchCount, fetchPage, refreshPageDebounced } from './utils'
|
||||
import { REFRESH_PAGE_IMMEDIATELY, TOTAL_ROWS_RESET } from './constants'
|
||||
import { Grid } from './components/grid'
|
||||
import { Shortcuts } from './components/common'
|
||||
import Header from './components/header'
|
||||
import Footer from './components/footer'
|
||||
import { cleanupProps, initTable, saveStorageDebounced } from './SupabaseGrid.utils'
|
||||
|
||||
/**
|
||||
* Supabase Grid.
|
||||
*
|
||||
* React component to render database table.
|
||||
*/
|
||||
export const SupabaseGrid = React.forwardRef<
|
||||
SupabaseGridRef,
|
||||
SupabaseGridProps
|
||||
>((props, ref) => {
|
||||
const monaco = useMonaco();
|
||||
const _props = cleanupProps(props);
|
||||
const { theme } = _props;
|
||||
export const SupabaseGrid = forwardRef<SupabaseGridRef, SupabaseGridProps>((props, ref) => {
|
||||
const monaco = useMonaco()
|
||||
const _props = cleanupProps(props)
|
||||
const { theme } = _props
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (monaco) {
|
||||
const darkTheme = theme && theme === 'dark' ? true : false;
|
||||
const darkTheme = theme && theme === 'dark' ? true : false
|
||||
|
||||
monaco.editor.defineTheme('supabase', {
|
||||
base: 'vs-dark', // can also be vs-dark or hc-black
|
||||
@@ -47,9 +40,9 @@ export const SupabaseGrid = React.forwardRef<
|
||||
colors: {
|
||||
'editor.background': darkTheme ? '#1f1f1f' : '#30313f',
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
}, [monaco]);
|
||||
}, [monaco])
|
||||
|
||||
return (
|
||||
<StoreProvider>
|
||||
@@ -57,103 +50,101 @@ export const SupabaseGrid = React.forwardRef<
|
||||
<SupabaseGridLayout ref={ref} {..._props} />
|
||||
</DndProvider>
|
||||
</StoreProvider>
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
const SupabaseGridLayout = React.forwardRef<SupabaseGridRef, SupabaseGridProps>(
|
||||
(props, ref) => {
|
||||
const { editable, storageRef, gridProps, headerActions } = props;
|
||||
const dispatch = useDispatch();
|
||||
const state = useTrackedState();
|
||||
const gridRef = React.useRef<DataGridHandle>(null);
|
||||
const [mounted, setMount] = React.useState(false);
|
||||
const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>((props, ref) => {
|
||||
const { editable, storageRef, gridProps, headerActions } = props
|
||||
const dispatch = useDispatch()
|
||||
const state = useTrackedState()
|
||||
const gridRef = useRef<DataGridHandle>(null)
|
||||
const [mounted, setMount] = useState(false)
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
rowAdded(row: Dictionary<any>) {
|
||||
dispatch({
|
||||
type: 'ADD_NEW_ROW',
|
||||
payload: row,
|
||||
});
|
||||
},
|
||||
rowEdited(row: Dictionary<any>, idx: number) {
|
||||
dispatch({
|
||||
type: 'EDIT_ROW',
|
||||
payload: { row, idx },
|
||||
});
|
||||
},
|
||||
}));
|
||||
useImperativeHandle(ref, () => ({
|
||||
rowAdded(row: Dictionary<any>) {
|
||||
dispatch({
|
||||
type: 'ADD_NEW_ROW',
|
||||
payload: row,
|
||||
})
|
||||
},
|
||||
rowEdited(row: Dictionary<any>, idx: number) {
|
||||
dispatch({
|
||||
type: 'EDIT_ROW',
|
||||
payload: { row, idx },
|
||||
})
|
||||
},
|
||||
}))
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!mounted) setMount(true);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (!mounted) setMount(true)
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.isInitialComplete && storageRef && state.table) {
|
||||
saveStorageDebounced(state, storageRef);
|
||||
}
|
||||
}, [
|
||||
state.table,
|
||||
state.isInitialComplete,
|
||||
state.gridColumns,
|
||||
state.sorts,
|
||||
state.filters,
|
||||
storageRef,
|
||||
]);
|
||||
useEffect(() => {
|
||||
if (state.isInitialComplete && storageRef && state.table) {
|
||||
saveStorageDebounced(state, storageRef)
|
||||
}
|
||||
}, [
|
||||
state.table,
|
||||
state.isInitialComplete,
|
||||
state.gridColumns,
|
||||
state.sorts,
|
||||
state.filters,
|
||||
storageRef,
|
||||
])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.refreshPageFlag == REFRESH_PAGE_IMMEDIATELY) {
|
||||
fetchPage(state, dispatch);
|
||||
} else if (state.refreshPageFlag != 0) {
|
||||
refreshPageDebounced(state, dispatch);
|
||||
}
|
||||
}, [state.refreshPageFlag]);
|
||||
useEffect(() => {
|
||||
if (state.refreshPageFlag == REFRESH_PAGE_IMMEDIATELY) {
|
||||
fetchPage(state, dispatch)
|
||||
} else if (state.refreshPageFlag != 0) {
|
||||
refreshPageDebounced(state, dispatch)
|
||||
}
|
||||
}, [state.refreshPageFlag])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.totalRows === TOTAL_ROWS_RESET) {
|
||||
fetchCount(state, dispatch);
|
||||
}
|
||||
}, [state.totalRows]);
|
||||
useEffect(() => {
|
||||
if (state.totalRows === TOTAL_ROWS_RESET) {
|
||||
fetchCount(state, dispatch)
|
||||
}
|
||||
}, [state.totalRows])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!state.metaService) {
|
||||
dispatch({
|
||||
type: 'INIT_CLIENT',
|
||||
payload: { onSqlQuery: props.onSqlQuery },
|
||||
});
|
||||
dispatch({
|
||||
type: 'INIT_CALLBACK',
|
||||
payload: { ...props },
|
||||
});
|
||||
}
|
||||
}, [state.metaService]);
|
||||
useEffect(() => {
|
||||
if (!state.metaService) {
|
||||
dispatch({
|
||||
type: 'INIT_CLIENT',
|
||||
payload: { onSqlQuery: props.onSqlQuery },
|
||||
})
|
||||
dispatch({
|
||||
type: 'INIT_CALLBACK',
|
||||
payload: { ...props },
|
||||
})
|
||||
}
|
||||
}, [state.metaService])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!state.metaService) return;
|
||||
useEffect(() => {
|
||||
if (!state.metaService) return
|
||||
|
||||
if (
|
||||
!state.table ||
|
||||
(typeof props.table == 'string' &&
|
||||
state.table!.name != props.table &&
|
||||
state.table!.schema != props.schema) ||
|
||||
(typeof props.table != 'string' &&
|
||||
JSON.stringify(props.table) !== JSON.stringify(state.table))
|
||||
) {
|
||||
initTable(props, state, dispatch);
|
||||
}
|
||||
}, [state.metaService, state.table, props.table, props.schema]);
|
||||
if (
|
||||
!state.table ||
|
||||
(typeof props.table == 'string' &&
|
||||
state.table!.name != props.table &&
|
||||
state.table!.schema != props.schema) ||
|
||||
(typeof props.table != 'string' &&
|
||||
JSON.stringify(props.table) !== JSON.stringify(state.table))
|
||||
) {
|
||||
initTable(props, state, dispatch)
|
||||
}
|
||||
}, [state.metaService, state.table, props.table, props.schema])
|
||||
|
||||
return (
|
||||
<div className="sb-grid">
|
||||
<Header
|
||||
onAddRow={editable ? props.onAddRow : undefined}
|
||||
onAddColumn={editable ? props.onAddColumn : undefined}
|
||||
headerActions={headerActions}
|
||||
/>
|
||||
<Grid ref={gridRef} {...gridProps} />
|
||||
<Footer />
|
||||
<Shortcuts gridRef={gridRef} />
|
||||
{mounted && createPortal(<RowContextMenu />, document.body)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<div className="sb-grid">
|
||||
<Header
|
||||
onAddRow={editable ? props.onAddRow : undefined}
|
||||
onAddColumn={editable ? props.onAddColumn : undefined}
|
||||
headerActions={headerActions}
|
||||
/>
|
||||
<Grid ref={gridRef} {...gridProps} rows={state.rows} />
|
||||
<Footer />
|
||||
<Shortcuts gridRef={gridRef} />
|
||||
{mounted && createPortal(<RowContextMenu />, document.body)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -1,14 +1,14 @@
|
||||
import * as React from 'react';
|
||||
import { Button, Modal } from '@supabase/ui';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import * as React from 'react'
|
||||
import { Button, Modal } from '@supabase/ui'
|
||||
import { render, unmountComponentAtNode } from 'react-dom'
|
||||
|
||||
type ConfirmModalProps = {
|
||||
title: string;
|
||||
message: string;
|
||||
onConfirm?: () => void;
|
||||
onAsyncConfirm?: () => Promise<void>;
|
||||
variant?: 'danger' | 'warning' | 'success' | undefined;
|
||||
};
|
||||
title: string
|
||||
message: string
|
||||
onConfirm?: () => void
|
||||
onAsyncConfirm?: () => Promise<void>
|
||||
variant?: 'danger' | 'warning' | 'success' | undefined
|
||||
}
|
||||
const ConfirmModal: React.FC<ConfirmModalProps> = ({
|
||||
title,
|
||||
message,
|
||||
@@ -16,26 +16,26 @@ const ConfirmModal: React.FC<ConfirmModalProps> = ({
|
||||
onAsyncConfirm,
|
||||
variant = 'danger',
|
||||
}) => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [loading, setLoading] = React.useState(false)
|
||||
|
||||
function onCancelClick() {
|
||||
if (!loading) onClose();
|
||||
if (!loading) onClose()
|
||||
}
|
||||
|
||||
async function onConfirmClick() {
|
||||
if (onAsyncConfirm) {
|
||||
setLoading(true);
|
||||
await onAsyncConfirm();
|
||||
setLoading(true)
|
||||
await onAsyncConfirm()
|
||||
|
||||
onClose();
|
||||
onClose()
|
||||
} else if (onConfirm) {
|
||||
onConfirm();
|
||||
onClose();
|
||||
onConfirm()
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
removeElement();
|
||||
removeElement()
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -55,11 +55,7 @@ const ConfirmModal: React.FC<ConfirmModalProps> = ({
|
||||
<Button type="default" onClick={() => onCancelClick()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={() => onConfirmClick()}
|
||||
>
|
||||
<Button type="primary" loading={loading} onClick={() => onConfirmClick()}>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
@@ -67,32 +63,32 @@ const ConfirmModal: React.FC<ConfirmModalProps> = ({
|
||||
}
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="text-sm text-scale-1100">{message}</p>
|
||||
<p className="text-scale-1100 py-4 text-sm">{message}</p>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
function removeElement() {
|
||||
const target = document.getElementById('supabase-ui-confirm-alert');
|
||||
const target = document.getElementById('supabase-ui-confirm-alert')
|
||||
if (target) {
|
||||
unmountComponentAtNode(target);
|
||||
target?.parentNode?.removeChild(target);
|
||||
unmountComponentAtNode(target)
|
||||
target?.parentNode?.removeChild(target)
|
||||
}
|
||||
}
|
||||
|
||||
function createElement(props: ConfirmModalProps) {
|
||||
let divTarget = document.getElementById('supabase-ui-confirm-alert');
|
||||
let divTarget = document.getElementById('supabase-ui-confirm-alert')
|
||||
if (divTarget) {
|
||||
render(<ConfirmModal {...props} />, divTarget);
|
||||
render(<ConfirmModal {...props} />, divTarget)
|
||||
} else {
|
||||
divTarget = document.createElement('div');
|
||||
divTarget.id = 'supabase-ui-confirm-alert';
|
||||
document.body.appendChild(divTarget);
|
||||
render(<ConfirmModal {...props} />, divTarget);
|
||||
divTarget = document.createElement('div')
|
||||
divTarget.id = 'supabase-ui-confirm-alert'
|
||||
document.body.appendChild(divTarget)
|
||||
render(<ConfirmModal {...props} />, divTarget)
|
||||
}
|
||||
}
|
||||
|
||||
export function showConfirmAlert(props: ConfirmModalProps) {
|
||||
createElement(props);
|
||||
createElement(props)
|
||||
}
|
||||
@@ -1,83 +1,120 @@
|
||||
import * as React from 'react';
|
||||
import AwesomeDebouncePromise from 'awesome-debounce-promise';
|
||||
import {
|
||||
Button,
|
||||
InputNumber,
|
||||
Typography,
|
||||
IconArrowRight,
|
||||
IconArrowLeft,
|
||||
} from '@supabase/ui';
|
||||
import { DropdownControl } from '../../common';
|
||||
import { useDispatch, useTrackedState } from '../../../store';
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
import AwesomeDebouncePromise from 'awesome-debounce-promise'
|
||||
import { Button, InputNumber, IconArrowRight, IconArrowLeft, IconLoader } from '@supabase/ui'
|
||||
import { DropdownControl, showConfirmAlert } from '../../common'
|
||||
import { useDispatch, useTrackedState } from '../../../store'
|
||||
|
||||
const updatePage = (payload: number, dispatch: (value: unknown) => void) => {
|
||||
dispatch({
|
||||
type: 'SET_PAGE',
|
||||
payload: payload,
|
||||
});
|
||||
};
|
||||
const updatePageDebounced = AwesomeDebouncePromise(updatePage, 550);
|
||||
})
|
||||
}
|
||||
const updatePageDebounced = AwesomeDebouncePromise(updatePage, 550)
|
||||
|
||||
const rowsPerPageOptions = [
|
||||
{ value: 100, label: '100 rows' },
|
||||
{ value: 500, label: '500 rows' },
|
||||
{ value: 1000, label: '1000 rows' },
|
||||
];
|
||||
]
|
||||
|
||||
type PaginationProps = {};
|
||||
type PaginationProps = {}
|
||||
|
||||
const Pagination: React.FC<PaginationProps> = () => {
|
||||
const state = useTrackedState();
|
||||
const dispatch = useDispatch();
|
||||
const [page, setPage] = React.useState(state.page);
|
||||
const maxPages = Math.ceil(state.totalRows / state.rowsPerPage);
|
||||
const totalPages = state.totalRows > 0 ? maxPages : 1;
|
||||
const Pagination: FC<PaginationProps> = () => {
|
||||
const state = useTrackedState()
|
||||
const dispatch = useDispatch()
|
||||
const [page, setPage] = useState(state.page)
|
||||
const maxPages = Math.ceil(state.totalRows / state.rowsPerPage)
|
||||
const totalPages = state.totalRows > 0 ? maxPages : 1
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.page != page) setPage(state.page);
|
||||
}, [state.page, page]);
|
||||
useEffect(() => {
|
||||
if (state.page != page) setPage(state.page)
|
||||
}, [state.page, page])
|
||||
|
||||
function onPreviousPage() {
|
||||
// [Joshen] Oddly without this, state.selectedRows will be stale
|
||||
useEffect(() => {}, [state.selectedRows])
|
||||
|
||||
// [Joshen] Note: I've made pagination buttons disabled while rows are being fetched for now
|
||||
// at least until we can send an abort signal to cancel requests if users are mashing the
|
||||
// pagination buttons to find the data they want
|
||||
|
||||
const onPreviousPage = () => {
|
||||
if (state.page > 1) {
|
||||
const previousPage = state.page - 1;
|
||||
setPage(previousPage);
|
||||
dispatch({ type: 'SET_PAGE', payload: previousPage });
|
||||
if (state.selectedRows.size >= 1) {
|
||||
showConfirmAlert({
|
||||
title: 'Confirm moving to previous page',
|
||||
message: 'The currently selected lines will be deselected, do you want to proceed?',
|
||||
onConfirm: () => {
|
||||
goToPreviousPage()
|
||||
dispatch({
|
||||
type: 'SELECTED_ROWS_CHANGE',
|
||||
payload: { selectedRows: new Set() },
|
||||
})
|
||||
},
|
||||
})
|
||||
} else {
|
||||
goToPreviousPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onNextPage() {
|
||||
const onNextPage = () => {
|
||||
if (state.page < maxPages) {
|
||||
const nextPage = state.page + 1;
|
||||
setPage(nextPage);
|
||||
dispatch({ type: 'SET_PAGE', payload: nextPage });
|
||||
if (state.selectedRows.size >= 1) {
|
||||
showConfirmAlert({
|
||||
title: 'Confirm moving to next page',
|
||||
message: 'The currently selected lines will be deselected, do you want to proceed?',
|
||||
onConfirm: () => {
|
||||
goToNextPage()
|
||||
dispatch({
|
||||
type: 'SELECTED_ROWS_CHANGE',
|
||||
payload: { selectedRows: new Set() },
|
||||
})
|
||||
},
|
||||
})
|
||||
} else {
|
||||
goToNextPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const goToPreviousPage = () => {
|
||||
const previousPage = state.page - 1
|
||||
setPage(previousPage)
|
||||
dispatch({ type: 'SET_PAGE', payload: previousPage })
|
||||
}
|
||||
|
||||
const goToNextPage = () => {
|
||||
const nextPage = state.page + 1
|
||||
setPage(nextPage)
|
||||
dispatch({ type: 'SET_PAGE', payload: nextPage })
|
||||
}
|
||||
|
||||
function onPageChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const value = event.target.value;
|
||||
const pageNum = Number(value);
|
||||
setPage(pageNum);
|
||||
updatePageDebounced(pageNum, dispatch);
|
||||
const value = event.target.value
|
||||
const pageNum = Number(value)
|
||||
setPage(pageNum)
|
||||
updatePageDebounced(pageNum, dispatch)
|
||||
}
|
||||
|
||||
function onRowsPerPageChange(value: string | number) {
|
||||
dispatch({ type: 'SET_ROWS_PER_PAGE', payload: value });
|
||||
dispatch({ type: 'SET_ROWS_PER_PAGE', payload: value })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="sb-grid-pagination">
|
||||
{state.totalRows < 0 ? (
|
||||
<Typography.Text>... loading total records</Typography.Text>
|
||||
<p className="text-scale-1100 text-sm">Loading records...</p>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
icon={<IconArrowLeft />}
|
||||
type="outline"
|
||||
disabled={state.page <= 1}
|
||||
disabled={state.page <= 1 || state.isLoading}
|
||||
onClick={onPreviousPage}
|
||||
style={{ padding: '3px 10px' }}
|
||||
/>
|
||||
<Typography.Text>Page</Typography.Text>
|
||||
<p className="text-scale-1100 text-sm">Page</p>
|
||||
<div className="sb-grid-pagination-input-container">
|
||||
<InputNumber
|
||||
value={page}
|
||||
@@ -91,11 +128,11 @@ const Pagination: React.FC<PaginationProps> = () => {
|
||||
min={1}
|
||||
/>
|
||||
</div>
|
||||
<Typography.Text>{`of ${totalPages}`}</Typography.Text>
|
||||
<p className="text-scale-1100 text-sm">{`of ${totalPages}`}</p>
|
||||
<Button
|
||||
icon={<IconArrowRight />}
|
||||
type="outline"
|
||||
disabled={state.page >= maxPages}
|
||||
disabled={state.page >= maxPages || state.isLoading}
|
||||
onClick={onNextPage}
|
||||
style={{ padding: '3px 10px' }}
|
||||
/>
|
||||
@@ -112,12 +149,11 @@ const Pagination: React.FC<PaginationProps> = () => {
|
||||
style={{ padding: '3px 10px' }}
|
||||
>{`${state.rowsPerPage} rows`}</Button>
|
||||
</DropdownControl>
|
||||
<Typography.Text>
|
||||
{`${state.totalRows.toLocaleString()} records`}
|
||||
</Typography.Text>
|
||||
<p className="text-scale-1100 text-sm">{`${state.totalRows.toLocaleString()} records`}</p>
|
||||
{state.isLoading && <IconLoader size={14} className="animate-spin" />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Pagination;
|
||||
)
|
||||
}
|
||||
export default Pagination
|
||||
@@ -1,54 +1,51 @@
|
||||
import * as React from 'react';
|
||||
import { memo } from 'react-tracked';
|
||||
import DataGrid, {
|
||||
DataGridHandle,
|
||||
RowsChangeData,
|
||||
} from '@supabase/react-data-grid';
|
||||
import { IconLoader } from '@supabase/ui';
|
||||
import { GridProps, SupaRow } from '../../types';
|
||||
import { useDispatch, useTrackedState } from '../../store';
|
||||
import RowRenderer from './RowRenderer';
|
||||
import AwesomeDebouncePromise from 'awesome-debounce-promise';
|
||||
import { forwardRef } from 'react'
|
||||
import { memo } from 'react-tracked'
|
||||
import DataGrid, { DataGridHandle, RowsChangeData } from '@supabase/react-data-grid'
|
||||
import { IconLoader } from '@supabase/ui'
|
||||
import { GridProps, SupaRow } from '../../types'
|
||||
import { useDispatch, useTrackedState } from '../../store'
|
||||
import RowRenderer from './RowRenderer'
|
||||
import AwesomeDebouncePromise from 'awesome-debounce-promise'
|
||||
|
||||
function rowKeyGetter(row: SupaRow) {
|
||||
return row.idx;
|
||||
return row.idx
|
||||
}
|
||||
|
||||
interface IGrid extends GridProps {
|
||||
rows: any[]
|
||||
}
|
||||
|
||||
export const Grid = memo(
|
||||
React.forwardRef<DataGridHandle, GridProps>(
|
||||
forwardRef<DataGridHandle, IGrid>(
|
||||
(
|
||||
{ width, height, containerClass, gridClass, rowClass },
|
||||
{ width, height, containerClass, gridClass, rowClass, rows },
|
||||
ref: React.Ref<DataGridHandle> | undefined
|
||||
) => {
|
||||
const dispatch = useDispatch();
|
||||
const state = useTrackedState();
|
||||
const dispatch = useDispatch()
|
||||
const state = useTrackedState()
|
||||
|
||||
// workaround to force state tracking on state.gridColumns
|
||||
const columnHeaders = state.gridColumns.map(
|
||||
(x) => `${x.key}_${x.frozen}`
|
||||
);
|
||||
const { gridColumns, rows, onError: onErrorFunc } = state;
|
||||
const columnHeaders = state.gridColumns.map((x) => `${x.key}_${x.frozen}`)
|
||||
const { gridColumns, onError: onErrorFunc } = state
|
||||
|
||||
function onColumnResize(index: number, width: number) {
|
||||
updateColumnResizeDebounced(index, width, dispatch);
|
||||
updateColumnResizeDebounced(index, width, dispatch)
|
||||
}
|
||||
|
||||
function onRowsChange(
|
||||
rows: SupaRow[],
|
||||
data: RowsChangeData<SupaRow, unknown>
|
||||
) {
|
||||
const rowData = rows[data.indexes[0]];
|
||||
const originRowData = state.rows.find((x) => x.idx == rowData.idx);
|
||||
const changedColumn = Object.keys(rowData).find((name) => rowData[name] !== originRowData![name]);
|
||||
async function onRowsChange(rows: SupaRow[], data: RowsChangeData<SupaRow, unknown>) {
|
||||
const rowData = rows[data.indexes[0]]
|
||||
const originRowData = state.rows.find((x) => x.idx == rowData.idx)
|
||||
const changedColumn = Object.keys(rowData).find(
|
||||
(name) => rowData[name] !== originRowData![name]
|
||||
)
|
||||
if (changedColumn) {
|
||||
const { error } = state.rowService!.update(rowData, changedColumn);
|
||||
if (error) {
|
||||
if (onErrorFunc) onErrorFunc(error);
|
||||
} else {
|
||||
const { error } = state.rowService!.update(rowData, changedColumn, (payload) => {
|
||||
dispatch({
|
||||
type: 'SET_ROWS',
|
||||
payload: { rows },
|
||||
});
|
||||
}
|
||||
type: 'EDIT_ROW',
|
||||
payload,
|
||||
})
|
||||
})
|
||||
if (error && onErrorFunc) onErrorFunc(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,14 +53,14 @@ export const Grid = memo(
|
||||
dispatch({
|
||||
type: 'SELECTED_ROWS_CHANGE',
|
||||
payload: { selectedRows },
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function onSelectedCellChange(position: { idx: number; rowIdx: number }) {
|
||||
dispatch({
|
||||
type: 'SELECTED_CELL_CHANGE',
|
||||
payload: { position },
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
if (!columnHeaders || columnHeaders.length == 0) {
|
||||
@@ -73,14 +70,15 @@ export const Grid = memo(
|
||||
style={{ width: width || '100%', height: height || '50vh' }}
|
||||
>
|
||||
<div className="sb-grid-grid--loading__inner flex items-center gap-2">
|
||||
<div className="animate-spin text-scale-900">
|
||||
<div className="text-scale-900 animate-spin">
|
||||
<IconLoader />
|
||||
</div>
|
||||
<div className="text-sm text-scale-1100">Loading...</div>
|
||||
<div className="text-scale-1100 text-sm">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={containerClass}
|
||||
@@ -102,22 +100,15 @@ export const Grid = memo(
|
||||
style={{ height: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
)
|
||||
);
|
||||
)
|
||||
|
||||
const updateColumnResize = (
|
||||
index: number,
|
||||
width: number,
|
||||
dispatch: (value: unknown) => void
|
||||
) => {
|
||||
const updateColumnResize = (index: number, width: number, dispatch: (value: unknown) => void) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_COLUMN_SIZE',
|
||||
payload: { index, width: Math.round(width) },
|
||||
});
|
||||
};
|
||||
const updateColumnResizeDebounced = AwesomeDebouncePromise(
|
||||
updateColumnResize,
|
||||
500
|
||||
);
|
||||
})
|
||||
}
|
||||
const updateColumnResizeDebounced = AwesomeDebouncePromise(updateColumnResize, 500)
|
||||
@@ -1,144 +1,140 @@
|
||||
import { IRowService } from '.';
|
||||
import { Filter, ServiceError, Sort, SupaRow, SupaTable } from '../../types';
|
||||
import { ERROR_PRIMARY_KEY_NOTFOUND, SupabaseGridQueue } from '../../constants';
|
||||
import Query from '../../query';
|
||||
import { isNumericalColumn } from '../../utils';
|
||||
import { IRowService } from '.'
|
||||
import { Filter, ServiceError, Sort, SupaRow, SupaTable } from '../../types'
|
||||
import { ERROR_PRIMARY_KEY_NOTFOUND, SupabaseGridQueue } from '../../constants'
|
||||
import Query from '../../query'
|
||||
import { isNumericalColumn } from '../../utils'
|
||||
import { useDispatch } from 'components/grid/store'
|
||||
|
||||
export class SqlRowService implements IRowService {
|
||||
protected query = new Query();
|
||||
protected query = new Query()
|
||||
protected dispatch = useDispatch()
|
||||
|
||||
constructor(
|
||||
protected table: SupaTable,
|
||||
protected onSqlQuery: (
|
||||
query: string
|
||||
) => Promise<{ data?: any; error?: any }>,
|
||||
protected onSqlQuery: (query: string) => Promise<{ data?: any; error?: any }>,
|
||||
protected onError: (error: any) => void
|
||||
) {}
|
||||
|
||||
async count(filters: Filter[]) {
|
||||
let queryChains = this.query
|
||||
.from(this.table.name, this.table.schema ?? undefined)
|
||||
.count();
|
||||
let queryChains = this.query.from(this.table.name, this.table.schema ?? undefined).count()
|
||||
filters
|
||||
.filter((x) => x.value && x.value != '')
|
||||
.forEach((x) => {
|
||||
const value = this.formatFilterValue(x);
|
||||
queryChains = queryChains.filter(x.column, x.operator, value);
|
||||
});
|
||||
const value = this.formatFilterValue(x)
|
||||
queryChains = queryChains.filter(x.column, x.operator, value)
|
||||
})
|
||||
|
||||
const query = queryChains.toSql();
|
||||
const { data, error } = await this.onSqlQuery(query);
|
||||
const query = queryChains.toSql()
|
||||
const { data, error } = await this.onSqlQuery(query)
|
||||
if (error) {
|
||||
return { error };
|
||||
return { error }
|
||||
} else {
|
||||
if (data?.length == 1) {
|
||||
return { data: data[0].count };
|
||||
return { data: data[0].count }
|
||||
} else {
|
||||
return { error: { message: 'fetch rows count failed' } };
|
||||
return { error: { message: 'fetch rows count failed' } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async create(row: SupaRow) {
|
||||
console.log('create: ', row);
|
||||
return { error: { message: 'not implemented' } };
|
||||
console.log('create: ', row)
|
||||
return { error: { message: 'not implemented' } }
|
||||
}
|
||||
|
||||
delete(rows: SupaRow[]) {
|
||||
const { primaryKeys, error } = this.getPrimaryKeys();
|
||||
if (error) return { error };
|
||||
const { primaryKeys, error } = this.getPrimaryKeys()
|
||||
if (error) return { error }
|
||||
|
||||
let queryChains = this.query
|
||||
.from(this.table.name, this.table.schema ?? undefined)
|
||||
.delete();
|
||||
let queryChains = this.query.from(this.table.name, this.table.schema ?? undefined).delete()
|
||||
|
||||
primaryKeys!.forEach((key) => {
|
||||
const primaryKeyValues = rows.map((x) => x[key]);
|
||||
queryChains = queryChains.filter(key, 'in', primaryKeyValues);
|
||||
});
|
||||
const primaryKeyValues = rows.map((x) => x[key])
|
||||
queryChains = queryChains.filter(key, 'in', primaryKeyValues)
|
||||
})
|
||||
|
||||
const query = queryChains.toSql();
|
||||
const query = queryChains.toSql()
|
||||
SupabaseGridQueue.add(async () => {
|
||||
const { error } = await this.onSqlQuery(query);
|
||||
if (error) throw error;
|
||||
const { error } = await this.onSqlQuery(query)
|
||||
if (error) throw error
|
||||
}).catch((error) => {
|
||||
this.onError(error);
|
||||
});
|
||||
this.onError(error)
|
||||
})
|
||||
|
||||
return {};
|
||||
return {}
|
||||
}
|
||||
|
||||
async fetchPage(
|
||||
page: number,
|
||||
rowsPerPage: number,
|
||||
filters: Filter[],
|
||||
sorts: Sort[]
|
||||
) {
|
||||
const pageFromZero = page > 0 ? page - 1 : page;
|
||||
const from = pageFromZero * rowsPerPage;
|
||||
const to = (pageFromZero + 1) * rowsPerPage - 1;
|
||||
let queryChains = this.query
|
||||
.from(this.table.name, this.table.schema ?? undefined)
|
||||
.select();
|
||||
async fetchPage(page: number, rowsPerPage: number, filters: Filter[], sorts: Sort[]) {
|
||||
const pageFromZero = page > 0 ? page - 1 : page
|
||||
const from = pageFromZero * rowsPerPage
|
||||
const to = (pageFromZero + 1) * rowsPerPage - 1
|
||||
let queryChains = this.query.from(this.table.name, this.table.schema ?? undefined).select()
|
||||
filters
|
||||
.filter((x) => x.value && x.value != '')
|
||||
.forEach((x) => {
|
||||
const value = this.formatFilterValue(x);
|
||||
queryChains = queryChains.filter(x.column, x.operator, value);
|
||||
});
|
||||
const value = this.formatFilterValue(x)
|
||||
queryChains = queryChains.filter(x.column, x.operator, value)
|
||||
})
|
||||
sorts.forEach((x) => {
|
||||
queryChains = queryChains.order(x.column, x.ascending, x.nullsFirst);
|
||||
});
|
||||
queryChains = queryChains.order(x.column, x.ascending, x.nullsFirst)
|
||||
})
|
||||
|
||||
const query = queryChains.range(from, to).toSql();
|
||||
const { data, error } = await this.onSqlQuery(query);
|
||||
const query = queryChains.range(from, to).toSql()
|
||||
const { data, error } = await this.onSqlQuery(query)
|
||||
if (error) {
|
||||
this.onError(error);
|
||||
return { data: { rows: [] } };
|
||||
this.onError(error)
|
||||
return { data: { rows: [] } }
|
||||
} else {
|
||||
const rows = data?.map((x: any, index: number) => {
|
||||
return { idx: index, ...x } as SupaRow;
|
||||
});
|
||||
return { data: { rows } };
|
||||
return { idx: index, ...x } as SupaRow
|
||||
})
|
||||
return { data: { rows } }
|
||||
}
|
||||
}
|
||||
|
||||
update(row: SupaRow, changedColumn?: string) {
|
||||
const { primaryKeys, error } = this.getPrimaryKeys();
|
||||
update(row: SupaRow, changedColumn?: string, onRowUpdate?: (value: any) => void) {
|
||||
const { primaryKeys, error } = this.getPrimaryKeys()
|
||||
if (error) {
|
||||
return { error };
|
||||
return { error }
|
||||
}
|
||||
const { idx, ...value } = row;
|
||||
const matchValues: any = {};
|
||||
const { idx, ...value } = row
|
||||
const matchValues: any = {}
|
||||
primaryKeys!.forEach((key) => {
|
||||
matchValues[key] = row[key];
|
||||
matchValues[key] = row[key]
|
||||
// fix: https://github.com/supabase/grid/issues/94
|
||||
// remove primary key from updated value object
|
||||
delete value[key];
|
||||
});
|
||||
delete value[key]
|
||||
})
|
||||
const query = this.query
|
||||
.from(this.table.name, this.table.schema ?? undefined)
|
||||
.update(changedColumn ? {
|
||||
[changedColumn]: value[changedColumn]
|
||||
}: value)
|
||||
.update(
|
||||
changedColumn
|
||||
? {
|
||||
[changedColumn]: value[changedColumn],
|
||||
}
|
||||
: value,
|
||||
{ returning: true }
|
||||
)
|
||||
.match(matchValues)
|
||||
.toSql();
|
||||
SupabaseGridQueue.add(async () => {
|
||||
const { error } = await this.onSqlQuery(query);
|
||||
if (error) throw error;
|
||||
}).catch((error) => {
|
||||
this.onError(error);
|
||||
});
|
||||
.toSql()
|
||||
|
||||
return {};
|
||||
SupabaseGridQueue.add(async () => {
|
||||
const { data, error } = await this.onSqlQuery(query)
|
||||
if (error) throw error
|
||||
if (onRowUpdate) onRowUpdate({ row: data[0], idx })
|
||||
}).catch((error) => {
|
||||
this.onError(error)
|
||||
})
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
getPrimaryKeys(): { primaryKeys?: string[]; error?: ServiceError } {
|
||||
const pkColumns = this.table.columns.filter((x) => x.isPrimaryKey);
|
||||
const pkColumns = this.table.columns.filter((x) => x.isPrimaryKey)
|
||||
if (!pkColumns || pkColumns.length == 0) {
|
||||
return { error: { message: ERROR_PRIMARY_KEY_NOTFOUND } };
|
||||
return { error: { message: ERROR_PRIMARY_KEY_NOTFOUND } }
|
||||
}
|
||||
return { primaryKeys: pkColumns.map((x) => x.name) };
|
||||
return { primaryKeys: pkColumns.map((x) => x.name) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,12 +142,12 @@ export class SqlRowService implements IRowService {
|
||||
* which validate input value base on the column type
|
||||
*/
|
||||
formatFilterValue(filter: Filter) {
|
||||
const column = this.table.columns.find((x) => x.name == filter.column);
|
||||
const column = this.table.columns.find((x) => x.name == filter.column)
|
||||
if (column && isNumericalColumn(column.format)) {
|
||||
const numberValue = Number(filter.value);
|
||||
if (Number.isNaN(numberValue)) return filter.value;
|
||||
else return Number(filter.value);
|
||||
const numberValue = Number(filter.value)
|
||||
if (Number.isNaN(numberValue)) return filter.value
|
||||
else return Number(filter.value)
|
||||
}
|
||||
return filter.value;
|
||||
return filter.value
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Filter, ServiceError, Sort, SupaRow } from '../../types';
|
||||
import { Filter, ServiceError, Sort, SupaRow } from '../../types'
|
||||
|
||||
export interface IRowService {
|
||||
count: (
|
||||
filters: Filter[]
|
||||
) => Promise<{ data?: number; error?: ServiceError }>;
|
||||
count: (filters: Filter[]) => Promise<{ data?: number; error?: ServiceError }>
|
||||
|
||||
create: (row: SupaRow) => Promise<{ data?: SupaRow; error?: ServiceError }>;
|
||||
create: (row: SupaRow) => Promise<{ data?: SupaRow; error?: ServiceError }>
|
||||
|
||||
fetchPage: (
|
||||
page: number,
|
||||
@@ -13,9 +11,9 @@ export interface IRowService {
|
||||
filters: Filter[],
|
||||
sorts: Sort[]
|
||||
) => Promise<{
|
||||
data?: { rows: SupaRow[] };
|
||||
error?: ServiceError;
|
||||
}>;
|
||||
data?: { rows: SupaRow[] }
|
||||
error?: ServiceError
|
||||
}>
|
||||
|
||||
/**
|
||||
* TODO: should return a promise.
|
||||
@@ -25,9 +23,13 @@ export interface IRowService {
|
||||
* Right now, we remove rows immediately, so it's confused when delete operation fails
|
||||
* and rows are already removed from the grid
|
||||
*/
|
||||
delete: (rows: SupaRow[]) => { error?: ServiceError };
|
||||
delete: (rows: SupaRow[]) => { error?: ServiceError }
|
||||
|
||||
update: (row: SupaRow, changedColumn?: string) => { error?: ServiceError };
|
||||
update: (
|
||||
row: SupaRow,
|
||||
changedColumn?: string,
|
||||
onRowUpdate?: (value: any) => void
|
||||
) => { row?: SupaRow; error?: ServiceError }
|
||||
}
|
||||
|
||||
export * from './SqlRowService';
|
||||
export * from './SqlRowService'
|
||||
@@ -1,54 +1,53 @@
|
||||
import update from 'immutability-helper';
|
||||
import {
|
||||
REFRESH_PAGE_IMMEDIATELY,
|
||||
TOTAL_ROWS_INITIAL,
|
||||
TOTAL_ROWS_RESET,
|
||||
} from '../../constants';
|
||||
import { Dictionary, SupaRow } from '../../types';
|
||||
import { INIT_ACTIONTYPE } from './base';
|
||||
import update from 'immutability-helper'
|
||||
import { REFRESH_PAGE_IMMEDIATELY, TOTAL_ROWS_INITIAL, TOTAL_ROWS_RESET } from '../../constants'
|
||||
import { Dictionary, SupaRow } from '../../types'
|
||||
import { INIT_ACTIONTYPE } from './base'
|
||||
|
||||
export interface RowInitialState {
|
||||
rows: SupaRow[];
|
||||
selectedRows: ReadonlySet<number>;
|
||||
selectedCellPosition: { idx: number; rowIdx: number } | null;
|
||||
page: number;
|
||||
rowsPerPage: number;
|
||||
totalRows: number;
|
||||
isLoading: boolean
|
||||
rows: SupaRow[]
|
||||
selectedRows: ReadonlySet<number>
|
||||
selectedCellPosition: { idx: number; rowIdx: number } | null
|
||||
page: number
|
||||
rowsPerPage: number
|
||||
totalRows: number
|
||||
}
|
||||
|
||||
export const rowInitialState: RowInitialState = {
|
||||
isLoading: false,
|
||||
rows: [],
|
||||
selectedRows: new Set(),
|
||||
selectedCellPosition: null,
|
||||
page: 1,
|
||||
rowsPerPage: 100,
|
||||
totalRows: TOTAL_ROWS_INITIAL,
|
||||
};
|
||||
}
|
||||
|
||||
type ROW_ACTIONTYPE =
|
||||
| INIT_ACTIONTYPE
|
||||
| {
|
||||
type: 'SELECTED_CELL_CHANGE';
|
||||
payload: { position: { idx: number; rowIdx: number } };
|
||||
type: 'SELECTED_CELL_CHANGE'
|
||||
payload: { position: { idx: number; rowIdx: number } }
|
||||
}
|
||||
| {
|
||||
type: 'SELECTED_ROWS_CHANGE';
|
||||
payload: { selectedRows: ReadonlySet<number> };
|
||||
type: 'SELECTED_ROWS_CHANGE'
|
||||
payload: { selectedRows: ReadonlySet<number> }
|
||||
}
|
||||
| { type: 'SET_PAGE'; payload: number }
|
||||
| { type: 'SET_ROWS_PER_PAGE'; payload: number }
|
||||
| {
|
||||
type: 'SET_ROWS';
|
||||
payload: { rows: SupaRow[] };
|
||||
type: 'SET_ROWS'
|
||||
payload: { rows: SupaRow[] }
|
||||
}
|
||||
| {
|
||||
type: 'SET_ROWS_COUNT';
|
||||
payload: number;
|
||||
type: 'SET_ROWS_COUNT'
|
||||
payload: number
|
||||
}
|
||||
| { type: 'ADD_ROWS'; payload: SupaRow[] }
|
||||
| { type: 'ADD_NEW_ROW'; payload: Dictionary<any> }
|
||||
| { type: 'EDIT_ROW'; payload: { row: Dictionary<any>; idx: number } }
|
||||
| { type: 'REMOVE_ROWS'; payload: { rowIdxs: number[] } };
|
||||
| { type: 'REMOVE_ROWS'; payload: { rowIdxs: number[] } }
|
||||
| { type: 'SET_IS_LOADING'; payload: { isLoading: boolean } }
|
||||
|
||||
const RowReducer = (state: RowInitialState, action: ROW_ACTIONTYPE) => {
|
||||
switch (action.type) {
|
||||
@@ -59,26 +58,26 @@ const RowReducer = (state: RowInitialState, action: ROW_ACTIONTYPE) => {
|
||||
selectedCellPosition: null,
|
||||
selectedRows: new Set(),
|
||||
totalRows: TOTAL_ROWS_RESET,
|
||||
};
|
||||
}
|
||||
}
|
||||
case 'SELECTED_CELL_CHANGE': {
|
||||
return {
|
||||
...state,
|
||||
selectedCellPosition: action.payload.position,
|
||||
};
|
||||
}
|
||||
}
|
||||
case 'SELECTED_ROWS_CHANGE': {
|
||||
return {
|
||||
...state,
|
||||
selectedRows: action.payload.selectedRows,
|
||||
};
|
||||
}
|
||||
}
|
||||
case 'SET_PAGE': {
|
||||
return {
|
||||
...state,
|
||||
page: action.payload,
|
||||
refreshPageFlag: REFRESH_PAGE_IMMEDIATELY,
|
||||
};
|
||||
}
|
||||
}
|
||||
case 'SET_ROWS_PER_PAGE': {
|
||||
return {
|
||||
@@ -86,58 +85,64 @@ const RowReducer = (state: RowInitialState, action: ROW_ACTIONTYPE) => {
|
||||
page: 1,
|
||||
rowsPerPage: action.payload,
|
||||
refreshPageFlag: REFRESH_PAGE_IMMEDIATELY,
|
||||
};
|
||||
}
|
||||
}
|
||||
case 'SET_ROWS': {
|
||||
return {
|
||||
...state,
|
||||
rows: action.payload.rows,
|
||||
refreshPageFlag: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
case 'SET_ROWS_COUNT': {
|
||||
return {
|
||||
...state,
|
||||
totalRows: action.payload,
|
||||
};
|
||||
}
|
||||
}
|
||||
case 'ADD_ROWS': {
|
||||
const totalRows = state.totalRows + action.payload.length;
|
||||
const totalRows = state.totalRows + action.payload.length
|
||||
return {
|
||||
...state,
|
||||
rows: update(state.rows, { $push: action.payload }),
|
||||
totalRows: totalRows,
|
||||
};
|
||||
}
|
||||
}
|
||||
case 'ADD_NEW_ROW': {
|
||||
const supaRow = { ...action.payload, idx: state.rows.length };
|
||||
const totalRows = state.totalRows + 1;
|
||||
const supaRow = { ...action.payload, idx: state.rows.length }
|
||||
const totalRows = state.totalRows + 1
|
||||
return {
|
||||
...state,
|
||||
rows: update(state.rows, { $push: [supaRow] }),
|
||||
totalRows: totalRows,
|
||||
};
|
||||
}
|
||||
}
|
||||
case 'EDIT_ROW': {
|
||||
const supaRow = { ...action.payload.row, idx: action.payload.idx };
|
||||
const supaRow = { ...action.payload.row, idx: action.payload.idx }
|
||||
return {
|
||||
...state,
|
||||
rows: update(state.rows, {
|
||||
[action.payload.idx]: { $set: supaRow },
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
case 'REMOVE_ROWS': {
|
||||
const totalRows = state.totalRows - action.payload.rowIdxs.length;
|
||||
const totalRows = state.totalRows - action.payload.rowIdxs.length
|
||||
return {
|
||||
...state,
|
||||
rows: state.rows.filter((x) => !action.payload.rowIdxs.includes(x.idx)),
|
||||
totalRows: totalRows,
|
||||
};
|
||||
}
|
||||
}
|
||||
case 'SET_IS_LOADING': {
|
||||
return {
|
||||
...state,
|
||||
isLoading: action.payload.isLoading,
|
||||
}
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
return state
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default RowReducer;
|
||||
export default RowReducer
|
||||
@@ -1,40 +1,36 @@
|
||||
import AwesomeDebouncePromise from 'awesome-debounce-promise';
|
||||
import { InitialStateType } from '../store/reducers';
|
||||
import AwesomeDebouncePromise from 'awesome-debounce-promise'
|
||||
import { InitialStateType } from '../store/reducers'
|
||||
|
||||
export async function fetchCount(
|
||||
state: InitialStateType,
|
||||
dispatch: (value: unknown) => void
|
||||
) {
|
||||
if (!state.rowService) return;
|
||||
const { data, error } = await state.rowService.count(state.filters);
|
||||
export async function fetchCount(state: InitialStateType, dispatch: (value: unknown) => void) {
|
||||
if (!state.rowService) return
|
||||
const { data, error } = await state.rowService.count(state.filters)
|
||||
if (error) {
|
||||
// TODO: handle fetch rows count error
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'SET_ROWS_COUNT',
|
||||
payload: data ?? 0,
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchPage(
|
||||
state: InitialStateType,
|
||||
dispatch: (value: unknown) => void
|
||||
) {
|
||||
if (!state.rowService) return;
|
||||
export async function fetchPage(state: InitialStateType, dispatch: (value: unknown) => void) {
|
||||
if (!state.rowService) return
|
||||
dispatch({ type: 'SET_IS_LOADING', payload: { isLoading: true } })
|
||||
const { data, error } = await state.rowService.fetchPage(
|
||||
state.page,
|
||||
state.rowsPerPage,
|
||||
state.filters,
|
||||
state.sorts
|
||||
);
|
||||
)
|
||||
if (error) {
|
||||
// TODO: handle fetch rows data error
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'SET_ROWS',
|
||||
payload: { rows: data?.rows ?? [] },
|
||||
});
|
||||
})
|
||||
}
|
||||
dispatch({ type: 'SET_IS_LOADING', payload: { isLoading: false } })
|
||||
}
|
||||
export const refreshPageDebounced = AwesomeDebouncePromise(fetchPage, 500);
|
||||
export const refreshPageDebounced = AwesomeDebouncePromise(fetchPage, 500)
|
||||
@@ -120,18 +120,21 @@ const PolicyEditorModal: FC<Props> = ({
|
||||
return ui.setNotification({
|
||||
category: 'error',
|
||||
message: 'You will need to allow at one operation in your policy',
|
||||
duration: 4000,
|
||||
})
|
||||
}
|
||||
if (['SELECT', 'DELETE'].includes(command) && !definition) {
|
||||
return ui.setNotification({
|
||||
category: 'error',
|
||||
message: 'Did you forget to provide a USING expression for your policy?',
|
||||
duration: 4000,
|
||||
})
|
||||
}
|
||||
if (command === 'INSERT' && !check) {
|
||||
return ui.setNotification({
|
||||
category: 'error',
|
||||
message: 'Did you forget to provide a WITH CHECK expression for your policy?',
|
||||
duration: 4000,
|
||||
})
|
||||
}
|
||||
if (command === 'UPDATE' && !definition && !check) {
|
||||
@@ -139,6 +142,7 @@ const PolicyEditorModal: FC<Props> = ({
|
||||
category: 'error',
|
||||
message:
|
||||
'You will need to provide either a USING, or WITH CHECK expression, or both for your policy',
|
||||
duration: 4000,
|
||||
})
|
||||
}
|
||||
const policySQLStatement = createSQLPolicy(policyFormFields, selectedPolicyToEdit)
|
||||
|
||||
@@ -54,11 +54,11 @@ const PublicationsTableItem: FC<Props> = ({ table, selectedPublication }) => {
|
||||
<Table.tr key={table.id}>
|
||||
<Table.td className="whitespace-nowrap">{table.name}</Table.td>
|
||||
<Table.td className="whitespace-nowrap">{table.schema}</Table.td>
|
||||
<Table.td className="whitespace-nowrap truncate max-w-sm hidden lg:table-cell">
|
||||
<Table.td className="hidden max-w-sm truncate whitespace-nowrap lg:table-cell">
|
||||
{table.comment}
|
||||
</Table.td>
|
||||
<Table.td className="px-4 py-3 pr-2">
|
||||
<div className="flex gap-2 justify-end">
|
||||
<div className="flex justify-end gap-2">
|
||||
{enabledForAllTables ? (
|
||||
// @ts-ignore
|
||||
<Badge
|
||||
@@ -74,7 +74,7 @@ const PublicationsTableItem: FC<Props> = ({ table, selectedPublication }) => {
|
||||
size="tiny"
|
||||
align="right"
|
||||
disabled={loading}
|
||||
className="m-0 p-0 ml-2 mt-1 -mb-1"
|
||||
className="m-0 ml-2 mt-1 -mb-1 p-0"
|
||||
checked={checked}
|
||||
onChange={() => toggleReplicationForTable(table, publication)}
|
||||
/>
|
||||
|
||||
+2
-2
@@ -33,8 +33,8 @@ const JsonEdit: FC<JsonEditProps> = ({ column, jsonString, visible, closePanel,
|
||||
} catch (error: any) {
|
||||
const message = error.message
|
||||
? `Error: ${error.message}`
|
||||
: 'Hmm, invalid JSON seems to have an invalid structure.'
|
||||
ui.setNotification({ category: 'error', message })
|
||||
: 'JSON seems to have an invalid structure.'
|
||||
ui.setNotification({ category: 'error', message, duration: 4000 })
|
||||
} finally {
|
||||
resolve()
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ const RowEditor: FC<Props> = ({
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Please enter a value in the ${row.name} field first`,
|
||||
duration: 4000,
|
||||
})
|
||||
}
|
||||
const foreignKey = row.foreignKey
|
||||
@@ -108,6 +109,7 @@ const RowEditor: FC<Props> = ({
|
||||
return ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Unable to find the corresponding row in ${foreignKey.target_table_schema}.${foreignKey.target_table_name} where ${foreignKey.target_column_name} equals ${row.value}`,
|
||||
duration: 4000,
|
||||
})
|
||||
}
|
||||
setReferenceRow({ loading: false, foreignKey, row: res[0] })
|
||||
|
||||
-2
@@ -283,8 +283,6 @@ export const generateRowObjectFromFields = (
|
||||
rowObject[field.name] = tryParseJson(value)
|
||||
}
|
||||
}
|
||||
} else if (NUMERICAL_TYPES.includes(field.format) && value) {
|
||||
rowObject[field.name] = Number(value)
|
||||
} else if (field.format === 'bool' && value) {
|
||||
rowObject[field.name] = value === 'true'
|
||||
} else if (DATETIME_TYPES.includes(field.format)) {
|
||||
|
||||
@@ -148,13 +148,15 @@ const SidePanelEditor: FC<Props> = ({
|
||||
tableId?: number
|
||||
importContent?: ImportContent
|
||||
isRLSEnabled: boolean
|
||||
isRealtimeEnabled: boolean
|
||||
isDuplicateRows: boolean
|
||||
},
|
||||
resolve: any
|
||||
) => {
|
||||
let toastId
|
||||
let saveTableError = false
|
||||
const { tableId, importContent, isRLSEnabled, isDuplicateRows } = configuration
|
||||
const { tableId, importContent, isRLSEnabled, isRealtimeEnabled, isDuplicateRows } =
|
||||
configuration
|
||||
|
||||
try {
|
||||
if (isDuplicating) {
|
||||
@@ -165,6 +167,7 @@ const SidePanelEditor: FC<Props> = ({
|
||||
})
|
||||
const table: any = await meta.duplicateTable(payload, {
|
||||
isRLSEnabled,
|
||||
isRealtimeEnabled,
|
||||
isDuplicateRows,
|
||||
duplicateTable,
|
||||
})
|
||||
@@ -179,7 +182,14 @@ const SidePanelEditor: FC<Props> = ({
|
||||
category: 'loading',
|
||||
message: `Creating new table: ${payload.name}...`,
|
||||
})
|
||||
const table = await meta.createTable(toastId, payload, isRLSEnabled, columns, importContent)
|
||||
const table = await meta.createTable(
|
||||
toastId,
|
||||
payload,
|
||||
columns,
|
||||
isRLSEnabled,
|
||||
isRealtimeEnabled,
|
||||
importContent
|
||||
)
|
||||
ui.setNotification({
|
||||
id: toastId,
|
||||
category: 'success',
|
||||
@@ -195,7 +205,8 @@ const SidePanelEditor: FC<Props> = ({
|
||||
toastId,
|
||||
selectedTableToEdit,
|
||||
payload,
|
||||
columns
|
||||
columns,
|
||||
isRealtimeEnabled
|
||||
)
|
||||
if (hasError) {
|
||||
ui.setNotification({
|
||||
@@ -258,8 +269,6 @@ const SidePanelEditor: FC<Props> = ({
|
||||
)}
|
||||
<TableEditor
|
||||
table={selectedTableToEdit}
|
||||
tables={tables}
|
||||
enumTypes={enumTypes}
|
||||
selectedSchema={selectedSchema}
|
||||
isDuplicating={isDuplicating}
|
||||
visible={sidePanelKey === 'table'}
|
||||
|
||||
+4
-2
@@ -74,6 +74,7 @@ const SpreadsheetImport: FC<Props> = ({
|
||||
error: errors,
|
||||
category: 'error',
|
||||
message: `Some issues have been detected on ${errors.length} rows. More details below the content preview.`,
|
||||
duration: 4000,
|
||||
})
|
||||
}
|
||||
setErrors(errors)
|
||||
@@ -98,6 +99,7 @@ const SpreadsheetImport: FC<Props> = ({
|
||||
error: errors,
|
||||
category: 'error',
|
||||
message: `Some issues have been detected on ${errors.length} rows. More details below the content preview.`,
|
||||
duration: 4000,
|
||||
})
|
||||
}
|
||||
setErrors(errors)
|
||||
@@ -160,7 +162,7 @@ const SpreadsheetImport: FC<Props> = ({
|
||||
</div>
|
||||
|
||||
{spreadsheetData.headers.length > 0 && (
|
||||
<div className="py-5 space-y-5">
|
||||
<div className="space-y-5 py-5">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Typography.Text>Content Preview</Typography.Text>
|
||||
@@ -188,7 +190,7 @@ const SpreadsheetImport: FC<Props> = ({
|
||||
<div key={key} className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconChevronRight
|
||||
className={`cursor-pointer transform ${isExpanded ? 'rotate-90' : ''}`}
|
||||
className={`transform cursor-pointer ${isExpanded ? 'rotate-90' : ''}`}
|
||||
size={14}
|
||||
onClick={() => onSelectExpandError(key)}
|
||||
/>
|
||||
|
||||
+4
-3
@@ -122,9 +122,10 @@ export const inferColumnType = (column: string, rows: object[]) => {
|
||||
|
||||
// Infer boolean type
|
||||
if (includes(['true', 'false'], columnData.toString().toLowerCase())) {
|
||||
const isAllBoolean = columnDataAcrossRows.every((item: any) =>
|
||||
includes(['true', 'false'], item.toString().toLowerCase())
|
||||
)
|
||||
const isAllBoolean = columnDataAcrossRows.every((item: any) => {
|
||||
if (item === null) return true
|
||||
else return includes(['true', 'false'], item.toString().toLowerCase())
|
||||
})
|
||||
if (isAllBoolean) {
|
||||
return 'boolean'
|
||||
}
|
||||
|
||||
+33
-9
@@ -20,8 +20,6 @@ import {
|
||||
|
||||
interface Props {
|
||||
table?: PostgresTable
|
||||
tables: PostgresTable[]
|
||||
enumTypes: PostgresType[]
|
||||
selectedSchema: string
|
||||
isDuplicating: boolean
|
||||
visible: boolean
|
||||
@@ -34,6 +32,7 @@ interface Props {
|
||||
tableId?: number
|
||||
importContent?: ImportContent
|
||||
isRLSEnabled: boolean
|
||||
isRealtimeEnabled: boolean
|
||||
isDuplicateRows: boolean
|
||||
},
|
||||
resolve: any
|
||||
@@ -43,8 +42,6 @@ interface Props {
|
||||
|
||||
const TableEditor: FC<Props> = ({
|
||||
table,
|
||||
tables = [],
|
||||
enumTypes = [] as PostgresType[],
|
||||
selectedSchema,
|
||||
isDuplicating,
|
||||
visible = false,
|
||||
@@ -52,9 +49,23 @@ const TableEditor: FC<Props> = ({
|
||||
saveChanges = () => {},
|
||||
updateEditorDirty = () => {},
|
||||
}) => {
|
||||
const { ui } = useStore()
|
||||
const { ui, meta } = useStore()
|
||||
const isNewRecord = isUndefined(table)
|
||||
|
||||
const tables = meta.tables.list()
|
||||
const enumTypes = meta.types.list(
|
||||
(type: PostgresType) => !meta.excludedSchemas.includes(type.schema)
|
||||
)
|
||||
|
||||
const publications = meta.publications.list()
|
||||
const realtimePublication = publications.find(
|
||||
(publication) => publication.name === 'supabase_realtime'
|
||||
)
|
||||
const realtimeEnabledTables = realtimePublication?.tables ?? []
|
||||
const isRealtimeEnabled = isNewRecord
|
||||
? false
|
||||
: realtimeEnabledTables.some((t: any) => t.id === table?.id)
|
||||
|
||||
const [errors, setErrors] = useState<any>({})
|
||||
const [tableFields, setTableFields] = useState<TableField>()
|
||||
const [isDuplicateRows, setIsDuplicateRows] = useState<boolean>(false)
|
||||
@@ -70,7 +81,11 @@ const TableEditor: FC<Props> = ({
|
||||
const tableFields = generateTableField()
|
||||
setTableFields(tableFields)
|
||||
} else {
|
||||
const tableFields = generateTableFieldFromPostgresTable(table!, isDuplicating)
|
||||
const tableFields = generateTableFieldFromPostgresTable(
|
||||
table!,
|
||||
isDuplicating,
|
||||
isRealtimeEnabled
|
||||
)
|
||||
setTableFields(tableFields)
|
||||
}
|
||||
}
|
||||
@@ -99,7 +114,7 @@ const TableEditor: FC<Props> = ({
|
||||
if (tableFields) {
|
||||
const errors: any = validateFields(tableFields)
|
||||
if (errors.columns) {
|
||||
ui.setNotification({ category: 'error', message: errors.columns })
|
||||
ui.setNotification({ category: 'error', message: errors.columns, duration: 4000 })
|
||||
}
|
||||
setErrors(errors)
|
||||
|
||||
@@ -122,6 +137,7 @@ const TableEditor: FC<Props> = ({
|
||||
tableId: table?.id,
|
||||
importContent,
|
||||
isRLSEnabled: tableFields.isRLSEnabled,
|
||||
isRealtimeEnabled: tableFields.isRealtimeEnabled,
|
||||
isDuplicateRows: isDuplicateRows,
|
||||
}
|
||||
|
||||
@@ -187,8 +203,16 @@ const TableEditor: FC<Props> = ({
|
||||
</div>
|
||||
}
|
||||
description="Restrict access to your table by enabling RLS and writing Postgres policies"
|
||||
checked={tableFields?.isRLSEnabled}
|
||||
onChange={() => onUpdateField({ isRLSEnabled: !tableFields?.isRLSEnabled })}
|
||||
checked={tableFields.isRLSEnabled}
|
||||
onChange={() => onUpdateField({ isRLSEnabled: !tableFields.isRLSEnabled })}
|
||||
size="medium"
|
||||
/>
|
||||
<Checkbox
|
||||
id="enable-realtime"
|
||||
label="Enable Realtime"
|
||||
description="Broadcast changes on this table to authorized subscribers"
|
||||
checked={tableFields.isRealtimeEnabled}
|
||||
onChange={() => onUpdateField({ isRealtimeEnabled: !tableFields.isRealtimeEnabled })}
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+1
@@ -7,6 +7,7 @@ export interface TableField {
|
||||
comment?: string
|
||||
columns: ColumnField[]
|
||||
isRLSEnabled: boolean
|
||||
isRealtimeEnabled: boolean
|
||||
}
|
||||
|
||||
export interface ImportContent {
|
||||
|
||||
+4
-1
@@ -30,12 +30,14 @@ export const generateTableField = (): TableField => {
|
||||
comment: '',
|
||||
columns: DEFAULT_COLUMNS,
|
||||
isRLSEnabled: false,
|
||||
isRealtimeEnabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
export const generateTableFieldFromPostgresTable = (
|
||||
table: PostgresTable,
|
||||
isDuplicating = false
|
||||
isDuplicating = false,
|
||||
isRealtimeEnabled = false
|
||||
): TableField => {
|
||||
return {
|
||||
id: table.id,
|
||||
@@ -46,6 +48,7 @@ export const generateTableFieldFromPostgresTable = (
|
||||
return generateColumnFieldFromPostgresColumn(column, table)
|
||||
}),
|
||||
isRLSEnabled: table.rls_enabled,
|
||||
isRealtimeEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ const TableEditorLayout: FC<Props> = ({
|
||||
meta.schemas.load()
|
||||
meta.tables.load()
|
||||
meta.types.load()
|
||||
meta.publications.load()
|
||||
}
|
||||
}, [ui.selectedProject])
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ export const StoreProvider: FC<StoreProvider> = ({ children, rootStore }) => {
|
||||
return toast.success(message, { id, duration: toastDuration })
|
||||
case 'error':
|
||||
console.error('Error:', { error, message })
|
||||
return toast.error(message, { id, duration: toastDuration })
|
||||
return toast.error(message, { id, duration: duration || Infinity })
|
||||
case 'loading':
|
||||
if (progress) {
|
||||
return toast.loading(
|
||||
|
||||
Generated
+55
-8
@@ -70,6 +70,7 @@
|
||||
"swr": "^1.2.1",
|
||||
"uniforms-bootstrap4": "^3.6.2",
|
||||
"uniforms-bridge-json-schema": "^3.6.2",
|
||||
"uuid": "^8.3.2",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -100,6 +101,7 @@
|
||||
"@types/react-window-infinite-loader": "^1.0.5",
|
||||
"@types/semver": "^7.3.9",
|
||||
"@types/sqlstring": "^2.3.0",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/zxcvbn": "^4.4.1",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"babel-loader": "^8.2.3",
|
||||
@@ -9048,6 +9050,12 @@
|
||||
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
|
||||
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/webpack": {
|
||||
"version": "4.41.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.32.tgz",
|
||||
@@ -9472,6 +9480,15 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/analytics-node/node_modules/uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
|
||||
"bin": {
|
||||
"uuid": "bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/anser": {
|
||||
"version": "1.4.9",
|
||||
"resolved": "https://registry.npmjs.org/anser/-/anser-1.4.9.tgz",
|
||||
@@ -27444,12 +27461,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "bin/uuid"
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid-browser": {
|
||||
@@ -28143,6 +28159,16 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-log/node_modules/uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"uuid": "bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-sources": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz",
|
||||
@@ -35769,6 +35795,12 @@
|
||||
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/uuid": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
|
||||
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/webpack": {
|
||||
"version": "4.41.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.32.tgz",
|
||||
@@ -36151,6 +36183,13 @@
|
||||
"ms": "^2.0.0",
|
||||
"remove-trailing-slash": "^0.1.0",
|
||||
"uuid": "^3.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"anser": {
|
||||
@@ -49981,9 +50020,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||
},
|
||||
"uuid-browser": {
|
||||
"version": "3.1.0",
|
||||
@@ -50845,6 +50884,14 @@
|
||||
"requires": {
|
||||
"ansi-colors": "^3.0.0",
|
||||
"uuid": "^3.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"webpack-sources": {
|
||||
|
||||
@@ -93,16 +93,28 @@ export interface IMetaStore {
|
||||
) => any
|
||||
duplicateTable: (
|
||||
payload: any,
|
||||
metadata: { duplicateTable: PostgresTable; isRLSEnabled: boolean; isDuplicateRows: boolean }
|
||||
metadata: {
|
||||
duplicateTable: PostgresTable
|
||||
isRLSEnabled: boolean
|
||||
isRealtimeEnabled: boolean
|
||||
isDuplicateRows: boolean
|
||||
}
|
||||
) => void
|
||||
createTable: (
|
||||
toastId: string,
|
||||
payload: any,
|
||||
isRLSEnabled: boolean,
|
||||
columns: ColumnField[],
|
||||
isRLSEnabled: boolean,
|
||||
isRealtimeEnabled: boolean,
|
||||
importContent?: ImportContent
|
||||
) => any
|
||||
updateTable: (toastId: string, table: PostgresTable, payload: any, columns: ColumnField[]) => any
|
||||
updateTable: (
|
||||
toastId: string,
|
||||
table: PostgresTable,
|
||||
payload: any,
|
||||
columns: ColumnField[],
|
||||
isRealtimeEnabled: boolean
|
||||
) => any
|
||||
}
|
||||
export default class MetaStore implements IMetaStore {
|
||||
rootStore: IRootStore
|
||||
@@ -297,9 +309,15 @@ export default class MetaStore implements IMetaStore {
|
||||
|
||||
async duplicateTable(
|
||||
payload: any,
|
||||
metadata: { duplicateTable: PostgresTable; isRLSEnabled: boolean; isDuplicateRows: boolean }
|
||||
metadata: {
|
||||
duplicateTable: PostgresTable
|
||||
isRLSEnabled: boolean
|
||||
isRealtimeEnabled: boolean
|
||||
isDuplicateRows: boolean
|
||||
}
|
||||
) {
|
||||
const sourceTableName = metadata.duplicateTable.name
|
||||
const { duplicateTable, isRLSEnabled, isRealtimeEnabled, isDuplicateRows } = metadata
|
||||
const sourceTableName = duplicateTable.name
|
||||
const duplicatedTableName = payload.name
|
||||
|
||||
// The following query will copy the structure of the table along with indexes, constraints and
|
||||
@@ -310,7 +328,7 @@ export default class MetaStore implements IMetaStore {
|
||||
if (table.error) throw table.error
|
||||
|
||||
// Duplicate foreign key constraints over
|
||||
const relationships = metadata.duplicateTable.relationships
|
||||
const relationships = duplicateTable.relationships
|
||||
if (relationships.length > 0) {
|
||||
// @ts-ignore, but might need to investigate, sounds bad:
|
||||
// Type instantiation is excessively deep and possibly infinite
|
||||
@@ -324,14 +342,15 @@ export default class MetaStore implements IMetaStore {
|
||||
}
|
||||
|
||||
// Duplicate rows if needed
|
||||
if (metadata.isDuplicateRows) {
|
||||
if (isDuplicateRows) {
|
||||
const rows = await this.rootStore.meta.query(
|
||||
`INSERT INTO "${duplicatedTableName}" SELECT * FROM ${sourceTableName};`
|
||||
)
|
||||
if (rows.error) throw rows.error
|
||||
|
||||
// Insert into does not copy over auto increment sequences, so we manually do it next if any
|
||||
const columns = metadata.duplicateTable.columns
|
||||
const columns = duplicateTable.columns
|
||||
// @ts-ignore
|
||||
const identityColumns = columns.filter((column) => column.identity_generation !== null)
|
||||
identityColumns.map(async (column) => {
|
||||
const identity = await this.rootStore.meta.query(
|
||||
@@ -345,21 +364,37 @@ export default class MetaStore implements IMetaStore {
|
||||
const tables = this.tables.list()
|
||||
const duplicatedTable = find(tables, { name: duplicatedTableName })
|
||||
|
||||
if (metadata.isRLSEnabled) {
|
||||
if (isRLSEnabled) {
|
||||
const updateTable: any = await this.tables.update(duplicatedTable!.id, {
|
||||
rls_enabled: metadata.isRLSEnabled,
|
||||
rls_enabled: isRLSEnabled,
|
||||
})
|
||||
if (updateTable.error) throw updateTable.error
|
||||
}
|
||||
|
||||
if (isRealtimeEnabled) {
|
||||
const publications = this.publications.list()
|
||||
const realtimePublication = publications.find(
|
||||
(publication) => publication.name === 'supabase_realtime'
|
||||
)
|
||||
|
||||
const id = realtimePublication?.id
|
||||
const realtimeTables = [`${duplicateTable.schema}.${duplicatedTableName}`].concat(
|
||||
realtimePublication.tables.map((t: any) => `${t.schema}.${t.name}`)
|
||||
)
|
||||
let payload = { id, tables: realtimeTables }
|
||||
const { error: publicationsUpdateError } = await this.publications.update(id, payload)
|
||||
if (publicationsUpdateError) throw publicationsUpdateError
|
||||
}
|
||||
|
||||
return duplicatedTable
|
||||
}
|
||||
|
||||
async createTable(
|
||||
toastId: string,
|
||||
payload: any,
|
||||
isRLSEnabled: boolean,
|
||||
columns: ColumnField[] = [],
|
||||
isRLSEnabled: boolean,
|
||||
isRealtimeEnabled: boolean,
|
||||
importContent?: ImportContent
|
||||
) {
|
||||
// Create the table first
|
||||
@@ -379,6 +414,22 @@ export default class MetaStore implements IMetaStore {
|
||||
if (updatedTable.error) throw updatedTable.error
|
||||
}
|
||||
|
||||
// Toggle Realtime if configured to be
|
||||
if (isRealtimeEnabled) {
|
||||
const publications = this.publications.list()
|
||||
const realtimePublication = publications.find(
|
||||
(publication) => publication.name === 'supabase_realtime'
|
||||
)
|
||||
|
||||
const id = realtimePublication?.id
|
||||
const realtimeTables = [`${table.schema}.${table.name}`].concat(
|
||||
realtimePublication.tables.map((t: any) => `${t.schema}.${t.name}`)
|
||||
)
|
||||
let payload = { id, tables: realtimeTables }
|
||||
const { error: publicationsUpdateError } = await this.publications.update(id, payload)
|
||||
if (publicationsUpdateError) throw publicationsUpdateError
|
||||
}
|
||||
|
||||
// Then insert the columns - we don't do Promise.all as we want to keep the integrity
|
||||
// of the column order during creation. Note that we add primary key constraints separately
|
||||
// via the query endpoint to support composite primary keys as pg-meta does not support that OOB
|
||||
@@ -483,11 +534,18 @@ export default class MetaStore implements IMetaStore {
|
||||
}
|
||||
}
|
||||
|
||||
async updateTable(toastId: string, table: PostgresTable, payload: any, columns: ColumnField[]) {
|
||||
async updateTable(
|
||||
toastId: string,
|
||||
table: PostgresTable,
|
||||
payload: any,
|
||||
columns: ColumnField[],
|
||||
isRealtimeEnabled: boolean
|
||||
) {
|
||||
// Prepare a check to see if primary keys to the tables were updated or not
|
||||
const primaryKeyColumns = columns
|
||||
.filter((column) => column.isPrimaryKey)
|
||||
.map((column) => `"${column.name}"`)
|
||||
// @ts-ignore
|
||||
const existingPrimaryKeyColumns = table.primary_keys.map(
|
||||
(pk: PostgresPrimaryKey) => `"${pk.name}"`
|
||||
)
|
||||
@@ -512,6 +570,7 @@ export default class MetaStore implements IMetaStore {
|
||||
const columnIds = columns.map((column) => column.id)
|
||||
|
||||
// Delete any removed columns
|
||||
// @ts-ignore
|
||||
const columnsToRemove = originalColumns.filter((column) => !columnIds.includes(column.id))
|
||||
for (const column of columnsToRemove) {
|
||||
this.rootStore.ui.setNotification({
|
||||
@@ -583,6 +642,30 @@ export default class MetaStore implements IMetaStore {
|
||||
if (primaryKeys.error) throw primaryKeys.error
|
||||
}
|
||||
|
||||
// Update realtime configuration
|
||||
const publications = this.publications.list()
|
||||
const realtimePublication = publications.find(
|
||||
(publication) => publication.name === 'supabase_realtime'
|
||||
)
|
||||
|
||||
const id = realtimePublication?.id
|
||||
const realtimeEnabled = realtimePublication.tables.some((x: any) => x.id == table.id)
|
||||
if (realtimeEnabled && !isRealtimeEnabled) {
|
||||
// Toggle realtime off
|
||||
const realtimeTables = realtimePublication.tables
|
||||
.filter((x: any) => x.id != table.id)
|
||||
.map((x: any) => `${x.schema}.${x.name}`)
|
||||
let payload = { id, tables: realtimeTables }
|
||||
await this.publications.update(id, payload)
|
||||
} else if (!realtimeEnabled && isRealtimeEnabled) {
|
||||
// Toggle realtime on
|
||||
const realtimeTables = [`${table.schema}.${table.name}`].concat(
|
||||
realtimePublication.tables.map((t: any) => `${t.schema}.${t.name}`)
|
||||
)
|
||||
let payload = { id, tables: realtimeTables }
|
||||
await this.publications.update(id, payload)
|
||||
}
|
||||
|
||||
return { table: await this.tables.loadById(table.id), hasError }
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,10 @@
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.table-container table span {
|
||||
@apply truncate;
|
||||
}
|
||||
|
||||
.table-container thead th {
|
||||
@apply text-scale-900 font-normal text-sm;
|
||||
@apply bg-table-header-light dark:bg-table-header-dark;
|
||||
|
||||
@@ -35,6 +35,15 @@ describe('SpreadsheedImport.utils: inferColumnType', () => {
|
||||
const type2 = inferColumnType('isRetired', mockData2)
|
||||
expect(type2).toBe('boolean')
|
||||
})
|
||||
test('should infer boolean type for a supposed boolean column if one of the rows has a null value', () => {
|
||||
const mockData3 = [
|
||||
{ name: 'bob', height: '161.72', isRetired: 'false' },
|
||||
{ name: 'bob', height: '161.72', isRetired: 'true' },
|
||||
{ name: 'bob', height: '161.72', isRetired: null },
|
||||
]
|
||||
const type3 = inferColumnType('isRetired', mockData3)
|
||||
expect(type3).toBe('boolean')
|
||||
})
|
||||
test('should infer objects as jsonb types correctly', () => {
|
||||
const mockData = [{ name: 'bob', metadata: '{}' }]
|
||||
const type = inferColumnType('metadata', mockData)
|
||||
|
||||
@@ -358,10 +358,10 @@ const handleInserts = (payload) => {
|
||||
console.log('Change received!', payload)
|
||||
}
|
||||
|
||||
// Listen to unserts
|
||||
// Listen to inserts
|
||||
const { data: todos, error } = await supabase
|
||||
.from('todos')
|
||||
.on('INSERT', handleUpdates)
|
||||
.on('INSERT', handleInserts)
|
||||
.subscribe()
|
||||
```
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ To make sure that everything works correctly, let’s try the connection string
|
||||
|
||||
If you already have one, all you need to do is set the `DATABASE_URL` to the connection string (including the password) in your `.env` file, and you’re good to go.
|
||||
|
||||
In case you don’t have a Prisma project or this is your first time working with Prisma, you’re going to use with the repo from the [quickstart](https://www.prisma.io/docs/getting-started/quickstart) guide.
|
||||
In case you don’t have a Prisma project or this is your first time working with Prisma, you’re going to use the repo from the [quickstart](https://www.prisma.io/docs/getting-started/quickstart) guide.
|
||||
|
||||
### Cloning the starter project
|
||||
|
||||
@@ -140,10 +140,10 @@ alter type "Incompatible" rename to compatible;
|
||||
|
||||
## Connection pooling with Supabase
|
||||
|
||||
If you’re working in a serverless environment (for example Node.js functions hosted on AWS Lambda, Vercel or Netlify Functions), you need to set up [connection pooling](https://www.prisma.io/docs/guides/performance-and-optimization/connection-management#serverless-environments-faas) using a tool like [PgBouncer](https://www.pgbouncer.org/). That’s because every function invocation may result in a [new connection to the database](https://www.prisma.io/docs/guides/performance-and-optimization/connection-management#the-serverless-challenge). Supabase [support connection management using PgBouncer](https://supabase.io/blog/2021/04/02/supabase-pgbouncer#what-is-connection-pooling) and are enabled by default.
|
||||
If you’re working in a serverless environment (for example Node.js functions hosted on AWS Lambda, Vercel or Netlify Functions), you need to set up [connection pooling](https://www.prisma.io/docs/guides/performance-and-optimization/connection-management#serverless-environments-faas) using a tool like [PgBouncer](https://www.pgbouncer.org/). That’s because every function invocation may result in a [new connection to the database](https://www.prisma.io/docs/guides/performance-and-optimization/connection-management#the-serverless-challenge). Supabase [supports connection management using PgBouncer](https://supabase.io/blog/2021/04/02/supabase-pgbouncer#what-is-connection-pooling) and are enabled by default.
|
||||
Go to the **Database** page from the sidebar in the Supabase dashboard and navigate to **connection pool** settings
|
||||

|
||||
When running migrations you need to use the non pooled connection URL (like the one we used in **step 4**). However, when deploying your app, you’ll use the pooled connection URL and add the `?pgbouncer=true` flag to the PostgreSQL connection URL. To minimize the number of concurrent connections, setting the `connection_limit` to `1` is also recommended. So The URL might look as follows:
|
||||
When running migrations you need to use the non pooled connection URL (like the one we used in **step 4**). However, when deploying your app, you’ll use the pooled connection URL and add the `?pgbouncer=true` flag to the PostgreSQL connection URL. To minimize the number of concurrent connections, setting the `connection_limit` to `1` is also recommended. So the URL might look as follows:
|
||||
|
||||
```env
|
||||
# prisma/.env
|
||||
|
||||
Reference in new issue
Block a user