diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx index fe019b312f3..a4995932a23 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx @@ -371,7 +371,7 @@ export const ColumnEditor = ({ isReactForm={false} layout="flex" id="isArray" - label="Define as Array" + label="Define as array" description="Allow column to be defined as variable-length multidimensional arrays" > Type} - + diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/InputWithSuggestions.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/InputWithSuggestions.tsx index b7bf2586727..570e89c883c 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/InputWithSuggestions.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/InputWithSuggestions.tsx @@ -6,7 +6,7 @@ // the component over to the UI library import { noop } from 'lodash' -import { List } from 'lucide-react' +import { ChevronDown } from 'lucide-react' import { useEffect, useRef, useState } from 'react' import { DropdownMenu, @@ -106,12 +106,13 @@ const InputWithSuggestions = ({ - + diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx index 59de2b572df..d6dbaba2076 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx @@ -1,6 +1,6 @@ import { useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' -import { Eye, EyeOff, GripVertical, Link, Plus, Settings, X } from 'lucide-react' +import { GripVertical, Link, Plus, Settings, X } from 'lucide-react' import { useState } from 'react' import { Badge, @@ -12,7 +12,9 @@ import { CommandItem, CommandList, CommandSeparator, - Input, + InputGroup, + InputGroupAddon, + InputGroupInput, Popover, PopoverContent, PopoverTrigger, @@ -57,6 +59,7 @@ interface ColumnProps { isNewRecord: boolean hasForeignKeys: boolean hasImportContent: boolean + gridTemplateColumns: string shouldAutoFocusName?: boolean onUpdateColumn: (changes: Partial) => void onRemoveColumn: () => void @@ -70,6 +73,7 @@ export const Column = ({ isNewRecord = false, hasForeignKeys = false, hasImportContent = false, + gridTemplateColumns, shouldAutoFocusName = false, onUpdateColumn, onRemoveColumn, @@ -83,6 +87,7 @@ export const Column = ({ column.isIdentity ? 1 : 0, column.isUnique ? 1 : 0, column.isArray ? 1 : 0, + column.isSensitiveData ? 1 : 0, ].reduce((a, b) => a + b, 0) const { data } = useForeignKeyConstraintsQuery({ @@ -107,6 +112,21 @@ export const Column = ({ const hasChangesInRelations = relations .map((r) => getRelationStatus(r)) .some((x) => x !== undefined) + const activeRelations = relations.filter((relation) => !relation.toRemove) + + const onToggleSensitiveData = () => { + const marker = '[SENSITIVE]' + const isSensitiveData = !column.isSensitiveData + let updatedComment = column.comment || '' + + if (isSensitiveData && !updatedComment.includes(marker)) { + updatedComment = `${updatedComment} ${marker}`.trim() + } else if (!isSensitiveData) { + updatedComment = updatedComment.replace(marker, '').trim() + } + + onUpdateColumn({ isSensitiveData, comment: updatedComment }) + } const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition } = useSortable({ @@ -119,183 +139,158 @@ export const Column = ({ } return ( -
-
- -
-
-
-
- div>div>div>input]:py-1.5 [&>div>div>div>input]:border-r-transparent [&>div>div>div>input]:rounded-r-none', - hasImportContent ? 'opacity-50' : '' - )} - onChange={(event) => onUpdateColumn({ name: event.target.value })} - /> - - {relations.filter((r) => !r.toRemove).length === 0 ? ( -
+
+ {isNewRecord && ( +
+ + -
- - - -
+ )} +
+
+ + onUpdateColumn({ name: event.target.value })} + /> + + {activeRelations.length === 0 ? ( + + + + + Reference a column in another table + + ) : ( + + + + + + + + + View and add foreign key relations + + + - {column.isSensitiveData ? ( - - ) : ( - - )} - - +
+ Involved in {activeRelations.length} foreign key + {activeRelations.length > 1 ? 's' : ''} +
+ + + + {activeRelations.map((relation, idx) => { + const key = String(relation?.id ?? `${column.id}-relation-${idx}`) - - {column.isSensitiveData - ? 'Data is masked in grid display. Actual data unchanged in database.' - : 'Mark as sensitive to mask in grid display'} - - -
- ) : ( - - - - - -
- Involved in {relations.length} foreign key{relations.length > 1 ? 's' : ''} -
- - - - {relations.map((relation, idx) => { - const key = String(relation?.id ?? `${column.id}-relation-${idx}`) - const status = getRelationStatus(relation) - if (status === 'REMOVE') return null - - return ( - onEditForeignKey(relation)} - onClick={() => onEditForeignKey(relation)} - > - {status === undefined ? ( -
- {relation.name} -
- ) : ( -
- - {status} - -

- {relation.name || ( - <> - To{' '} - {relation.columns - .filter((c) => c.source === column.name) - .map((c) => { - return ( - - {relation.schema}.{relation.table}.{c.target} - - ) - })} - {relation.columns.length > 1 && ( + return ( + onEditForeignKey(relation)} + onClick={() => onEditForeignKey(relation)} + > + {status === undefined ? ( +

+ {relation.name} +
+ ) : ( +
+ + {status} + +

+ {relation.name || ( <> - and {relation.columns.length - 1} other column - {relation.columns.length > 2 ? 's' : ''} + To{' '} + {relation.columns + .filter((c) => c.source === column.name) + .map((c) => ( + + {relation.schema}.{relation.table}.{c.target} + + ))} + {relation.columns.length > 1 && ( + <> + and {relation.columns.length - 1} other column + {relation.columns.length > 2 ? 's' : ''} + + )} )} - - )} -

-
- )} +

+
+ )} +
+ ) + })} +
+ + + onEditForeignKey()} + onClick={() => onEditForeignKey()} + > + +

Add foreign key relation

- ) - })} -
- - - onEditForeignKey()} - onClick={() => onEditForeignKey()} - > - -

Add foreign key relation

-
-
-
-
-
-
- )} + + + + + + )} + +
-
+
-
+
-
+
-
-
- {(!column.isPrimaryKey || column.format.includes('int')) && ( - - - {settingsCount > 0 && ( -
- {settingsCount} -
- )} -
- -
-
- -
-
Extra options
-
+ {hasImportContent &&
} +
+ + + + +
+ +
{!hasImportContent && ( -
- +
+ + +
)}
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx index 6576b692ba0..bb89bded055 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx @@ -72,6 +72,18 @@ export const ColumnManagement = ({ const [columnIdToFocus, setColumnIdToFocus] = useState(null) const hasImportContent = !isEmpty(importContent) + const columnGridTemplate = [ + isNewRecord && '26px', + 'minmax(0, 30fr)', + 'minmax(0, 25fr)', + `minmax(0, ${isNewRecord ? 25 : 30}fr)`, + 'minmax(0, 10fr)', + hasImportContent && 'minmax(0, 10fr)', + '26px', + !hasImportContent && '26px', + ] + .filter(Boolean) + .join(' ') const [primaryKeyColumns, otherColumns] = partition( columns, (column: ColumnField) => column.isPrimaryKey @@ -222,14 +234,24 @@ export const ColumnManagement = ({
{/* Headers */} -
+
{/* Drag handle */} - {isNewRecord &&
} -
+ {isNewRecord &&
} +
Name
- - + +
-
+
Type
-
-
Default Value
+
+
Default value
- - + +
-
+
Primary
{/* Empty space */} -
+ {hasImportContent &&
} {/* More config button */} -
+
{/* Delete button */} - {!hasImportContent &&
} + {!hasImportContent &&
}
{primaryKeyColumns.length > 0 && ( @@ -286,6 +315,7 @@ export const ColumnManagement = ({ hasForeignKeys={checkIfHaveForeignKeys(column)} isNewRecord={isNewRecord} hasImportContent={hasImportContent} + gridTemplateColumns={columnGridTemplate} shouldAutoFocusName={column.id === columnIdToFocus} onUpdateColumn={(changes) => onUpdateColumn(column, changes)} onRemoveColumn={() => onRemoveColumn(column)} @@ -318,6 +348,7 @@ export const ColumnManagement = ({ isNewRecord={isNewRecord} hasForeignKeys={checkIfHaveForeignKeys(column)} hasImportContent={hasImportContent} + gridTemplateColumns={columnGridTemplate} shouldAutoFocusName={column.id === columnIdToFocus} onUpdateColumn={(changes) => onUpdateColumn(column, changes)} onRemoveColumn={() => onRemoveColumn(column)} diff --git a/apps/www/_blog/2021-10-04-supabase-beta-sept-2021.mdx b/apps/www/_blog/2021-10-04-supabase-beta-sept-2021.mdx index 3d029fa3edc..239a29c975c 100644 --- a/apps/www/_blog/2021-10-04-supabase-beta-sept-2021.mdx +++ b/apps/www/_blog/2021-10-04-supabase-beta-sept-2021.mdx @@ -40,11 +40,11 @@ We've improved the column Type field so that it supports your [custom types](htt ![Column types](/images/blog/2021-sept/columns.png) -### Is Unique +### Is unique We've made it simple to add a unique constraint when creating or editing a table. -![Is Unique](/images/blog/2021-sept/is-unique.png) +![Is unique](/images/blog/2021-sept/is-unique.png) ### Edit columns diff --git a/e2e/studio/features/column-editor-types.spec.ts b/e2e/studio/features/column-editor-types.spec.ts index 02c47c91d3e..b049413c036 100644 --- a/e2e/studio/features/column-editor-types.spec.ts +++ b/e2e/studio/features/column-editor-types.spec.ts @@ -58,8 +58,8 @@ test.describe('column editor complex types', () => { 'Type dropdown should show the bare base type, not the raw _int4 pg format' ).toBeVisible() await expect( - columnEditor.getByRole('checkbox', { name: 'Define as Array' }), - 'Define as Array should be checked for an existing array column' + columnEditor.getByRole('checkbox', { name: 'Define as array' }), + 'Define as array should be checked for an existing array column' ).toBeChecked() // Make an unrelated change and save - this must not corrupt the column's type. diff --git a/e2e/studio/features/table-editor.spec.ts b/e2e/studio/features/table-editor.spec.ts index 3cf3afea341..c46a6cc57dd 100644 --- a/e2e/studio/features/table-editor.spec.ts +++ b/e2e/studio/features/table-editor.spec.ts @@ -263,10 +263,10 @@ testRunner('table editor', () => { await page.getByRole('button', { name: 'New table', exact: true }).click() await page.getByTestId('table-name-input').fill(tableNameEnum) await page.getByTestId('created_at-extra-options').click() - await page.getByText('Is Nullable').click() + await page.getByText('Is nullable').click() await page.getByTestId('created_at-extra-options').click() await page.getByRole('button', { name: 'Add column' }).click() - await page.getByLabel('Column name').nth(2).fill(columnNameEnum) + await page.getByLabel('Column name').nth(3).fill(columnNameEnum) await page.getByRole('combobox').filter({ hasText: 'Choose a column type...' }).click() await page.getByPlaceholder('Search types...').fill(enum_name) // wait for response, then click @@ -357,7 +357,7 @@ testRunner('table editor', () => { .click() await page.getByRole('menuitem', { name: 'Edit table' }).click() await page.getByTestId('table-name-input').fill(tableNameUpdated) - await page.getByLabel('Column name').nth(2).fill(columnNameUpdated) + await page.getByLabel('Column name').nth(3).fill(columnNameUpdated) const updateTablePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=column-update', { method: 'POST', }) @@ -948,7 +948,7 @@ testRunner('table editor', () => { // Add boolean column await page.getByRole('button', { name: 'Add column' }).click() - await page.getByLabel('Column name').nth(3).fill(boolColName) + await page.getByLabel('Column name').nth(4).fill(boolColName) await page.getByText('Choose a column type...').click() await page.getByPlaceholder('Search types...').fill('bool') await page.getByRole('option', { name: 'bool' }).first().click() @@ -1064,7 +1064,7 @@ testRunner('table editor', () => { // Add nullable boolean column await page.getByRole('button', { name: 'Add column' }).click() - await page.getByLabel('Column name').nth(3).fill(boolColName) + await page.getByLabel('Column name').nth(4).fill(boolColName) await page.getByText('Choose a column type...').click() await page.getByPlaceholder('Search types...').fill('bool') await page.getByRole('option', { name: 'bool' }).first().click() @@ -1985,14 +1985,14 @@ testRunner('table editor', () => { await page.getByRole('button', { name: 'New table' }).click() await page.getByLabel('Name', { exact: true }).fill(tableName) await page.getByRole('button', { name: 'Add column' }).click() - await page.getByLabel('Column name').nth(2).fill('pw_column') + await page.getByLabel('Column name').nth(3).fill('pw_column') await page.getByRole('combobox').filter({ hasText: 'Choose a column type...' }).click() await page.getByRole('option').filter({ hasText: 'int8' }).click() - await page.getByLabel('Column default value').nth(2).fill('invalid') + await page.getByLabel('Column default value').nth(3).fill('invalid') await page.getByRole('button', { name: 'Save' }).click() await expect(page.getByText('invalid input syntax')).toBeVisible() - await page.getByLabel('Column default value').nth(2).fill('10') + await page.getByLabel('Column default value').nth(3).fill('10') await page.getByRole('button', { name: 'Save' }).click() await expect(page.getByText(`Table ${tableName} is good to go!`)).toBeVisible() await expect(page.getByRole('button', { name: `View ${tableName}`, exact: true })).toBeVisible()