Extra options
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx
index c4aec7cb423..4bf450d4c53 100644
--- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx
@@ -255,6 +255,7 @@ const TableEditor = ({
>
Recommended
}
- // @ts-ignore
- description={
- <>
- Restrict access to your table by enabling RLS and writing Postgres policies.
- >
- }
+ description="Restrict access to your table by enabling RLS and writing Postgres policies."
checked={tableFields.isRLSEnabled}
onChange={() => {
// if isEnabled, show confirm modal to turn off
diff --git a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx
index 7576fc81c9b..061d1306273 100644
--- a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx
+++ b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx
@@ -213,7 +213,7 @@ const TableEditorMenu = () => {
/>
)}
{(entityTypes?.length ?? 0) > 0 && (
-
+
-
-
+ {!isTestEnv && }
+ {!isTestEnv && }
{!isTestEnv && }
diff --git a/apps/studio/pages/api/projects/[ref]/run-lints.ts b/apps/studio/pages/api/projects/[ref]/run-lints.ts
new file mode 100644
index 00000000000..91c1e8d3ec2
--- /dev/null
+++ b/apps/studio/pages/api/projects/[ref]/run-lints.ts
@@ -0,0 +1,24 @@
+import { NextApiRequest, NextApiResponse } from 'next'
+
+import apiWrapper from 'lib/api/apiWrapper'
+import { extractResponse } from 'pages/api/constants'
+
+export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
+
+async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const { method } = req
+
+ switch (method) {
+ case 'GET':
+ return handleGet(req, res)
+ default:
+ res.setHeader('Allow', ['GET'])
+ res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
+ }
+}
+
+type ResponseData = extractResponse<'/platform/projects/{ref}/run-lints', 'get'>
+
+const handleGet = async (req: NextApiRequest, res: NextApiResponse) => {
+ return res.status(200).json([])
+}
diff --git a/packages/ui/src/components/NavMenu/index.tsx b/packages/ui/src/components/NavMenu/index.tsx
index fa2ba5e26be..f58afc18c4d 100644
--- a/packages/ui/src/components/NavMenu/index.tsx
+++ b/packages/ui/src/components/NavMenu/index.tsx
@@ -20,17 +20,16 @@ export const NavMenu = forwardRef(
}
)
-export const NavMenuItem = forwardRef(
- ({
- children,
- className,
- active,
- ...props
- }: PropsWithChildren<{
+interface NavMenuItemProps
+ extends PropsWithChildren<{
className?: string
active: boolean
- }>) => (
+ }> {}
+
+export const NavMenuItem = forwardRef(
+ ({ children, className, active, ...props }, ref) => (
{children}
diff --git a/playwright-tests/playwright.config.ts b/playwright-tests/playwright.config.ts
index 41f761f1a94..c75031d9658 100644
--- a/playwright-tests/playwright.config.ts
+++ b/playwright-tests/playwright.config.ts
@@ -29,7 +29,7 @@ export default defineConfig({
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
// record a video for failed tests, but only for local testing. We can't store videos on CI atm.
- video: process.env.CI ? 'off' : 'retain-on-failure',
+ video: 'retain-on-failure',
},
/* Configure projects for major browsers */
@@ -72,8 +72,16 @@ export default defineConfig({
/* Run your local dev server before starting the tests */
webServer: {
- command: 'NODE_ENV=test npm --prefix ../apps/studio run dev',
+ // using npm run dev instead of turbo because turbo doesn't stop the server after a test (doesn't handle SIGTERM).
+ command: 'npm run dev',
+ cwd: '../apps/studio',
url: 'http://localhost:8082',
reuseExistingServer: !process.env.CI,
+ env: {
+ NODE_ENV: 'test',
+ },
+ // Show the nextjs logs in the terminal. Useful for debugging.
+ // stdout: 'pipe',
+ // stderr: 'pipe',
},
})
diff --git a/playwright-tests/tests/snapshot/spec/table-editor.spec.ts b/playwright-tests/tests/snapshot/spec/table-editor.spec.ts
index 3a230f88a27..994784a3b0e 100644
--- a/playwright-tests/tests/snapshot/spec/table-editor.spec.ts
+++ b/playwright-tests/tests/snapshot/spec/table-editor.spec.ts
@@ -1,30 +1,148 @@
-import { expect, test } from '@playwright/test'
+import { Page, expect, test } from '@playwright/test'
+import { kebabCase } from 'lodash'
+
+const dismissToast = async (page: Page) => {
+ await page.locator('#toast').getByRole('button').waitFor({ state: 'visible' })
+ await page.locator('#toast').getByRole('button').click()
+}
test.describe('Table Editor page', () => {
- test('should create a column and insert a row', async ({ page }) => {
- const name = 'TestTable' + Math.floor(Math.random() * 10)
-
+ test.beforeEach(async ({ page }) => {
+ const tableResponsePromise = page.waitForResponse(
+ 'http://localhost:8082/api/pg-meta/default/query?key=public-entity-types',
+ { timeout: 0 }
+ )
await page.goto('/project/default/editor')
+ await tableResponsePromise
+ })
+
+ test('should create a new table, view its definition, add new rows, sort and filter', async ({
+ page,
+ }, testInfo) => {
+ const tableName = `${kebabCase(testInfo.title).slice(0, 24)}-${testInfo.retry}-${Math.floor(Math.random() * 100)}`
+
+ // The page has been loaded with the table data, we can now interact with the page
await page.getByRole('button', { name: 'New table', exact: true }).click()
- // need to wait for the panel animation
- await page.waitForTimeout(1000)
- await page.locator('.col-span-8 > div > .relative > .peer\\/input').first().click()
- await page.locator('.col-span-8 > div > .relative > .peer\\/input').first().fill(name)
+ await page.getByTestId('table-name-input').waitFor({ state: 'visible' })
+ await page.getByTestId('table-name-input').click()
+ await page.getByTestId('table-name-input').fill(tableName)
+
+ // make the built-in created_at column nullable
+ await page.getByTestId('created_at-extra-options').click()
+ await page.getByText('Is Nullable').click()
+ // the force option is needed because the button is obscured by the popover but we just want to close the popover.
+ await page.getByTestId('created_at-extra-options').click({ force: true })
+
+ // add a new column and add default value
await page.getByRole('button', { name: 'Add column' }).click()
await page.getByRole('textbox', { name: 'column_name' }).click()
- await page.getByRole('textbox', { name: 'column_name' }).fill('textColumn')
+ await page.getByRole('textbox', { name: 'column_name' }).fill('defaultValueColumn')
await page.getByRole('button', { name: '---' }).click()
- await page.getByText('textVariable-length character').click()
- await page.waitForTimeout(1000)
+ await page.getByText('Signed two-byte integer').click()
+ await page.getByTestId('defaultValueColumn-default-value').click()
+ await page.getByTestId('defaultValueColumn-default-value').fill('2')
+
+ await page.getByRole('button', { name: 'Save' }).waitFor({ state: 'visible' })
await page.getByRole('button', { name: 'Save' }).click()
- await page.locator('#toast').getByRole('button').click()
- await page.getByRole('link', { name }).click()
+ await dismissToast(page)
+
+ // view its definition
+ await page.getByText('definition').click()
+ await expect(page.locator('div.view-lines')).toContainText(
+ `CREATE TABLE public.${tableName} ( id bigint GENERATED BY DEFAULT AS IDENTITY , created_at timestamp with time zone NULL DEFAULT now(), \"defaultValueColumn\" smallint NULL DEFAULT '2'::smallint, CONSTRAINT ${tableName}_pkey PRIMARY KEY (id)) TABLESPACE pg_default;`
+ )
+
+ // add a new row
+ await page.getByRole('button', { name: tableName }).click()
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByText('Insert a new row into').click()
- await page.getByPlaceholder('NULL').click()
- await page.getByPlaceholder('NULL').fill('some text')
+ await page.getByTestId('defaultValueColumn-input').click()
+ await page.getByTestId('defaultValueColumn-input').fill('100')
await page.getByTestId('action-bar-save-row').click()
- await page.locator('#toast').getByRole('button').click()
- await expect(page.getByRole('grid')).toContainText('some text')
+ await dismissToast(page)
+
+ // add a second row
+ await page.getByRole('button', { name: tableName }).click()
+ await page.getByTestId('table-editor-insert-new-row').click()
+ await page.getByText('Insert a new row into').click()
+ // the default value should be '100' for defaultValueColumn
+ await page.getByTestId('action-bar-save-row').click()
+ await dismissToast(page)
+
+ await expect(page.getByRole('grid')).toContainText('2')
+ await expect(page.getByRole('grid')).toContainText('100')
+
+ // sort by the a column
+ await page.getByRole('button', { name: 'Sort' }).click()
+ await page.getByTestId('table-editor-pick-column-to-sort-button').click()
+ await page.getByLabel('Pick a column to sort by').getByText('defaultValueColumn').click()
+ await page.getByRole('button', { name: 'Apply sorting' }).click()
+ // click away to close the sorting dialog
+ await page
+ .locator('div')
+ .filter({ hasText: /^Table Editor$/ })
+ .click()
+ // expect the row to be sorted by defaultValueColumn. They're inserted in the order 100, 2
+ await expect(page.locator('div.rdg-row:nth-child(2)')).toContainText('2')
+ await expect(page.locator('div.rdg-row:nth-child(3)')).toContainText('100')
+ // remove the sorting
+ await page.getByRole('button', { name: 'Sorted by 1 rule' }).click()
+ await page.getByRole('dialog').getByRole('button').nth(1).click()
+
+ // filter by a column
+ await page.getByRole('button', { name: 'Filter' }).click()
+ await page.getByRole('button', { name: 'Add filter' }).click()
+ await page.getByRole('button', { name: 'id' }).click()
+ await page.getByLabel('id').getByText('defaultValueColumn').click()
+ await page.getByPlaceholder('Enter a value').click()
+ await page.getByPlaceholder('Enter a value').fill('2')
+ await page.getByRole('button', { name: 'Apply filter' }).click()
+ // click away to close the filter dialog
+ await page
+ .locator('div')
+ .filter({ hasText: /^Table Editor$/ })
+ .click()
+ await expect(page.getByRole('grid')).toContainText('2')
+ await expect(page.getByRole('grid')).not.toContainText('100')
+ })
+
+ test('should check the auth schema', async ({ page }) => {
+ const tableResponsePromise = page.waitForResponse(
+ 'http://localhost:8082/api/pg-meta/default/query?key=public-entity-types',
+ { timeout: 0 }
+ )
+
+ await page.getByRole('button', { name: 'schema: public' }).click()
+ await page.getByRole('option', { name: 'auth' }).click()
+
+ // wait for the table data to load for the auth schema
+ await tableResponsePromise
+
+ // extract the tables names from the sidebar
+ const tables = await page
+ .getByTestId('tables-list')
+ .innerText()
+ .then((text) => text.split('\n'))
+
+ // expect the tables list to contain the following tables (additional tables may be present)
+ expect(tables).toEqual(
+ expect.arrayContaining([
+ 'audit_log_entries',
+ 'flow_state',
+ 'identities',
+ 'instances',
+ 'mfa_amr_claims',
+ 'mfa_challenges',
+ 'mfa_factors',
+ 'refresh_tokens',
+ 'saml_providers',
+ 'saml_relay_states',
+ 'schema_migrations',
+ 'sessions',
+ 'sso_domains',
+ 'sso_providers',
+ 'users',
+ ])
+ )
})
})