+
+
+
+ Hackathon
+
Starts this Friday.
-
-
-
-
Build something amazing in 10 days using Supabase.
-
- Compete solo or in a team,
-
- submit a quick demo and win.
-
-
+
+
+
+
+
+ Build something amazing
in 10 days using Supabase.
+
+
Compete solo or in a team, submit a quick demo and win.
+
-
- >
+
+
)
}
diff --git a/apps/www/components/LaunchWeek/15/LW15MainStage.tsx b/apps/www/components/LaunchWeek/15/LW15MainStage.tsx
index c366ef1eb82e3..37cc3ed75c2bc 100644
--- a/apps/www/components/LaunchWeek/15/LW15MainStage.tsx
+++ b/apps/www/components/LaunchWeek/15/LW15MainStage.tsx
@@ -198,7 +198,7 @@ const CardsSlider: React.FC
= ({
ref={swiperRef}
onSwiper={setControlledSwiper}
modules={[Controller, Navigation, A11y]}
- initialSlide={4}
+ initialSlide={0}
spaceBetween={8}
slidesPerView={1.5}
breakpoints={{
diff --git a/apps/www/components/LaunchWeek/15/data/lw15_data.tsx b/apps/www/components/LaunchWeek/15/data/lw15_data.tsx
index 977ae334fafdd..d7c40e3e7dc55 100644
--- a/apps/www/components/LaunchWeek/15/data/lw15_data.tsx
+++ b/apps/www/components/LaunchWeek/15/data/lw15_data.tsx
@@ -186,7 +186,7 @@ const days: (isDark?: boolean) => WeekDayProps[] = (isDark = true) => [
d: 5,
dd: 'Fri',
shipped: true,
- isToday: true,
+ isToday: false,
hasCountdown: false,
blog: '/blog/persistent-storage-for-faster-edge-functions',
date: 'Friday',
diff --git a/apps/www/public/rss.xml b/apps/www/public/rss.xml
index d486e2c8e6588..8ec0f09ffee83 100644
--- a/apps/www/public/rss.xml
+++ b/apps/www/public/rss.xml
@@ -280,20 +280,6 @@
Technical deep dive into the new DBOS integration for Supabase
Tue, 10 Dec 2024 00:00:00 -0700
--
- https://supabase.com/blog/database-build-v2
- database.build v2: Bring-your-own-LLM
- https://supabase.com/blog/database-build-v2
- Use any OpenAI API compatible LLMs in database.build
- Fri, 06 Dec 2024 00:00:00 -0700
-
--
- https://supabase.com/blog/restore-to-a-new-project
- Restore to a New Project
- https://supabase.com/blog/restore-to-a-new-project
- Effortlessly Clone Data into a New Supabase Project
- Fri, 06 Dec 2024 00:00:00 -0700
-
-
https://supabase.com/blog/hack-the-base
Hack the Base! with Supabase
@@ -308,6 +294,20 @@
Highlights from Launch Week 13
Fri, 06 Dec 2024 00:00:00 -0700
+-
+ https://supabase.com/blog/database-build-v2
+ database.build v2: Bring-your-own-LLM
+ https://supabase.com/blog/database-build-v2
+ Use any OpenAI API compatible LLMs in database.build
+ Fri, 06 Dec 2024 00:00:00 -0700
+
+-
+ https://supabase.com/blog/restore-to-a-new-project
+ Restore to a New Project
+ https://supabase.com/blog/restore-to-a-new-project
+ Effortlessly Clone Data into a New Supabase Project
+ Fri, 06 Dec 2024 00:00:00 -0700
+
-
https://supabase.com/blog/supabase-queues
Supabase Queues
diff --git a/e2e/studio/README.md b/e2e/studio/README.md
index 21e66a97f75c9..6ddfe554a10bc 100644
--- a/e2e/studio/README.md
+++ b/e2e/studio/README.md
@@ -10,6 +10,8 @@ Edit the `.env.local` file with your credentials and environment.
### Install the playwright browser
+⚠️ This should be done in the `e2e/studio` directory
+
```bash
pnpm exec playwright install
```
diff --git a/e2e/studio/features/sql-editor.spec.ts b/e2e/studio/features/sql-editor.spec.ts
index cf6428ab6c751..32f30dcd8cde5 100644
--- a/e2e/studio/features/sql-editor.spec.ts
+++ b/e2e/studio/features/sql-editor.spec.ts
@@ -1,55 +1,159 @@
-import { expect } from '@playwright/test'
-import { env } from '../env.config'
+import { expect, Page } from '@playwright/test'
+import { isCLI } from '../utils/is-cli'
import { test } from '../utils/test'
import { toUrl } from '../utils/to-url'
+const deleteQuery = async (page: Page, queryName: string) => {
+ const privateSnippet = page.getByLabel('private-snippets')
+ await privateSnippet.getByText(queryName).first().click({ button: 'right' })
+ await page.getByRole('menuitem', { name: 'Delete query' }).click()
+ await expect(page.getByRole('heading', { name: 'Confirm to delete query' })).toBeVisible()
+ await page.getByRole('button', { name: 'Delete 1 query' }).click()
+}
+
test.describe('SQL Editor', () => {
- test('should check if SQL editor can run simple commands', async ({ page }) => {
- await page.goto(toUrl(`/project/${env.PROJECT_REF}/sql/new?skip=true`))
+ let page: Page
+ const pwTestQueryName = 'pw-test-query'
- const editor = page.getByRole('code').nth(0)
+ test.beforeAll(async ({ browser, ref }) => {
+ test.setTimeout(60000)
+
+ // Create a new table for the tests
+ page = await browser.newPage()
+ await page.goto(toUrl(`/project/${ref}/sql/new?skip=true`))
+
+ await page.evaluate((ref) => {
+ localStorage.removeItem('dashboard-history-default')
+ localStorage.removeItem(`dashboard-history-${ref}`)
+ }, ref)
+
+ // intercept AI title generation to prevent flaky tests
+ await page.route('**/dashboard/api/ai/sql/title-v2', async (route) => {
+ await route.abort()
+ })
+ })
+
+ test.beforeEach(async ({ ref }) => {
+ if ((await page.getByLabel('private-snippets').count()) === 0) {
+ return
+ }
+
+ // since in local, we don't have access to the supabase platform, reloading would reload all the sql snippets.
+ if (isCLI()) {
+ await page.reload()
+ }
+
+ // remove sql snippets for - "Untitled query" and "pw test query"
+ const privateSnippet = page.getByLabel('private-snippets')
+ let privateSnippetText = await privateSnippet.textContent()
+ while (privateSnippetText.includes('Untitled query')) {
+ deleteQuery(page, 'Untitled query')
+
+ await page.waitForResponse(
+ (response) =>
+ (response.url().includes(`projects/${ref}/content`) ||
+ response.url().includes('projects/default/content')) &&
+ response.request().method() === 'DELETE'
+ )
+ await expect(
+ page.getByText('Successfully deleted 1 query'),
+ 'Delete confirmation toast should be visible'
+ ).toBeVisible({
+ timeout: 50000,
+ })
+ await page.waitForTimeout(1000)
+ privateSnippetText =
+ (await page.getByLabel('private-snippets').count()) > 0
+ ? await privateSnippet.textContent()
+ : ''
+ }
+
+ while (privateSnippetText.includes(pwTestQueryName)) {
+ deleteQuery(page, pwTestQueryName)
+ await page.waitForResponse(
+ (response) =>
+ (response.url().includes(`projects/${ref}/content`) ||
+ response.url().includes('projects/default/content')) &&
+ response.request().method() === 'DELETE'
+ )
+ await expect(
+ page.getByText('Successfully deleted 1 query'),
+ 'Delete confirmation toast should be visible'
+ ).toBeVisible({
+ timeout: 50000,
+ })
+ await page.waitForTimeout(1000)
+ privateSnippetText =
+ (await page.getByLabel('private-snippets').count()) > 0
+ ? await privateSnippet.textContent()
+ : ''
+ }
+ })
+
+ test('should check if SQL editor can run simple commands', async () => {
+ await page.getByTestId('sql-editor-new-query-button').click()
+ await page.getByRole('menuitem', { name: 'Create a new snippet' }).click()
// write some sql in the editor
// This has to be done since the editor is not editable (input, textarea, etc.)
- await editor.click()
+ await page.waitForTimeout(1000)
+ const editor = page.getByRole('code').nth(0)
await editor.click()
await page.keyboard.press('ControlOrMeta+KeyA')
await page.keyboard.type(`select 'hello world';`)
+ await page.getByTestId('sql-run-button').click()
- await page.getByRole('button', { name: /^Run( CTRL)?$/, exact: false }).click()
+ // verify the result
+ await expect(page.getByRole('gridcell', { name: 'hello world' })).toBeVisible({
+ timeout: 5000,
+ })
- // Should say "Running..."
- await expect(page.getByText('Running...')).toBeVisible()
+ // SQL written in the editor should not be the previous query.
+ await page.waitForTimeout(1000)
+ await editor.click()
+ await page.keyboard.press('ControlOrMeta+KeyA')
+ await page.keyboard.type(`select length('hello');`)
+ await page.getByTestId('sql-run-button').click()
- // Wait until Running... is not visible
- await expect(page.getByText('Running...')).not.toBeVisible()
+ // verify the result is updated.
+ await expect(page.getByRole('gridcell', { name: '5' })).toBeVisible({
+ timeout: 5000,
+ })
+ })
+
+ test('destructive query would tripper a warning modal', async () => {
+ await page.getByTestId('sql-editor-new-query-button').click()
+ await page.getByRole('menuitem', { name: 'Create a new snippet' }).click()
- // clear the editor
+ // write some sql in the editor
+ // This has to be done since the editor is not editable (input, textarea, etc.)
+ await page.waitForTimeout(1000)
+ const editor = page.getByRole('code').nth(0)
await editor.click()
await page.keyboard.press('ControlOrMeta+KeyA')
- await page.keyboard.press('Backspace')
+ await page.keyboard.type(`delete table 'test';`)
+ await page.getByTestId('sql-run-button').click()
- // verify the result
- const result = page.getByRole('gridcell', { name: 'hello world' })
- await expect(result).toBeVisible()
- })
-})
+ // verify warning modal is visible
+ expect(page.getByRole('heading', { name: 'Potential issue detected with' })).toBeVisible()
+ expect(page.getByText('Query has destructive')).toBeVisible()
-test.describe('SQL Snippets', () => {
- test('should create and load a new snippet', async ({ page }) => {
- await page.goto(toUrl(`/project/${env.PROJECT_REF}/sql`))
+ // reset test
+ await page.getByRole('button', { name: 'Cancel' }).click()
+ await page.waitForTimeout(500)
+ await editor.click()
+ await page.keyboard.press('ControlOrMeta+KeyA')
+ await page.keyboard.press('Backspace')
+ })
- const addButton = page.getByTestId('sql-editor-new-query-button')
+ test('should create and load a new snippet', async ({ ref }) => {
const runButton = page.getByTestId('sql-run-button')
await page.getByRole('button', { name: 'Favorites' }).click()
await page.getByRole('button', { name: 'Shared' }).click()
- await expect(page.getByText('No shared queries')).toBeVisible()
- await expect(page.getByText('No favorite queries')).toBeVisible()
// write some sql in the editor
- await addButton.click()
+ await page.getByTestId('sql-editor-new-query-button').click()
await page.getByRole('menuitem', { name: 'Create a new snippet' }).click()
-
const editor = page.getByRole('code').nth(0)
await page.waitForTimeout(1000)
await editor.click()
@@ -77,25 +181,52 @@ test.describe('SQL Snippets', () => {
await privateSnippet.getByText('Untitled query').click({ button: 'right' })
await page.getByRole('menuitem', { name: 'Rename query', exact: true }).click()
await expect(page.getByRole('heading', { name: 'Rename' })).toBeVisible()
- await page.getByRole('textbox', { name: 'Name' }).fill('test snippet')
+ await page.getByRole('textbox', { name: 'Name' }).fill(pwTestQueryName)
await page.getByRole('button', { name: 'Rename query', exact: true }).click()
-
- const privateSnippet2 = privateSnippet.getByText('test snippet', { exact: true })
- await expect(privateSnippet2).toBeVisible()
+ await page.waitForResponse(
+ (response) =>
+ (response.url().includes(`projects/${ref}/content`) ||
+ response.url().includes('projects/default/content')) &&
+ response.request().method() === 'PUT' &&
+ response.status().toString().startsWith('2')
+ )
+ await expect(privateSnippet.getByText(pwTestQueryName, { exact: true })).toBeVisible({
+ timeout: 50000,
+ })
+ const privateSnippet2 = await privateSnippet.getByText(pwTestQueryName, { exact: true })
// share with a team
await privateSnippet2.click({ button: 'right' })
await page.getByRole('menuitem', { name: 'Share query with team' }).click()
- await expect(page.getByRole('heading', { name: 'Confirm to share query: test' })).toBeVisible()
+ await expect(page.getByRole('heading', { name: 'Confirm to share query' })).toBeVisible()
await page.getByRole('button', { name: 'Share query', exact: true }).click()
+ await page.waitForResponse(
+ (response) =>
+ (response.url().includes(`projects/${ref}/content`) ||
+ response.url().includes('projects/default/content')) &&
+ response.request().method() === 'PUT' &&
+ response.status().toString().startsWith('2')
+ )
const sharedSnippet = await page.getByLabel('project-level-snippets')
- await expect(sharedSnippet).toContainText('test snippet')
+ await expect(sharedSnippet).toContainText(pwTestQueryName)
// unshare a snippet
- await sharedSnippet.getByText('test snippet').click({ button: 'right' })
+ await sharedSnippet.getByText(pwTestQueryName).click({ button: 'right' })
await page.getByRole('menuitem', { name: 'Unshare query with team' }).click()
await expect(page.getByRole('heading', { name: 'Confirm to unshare query:' })).toBeVisible()
await page.getByRole('button', { name: 'Unshare query', exact: true }).click()
await expect(sharedSnippet).not.toBeVisible()
+
+ // delete snippet (for non-local environment)
+ if (!isCLI()) {
+ deleteQuery(page, pwTestQueryName)
+
+ await expect(
+ page.getByText('Successfully deleted 1 query'),
+ 'Delete confirmation toast should be visible'
+ ).toBeVisible({
+ timeout: 50000,
+ })
+ }
})
})
diff --git a/e2e/studio/features/table-editor.spec.ts b/e2e/studio/features/table-editor.spec.ts
index 281c3f220d272..da74fe95ce3e3 100644
--- a/e2e/studio/features/table-editor.spec.ts
+++ b/e2e/studio/features/table-editor.spec.ts
@@ -1,4 +1,5 @@
import { expect, Page } from '@playwright/test'
+import fs from 'fs'
import { test } from '../utils/test'
import { toUrl } from '../utils/to-url'
@@ -15,8 +16,9 @@ const getSelectors = (tableName: string) => ({
saveBtn: (page) => page.getByRole('button', { name: 'Save' }),
definitionTab: (page) => page.getByText('definition', { exact: true }),
viewLines: (page) => page.locator('div.view-lines'),
- insertRowBtn: (page) => page.getByTestId('table-editor-insert-new-row'),
- insertModal: (page) => page.getByText('Insert a new row into'),
+ insertBtn: (page) => page.getByTestId('table-editor-insert-new-row'),
+ insertRow: (page) => page.getByText('Insert a new row into'),
+ insertColumn: (page) => page.getByText('Insert a new column into'),
defaultValueInput: (page) => page.getByTestId('defaultValueColumn-input'),
actionBarSaveRow: (page) => page.getByTestId('action-bar-save-row'),
grid: (page) => page.getByRole('grid'),
@@ -61,9 +63,6 @@ const createTable = async (page: Page, tableName: string) => {
await s.saveBtn(page).click()
- // wait till we see the success toast
- // Text: Table tableName is good to go!
-
await expect(
page.getByText(`Table ${tableName} is good to go!`),
'Success toast should be visible after table creation'
@@ -77,7 +76,7 @@ const createTable = async (page: Page, tableName: string) => {
).toBeVisible()
}
-const deleteTables = async (page: Page, tableName: string) => {
+const deleteTable = async (page: Page, tableName: string) => {
const s = getSelectors(tableName)
await page.waitForTimeout(500)
@@ -94,11 +93,35 @@ const deleteTables = async (page: Page, tableName: string) => {
).toBeVisible()
}
+const deleteEnum = async (page: Page, enumName: string, ref: string) => {
+ // give it a second for interactions to load
+ await page.waitForResponse(
+ (response) =>
+ response.url().includes(`pg-meta/${ref}/types`) ||
+ response.url().includes('pg-meta/default/types')
+ )
+
+ // if enum (test) exists, delete it.
+ const exists = (await page.getByRole('cell', { name: enumName, exact: true }).count()) > 0
+ if (!exists) return
+
+ await page
+ .getByRole('row', { name: `public ${enumName}` })
+ .getByRole('button')
+ .click()
+ await page.getByRole('menuitem', { name: 'Delete type' }).click()
+ await page.getByRole('heading', { name: 'Confirm to delete enumerated' }).click()
+ await page.getByRole('button', { name: 'Confirm delete' }).click()
+ await expect(page.getByText(`Successfully deleted "${enumName}"`)).toBeVisible()
+}
+
test.describe('Table Editor', () => {
let page: Page
const testTableName = `pw-test-table-editor`
const tableNameRlsEnabled = `pw-test-rls-enabled`
const tableNameRlsDisabled = `pw-test-rls-disabled`
+ const tableNameEnum = `pw-test-enum`
+ const tableNameCsv = `pw-test-csv`
test.beforeAll(async ({ browser, ref }) => {
test.setTimeout(60000)
@@ -111,18 +134,22 @@ test.describe('Table Editor', () => {
await page.waitForTimeout(2000)
// delete table name if it exists
- await deleteTables(page, testTableName)
- await deleteTables(page, tableNameRlsEnabled)
- await deleteTables(page, tableNameRlsDisabled)
+ await deleteTable(page, testTableName)
+ await deleteTable(page, tableNameRlsEnabled)
+ await deleteTable(page, tableNameRlsDisabled)
+ await deleteTable(page, tableNameEnum)
+ await deleteTable(page, tableNameCsv)
})
test.afterAll(async () => {
test.setTimeout(60000)
// delete all tables related to this test
- await deleteTables(page, testTableName)
- await deleteTables(page, tableNameRlsEnabled)
- await deleteTables(page, tableNameRlsDisabled)
+ await deleteTable(page, testTableName)
+ await deleteTable(page, tableNameRlsEnabled)
+ await deleteTable(page, tableNameRlsDisabled)
+ await deleteTable(page, tableNameEnum)
+ await deleteTable(page, tableNameCsv)
})
test('should perform all table operations sequentially', async ({ ref }) => {
@@ -143,14 +170,14 @@ test.describe('Table Editor', () => {
// 2. Insert test data
await page.getByRole('button', { name: `View ${testTableName}` }).click()
- await s.insertRowBtn(page).click()
- await s.insertModal(page).click()
+ await s.insertBtn(page).click()
+ await s.insertRow(page).click()
await s.defaultValueInput(page).fill('100')
await s.actionBarSaveRow(page).click()
await page.getByRole('button', { name: `View ${testTableName}` }).click()
- await s.insertRowBtn(page).click()
- await s.insertModal(page).click()
+ await s.insertBtn(page).click()
+ await s.insertRow(page).click()
await s.defaultValueInput(page).fill('4')
await s.actionBarSaveRow(page).click()
@@ -225,7 +252,7 @@ test.describe('Table Editor', () => {
'Tables list should be visible in public schema'
).toBeVisible()
- await deleteTables(page, testTableName)
+ await deleteTable(page, testTableName)
})
test('should show rls accordingly', async () => {
@@ -253,7 +280,140 @@ test.describe('Table Editor', () => {
await page.getByRole('button', { name: `View ${tableNameRlsDisabled}` }).click()
await expect(page.getByRole('button', { name: 'RLS disabled' })).toBeVisible()
- await deleteTables(page, tableNameRlsEnabled)
- await deleteTables(page, tableNameRlsDisabled)
+ await deleteTable(page, tableNameRlsEnabled)
+ await deleteTable(page, tableNameRlsDisabled)
+ })
+
+ test('add enums and show enums on table', async ({ ref }) => {
+ const ENUM_NAME = 'test_enum'
+ const ENUM_COLUMN_NAME = 'test_column'
+
+ // clear local storage, as it might result in some flakiness
+ await page.evaluate((ref) => {
+ localStorage.removeItem('dashboard-history-default')
+ localStorage.removeItem(`dashboard-history-${ref}`)
+ }, ref)
+ await page.goto(toUrl(`/project/${ref}/database/types?schema=public`))
+
+ // delete enum if it exists
+ await deleteEnum(page, ENUM_NAME, ref)
+
+ // create a new enum
+ await page.getByRole('button', { name: 'Create type' }).click()
+ await page.getByRole('textbox', { name: 'Name' }).fill(ENUM_NAME)
+ await page.locator('input[name="values.0.value"]').fill('value1')
+ await page.getByRole('button', { name: 'Add value' }).click()
+ await page.locator('input[name="values.1.value"]').fill('value2')
+ await page.getByRole('button', { name: 'Create type' }).click()
+
+ // Wait for enum response to be completed
+ await page.waitForResponse(
+ (response) =>
+ response.url().includes(`pg-meta/${ref}/types`) ||
+ response.url().includes('pg-meta/default/types')
+ )
+
+ // verify enum is created
+ await expect(page.getByRole('cell', { name: ENUM_NAME, exact: true })).toBeVisible()
+ await expect(page.getByRole('cell', { name: 'value1, value2', exact: true })).toBeVisible()
+
+ // create a new table with new column for enums
+ await page.goto(toUrl(`/project/${ref}/editor`))
+
+ const s = getSelectors(tableNameEnum)
+ await s.newTableBtn(page).click()
+ await s.tableNameInput(page).fill(tableNameEnum)
+ await s.createdAtExtraOptions(page).click()
+ await page.getByText('Is Nullable').click()
+ await s.createdAtExtraOptions(page).click()
+ await s.addColumnBtn(page).click()
+ await s.columnNameInput(page).fill(ENUM_COLUMN_NAME)
+ await page.getByRole('combobox').filter({ hasText: 'Choose a column type...' }).click()
+ await page.getByPlaceholder('Search types...').fill(ENUM_NAME)
+ await page.getByRole('option', { name: ENUM_NAME }).click()
+ await s.saveBtn(page).click()
+
+ await expect(
+ page.getByText(`Table ${tableNameEnum} is good to go!`),
+ 'Success toast should be visible after table creation'
+ ).toBeVisible({
+ timeout: 50000,
+ })
+
+ // Wait for the grid to be visible and data to be loaded
+ await expect(s.grid(page), 'Grid should be visible after inserting data').toBeVisible()
+ await expect(page.getByRole('columnheader', { name: ENUM_NAME })).toBeVisible()
+
+ // insert row with enum value
+ await s.insertBtn(page).click()
+ await s.insertRow(page).click()
+ await page.getByRole('combobox').selectOption('value1')
+ await s.actionBarSaveRow(page).click()
+ await expect(page.getByRole('gridcell', { name: 'value1' })).toBeVisible()
+
+ // insert row with another enum value
+ await s.insertBtn(page).click()
+ await s.insertRow(page).click()
+ await page.getByRole('combobox').selectOption('value2')
+ await s.actionBarSaveRow(page).click()
+ await expect(page.getByRole('gridcell', { name: 'value2' })).toBeVisible()
+
+ // delete enum and enum table
+ await deleteTable(page, tableNameEnum)
+ await page.goto(toUrl(`/project/${ref}/database/types?schema=public`))
+ await deleteEnum(page, ENUM_NAME, ref)
+
+ // should end at the init link
+ // clear local storage, as it might result in some flakiness
+ await page.evaluate((ref) => {
+ localStorage.removeItem('dashboard-history-default')
+ localStorage.removeItem(`dashboard-history-${ref}`)
+ }, ref)
+ await page.goto(toUrl(`/project/${ref}/editor`))
+ })
+
+ test('csv import works properly', async () => {
+ // create a new table and insert some data
+ await createTable(page, tableNameCsv)
+ const s = getSelectors(tableNameCsv)
+ await page.getByRole('button', { name: `View ${tableNameCsv}` }).click()
+ await s.insertBtn(page).click()
+ await s.insertRow(page).click()
+ await s.defaultValueInput(page).fill('123')
+ await s.actionBarSaveRow(page).click()
+ await s.insertBtn(page).click()
+ await s.insertRow(page).click()
+ await s.defaultValueInput(page).fill('456')
+ await s.actionBarSaveRow(page).click()
+ await s.insertBtn(page).click()
+ await s.insertRow(page).click()
+ await s.defaultValueInput(page).fill('789')
+ await s.actionBarSaveRow(page).click()
+
+ // download csv
+ const tableBtn = await page.getByRole('button', { name: 'View pw-test-csv' })
+ await tableBtn.getByRole('button').last().click()
+ await page.getByRole('menuitem', { name: 'Export data' }).click()
+ const downloadPromise = page.waitForEvent('download')
+ await page.getByRole('menuitem', { name: 'Export table as CSV' }).click()
+ const download = await downloadPromise
+ expect(download.suggestedFilename()).toContain('.csv')
+ const downloadPath = await download.path()
+
+ // verify file contents
+ const csvContent = fs.readFileSync(downloadPath, 'utf-8').replace(/\r?\n/g, '\n')
+ const rows = csvContent.trim().split('\n')
+ const defaultColumnValues = rows.map((row) => {
+ const columns = row.split(',')
+ return columns[2].trim()
+ })
+ const expectedDefaultColumnValues = ['defaultValueColumn', '123', '456', '789']
+ defaultColumnValues.forEach((expectedValue) => {
+ expect(expectedDefaultColumnValues).toContain(expectedValue)
+ })
+
+ // remove the downloaded file + clean up tables
+ fs.unlinkSync(downloadPath)
+ await deleteTable(page, tableNameCsv)
})
})
diff --git a/e2e/studio/utils/is-cli.ts b/e2e/studio/utils/is-cli.ts
new file mode 100644
index 0000000000000..6387f4c3a9b12
--- /dev/null
+++ b/e2e/studio/utils/is-cli.ts
@@ -0,0 +1,11 @@
+import { env } from '../env.config'
+
+/**
+ * Returns true if running in CLI/self-hosted mode (IS_PLATFORM=false),
+ * false if running in hosted mode (IS_PLATFORM=true).
+ */
+export function isCLI(): boolean {
+ // IS_PLATFORM=true = hosted mode
+ // IS_PLATFORM=false = CLI/self-hosted mode
+ return env.IS_PLATFORM === 'false'
+}
diff --git a/packages/ui-patterns/src/Banners/data.json b/packages/ui-patterns/src/Banners/data.json
index ab01eb3abafe6..ebbb6e84c6030 100644
--- a/packages/ui-patterns/src/Banners/data.json
+++ b/packages/ui-patterns/src/Banners/data.json
@@ -1,7 +1,7 @@
{
- "text": "LW15: Day 5",
- "launch": "Persistent Storage for Edge Functions",
+ "text": "Launch Week 15",
+ "launch": "View all the announcements",
"launchDate": "2025-07-17T08:00:00.000-07:00",
- "link": "/launch-week#main-stage",
+ "link": "/launch-week",
"cta": "Learn more"
}