diff --git a/apps/www/components/LaunchWeek/LaunchSection/TicketBrickWallSlider.tsx b/apps/www/components/LaunchWeek/LaunchSection/TicketBrickWallSlider.tsx index 7565f658aea..59271c61e43 100644 --- a/apps/www/components/LaunchWeek/LaunchSection/TicketBrickWallSlider.tsx +++ b/apps/www/components/LaunchWeek/LaunchSection/TicketBrickWallSlider.tsx @@ -18,7 +18,7 @@ interface Props { export function TicketBrickWallSlider({ users, reverse, speed = 10000 }: Props) { const ref = useRef(null) - const [swiperInstace, setSwiperInstance] = useState(null) + const [swiperInstance, setSwiperInstance] = useState(null) const { width } = useWindowSize() const STORAGE_URL = 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/lw7' const BUCKET_FOLDER_VERSION = 'v3' @@ -28,8 +28,8 @@ export function TicketBrickWallSlider({ users, reverse, speed = 10000 }: Props) }/${BUCKET_FOLDER_VERSION}/${username}.png` useEffect(() => { - // trigger autplay if viewport resize - if (swiperInstace) swiperInstace?.autoplay?.run() + // trigger autoplay if viewport resize + if (swiperInstance) swiperInstance?.autoplay?.run() }, [width]) return ( diff --git a/packages/ui/src/lib/theme/defaultTheme.ts b/packages/ui/src/lib/theme/defaultTheme.ts index b9769a03a46..c33ae2a99da 100644 --- a/packages/ui/src/lib/theme/defaultTheme.ts +++ b/packages/ui/src/lib/theme/defaultTheme.ts @@ -369,6 +369,7 @@ export default { transition shadow-sm rounded + border focus:outline-none focus-visible:ring focus-visible:ring-scale-700 @@ -376,13 +377,13 @@ export default { `, inactive: ` bg-scale-200 - border border-scale-700 hover:border-scale-900 + border-scale-700 hover:border-scale-900 text-scale-900 hover:text-scale-1200 `, active: ` - bg-scale-1200 - text-scale-200 - border-scale-1200 + bg-scale-600 + text-scale-1200 + border-scale-800 `, }, 'rounded-pills': { diff --git a/studio/components/grid/SupabaseGrid.tsx b/studio/components/grid/SupabaseGrid.tsx index 5318d088f19..3a1f1fbaffb 100644 --- a/studio/components/grid/SupabaseGrid.tsx +++ b/studio/components/grid/SupabaseGrid.tsx @@ -66,6 +66,11 @@ const SupabaseGridLayout = forwardRef((props showCustomChildren, customHeader, children, + onAddRow, + onAddColumn, + updateTableRow, + onEditForeignKeyColumnValue, + onImportData, } = props const dispatch = useDispatch() const state = useTrackedState() @@ -79,15 +84,13 @@ const SupabaseGridLayout = forwardRef((props const sorts = formatSortURLParams(sort as string[]) const filters = formatFilterURLParams(filter as string[]) - const table = props.table - const { project } = useProjectContext() const { data, isLoading, isRefetching } = useTableRowsQuery( { - queryKey: [table.schema, table.name], + queryKey: [props.table.schema, props.table.name], projectRef: project?.ref, connectionString: project?.connectionString, - table, + table: props.table, sorts, filters, page: state.page, @@ -194,17 +197,18 @@ const SupabaseGridLayout = forwardRef((props if (!state.table || refreshView || refreshTable) { initializeData() } - }, [state.metaService, state.table, table, props.schema]) + }, [state.metaService, state.table, props.table, props.schema]) return (
@@ -216,8 +220,8 @@ const SupabaseGridLayout = forwardRef((props ref={gridRef} {...gridProps} rows={data?.rows ?? []} - updateRow={props.updateTableRow} - onEditForeignKeyColumnValue={props.onEditForeignKeyColumnValue} + updateRow={updateTableRow} + onEditForeignKeyColumnValue={onEditForeignKeyColumnValue} />
@@ -225,7 +229,7 @@ const SupabaseGridLayout = forwardRef((props )} {mounted && - createPortal(, document.body)} + createPortal(, document.body)}
) }) diff --git a/studio/components/grid/components/header/Header.tsx b/studio/components/grid/components/header/Header.tsx index 61efa5c7b37..fbe92a1ff67 100644 --- a/studio/components/grid/components/header/Header.tsx +++ b/studio/components/grid/components/header/Header.tsx @@ -1,6 +1,15 @@ import { saveAs } from 'file-saver' import { useState, ReactNode } from 'react' -import { Button, IconDownload, IconX, IconTrash, Dropdown, IconChevronDown } from 'ui' +import { + Button, + IconDownload, + IconX, + IconTrash, + Dropdown, + IconChevronDown, + IconFileText, + IconArrowUp, +} from 'ui' import { PermissionAction } from '@supabase/shared-types/out/constants' import { checkPermissions, useStore, useUrlState } from 'hooks' @@ -18,6 +27,7 @@ import { useTableRowTruncateMutation } from 'data/table-rows/table-row-truncate- import { useTableRowsCountQuery } from 'data/table-rows/table-rows-count-query' import { useTableRowsQuery } from 'data/table-rows/table-rows-query' import RLSBannerWarning from './RLSBannerWarning' +import clsx from 'clsx' // [Joshen] CSV exports require this guard as a fail-safe if the table is // just too large for a browser to keep all the rows in memory before @@ -31,6 +41,7 @@ export type HeaderProps = { isRefetching: boolean onAddColumn?: () => void onAddRow?: () => void + onImportData?: () => void headerActions?: ReactNode customHeader: ReactNode } @@ -41,6 +52,7 @@ const Header = ({ filters, onAddColumn, onAddRow, + onImportData, headerActions, customHeader, isRefetching, @@ -63,6 +75,7 @@ const Header = ({ isRefetching={isRefetching} onAddColumn={onAddColumn} onAddRow={onAddRow} + onImportData={onImportData} /> )} @@ -81,8 +94,15 @@ type DefaultHeaderProps = { isRefetching: boolean onAddColumn?: () => void onAddRow?: () => void + onImportData?: () => void } -const DefaultHeader = ({ table, isRefetching, onAddColumn, onAddRow }: DefaultHeaderProps) => { +const DefaultHeader = ({ + table, + isRefetching, + onAddColumn, + onAddRow, + onImportData, +}: DefaultHeaderProps) => { const canAddNew = onAddRow !== undefined || onAddColumn !== undefined // [Joshen] Using this logic to block both column and row creation/update/delete @@ -115,7 +135,6 @@ const DefaultHeader = ({ table, isRefetching, onAddColumn, onAddRow }: DefaultHe key="add-row" className="group" onClick={onAddRow} - disabled={onAddRow === undefined} icon={
@@ -142,7 +161,6 @@ const DefaultHeader = ({ table, isRefetching, onAddColumn, onAddRow }: DefaultHe key="add-column" className="group" onClick={onAddColumn} - disabled={onAddColumn === undefined} icon={
@@ -163,6 +181,33 @@ const DefaultHeader = ({ table, isRefetching, onAddColumn, onAddRow }: DefaultHe , ] : []), + ...(onImportData !== undefined + ? [ + + + +
+ } + > +
+

Import data from CSV

+

Insert new rows from a CSV

+
+ , + ] + : []), ]} >
diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetImportConfiguration.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetImportConfiguration.tsx new file mode 100644 index 00000000000..f77a9d7194d --- /dev/null +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetImportConfiguration.tsx @@ -0,0 +1,71 @@ +import clsx from 'clsx' +import { useState } from 'react' +import { SidePanel, Button, IconChevronDown, Collapsible } from 'ui' +import { SpreadsheetData } from './SpreadsheetImport.types' + +interface SpreadSheetImportConfigurationProps { + spreadsheetData: SpreadsheetData + selectedHeaders: string[] + onToggleHeader: (header: string) => void +} + +const SpreadsheetImportConfiguration = ({ + spreadsheetData, + selectedHeaders, + onToggleHeader, +}: SpreadSheetImportConfigurationProps) => { + const [expandConfiguration, setExpandConfiguration] = useState(false) + + return ( + + + +
+

Configure import data

+
+
+
+ + +
+
+

Select which columns to import

+

+ By default, all columns are selected to be imported from your CSV +

+
+
+ {spreadsheetData.headers.map((header) => { + const isSelected = selectedHeaders.includes(header) + return ( + + ) + })} +
+
+
+
+
+ ) +} + +export default SpreadsheetImportConfiguration diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadSheetTextInput.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetTextInput.tsx similarity index 95% rename from studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadSheetTextInput.tsx rename to studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetTextInput.tsx index a5641296883..54b30ce6c94 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadSheetTextInput.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetTextInput.tsx @@ -15,7 +15,7 @@ const SpreadSheetTextInput: FC = ({ input, onInputChange }) => ( include any special characters other than hyphens (-) or underscores ( _).

-

+

Tip: Datetime columns should be formatted as YYYY-MM-DD HH:mm:ss

diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadsheetImport.constants.ts b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.constants.ts similarity index 85% rename from studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadsheetImport.constants.ts rename to studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.constants.ts index ac5a636ebcc..7c29186ce9a 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadsheetImport.constants.ts +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.constants.ts @@ -7,10 +7,7 @@ export const UPLOAD_FILE_TYPES = [ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ] -export const UPLOAD_FILE_EXTENSIONS = [ - 'csv', - 'tsv', -] +export const UPLOAD_FILE_EXTENSIONS = ['csv', 'tsv'] export const EMPTY_SPREADSHEET_DATA: SpreadsheetData = { headers: [], diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadsheetImport.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.tsx similarity index 53% rename from studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadsheetImport.tsx rename to studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.tsx index 5720920afbb..4a2c7954a3c 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadsheetImport.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.tsx @@ -1,12 +1,12 @@ import { useCallback, useState, FC, useEffect } from 'react' -import { debounce, includes } from 'lodash' -import { SidePanel, Tabs, IconArrowRight, IconChevronRight } from 'ui' +import { debounce, includes, noop } from 'lodash' +import { SidePanel, Tabs } from 'ui' +import { PostgresTable } from '@supabase/postgres-meta' import { useStore } from 'hooks' -import ActionBar from '../../ActionBar' +import ActionBar from '../ActionBar' import SpreadSheetTextInput from './SpreadSheetTextInput' import SpreadSheetFileUpload from './SpreadSheetFileUpload' -import SpreadsheetPreview from './SpreadsheetPreview' import { SpreadsheetData } from './SpreadsheetImport.types' import { acceptedFileExtension, @@ -14,31 +14,36 @@ import { parseSpreadsheetText, } from './SpreadsheetImport.utils' import { UPLOAD_FILE_TYPES, EMPTY_SPREADSHEET_DATA } from './SpreadsheetImport.constants' +import { ImportContent } from '../TableEditor/TableEditor.types' +import SpreadsheetImportConfiguration from './SpreadSheetImportConfiguration' +import SpreadsheetImportPreview from './SpreadsheetImportPreview' interface Props { debounceDuration?: number headers?: string[] rows?: any[] visible: boolean - saveContent: (prefillData: any) => void + selectedTable?: PostgresTable + saveContent: (prefillData: ImportContent) => void closePanel: () => void + updateEditorDirty?: (value: boolean) => void } const SpreadsheetImport: FC = ({ + visible = false, debounceDuration = 250, headers = [], rows = [], + selectedTable, saveContent, closePanel, - visible = false, + updateEditorDirty = noop, }) => { const { ui } = useStore() useEffect(() => { - if (visible) { - if (headers.length === 0) { - resetSpreadsheetImport() - } + if (visible && headers.length === 0) { + resetSpreadsheetImport() } }, [visible]) @@ -52,7 +57,13 @@ const SpreadsheetImport: FC = ({ columnTypeMap: {}, }) const [errors, setErrors] = useState([]) - const [expandedErrors, setExpandedErrors] = useState([]) + const [selectedHeaders, setSelectedHeaders] = useState([]) + + const selectedTableColumns = (selectedTable?.columns ?? []).map((column) => column.name) + const incompatibleHeaders = selectedHeaders.filter( + (header) => !selectedTableColumns.includes(header) + ) + const isCompatible = selectedTable !== undefined ? incompatibleHeaders.length === 0 : true const onProgressUpdate = (progress: number) => { setParseProgress(progress) @@ -68,6 +79,7 @@ const SpreadsheetImport: FC = ({ message: 'Sorry! We only accept CSV or TSV file types, please upload another file.', }) } else { + updateEditorDirty(true) setUploadedFile(file) const { headers, rowCount, columnTypeMap, errors, previewRows } = await parseSpreadsheet( file, @@ -83,6 +95,7 @@ const SpreadsheetImport: FC = ({ } setErrors(errors) + setSelectedHeaders(headers) setSpreadsheetData({ headers, rows: previewRows, rowCount, columnTypeMap }) } event.target.value = '' @@ -93,7 +106,7 @@ const SpreadsheetImport: FC = ({ setSpreadsheetData(EMPTY_SPREADSHEET_DATA) setUploadedFile(null) setErrors([]) - setExpandedErrors([]) + updateEditorDirty(false) } const readSpreadsheetText = async (text: string) => { @@ -108,6 +121,7 @@ const SpreadsheetImport: FC = ({ }) } setErrors(errors) + setSelectedHeaders(headers) setSpreadsheetData({ headers, rows, rowCount: rows.length, columnTypeMap }) } else { setSpreadsheetData(EMPTY_SPREADSHEET_DATA) @@ -120,11 +134,28 @@ const SpreadsheetImport: FC = ({ handler(event.target.value) } - const onSelectExpandError = (key: string) => { - if (expandedErrors.includes(key)) { - setExpandedErrors(expandedErrors.filter((error) => error !== key)) + const onToggleHeader = (header: string) => { + const updatedSelectedHeaders = selectedHeaders.includes(header) + ? selectedHeaders.filter((h) => h !== header) + : selectedHeaders.concat([header]) + setSelectedHeaders(updatedSelectedHeaders) + } + + const onConfirm = (resolve: () => void) => { + if (selectedHeaders.length === 0) { + ui.setNotification({ + category: 'error', + message: 'Please select at least one header from your CSV', + }) + resolve() + } else if (!isCompatible) { + ui.setNotification({ + category: 'error', + message: 'The data that you are trying to import is incompatible with your table structure', + }) + resolve() } else { - setExpandedErrors(expandedErrors.concat([key])) + saveContent({ file: uploadedFile, ...spreadsheetData, selectedHeaders, resolve }) } } @@ -133,24 +164,34 @@ const SpreadsheetImport: FC = ({ size="large" visible={visible} align="right" - header="Add content to new table" + header={ + selectedTable !== undefined ? ( + <> + Add data to{' '} + + {selectedTable.schema}.{selectedTable.name} + + + ) : ( + 'Add content to new table' + ) + } onCancel={() => closePanel()} customFooter={ { - saveContent({ - file: uploadedFile, - ...spreadsheetData, - }) - }} + applyFunction={onConfirm} /> } + onInteractOutside={(event) => { + const isToast = (event.target as Element)?.closest('#toast') + if (isToast) event.preventDefault() + }} > -
+
= ({
- - {spreadsheetData.headers.length > 0 && ( -
-
-
-

Content Preview

-

- Your table will have {spreadsheetData.rowCount.toLocaleString()} rows and the - following {spreadsheetData.headers.length} columns. -

-

- Here is a preview of your table (up to the first 20 columns and first 20 rows). -

-
- -
- {errors.length > 0 && ( -
-
-

Issues found in spreadsheet

-

- Your table can still be created nonetheless despite issues in the following - rows. -

-
-
- {errors.map((error: any, idx: number) => { - const key = `import-error-${idx}` - const isExpanded = expandedErrors.includes(key) - return ( -
-
- onSelectExpandError(key)} - /> -

Row: {error.row}

-

{error.message}

- {error.data?.__parsed_extra && ( - <> - -

Extra field(s):

- {error.data?.__parsed_extra.map((value: any, i: number) => ( - {value} - ))} - - )} -
- {isExpanded && ( - - )} -
- ) - })} -
-
- )} -
- )} + {spreadsheetData.headers.length > 0 && ( + <> +
+ +
+ + + + + + )} ) } diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadsheetImport.types.ts b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.types.ts similarity index 100% rename from studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadsheetImport.types.ts rename to studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.types.ts diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadsheetImport.utils.ts b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.utils.ts similarity index 100% rename from studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadsheetImport.utils.ts rename to studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.utils.ts diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImportPreview.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImportPreview.tsx new file mode 100644 index 00000000000..5e967af0f64 --- /dev/null +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImportPreview.tsx @@ -0,0 +1,200 @@ +import clsx from 'clsx' +import { useEffect, useState } from 'react' +import { + SidePanel, + Badge, + Button, + Collapsible, + IconChevronDown, + IconChevronRight, + IconArrowRight, + IconAlertCircle, +} from 'ui' + +import { SpreadsheetData } from './SpreadsheetImport.types' +import SpreadsheetPreviewGrid from './SpreadsheetPreviewGrid' +import { PostgresTable } from '@supabase/postgres-meta' + +const MAX_ROWS = 20 +const MAX_HEADERS = 20 + +interface SpreadsheetImportPreviewProps { + selectedTable?: PostgresTable + spreadsheetData: SpreadsheetData + errors?: any[] + selectedHeaders: string[] + incompatibleHeaders: string[] +} + +const SpreadsheetImportPreview = ({ + selectedTable, + spreadsheetData, + errors = [], + selectedHeaders, + incompatibleHeaders, +}: SpreadsheetImportPreviewProps) => { + const [expandPreview, setExpandPreview] = useState(false) + const [expandedErrors, setExpandedErrors] = useState([]) + + const { headers, rows } = spreadsheetData + const previewHeaders = headers + .filter((header) => selectedHeaders.includes(header)) + .slice(0, MAX_HEADERS) + const previewRows = rows.slice(0, MAX_ROWS) + + const isCompatible = selectedTable !== undefined ? incompatibleHeaders.length === 0 : true + + useEffect(() => { + setExpandPreview(true) + }, [spreadsheetData]) + + const onSelectExpandError = (key: string) => { + if (expandedErrors.includes(key)) { + setExpandedErrors(expandedErrors.filter((error) => error !== key)) + } else { + setExpandedErrors(expandedErrors.concat([key])) + } + } + + return ( + + + +
+
+

Preview data to be imported

+ {!isCompatible && Data incompatible} + {errors.length > 0 && {errors.length} issues found} +
+
+
+
+ + +
+

+ {selectedTable === undefined + ? `Your table will have ${spreadsheetData.rowCount.toLocaleString()} rows and the + following ${spreadsheetData.headers.length} columns.` + : `A total of ${spreadsheetData.rowCount.toLocaleString()} rows will be added to the table "${ + selectedTable.name + }"`} +

+

+ Here is a preview of the data that will be added (up to the first 20 columns and first + 20 rows). +

+
+
+ {previewHeaders.length > 0 && previewRows.length > 0 ? ( + + ) : ( +
+ +

+ {previewHeaders.length === 0 + ? 'No headers have been selected' + : previewRows.length === 0 + ? 'Your CSV contains no data' + : ''} +

+
+ )} +
+ {(!isCompatible || errors.length > 0) && ( +
+
+

Issues found in spreadsheet

+ {isCompatible && ( +

+ {selectedTable !== undefined + ? 'This CSV can still be imported into your table despite issues in the following rows.' + : 'Your table can still be created nonetheless despite issues in the following rows.'} +

+ )} +
+
+ {!isCompatible && ( +
+
+
+
+
+

+ This CSV cannot be imported into your + table due to incompatible headers: +
+ The column{incompatibleHeaders.length > 1 ? 's' : ''}{' '} + {incompatibleHeaders.map((x) => `"${x}"`).join(', ')}{' '} + {incompatibleHeaders.length > 1 ? 'are' : 'is'} not present in your table +

+
+
+ )} + {errors.map((error: any, idx: number) => { + const key = `import-error-${idx}` + const isExpanded = expandedErrors.includes(key) + + return ( +
+
onSelectExpandError(key)} + > + {error.data !== undefined ? ( + + ) : ( +
+
+
+ )} + {error.data !== undefined && ( +

Row: {error.row}

+ )} +

{error.message}

+ {error.data?.__parsed_extra && ( + <> + +

Extra field(s):

+ {error.data?.__parsed_extra.map((value: any, i: number) => ( + + {value} + + ))} + + )} +
+ {error.data !== undefined && isExpanded && ( + + )} +
+ ) + })} +
+
+ )} + + + + ) +} + +export default SpreadsheetImportPreview diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetPreviewGrid.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetPreviewGrid.tsx new file mode 100644 index 00000000000..671ace28386 --- /dev/null +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetPreviewGrid.tsx @@ -0,0 +1,55 @@ +import clsx from 'clsx' +import DataGrid from '@supabase/react-data-grid' + +const MAX_ROWS = 20 +const MAX_HEADERS = 20 + +interface SpreadsheetPreviewGridProps { + headers: string[] + rows?: any[] + height?: number +} + +const SpreadsheetPreviewGrid = ({ + headers = [], + rows = [], + height, +}: SpreadsheetPreviewGridProps) => { + const previewHeaders = headers.slice(0, MAX_HEADERS) + const previewRows = rows.slice(0, MAX_ROWS) + + return ( + { + const columnValues = previewRows.map((row) => row[header]?.toString() ?? '') + const maxLength = Math.max(...columnValues.map((el) => el.length)) + const maxWidth = maxLength > 20 ? 200 : maxLength * 10 + + return { + key: header, + name: header, + width: maxWidth, + resizable: true, + headerRenderer: () => ( +
+

{header}

+
+ ), + formatter: ({ row }: { row: any }) => { + const isEmpty = !row[header] + return ( + + {isEmpty ? 'NULL' : row[header]} + + ) + }, + } + })} + rows={previewRows} + className="!border-l !border-r" + style={{ height: height || `${34 + 34 * (previewRows.length || 1)}px` }} + /> + ) +} + +export default SpreadsheetPreviewGrid diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx index d01b213b3ab..5c55f64b17e 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx @@ -163,12 +163,10 @@ const ColumnManagement: FC = ({
{hasImportContent && ( -
-

- Your table will be created with {importContent?.rowCount?.toLocaleString()} rows and - the following {columns.length} columns. -

-
+

+ Your table will be created with {importContent?.rowCount?.toLocaleString()} rows and the + following {columns.length} columns. +

)} {primaryKeyColumns.length === 0 && ( diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadsheetPreview.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadsheetPreview.tsx deleted file mode 100644 index dc8a98ead37..00000000000 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadsheetPreview.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { FC } from 'react' -import { isUndefined } from 'lodash' -import DataGrid from '@supabase/react-data-grid' - -interface Props { - headers: string[] - rows?: any[] -} - -const MAX_ROWS = 20 -const MAX_HEADERS = 20 - -const SpreadsheetPreview: FC = ({ headers = [], rows = [] }) => { - const previewHeaders = headers.slice(0, MAX_HEADERS) - const previewRows = rows.slice(0, MAX_ROWS) - return ( - { - return { - key: header, - name: header, - width: header.length * 10, - resizable: true, - headerRenderer: () => ( -
-

{header}

-
- ), - formatter: ({ row }: { row: any }) => ( - - {isUndefined(row[header]) ? 'NULL' : row[header]} - - ), - } - })} - rows={previewRows} - className="!border-l !border-r" - style={{ height: `${34 + 34 * (previewRows.length || 1)}px` }} - /> - ) -} - -export default SpreadsheetPreview diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx index caec9de9950..7f0c9ad1464 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx @@ -1,3 +1,4 @@ +import Link from 'next/link' import { FC, useEffect, useState } from 'react' import { isUndefined, isEmpty } from 'lodash' import { Badge, Checkbox, SidePanel, Input, Alert, IconBookOpen, Button } from 'ui' @@ -7,7 +8,7 @@ import { useStore } from 'hooks' import ActionBar from '../ActionBar' import HeaderTitle from './HeaderTitle' import ColumnManagement from './ColumnManagement' -import SpreadsheetImport from './SpreadsheetImport/SpreadsheetImport' +import { SpreadsheetImport } from '../' import { ColumnField, CreateTablePayload, UpdateTablePayload } from '../SidePanelEditor.types' import { DEFAULT_COLUMNS } from './TableEditor.constants' import { TableField, ImportContent } from './TableEditor.types' @@ -19,7 +20,6 @@ import { } from './TableEditor.utils' import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import Link from 'next/link' interface Props { table?: PostgresTable diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.types.ts b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.types.ts index da2fc543765..eca1af27df7 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.types.ts +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.types.ts @@ -16,4 +16,6 @@ export interface ImportContent { rowCount: number rows: object[] columnTypeMap: Dictionary + selectedHeaders: string[] + resolve: () => void } diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.utils.ts b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.utils.ts index e58420703c6..c79570968ce 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.utils.ts +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.utils.ts @@ -55,9 +55,12 @@ export const generateTableFieldFromPostgresTable = ( } export const formatImportedContentToColumnFields = (importContent: ImportContent) => { - const columnFields = importContent.headers.map((header: string) => { - const columnType = importContent.columnTypeMap[header] - return generateColumnField({ name: header, format: columnType }) - }) + const { headers, selectedHeaders, columnTypeMap } = importContent + const columnFields = headers + .filter((header) => selectedHeaders.includes(header)) + .map((header) => { + const columnType = columnTypeMap[header] + return generateColumnField({ name: header, format: columnType }) + }) return columnFields } diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/index.ts b/studio/components/interfaces/TableGridEditor/SidePanelEditor/index.ts index 373f44921dd..6b0602c7648 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/index.ts +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/index.ts @@ -2,7 +2,8 @@ import SidePanelEditor from './SidePanelEditor' import RowEditor from './RowEditor/RowEditor' import ColumnEditor from './ColumnEditor/ColumnEditor' import TableEditor from './TableEditor/TableEditor' +import SpreadsheetImport from './SpreadsheetImport/SpreadsheetImport' import ForeignKeySelector from './ForeignKeySelector/ForeignKeySelector' -export { RowEditor, ColumnEditor, TableEditor, ForeignKeySelector } +export { RowEditor, ColumnEditor, TableEditor, ForeignKeySelector, SpreadsheetImport } export default SidePanelEditor diff --git a/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index e7daf73a3ae..f8186be45a4 100644 --- a/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -44,7 +44,7 @@ export interface TableGridEditorProps { selectedTable: any // PostgresTable | SchemaView /** Determines what side panel editor to show */ - sidePanelKey?: 'row' | 'column' | 'table' | 'json' | 'foreign-row-selector' + sidePanelKey?: 'row' | 'column' | 'table' | 'json' | 'foreign-row-selector' | 'csv-import' /** Toggles if we're duplicating a table */ isDuplicating: boolean /** Selected entities if we're editing a row, column or table */ @@ -70,6 +70,7 @@ export interface TableGridEditorProps { column: any }) => void onClosePanel: () => void + onImportData: () => void } const TableGridEditor = ({ @@ -93,6 +94,7 @@ const TableGridEditor = ({ onExpandJSONEditor = noop, onEditForeignKeyColumnValue = noop, onClosePanel = noop, + onImportData = noop, }: TableGridEditorProps) => { const { meta, ui, vault } = useStore() const router = useRouter() @@ -381,6 +383,7 @@ const TableGridEditor = ({ onAddRow={onAddRow} updateTableRow={updateTableRow} onEditRow={onEditRow} + onImportData={onImportData} onError={onError} onSqlQuery={onSqlQuery} onExpandJSONEditor={onExpandJSONEditor} diff --git a/studio/pages/project/[ref]/editor/[id].tsx b/studio/pages/project/[ref]/editor/[id].tsx index fd6e82e003d..b59be8949b5 100644 --- a/studio/pages/project/[ref]/editor/[id].tsx +++ b/studio/pages/project/[ref]/editor/[id].tsx @@ -44,7 +44,7 @@ const TableEditorPage: NextPageWithLayout = () => { const [selectedTableToDelete, setSelectedTableToDelete] = useState() const [sidePanelKey, setSidePanelKey] = useState< - 'row' | 'column' | 'table' | 'json' | 'foreign-row-selector' + 'row' | 'column' | 'table' | 'json' | 'foreign-row-selector' | 'csv-import' >() const [selectedRowToEdit, setSelectedRowToEdit] = useState>() const [selectedColumnToEdit, setSelectedColumnToEdit] = useState() @@ -137,6 +137,16 @@ const TableEditorPage: NextPageWithLayout = () => { setSelectedValueForJsonEdit({ column, row, jsonString: JSON.stringify(row[column]) || '' }) } + const onImportData = () => { + if (id) { + setSidePanelKey('csv-import') + const table = meta.tables.byId(id) + setSelectedTableToEdit(table) + } else { + console.error('Table ID not found') + } + } + const onEditForeignKeyColumnValue = ({ foreignKey, row, @@ -280,6 +290,7 @@ const TableEditorPage: NextPageWithLayout = () => { onEditForeignKeyColumnValue={onEditForeignKeyColumnValue} onClosePanel={onClosePanel} theme={ui.themeOption == 'dark' ? 'dark' : 'light'} + onImportData={onImportData} /> any - + insertRowsViaSpreadsheet: ( + file: any, + table: PostgresTable, + selectedHeaders: string[], + onProgressUpdate: (progress: number) => void + ) => void setProjectDetails: (details: { ref: string; connectionString?: string }) => void } export default class MetaStore implements IMetaStore { @@ -652,6 +657,7 @@ export default class MetaStore implements IMetaStore { const { error }: any = await this.insertRowsViaSpreadsheet( importContent.file, table, + importContent.selectedHeaders, (progress: number) => { this.rootStore.ui.setNotification({ id: toastId, @@ -834,6 +840,7 @@ export default class MetaStore implements IMetaStore { async insertRowsViaSpreadsheet( file: any, table: PostgresTable, + selectedHeaders: string[], onProgressUpdate: (progress: number) => void ) { let chunkNumber = 0 @@ -849,9 +856,15 @@ export default class MetaStore implements IMetaStore { chunk: async (results: any, parser: any) => { parser.pause() + const formattedData = results.data.map((row: any) => { + const formattedRow: any = {} + selectedHeaders.forEach((header) => (formattedRow[header] = row[header])) + return formattedRow + }) + const insertQuery = new Query() .from(table.name, table.schema) - .insert(results.data) + .insert(formattedData) .toSql() const res = await this.query(insertQuery) diff --git a/studio/tests/components/Editor/SpreadsheetImport.utils.test.js b/studio/tests/components/Editor/SpreadsheetImport.utils.test.js index 061d2944b93..373da64a4e6 100644 --- a/studio/tests/components/Editor/SpreadsheetImport.utils.test.js +++ b/studio/tests/components/Editor/SpreadsheetImport.utils.test.js @@ -1,4 +1,4 @@ -import { inferColumnType } from 'components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/SpreadsheetImport/SpreadsheetImport.utils' +import { inferColumnType } from 'components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.utils' describe('SpreadsheedImport.utils: inferColumnType', () => { test('should default column type to text if no rows to infer from', () => {