diff --git a/apps/www/data/Community.tsx b/apps/www/data/Community.tsx index 618092900c6..1da7c6df686 100644 --- a/apps/www/data/Community.tsx +++ b/apps/www/data/Community.tsx @@ -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: () => (
diff --git a/apps/www/data/Press.ts b/apps/www/data/Press.ts index 43f1e2aeb27..598c3e712d6 100644 --- a/apps/www/data/Press.ts +++ b/apps/www/data/Press.ts @@ -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', }, ] diff --git a/apps/www/pages/company.tsx b/apps/www/pages/company.tsx index 7563952bb65..ff673e8de4e 100644 --- a/apps/www/pages/company.tsx +++ b/apps/www/pages/company.tsx @@ -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 = () => {
{PressData.filter((x) => x.type == 'article').map((x) => ( - - - -
-

{x.type.toUpperCase()}

-

{x.title}

-
-
-
-
+ + + + +
+

{x.type.toUpperCase()}

+

+ {x.title} +

+
+
+
+
+ ))}
{PressData.filter((x) => x.type == 'podcast').map((x) => ( - - - -
-

{x.type.toUpperCase()}

-

{x.title}

-
-
-
-
+ + + + +
+

{x.type.toUpperCase()}

+

+ {x.title} +

+
+
+
+
+ ))}
diff --git a/apps/www/styles/index.css b/apps/www/styles/index.css index 13b6b8b6e55..163baf6c97c 100644 --- a/apps/www/styles/index.css +++ b/apps/www/styles/index.css @@ -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; +} diff --git a/studio/components/grid/SupabaseGrid.tsx b/studio/components/grid/SupabaseGrid.tsx index 4f192d036d5..6a09b66a572 100644 --- a/studio/components/grid/SupabaseGrid.tsx +++ b/studio/components/grid/SupabaseGrid.tsx @@ -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((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 ( @@ -57,103 +50,101 @@ export const SupabaseGrid = React.forwardRef< - ); -}); + ) +}) -const SupabaseGridLayout = React.forwardRef( - (props, ref) => { - const { editable, storageRef, gridProps, headerActions } = props; - const dispatch = useDispatch(); - const state = useTrackedState(); - const gridRef = React.useRef(null); - const [mounted, setMount] = React.useState(false); +const SupabaseGridLayout = forwardRef((props, ref) => { + const { editable, storageRef, gridProps, headerActions } = props + const dispatch = useDispatch() + const state = useTrackedState() + const gridRef = useRef(null) + const [mounted, setMount] = useState(false) - React.useImperativeHandle(ref, () => ({ - rowAdded(row: Dictionary) { - dispatch({ - type: 'ADD_NEW_ROW', - payload: row, - }); - }, - rowEdited(row: Dictionary, idx: number) { - dispatch({ - type: 'EDIT_ROW', - payload: { row, idx }, - }); - }, - })); + useImperativeHandle(ref, () => ({ + rowAdded(row: Dictionary) { + dispatch({ + type: 'ADD_NEW_ROW', + payload: row, + }) + }, + rowEdited(row: Dictionary, 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 ( -
-
- -
- - {mounted && createPortal(, document.body)} -
- ); - } -); + return ( +
+
+ +
+ + {mounted && createPortal(, document.body)} +
+ ) +}) diff --git a/studio/components/grid/components/common/ConfirmAlert.tsx b/studio/components/grid/components/common/ConfirmAlert.tsx index d1e0dd7f03a..294e3916cc0 100644 --- a/studio/components/grid/components/common/ConfirmAlert.tsx +++ b/studio/components/grid/components/common/ConfirmAlert.tsx @@ -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; - variant?: 'danger' | 'warning' | 'success' | undefined; -}; + title: string + message: string + onConfirm?: () => void + onAsyncConfirm?: () => Promise + variant?: 'danger' | 'warning' | 'success' | undefined +} const ConfirmModal: React.FC = ({ title, message, @@ -16,26 +16,26 @@ const ConfirmModal: React.FC = ({ 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 = ({ - @@ -67,32 +63,32 @@ const ConfirmModal: React.FC = ({ } > -

{message}

+

{message}

- ); -}; + ) +} 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(, divTarget); + render(, divTarget) } else { - divTarget = document.createElement('div'); - divTarget.id = 'supabase-ui-confirm-alert'; - document.body.appendChild(divTarget); - render(, divTarget); + divTarget = document.createElement('div') + divTarget.id = 'supabase-ui-confirm-alert' + document.body.appendChild(divTarget) + render(, divTarget) } } export function showConfirmAlert(props: ConfirmModalProps) { - createElement(props); + createElement(props) } diff --git a/studio/components/grid/components/footer/pagination/Pagination.tsx b/studio/components/grid/components/footer/pagination/Pagination.tsx index 4e9a317db08..1c3ef9f5847 100644 --- a/studio/components/grid/components/footer/pagination/Pagination.tsx +++ b/studio/components/grid/components/footer/pagination/Pagination.tsx @@ -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 = () => { - 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 = () => { + 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) { - 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 (
{state.totalRows < 0 ? ( - ... loading total records +

Loading records...

) : ( <> - - {`${state.totalRows.toLocaleString()} records`} - +

{`${state.totalRows.toLocaleString()} records`}

+ {state.isLoading && } )}
- ); -}; -export default Pagination; + ) +} +export default Pagination diff --git a/studio/components/grid/components/grid/Grid.tsx b/studio/components/grid/components/grid/Grid.tsx index 43f2757aa2b..471ee85f128 100644 --- a/studio/components/grid/components/grid/Grid.tsx +++ b/studio/components/grid/components/grid/Grid.tsx @@ -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( + forwardRef( ( - { width, height, containerClass, gridClass, rowClass }, + { width, height, containerClass, gridClass, rowClass, rows }, ref: React.Ref | 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 - ) { - 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) { + 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' }} >
-
+
-
Loading...
+
Loading...
- ); + ) } + return (
- ); + ) } ) -); +) -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) diff --git a/studio/components/grid/services/row/SqlRowService.ts b/studio/components/grid/services/row/SqlRowService.ts index 057d56ad3d0..9605133ef2f 100644 --- a/studio/components/grid/services/row/SqlRowService.ts +++ b/studio/components/grid/services/row/SqlRowService.ts @@ -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 } } diff --git a/studio/components/grid/services/row/index.tsx b/studio/components/grid/services/row/index.tsx index 36bbb934ff3..666f4f1a728 100644 --- a/studio/components/grid/services/row/index.tsx +++ b/studio/components/grid/services/row/index.tsx @@ -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' diff --git a/studio/components/grid/store/reducers/row.ts b/studio/components/grid/store/reducers/row.ts index 569aa5a5815..a3de4a47f9f 100644 --- a/studio/components/grid/store/reducers/row.ts +++ b/studio/components/grid/store/reducers/row.ts @@ -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; - selectedCellPosition: { idx: number; rowIdx: number } | null; - page: number; - rowsPerPage: number; - totalRows: number; + isLoading: boolean + rows: SupaRow[] + selectedRows: ReadonlySet + 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 }; + type: 'SELECTED_ROWS_CHANGE' + payload: { selectedRows: ReadonlySet } } | { 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 } | { type: 'EDIT_ROW'; payload: { row: Dictionary; 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 diff --git a/studio/components/grid/utils/row.ts b/studio/components/grid/utils/row.ts index 718fa4b1d77..b0c45f8f270 100644 --- a/studio/components/grid/utils/row.ts +++ b/studio/components/grid/utils/row.ts @@ -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) diff --git a/studio/components/interfaces/Authentication/Policies/PolicyEditorModal/index.tsx b/studio/components/interfaces/Authentication/Policies/PolicyEditorModal/index.tsx index 1f63042e15c..5503d234ac4 100644 --- a/studio/components/interfaces/Authentication/Policies/PolicyEditorModal/index.tsx +++ b/studio/components/interfaces/Authentication/Policies/PolicyEditorModal/index.tsx @@ -120,18 +120,21 @@ const PolicyEditorModal: FC = ({ 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 = ({ 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) diff --git a/studio/components/interfaces/Database/Publications/PublicationsTableItem.tsx b/studio/components/interfaces/Database/Publications/PublicationsTableItem.tsx index 63074d6b764..2fa4a01b8eb 100644 --- a/studio/components/interfaces/Database/Publications/PublicationsTableItem.tsx +++ b/studio/components/interfaces/Database/Publications/PublicationsTableItem.tsx @@ -54,11 +54,11 @@ const PublicationsTableItem: FC = ({ table, selectedPublication }) => { {table.name} {table.schema} - + {table.comment} -
+
{enabledForAllTables ? ( // @ts-ignore = ({ 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)} /> diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/JsonEditor.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/JsonEditor.tsx index 2298bc6cddb..67ce557846c 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/JsonEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/JsonEditor.tsx @@ -33,8 +33,8 @@ const JsonEdit: FC = ({ 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() } diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.tsx index 40dc8b0ac7e..02d985c796d 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.tsx @@ -82,6 +82,7 @@ const RowEditor: FC = ({ 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 = ({ 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] }) diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils.ts b/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils.ts index 81826dcbf9b..fb93bd3f0e6 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils.ts +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils.ts @@ -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)) { diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx index eff9bad6c29..e1403753d2d 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx @@ -148,13 +148,15 @@ const SidePanelEditor: FC = ({ 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 = ({ }) const table: any = await meta.duplicateTable(payload, { isRLSEnabled, + isRealtimeEnabled, isDuplicateRows, duplicateTable, }) @@ -179,7 +182,14 @@ const SidePanelEditor: FC = ({ 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 = ({ toastId, selectedTableToEdit, payload, - columns + columns, + isRealtimeEnabled ) if (hasError) { ui.setNotification({ @@ -258,8 +269,6 @@ const SidePanelEditor: FC = ({ )} = ({ 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 = ({ 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 = ({
{spreadsheetData.headers.length > 0 && ( -
+
Content Preview @@ -188,7 +190,7 @@ const SpreadsheetImport: FC = ({
onSelectExpandError(key)} /> diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadsheetImport.utils.ts b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadsheetImport.utils.ts index 4ba1a2c91b3..ed3dca9ffcd 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadsheetImport.utils.ts +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadsheetImport.utils.ts @@ -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' } diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx index 6fcba4fe396..d03c3478f9b 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx @@ -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 = ({ table, - tables = [], - enumTypes = [] as PostgresType[], selectedSchema, isDuplicating, visible = false, @@ -52,9 +49,23 @@ const TableEditor: FC = ({ 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({}) const [tableFields, setTableFields] = useState() const [isDuplicateRows, setIsDuplicateRows] = useState(false) @@ -70,7 +81,11 @@ const TableEditor: FC = ({ 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 = ({ 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 = ({ tableId: table?.id, importContent, isRLSEnabled: tableFields.isRLSEnabled, + isRealtimeEnabled: tableFields.isRealtimeEnabled, isDuplicateRows: isDuplicateRows, } @@ -187,8 +203,16 @@ const TableEditor: FC = ({
} 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" + /> + onUpdateField({ isRealtimeEnabled: !tableFields.isRealtimeEnabled })} size="medium" />
diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.types.ts b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.types.ts index 4e7d4ecdc64..da2fc543765 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.types.ts +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.types.ts @@ -7,6 +7,7 @@ export interface TableField { comment?: string columns: ColumnField[] isRLSEnabled: boolean + isRealtimeEnabled: boolean } export interface ImportContent { diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.utils.ts b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.utils.ts index 2e02f1ff191..b17c714db37 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.utils.ts +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.utils.ts @@ -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, } } diff --git a/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx b/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx index 7a73bb73d18..1ab3f658cdd 100644 --- a/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx +++ b/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx @@ -37,6 +37,7 @@ const TableEditorLayout: FC = ({ meta.schemas.load() meta.tables.load() meta.types.load() + meta.publications.load() } }, [ui.selectedProject]) diff --git a/studio/hooks/misc/useStore.tsx b/studio/hooks/misc/useStore.tsx index 15f553cb98b..b267cb010a4 100644 --- a/studio/hooks/misc/useStore.tsx +++ b/studio/hooks/misc/useStore.tsx @@ -58,7 +58,7 @@ export const StoreProvider: FC = ({ 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( diff --git a/studio/package-lock.json b/studio/package-lock.json index 346948ff30f..5a72584b7e0 100644 --- a/studio/package-lock.json +++ b/studio/package-lock.json @@ -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": { diff --git a/studio/stores/pgmeta/MetaStore.ts b/studio/stores/pgmeta/MetaStore.ts index 874bfa65ada..986680b48dc 100644 --- a/studio/stores/pgmeta/MetaStore.ts +++ b/studio/stores/pgmeta/MetaStore.ts @@ -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 } } diff --git a/studio/styles/ui.scss b/studio/styles/ui.scss index 5db599ca008..edd4643abf4 100644 --- a/studio/styles/ui.scss +++ b/studio/styles/ui.scss @@ -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; diff --git a/studio/tests/components/Editor/SpreadsheetImport.utils.test.js b/studio/tests/components/Editor/SpreadsheetImport.utils.test.js index a9d49688477..11ebfbe75c9 100644 --- a/studio/tests/components/Editor/SpreadsheetImport.utils.test.js +++ b/studio/tests/components/Editor/SpreadsheetImport.utils.test.js @@ -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) diff --git a/web/docs/guides/api.mdx b/web/docs/guides/api.mdx index edebca59f58..8883bb38142 100644 --- a/web/docs/guides/api.mdx +++ b/web/docs/guides/api.mdx @@ -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() ``` diff --git a/web/docs/guides/integrations/prisma.mdx b/web/docs/guides/integrations/prisma.mdx index 635e3bb9124..09411d99dfe 100644 --- a/web/docs/guides/integrations/prisma.mdx +++ b/web/docs/guides/integrations/prisma.mdx @@ -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 ![Connection pool settings](/img/guides/integrations/prisma/w0oowg8vq435ob5c3gf0.png) -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