diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index efc40c1f2..64a020283 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -189,6 +189,10 @@ jobs: rm -rf gh-pages/$REPORT_DIR mkdir -p gh-pages/$REPORT_DIR cp -r playwright-artifacts/playwright-report/* gh-pages/$REPORT_DIR/ + # Also copy test-results.json if it exists + if [ -f "playwright-artifacts/test-results.json" ]; then + cp playwright-artifacts/test-results.json gh-pages/$REPORT_DIR/ + fi - name: Deploy report to GitHub Pages uses: peaceiris/actions-gh-pages@v3 @@ -217,34 +221,6 @@ jobs: name: playwright-artifacts path: playwright-artifacts - - name: Count new tests - id: count_tests - run: | - git fetch origin main:main - new_tests=0 - - # Get list of changed test files - for file in $(git diff --name-only main...HEAD | grep -E '^tests/suites/.*\.(spec|test)\.(ts|tsx|js|jsx)$'); do - # Count tests in current version - if git show HEAD:"$file" > /dev/null 2>&1; then - current_tests=$(git show HEAD:"$file" | grep -E "test\([\'\"]" | wc -l) - else - current_tests=0 - fi - - # Count tests in main version - if git show main:"$file" > /dev/null 2>&1; then - base_tests=$(git show main:"$file" | grep -E "test\([\'\"]" | wc -l) - else - base_tests=0 - fi - - # Add difference to total - ((new_tests += current_tests - base_tests)) - done - - echo "new_tests=$new_tests" >> $GITHUB_OUTPUT - - name: Update PR description uses: actions/github-script@v6 with: @@ -289,17 +265,14 @@ jobs: parseFloat(percent) > 0 ? '🔺' : parseFloat(percent) < 0 ? '🔽' : '✅'; - const newTests = parseInt('${{ steps.count_tests.outputs.new_tests }}'); - const testsStatus = newTests > 0 ? '✨' : '➖'; - const ciSection = `## CI Results ### Test Status: ${status} 📊 [Full Report](${reportUrl}) - | Total | Passed | Failed | Flaky | Skipped | New Tests | - |:-----:|:------:|:------:|:-----:|:-------:|:---------:| - | ${testResults.total} | ${testResults.passed} | ${testResults.failed} | ${testResults.flaky} | ${testResults.skipped} | ${testsStatus} ${newTests} | + | Total | Passed | Failed | Flaky | Skipped | + |:-----:|:------:|:------:|:-----:|:-------:| + | ${testResults.total} | ${testResults.passed} | ${testResults.failed} | ${testResults.flaky} | ${testResults.skipped} | ### Bundle Size: ${bundleStatus} Current: ${formatSize(currentSize)} | Main: ${formatSize(mainSize)} @@ -318,7 +291,6 @@ jobs: - Bundle size is measured for the entire 'dist' directory. - 📊 indicates links to detailed reports. - 🔺 indicates increase, 🔽 decrease, and ✅ no change in bundle size. - - ${testsStatus} indicates ${newTests} new test cases added in this PR. `; const { data: pullRequest } = await github.rest.pulls.get({ diff --git a/package-lock.json b/package-lock.json index 62fd78bf4..63497f921 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,7 @@ "use-query-params": "^2.2.1", "uuid": "^10.0.0", "web-vitals": "^1.1.2", - "ydb-ui-components": "^4.2.0", + "ydb-ui-components": "^4.3.2", "zod": "^3.23.8" }, "devDependencies": { @@ -26904,9 +26904,10 @@ } }, "node_modules/ydb-ui-components": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ydb-ui-components/-/ydb-ui-components-4.2.0.tgz", - "integrity": "sha512-u+GMfgFwTnUjDu1BBh8FI8TUZtqdrDXA/4XtP8ypDVS+QJgBzbgepNI6PpjqrpuaMWrW5cYSa2HLmKunHNL9Ug==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ydb-ui-components/-/ydb-ui-components-4.3.2.tgz", + "integrity": "sha512-rWGgCfhvBsJCoTbJ9xo9l7yoCb3a0pxY/0G7vG2rrcXs9jregvqHNBORnFxFl4LgxNUCfH4A4yhYd3MdlLNLxw==", + "license": "MIT", "dependencies": { "@bem-react/classname": "^1.6.0", "react-list": "^0.8.17", diff --git a/package.json b/package.json index eb09d5eaf..d64d666e2 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@bem-react/classname": "^1.6.0", + "@ebay/nice-modal-react": "^1.2.13", "@gravity-ui/axios-wrapper": "^1.4.1", "@gravity-ui/chartkit": "^5.9.0", "@gravity-ui/components": "^3.7.0", @@ -27,7 +28,6 @@ "@hookform/resolvers": "^3.9.0", "@reduxjs/toolkit": "^2.2.3", "@tanstack/react-table": "^8.19.3", - "@ebay/nice-modal-react": "^1.2.13", "axios": "^1.7.3", "axios-retry": "^4.4.2", "colord": "^2.9.3", @@ -53,7 +53,7 @@ "use-query-params": "^2.2.1", "uuid": "^10.0.0", "web-vitals": "^1.1.2", - "ydb-ui-components": "^4.2.0", + "ydb-ui-components": "^4.3.2", "zod": "^3.23.8" }, "scripts": { diff --git a/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx b/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx index c2416eb71..eb04cb650 100644 --- a/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx +++ b/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx @@ -9,10 +9,7 @@ import {useCreateDirectoryFeatureAvailable} from '../../../../store/reducers/cap import {selectUserInput} from '../../../../store/reducers/query/query'; import {schemaApi} from '../../../../store/reducers/schema/schema'; import {tableSchemaDataApi} from '../../../../store/reducers/tableSchemaData'; -import type {GetTableSchemaDataParams} from '../../../../store/reducers/tableSchemaData'; import type {EPathType, TEvDescribeSchemeResult} from '../../../../types/api/schema'; -import {wait} from '../../../../utils'; -import {SECOND_IN_MS} from '../../../../utils/constants'; import { useQueryExecutionSettings, useTypedDispatch, @@ -20,7 +17,11 @@ import { } from '../../../../utils/hooks'; import {getConfirmation} from '../../../../utils/hooks/withConfirmation/useChangeInputWithConfirmation'; import {getSchemaControls} from '../../utils/controls'; -import {isChildlessPathType, mapPathTypeToNavigationTreeType} from '../../utils/schema'; +import { + isChildlessPathType, + mapPathTypeToNavigationTreeType, + nodeTableTypeToPathType, +} from '../../utils/schema'; import {getActions} from '../../utils/schemaActions'; import {CreateDirectoryDialog} from '../CreateDirectoryDialog/CreateDirectoryDialog'; import {useDispatchTreeKey, useTreeKey} from '../UpdateTreeContext'; @@ -33,29 +34,15 @@ interface SchemaTreeProps { onActivePathUpdate: (path: string) => void; } -const TABLE_SCHEMA_TIMEOUT = SECOND_IN_MS * 2; - export function SchemaTree(props: SchemaTreeProps) { const createDirectoryFeatureAvailable = useCreateDirectoryFeatureAvailable(); const {rootPath, rootName, rootType, currentPath, onActivePathUpdate} = props; const dispatch = useTypedDispatch(); const input = useTypedSelector(selectUserInput); - const [getTableSchemaDataMutation] = tableSchemaDataApi.useGetTableSchemaDataMutation(); - - const getTableSchemaDataPromise = React.useCallback( - async (args: GetTableSchemaDataParams) => { - try { - const result = await Promise.race([ - getTableSchemaDataMutation(args).unwrap(), - wait(TABLE_SCHEMA_TIMEOUT), - ]); - return result; - } catch (e) { - return undefined; - } - }, - [getTableSchemaDataMutation], - ); + const [ + getTableSchemaDataQuery, + {currentData: actionsSchemaData, isFetching: isActionsDataFetching}, + ] = tableSchemaDataApi.useLazyGetTableSchemaDataQuery(); const [querySettings, setQueryExecutionSettings] = useQueryExecutionSettings(); const [createDirectoryOpen, setCreateDirectoryOpen] = React.useState(false); @@ -123,6 +110,36 @@ export function SchemaTree(props: SchemaTreeProps) { setParentPath(value); setCreateDirectoryOpen(true); }; + + const getTreeNodeActions = React.useMemo(() => { + return getActions( + dispatch, + { + setActivePath: onActivePathUpdate, + updateQueryExecutionSettings: (settings) => + setQueryExecutionSettings({...querySettings, ...settings}), + showCreateDirectoryDialog: createDirectoryFeatureAvailable + ? handleOpenCreateDirectoryDialog + : undefined, + getConfirmation: input ? getConfirmation : undefined, + + schemaData: actionsSchemaData, + isSchemaDataLoading: isActionsDataFetching, + }, + rootPath, + ); + }, [ + actionsSchemaData, + createDirectoryFeatureAvailable, + dispatch, + input, + isActionsDataFetching, + onActivePathUpdate, + querySettings, + rootPath, + setQueryExecutionSettings, + ]); + return ( - setQueryExecutionSettings({...querySettings, ...settings}), - showCreateDirectoryDialog: createDirectoryFeatureAvailable - ? handleOpenCreateDirectoryDialog - : undefined, - getTableSchemaDataPromise, - getConfirmation: input ? getConfirmation : undefined, - }, - rootPath, - )} + getActions={getTreeNodeActions} + onActionsOpenToggle={({path, type, isOpen}) => { + const pathType = nodeTableTypeToPathType[type]; + if (isOpen && pathType) { + getTableSchemaDataQuery({path, tenantName: rootPath, type: pathType}); + } + + return []; + }} renderAdditionalNodeElements={getSchemaControls(dispatch, { setActivePath: onActivePathUpdate, })} diff --git a/src/containers/Tenant/utils/schemaActions.ts b/src/containers/Tenant/utils/schemaActions.tsx similarity index 87% rename from src/containers/Tenant/utils/schemaActions.ts rename to src/containers/Tenant/utils/schemaActions.tsx index 85d5851e4..4d97c673a 100644 --- a/src/containers/Tenant/utils/schemaActions.ts +++ b/src/containers/Tenant/utils/schemaActions.tsx @@ -1,8 +1,8 @@ +import {Flex, Spin} from '@gravity-ui/uikit'; import copy from 'copy-to-clipboard'; import type {NavigationTreeNodeType, NavigationTreeProps} from 'ydb-ui-components'; import type {AppDispatch} from '../../../store'; -import type {GetTableSchemaDataParams} from '../../../store/reducers/tableSchemaData'; import {TENANT_PAGES_IDS, TENANT_QUERY_TABS_ID} from '../../../store/reducers/tenant/constants'; import {setQueryTab, setTenantPage} from '../../../store/reducers/tenant/tenant'; import type {QuerySettings} from '../../../types/store/query'; @@ -12,7 +12,6 @@ import {transformPath} from '../ObjectSummary/transformPath'; import type {SchemaData} from '../Schema/SchemaViewer/types'; import i18n from '../i18n'; -import {nodeTableTypeToPathType} from './schema'; import type {TemplateFn} from './schemaQueryTemplates'; import { addTableIndex, @@ -36,14 +35,13 @@ import { upsertQueryTemplate, } from './schemaQueryTemplates'; -interface ActionsAdditionalEffects { +interface ActionsAdditionalParams { updateQueryExecutionSettings: (settings?: Partial) => void; setActivePath: (path: string) => void; showCreateDirectoryDialog?: (path: string) => void; - getTableSchemaDataPromise?: ( - params: GetTableSchemaDataParams, - ) => Promise; getConfirmation?: () => Promise; + schemaData?: SchemaData[]; + isSchemaDataLoading?: boolean; } interface BindActionParams { @@ -56,32 +54,18 @@ interface BindActionParams { const bindActions = ( params: BindActionParams, dispatch: AppDispatch, - additionalEffects: ActionsAdditionalEffects, + additionalEffects: ActionsAdditionalParams, ) => { - const {setActivePath, showCreateDirectoryDialog, getTableSchemaDataPromise, getConfirmation} = + const {setActivePath, showCreateDirectoryDialog, getConfirmation, schemaData} = additionalEffects; const inputQuery = (tmpl: TemplateFn) => () => { const applyInsert = () => { - const pathType = nodeTableTypeToPathType[params.type]; - const withTableData = [selectQueryTemplate, upsertQueryTemplate].includes(tmpl); - - const userInputDataPromise = - withTableData && pathType && getTableSchemaDataPromise - ? getTableSchemaDataPromise({ - path: params.path, - tenantName: params.tenantName, - type: pathType, - }) - : Promise.resolve(undefined); - //order is important here: firstly we should open query tab and initialize editor (it will be set to window.ydbEditor), after that it is possible to insert snippet dispatch(setTenantPage(TENANT_PAGES_IDS.query)); dispatch(setQueryTab(TENANT_QUERY_TABS_ID.newQuery)); setActivePath(params.path); - userInputDataPromise.then((tableData) => { - insertSnippetToEditor(tmpl({...params, tableData})); - }); + insertSnippetToEditor(tmpl({...params, schemaData})); }; if (getConfirmation) { const confirmedPromise = getConfirmation(); @@ -142,8 +126,25 @@ const bindActions = ( type ActionsSet = ReturnType['getActions']>; +interface ActionConfig { + text: string; + action: () => void; + isLoading?: boolean; +} + +const getActionWithLoader = ({text, action, isLoading}: ActionConfig) => ({ + text: ( + + {text} + {isLoading && } + + ), + action, + disabled: isLoading, +}); + export const getActions = - (dispatch: AppDispatch, additionalEffects: ActionsAdditionalEffects, rootPath = '') => + (dispatch: AppDispatch, additionalEffects: ActionsAdditionalParams, rootPath = '') => (path: string, type: NavigationTreeNodeType) => { const relativePath = transformPath(path, rootPath); const actions = bindActions( @@ -176,8 +177,16 @@ export const getActions = [ {text: i18n('actions.alterTable'), action: actions.alterTable}, {text: i18n('actions.dropTable'), action: actions.dropTable}, - {text: i18n('actions.selectQuery'), action: actions.selectQuery}, - {text: i18n('actions.upsertQuery'), action: actions.upsertQuery}, + getActionWithLoader({ + text: i18n('actions.selectQuery'), + action: actions.selectQuery, + isLoading: additionalEffects.isSchemaDataLoading, + }), + getActionWithLoader({ + text: i18n('actions.upsertQuery'), + action: actions.upsertQuery, + isLoading: additionalEffects.isSchemaDataLoading, + }), {text: i18n('actions.addTableIndex'), action: actions.addTableIndex}, {text: i18n('actions.createCdcStream'), action: actions.createCdcStream}, ], diff --git a/src/containers/Tenant/utils/schemaQueryTemplates.ts b/src/containers/Tenant/utils/schemaQueryTemplates.ts index b7d5f1f3f..de4c4689e 100644 --- a/src/containers/Tenant/utils/schemaQueryTemplates.ts +++ b/src/containers/Tenant/utils/schemaQueryTemplates.ts @@ -3,7 +3,7 @@ import type {SchemaData} from '../Schema/SchemaViewer/types'; export interface SchemaQueryParams { path: string; relativePath: string; - tableData?: SchemaData[]; + schemaData?: SchemaData[]; } export type TemplateFn = (params?: SchemaQueryParams) => string; @@ -87,7 +87,7 @@ ALTER TABLE ${path} export const selectQueryTemplate = (params?: SchemaQueryParams) => { const path = params?.relativePath ? `\`${params?.relativePath}\`` : '${2:}'; const columns = - params?.tableData?.map((column) => '`' + column.name + '`').join(', ') || '${1:*}'; + params?.schemaData?.map((column) => '`' + column.name + '`').join(', ') || '${1:*}'; const filters = params?.relativePath ? '' : 'WHERE ${3:Key1 = 1}\nORDER BY ${4:Key1}\n'; return `SELECT ${columns} FROM ${path} @@ -96,8 +96,8 @@ ${filters}LIMIT \${5:10};`; export const upsertQueryTemplate = (params?: SchemaQueryParams) => { const path = params?.relativePath ? `\`${params?.relativePath}\`` : '${1:}'; const columns = - params?.tableData?.map((column) => `\`${column.name}\``).join(', ') || '${2:id, name}'; - const values = params?.tableData ? '${3: }' : '${3:1, "foo"}'; + params?.schemaData?.map((column) => `\`${column.name}\``).join(', ') || '${2:id, name}'; + const values = params?.schemaData ? '${3: }' : '${3:1, "foo"}'; return `UPSERT INTO ${path} ( ${columns} ) VALUES ( ${values} );`; diff --git a/src/services/api.ts b/src/services/api.ts index 35031e28a..06b5229be 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -384,7 +384,7 @@ export class YdbEmbeddedAPI extends AxiosWrapper { ); } getDescribe( - {path, database}: {path: string; database: string}, + {path, database, timeout}: {path: string; database: string; timeout?: number}, {concurrentId, signal}: AxiosOptions = {}, ) { return this.get>( @@ -396,7 +396,7 @@ export class YdbEmbeddedAPI extends AxiosWrapper { partition_stats: true, subs: 0, }, - {concurrentId: concurrentId || `getDescribe|${path}`, requestConfig: {signal}}, + {concurrentId: concurrentId || `getDescribe|${path}`, requestConfig: {signal}, timeout}, ); } getSchemaAcl( diff --git a/src/store/reducers/overview/overview.ts b/src/store/reducers/overview/overview.ts index a0ba9e1eb..ebb7d2e83 100644 --- a/src/store/reducers/overview/overview.ts +++ b/src/store/reducers/overview/overview.ts @@ -32,12 +32,16 @@ export const overviewApi = api.injectEndpoints({ providesTags: ['All'], }), getOverview: build.query({ - queryFn: async ({path, database}: {path: string; database: string}, {signal}) => { + queryFn: async ( + {path, database, timeout}: {path: string; database: string; timeout?: number}, + {signal}, + ) => { try { const data = await window.api.getDescribe( { path, database, + timeout, }, {signal}, ); diff --git a/src/store/reducers/tableSchemaData.ts b/src/store/reducers/tableSchemaData.ts index 93485c34b..9bd4fd3a7 100644 --- a/src/store/reducers/tableSchemaData.ts +++ b/src/store/reducers/tableSchemaData.ts @@ -5,6 +5,7 @@ import { import type {SchemaData} from '../../containers/Tenant/Schema/SchemaViewer/types'; import {isViewType} from '../../containers/Tenant/utils/schema'; import type {EPathType} from '../../types/api/schema'; +import {SECOND_IN_MS} from '../../utils/constants'; import {isQueryErrorResponse} from '../../utils/query'; import {api} from './api'; @@ -17,9 +18,11 @@ export interface GetTableSchemaDataParams { type: EPathType; } +const TABLE_SCHEMA_TIMEOUT = SECOND_IN_MS * 5; + export const tableSchemaDataApi = api.injectEndpoints({ endpoints: (build) => ({ - getTableSchemaData: build.mutation({ + getTableSchemaData: build.query({ queryFn: async ({path, tenantName, type}, {dispatch}) => { try { if (isViewType(type)) { @@ -27,6 +30,7 @@ export const tableSchemaDataApi = api.injectEndpoints({ viewSchemaApi.endpoints.getViewSchema.initiate({ database: tenantName, path, + timeout: TABLE_SCHEMA_TIMEOUT, }), ); @@ -42,6 +46,7 @@ export const tableSchemaDataApi = api.injectEndpoints({ overviewApi.endpoints.getOverview.initiate({ path, database: tenantName, + timeout: TABLE_SCHEMA_TIMEOUT, }), ); const result = prepareSchemaData(type, schemaData.data); diff --git a/src/store/reducers/viewSchema/viewSchema.ts b/src/store/reducers/viewSchema/viewSchema.ts index eba8c3d11..208135553 100644 --- a/src/store/reducers/viewSchema/viewSchema.ts +++ b/src/store/reducers/viewSchema/viewSchema.ts @@ -8,13 +8,22 @@ function createViewSchemaQuery(path: string) { export const viewSchemaApi = api.injectEndpoints({ endpoints: (build) => ({ getViewSchema: build.query({ - queryFn: async ({database, path}: {database: string; path: string}) => { + queryFn: async ({ + database, + path, + timeout, + }: { + database: string; + path: string; + timeout?: number; + }) => { try { const response = await window.api.sendQuery( { query: createViewSchemaQuery(path), database, action: 'execute-scan', + timeout, }, {withRetries: true}, ); diff --git a/tests/suites/tenant/summary/ActionsMenu.ts b/tests/suites/tenant/summary/ActionsMenu.ts index 9b8d6859b..6f2002a41 100644 --- a/tests/suites/tenant/summary/ActionsMenu.ts +++ b/tests/suites/tenant/summary/ActionsMenu.ts @@ -25,7 +25,7 @@ export class ActionsMenu { } async clickItem(itemText: string): Promise { - const menuItem = this.menu.locator(`.g-menu__item-content:text("${itemText}")`); + const menuItem = this.menu.locator(`.g-menu__item-content:has-text("${itemText}")`); await menuItem.click(); } @@ -35,6 +35,13 @@ export class ActionsMenu { return className.includes('g-menu__item_selected'); } + async isItemLoading(itemText: string): Promise { + const menuItem = this.menu.locator(`.g-menu__item:has-text("${itemText}")`); + const className = (await menuItem.getAttribute('class')) || ''; + const hasSpinner = await menuItem.locator('.g-spin').isVisible(); + return className.includes('g-menu__item_disabled') && hasSpinner; + } + async getTableTemplates(): Promise { const items = this.menu.locator('.g-menu__item-content'); const contents = await items.allTextContents(); diff --git a/tests/suites/tenant/summary/ObjectSummary.ts b/tests/suites/tenant/summary/ObjectSummary.ts index 22966d789..09df3ad09 100644 --- a/tests/suites/tenant/summary/ObjectSummary.ts +++ b/tests/suites/tenant/summary/ObjectSummary.ts @@ -82,11 +82,18 @@ export class ObjectSummary { return this.actionsMenu.isVisible(); } + async isActionItemLoading(itemText: string): Promise { + return this.actionsMenu.isItemLoading(itemText); + } + async getActionsMenuItems(): Promise { return this.actionsMenu.getItems(); } async clickActionsMenuItem(itemText: string): Promise { + if (await this.isActionItemLoading(itemText)) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + } await this.actionsMenu.clickItem(itemText); } @@ -107,6 +114,6 @@ export class ObjectSummary { async clickActionMenuItem(treeItemText: string, menuItemText: string): Promise { await this.clickActionsButton(treeItemText); - await this.actionsMenu.clickItem(menuItemText); + await this.clickActionsMenuItem(menuItemText); } } diff --git a/tests/suites/tenant/summary/objectSummary.test.ts b/tests/suites/tenant/summary/objectSummary.test.ts index d09efdeb7..438df5e17 100644 --- a/tests/suites/tenant/summary/objectSummary.test.ts +++ b/tests/suites/tenant/summary/objectSummary.test.ts @@ -1,8 +1,16 @@ import {expect, test} from '@playwright/test'; -import {dsVslotsSchema, dsVslotsTableName, tenantName} from '../../../utils/constants'; +import {wait} from '../../../../src/utils'; +import { + backend, + dsStoragePoolsTableName, + dsVslotsSchema, + dsVslotsTableName, + tenantName, +} from '../../../utils/constants'; import {TenantPage} from '../TenantPage'; import {QueryEditor} from '../queryEditor/models/QueryEditor'; +import {UnsavedChangesModal} from '../queryEditor/models/UnsavedChangesModal'; import {ObjectSummary, ObjectSummaryTab} from './ObjectSummary'; import {RowTableAction} from './types'; @@ -81,4 +89,75 @@ test.describe('Object Summary', async () => { await expect(queryEditor.editorTextArea).toBeVisible(); await expect(queryEditor.editorTextArea).not.toBeEmpty(); }); + + test('Select and Upsert actions show loading state', async ({page}) => { + await page.route(`${backend}/viewer/json/describe?*`, async (route) => { + await wait(1000); + await route.continue(); + }); + + const objectSummary = new ObjectSummary(page); + await expect(objectSummary.isTreeVisible()).resolves.toBe(true); + + // Open actions menu + await objectSummary.clickActionsButton(dsStoragePoolsTableName); + await expect(objectSummary.isActionsMenuVisible()).resolves.toBe(true); + + // Verify loading states + await expect(objectSummary.isActionItemLoading(RowTableAction.SelectQuery)).resolves.toBe( + true, + ); + await expect(objectSummary.isActionItemLoading(RowTableAction.UpsertQuery)).resolves.toBe( + true, + ); + }); + + test('Monaco editor shows column list after select query loading completes', async ({page}) => { + const objectSummary = new ObjectSummary(page); + const queryEditor = new QueryEditor(page); + + await objectSummary.clickActionMenuItem(dsVslotsTableName, RowTableAction.SelectQuery); + + const selectContent = await queryEditor.editorTextArea.inputValue(); + expect(selectContent).toContain('SELECT'); + expect(selectContent).toContain('FROM'); + expect(selectContent).toMatch(/`\w+`,\s*`\w+`/); // At least two backticked columns + }); + + test('Monaco editor shows column list after upsert query loading completes', async ({page}) => { + const objectSummary = new ObjectSummary(page); + const queryEditor = new QueryEditor(page); + + await objectSummary.clickActionMenuItem(dsVslotsTableName, RowTableAction.UpsertQuery); + + const upsertContent = await queryEditor.editorTextArea.inputValue(); + expect(upsertContent).toContain('UPSERT INTO'); + expect(upsertContent).toContain('VALUES'); + expect(upsertContent).toMatch(/\(\s*`\w+`\s*(,\s*`\w+`\s*)*\)/); // Backticked columns in parentheses + }); + + test('Different tables show different column lists in Monaco editor', async ({page}) => { + const objectSummary = new ObjectSummary(page); + const queryEditor = new QueryEditor(page); + const unsavedChangesModal = new UnsavedChangesModal(page); + + // Get columns for first table + await objectSummary.clickActionMenuItem(dsVslotsTableName, RowTableAction.SelectQuery); + const vslotsColumns = await queryEditor.editorTextArea.inputValue(); + + // Get columns for second table + await objectSummary.clickActionMenuItem( + dsStoragePoolsTableName, + RowTableAction.SelectQuery, + ); + + await page.waitForTimeout(500); + // Click Don't save in the modal + await unsavedChangesModal.clickDontSave(); + + const storagePoolsColumns = await queryEditor.editorTextArea.inputValue(); + + // Verify the column lists are different + expect(vslotsColumns).not.toEqual(storagePoolsColumns); + }); }); diff --git a/tests/utils/constants.ts b/tests/utils/constants.ts index c8af86810..e6d44da3f 100644 --- a/tests/utils/constants.ts +++ b/tests/utils/constants.ts @@ -10,6 +10,8 @@ export const tenantPage = 'tenant'; export const tenantName = '/local'; export const dsVslotsSchema = '/local/.sys/ds_vslots'; export const dsVslotsTableName = 'ds_vslots'; +export const dsStorageStatsTableName = 'ds_storage_stats'; +export const dsStoragePoolsTableName = 'ds_storage_pools'; // URLs export const backend = process.env.PLAYWRIGHT_APP_BACKEND || 'http://localhost:8765';