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';