mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
data table + db manager e2e test
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { test, expect, Page } from '@playwright/test'
|
||||
|
||||
test('create new data table with custom instance', async ({ page }) => {
|
||||
async function setupNewDataTable(page: Page): Promise<{ datatableId: string }> {
|
||||
// Generate unique ID with timestamp
|
||||
const timestamp = Date.now()
|
||||
const datatableId = `datatable_${timestamp}`
|
||||
@@ -67,4 +67,86 @@ test('create new data table with custom instance', async ({ page }) => {
|
||||
// Verify success toast appears
|
||||
const saveSuccessToast = page.locator('text=saved successfully')
|
||||
await expect(saveSuccessToast).toBeVisible({ timeout: 10000 })
|
||||
|
||||
return { datatableId }
|
||||
}
|
||||
|
||||
test('create new data table, add a new table, CRUD rows and delete the table', async ({ page }) => {
|
||||
let { datatableId } = await setupNewDataTable(page)
|
||||
const table = page.locator('table')
|
||||
const lastRow = table.locator('tr').nth(-2)
|
||||
lastRow.locator('button:has-text("Manage")').click()
|
||||
|
||||
// Wait for DB Manager drawer to appear
|
||||
const dbManager = page.locator('#db-manager-drawer')
|
||||
await expect(dbManager).toBeVisible()
|
||||
await dbManager.locator('button:has-text("New table")').click()
|
||||
const tableEditor = page.locator('#db-table-editor-drawer')
|
||||
await expect(tableEditor).toBeVisible()
|
||||
|
||||
const nameInput = page.locator('label:has-text("Name")').locator('input')
|
||||
await nameInput.fill('friend')
|
||||
|
||||
const columnsTable = page.locator('#columns-section table')
|
||||
const addColumnButton = columnsTable.locator('button:has-text("Add")')
|
||||
await addColumnButton.click()
|
||||
|
||||
const newColRow = columnsTable.locator('tr').nth(-2)
|
||||
const newColNameInput = newColRow.locator('td').nth(0).locator('input')
|
||||
await newColNameInput.fill('name')
|
||||
|
||||
const newColTypeSelect = newColRow.locator('td').nth(1).locator('input')
|
||||
await newColTypeSelect.click()
|
||||
|
||||
await page.locator('li:has-text("TEXT")').first().click()
|
||||
|
||||
await page.locator('button:has-text("Create table")').click()
|
||||
|
||||
await page.locator('#db-table-editor-confirmation-modal button:has-text("Create")').click()
|
||||
|
||||
// Verify success toast appears
|
||||
const saveSuccessToast = page.locator('text=friend created!')
|
||||
await expect(saveSuccessToast).toBeVisible({ timeout: 10000 })
|
||||
|
||||
const friendTableKey = page.locator('.db-manager-table-key', { hasText: 'friend' })
|
||||
await expect(friendTableKey).toBeVisible({ timeout: 10000 })
|
||||
await friendTableKey.click()
|
||||
|
||||
// Add a new row
|
||||
await dbManager.locator('button:has-text("Insert")').click()
|
||||
let insertRowDrawer = page.locator('#insert-row-drawer')
|
||||
await expect(insertRowDrawer).toBeVisible({ timeout: 10000 })
|
||||
await insertRowDrawer.locator('textarea').fill('Alice', { force: true }) // Not sure why force is needed here
|
||||
await insertRowDrawer.locator('button:has-text("Insert")').click()
|
||||
|
||||
const rowInsertedToast = page.locator('text=Row inserted')
|
||||
await expect(rowInsertedToast).toBeVisible({ timeout: 10000 })
|
||||
|
||||
const insertedRow = dbManager.locator('.ag-cell-value', { hasText: 'Alice' })
|
||||
await expect(insertedRow).toBeVisible({ timeout: 10000 })
|
||||
|
||||
// Edit the row
|
||||
await insertedRow.dblclick()
|
||||
const cellEditor = dbManager.locator('.ag-cell-editor input')
|
||||
await cellEditor.fill('Bob')
|
||||
await cellEditor.press('Enter')
|
||||
|
||||
const rowUpdatedToast = page.locator('text=Value updated')
|
||||
await expect(rowUpdatedToast).toBeVisible({ timeout: 10000 })
|
||||
const updatedRow = dbManager.locator('.ag-cell-value', { hasText: 'Bob' })
|
||||
await expect(updatedRow).toBeVisible({ timeout: 10000 })
|
||||
|
||||
const actionsBtn = dbManager.locator('#db-manager-table-actions-friend')
|
||||
await actionsBtn.click()
|
||||
|
||||
await page.locator('button:has-text("Delete table")').click()
|
||||
|
||||
let deletePermanentlyBtn = page.locator(
|
||||
'#db-manager-delete-table-confirmation-modal button:has-text("Delete")'
|
||||
)
|
||||
await deletePermanentlyBtn.click()
|
||||
|
||||
await expect(page.locator("text=Table 'friend' deleted successfully")).toBeVisible({
|
||||
timeout: 10000
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { type DBSchema } from '$lib/stores'
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
EditIcon,
|
||||
Loader2,
|
||||
MoreVertical,
|
||||
Plus,
|
||||
Table2,
|
||||
Trash2Icon
|
||||
} from 'lucide-svelte'
|
||||
import { ChevronDownIcon, EditIcon, Loader2, Plus, Table2, Trash2Icon } from 'lucide-svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { ClearableInput, Drawer, DrawerContent } from './common'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
@@ -29,7 +21,7 @@
|
||||
diffTableEditorValues
|
||||
} from './apps/components/display/dbtable/queries/alterTable'
|
||||
import { resource } from 'runed'
|
||||
import { capitalize, pluralize } from '$lib/utils'
|
||||
import { capitalize, onlyAlphaNumAndUnderscore, pluralize } from '$lib/utils'
|
||||
|
||||
/** Represents a selected table with its schema */
|
||||
export interface SelectedTable {
|
||||
@@ -450,7 +442,10 @@
|
||||
onclick={() => (selected.tableKey = tableKey)}
|
||||
>
|
||||
<Table2 class="text-primary shrink-0" size={16} />
|
||||
<p class="truncate text-ellipsis grow text-left text-emphasis text-xs">{tableKey}</p>
|
||||
<p
|
||||
class="db-manager-table-key truncate text-ellipsis grow text-left text-emphasis text-xs"
|
||||
>{tableKey}</p
|
||||
>
|
||||
<DropdownV2
|
||||
items={() => [
|
||||
{
|
||||
@@ -461,6 +456,7 @@
|
||||
title: `Are you sure you want to delete ${tableKey} ? This action is irreversible`,
|
||||
confirmationText: 'Delete permanently',
|
||||
open: true,
|
||||
id: 'db-manager-delete-table-confirmation-modal',
|
||||
onConfirm: async () => {
|
||||
askingForConfirmation && (askingForConfirmation.loading = true)
|
||||
try {
|
||||
@@ -488,14 +484,8 @@
|
||||
}
|
||||
]}
|
||||
class="w-fit"
|
||||
>
|
||||
<svelte:fragment slot="buttonReplacement">
|
||||
<MoreVertical
|
||||
size={8}
|
||||
class="w-8 h-8 p-2 hover:bg-surface-hover cursor-pointer rounded-md"
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</DropdownV2>
|
||||
btnId={'db-manager-table-actions-' + onlyAlphaNumAndUnderscore(tableKey)}
|
||||
/>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -537,6 +527,7 @@
|
||||
on:close={() => (dbTableEditorState = { open: false })}
|
||||
>
|
||||
<DrawerContent
|
||||
id="db-table-editor-drawer"
|
||||
on:close={() => (dbTableEditorState = { open: false })}
|
||||
title={dbTableEditorState.alterTableKey
|
||||
? `Alter ${dbTableEditorState.alterTableKey}`
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
}}
|
||||
CloseIcon={hasReplResult ? ArrowLeft : undefined}
|
||||
noPadding
|
||||
id="db-manager-drawer"
|
||||
>
|
||||
{#if effectiveInput && $workspaceStore}
|
||||
{#key selectedDatatable}
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col" id="columns-section">
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label>Columns</label>
|
||||
<DataTable>
|
||||
@@ -288,7 +288,7 @@
|
||||
</DataTable>
|
||||
</div>
|
||||
{#if features?.foreignKeys}
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col" id="foreign-keys-section">
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label>Foreign Keys</label>
|
||||
<DataTable>
|
||||
@@ -454,6 +454,7 @@
|
||||
|
||||
<Portal>
|
||||
<ConfirmationModal
|
||||
id="db-table-editor-confirmation-modal"
|
||||
{...askingForConfirmation ?? { confirmationText: '', title: '' }}
|
||||
on:canceled={() => (askingForConfirmation = undefined)}
|
||||
on:confirmed={askingForConfirmation?.onConfirm ?? (() => {})}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
customWidth?: number | undefined
|
||||
customMenu?: boolean
|
||||
class?: string | undefined
|
||||
btnId?: string | undefined
|
||||
enableFlyTransition?: boolean
|
||||
size?: ButtonType.UnifiedSize
|
||||
btnText?: string
|
||||
@@ -60,6 +61,7 @@
|
||||
enableFlyTransition = false,
|
||||
size = 'md',
|
||||
btnText = '',
|
||||
btnId = undefined,
|
||||
buttonReplacement,
|
||||
menu,
|
||||
maxHeight = undefined
|
||||
@@ -142,6 +144,7 @@
|
||||
}
|
||||
}
|
||||
}}
|
||||
id={btnId}
|
||||
data-menu
|
||||
>
|
||||
{#if buttonReplacement}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
</Button>
|
||||
|
||||
<Drawer bind:this={insertDrawer} size="800px">
|
||||
<DrawerContent title="Insert row" on:close={insertDrawer.closeDrawer}>
|
||||
<DrawerContent title="Insert row" on:close={insertDrawer.closeDrawer} id="insert-row-drawer">
|
||||
{#snippet actions()}
|
||||
<Button
|
||||
variant="accent"
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
small?: boolean
|
||||
Icon?: any | undefined
|
||||
class?: string
|
||||
id?: string | undefined
|
||||
onClick?: () => void | undefined | any
|
||||
}
|
||||
|
||||
let { noBg = false, small = false, Icon, class: className, onClick }: Props = $props()
|
||||
let { noBg = false, small = false, Icon, class: className, id, onClick }: Props = $props()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
@@ -20,6 +21,7 @@
|
||||
<Button
|
||||
on:click={() => (dispatch('close'), onClick?.())}
|
||||
on:pointerdown={(e) => e.stopPropagation()}
|
||||
{id}
|
||||
startIcon={{ icon: Icon ?? X }}
|
||||
iconOnly
|
||||
unifiedSize="sm"
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
open?: boolean
|
||||
type?: 'danger' | 'reload'
|
||||
showIcon?: boolean
|
||||
id?: string
|
||||
children?: Snippet
|
||||
onConfirmed?: () => void | Promise<void>
|
||||
onCanceled?: () => void
|
||||
@@ -27,6 +28,7 @@
|
||||
open = false,
|
||||
type: _type,
|
||||
showIcon = true,
|
||||
id,
|
||||
children,
|
||||
onConfirmed,
|
||||
onCanceled
|
||||
@@ -84,6 +86,7 @@
|
||||
transition:fadeFast|local
|
||||
class={'fixed top-0 bottom-0 left-0 right-0 z-[5000]'}
|
||||
role="dialog"
|
||||
{id}
|
||||
>
|
||||
<div
|
||||
class={classNames(
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
CloseIcon?: any | undefined
|
||||
fullScreen?: boolean
|
||||
eeOnly?: boolean
|
||||
id?: string | undefined
|
||||
actions?: import('svelte').Snippet
|
||||
titleExtra?: import('svelte').Snippet
|
||||
children?: import('svelte').Snippet
|
||||
@@ -36,6 +37,7 @@
|
||||
CloseIcon = undefined,
|
||||
fullScreen = true,
|
||||
eeOnly = false,
|
||||
id,
|
||||
actions,
|
||||
titleExtra,
|
||||
children
|
||||
@@ -44,7 +46,10 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class={classNames('flex flex-col divide-y', fullScreen ? 'h-screen max-h-screen' : 'h-full')}>
|
||||
<div
|
||||
class={classNames('flex flex-col divide-y', fullScreen ? 'h-screen max-h-screen' : 'h-full')}
|
||||
{id}
|
||||
>
|
||||
<div class="flex justify-between w-full items-center pl-2 pr-4 py-2 gap-2">
|
||||
<div class="flex items-center gap-2 w-full truncate">
|
||||
<div
|
||||
@@ -56,7 +61,7 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CloseButton on:close Icon={CloseIcon} />
|
||||
<CloseButton on:close Icon={CloseIcon} id="{id}-close-btn" />
|
||||
</div>
|
||||
<span class="font-semibold text-emphasis truncate text-lg max-w-sm"
|
||||
>{title ?? ''}
|
||||
|
||||
@@ -1956,3 +1956,7 @@ export function countChars(str: string, char: string): number {
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
export function onlyAlphaNumAndUnderscore(str: string): string {
|
||||
return str.replace(/[^a-zA-Z0-9_]/g, '')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user