diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx index 4ca735408c..c72044c30c 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx @@ -46,7 +46,7 @@ export function TopQueries({tenantName}: TopQueriesProps) { ); const loading = isFetching && currentData === undefined; - const {result: data} = currentData || {}; + const data = currentData?.resultSets?.[0]?.result || []; const handleRowClick = React.useCallback( (row: any) => { diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx index 1091935b87..c1e4a0c784 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx @@ -32,7 +32,7 @@ export const TopShards = ({tenantName, path}: TopShardsProps) => { ); const loading = isFetching && currentData === undefined; - const {result: data} = currentData || {}; + const data = currentData?.resultSets?.[0]?.result || []; const columns = getTopShardsColumns(tenantName, location); diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopTables.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopTables.tsx index d9402e1c56..89b665de3e 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopTables.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopTables.tsx @@ -32,7 +32,7 @@ export function TopTables({path}: TopTablesProps) { ); const loading = isFetching && currentData === undefined; - const {result: data} = currentData || {}; + const data = currentData?.resultSets?.[0]?.result || []; const formatSize = (value?: number) => { const size = getSizeWithSignificantDigits(data?.length ? Number(data[0].Size) : 0, 0); diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx index 9a40f8a580..f5e85059a9 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx @@ -21,11 +21,7 @@ interface Props { export const RunningQueriesData = ({database}: Props) => { const [autoRefreshInterval] = useAutoRefreshInterval(); const filters = useTypedSelector((state) => state.executeTopQueries); - const { - currentData: data, - isFetching, - error, - } = topQueriesApi.useGetRunningQueriesQuery( + const {currentData, isFetching, error} = topQueriesApi.useGetRunningQueriesQuery( { database, filters, @@ -33,6 +29,8 @@ export const RunningQueriesData = ({database}: Props) => { {pollingInterval: autoRefreshInterval}, ); + const data = currentData?.resultSets?.[0].result || []; + return ( {error ? : null} @@ -41,7 +39,7 @@ export const RunningQueriesData = ({database}: Props) => { emptyDataMessage={i18n('no-data')} columnsWidthLSKey={RUNNING_QUERIES_COLUMNS_WIDTH_LS_KEY} columns={RUNNING_QUERIES_COLUMNS} - data={data || []} + data={data} settings={QUERY_TABLE_SETTINGS} /> diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx index da3aa82a87..f815e64df8 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx @@ -31,7 +31,7 @@ export const TopQueriesData = ({database, onRowClick}: Props) => { }, {pollingInterval: autoRefreshInterval}, ); - const {result: data} = currentData || {}; + const data = currentData?.resultSets?.[0]?.result || []; const rawColumns = TOP_QUERIES_COLUMNS; const columns = rawColumns.map((column) => ({ diff --git a/src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx b/src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx index 30d0c48c04..1850ac326e 100644 --- a/src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx +++ b/src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx @@ -120,7 +120,7 @@ export const TopShards = ({tenantName, path, type}: TopShardsProps) => { {pollingInterval: autoRefreshInterval}, ); const loading = isFetching && result === undefined; - const {result: data} = result ?? {}; + const data = result?.resultSets?.[0]?.result || []; const onSort = (newSortOrder?: SortOrder | SortOrder[]) => { // omit information about sort order to disable ASC order, only DESC makes sense for top shards diff --git a/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx b/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx index 7601490788..54a70ac904 100644 --- a/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx +++ b/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx @@ -72,9 +72,8 @@ export function ExecuteResult({ const {error, isLoading, queryId, data} = result; const stats: TKqpStatsQuery | undefined = data?.stats; - const resultsSetsCount = data?.resultSets?.length; - const isMulti = resultsSetsCount && resultsSetsCount > 0; - const currentResult = isMulti ? data?.resultSets?.[selectedResultSet] : data; + const resultsSetsCount = data?.resultSets?.length || 0; + const currentResult = data?.resultSets?.[selectedResultSet]; const {plan, simplifiedPlan} = React.useMemo(() => getPlan(data), [data]); const resultOptions: ControlGroupOption[] = [ @@ -106,7 +105,7 @@ export function ExecuteResult({ const renderResult = () => { return (
- {isMulti && resultsSetsCount > 1 && ( + {resultsSetsCount > 1 && (
{ const [autoRefreshInterval] = useAutoRefreshInterval(); - const query = `--!syntax_v1\nselect * from \`${path}\` limit 32`; + const query = `select * from \`${path}\` limit 32`; const {currentData, isFetching, error} = previewApi.useSendQueryQuery( {database, query, action: isExternalTableType(type) ? 'execute-query' : 'execute-scan'}, { @@ -40,7 +40,7 @@ export const Preview = ({database, path, type}: PreviewProps) => { }, ); const loading = isFetching && currentData === undefined; - const data = currentData ?? {}; + const data = currentData?.resultSets?.[0] ?? {}; const handleClosePreview = () => { dispatch(setShowPreview(false)); diff --git a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx index 3832177280..eb188451e4 100644 --- a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx +++ b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx @@ -30,7 +30,6 @@ import { DEFAULT_IS_QUERY_RESULT_COLLAPSED, DEFAULT_SIZE_RESULT_PANE_KEY, LAST_USED_QUERY_ACTION_KEY, - QUERY_USE_MULTI_SCHEMA_KEY, } from '../../../../utils/constants'; import {useEventHandler, useQueryExecutionSettings, useSetting} from '../../../../utils/hooks'; import {useChangedQuerySettings} from '../../../../utils/hooks/useChangedQuerySettings'; @@ -105,7 +104,6 @@ function QueryEditor(props: QueryEditorProps) { useLastQueryExecutionSettings(); const {resetBanner} = useChangedQuerySettings(); - const [useMultiSchema] = useSetting(QUERY_USE_MULTI_SCHEMA_KEY); const [lastUsedQueryAction, setLastUsedQueryAction] = useSetting( LAST_USED_QUERY_ACTION_KEY, ); @@ -148,8 +146,6 @@ function QueryEditor(props: QueryEditorProps) { const handleSendExecuteClick = useEventHandler((text?: string) => { const {input, history} = executeQuery; - const schema = useMultiSchema ? 'multi' : 'modern'; - const query = text ?? input; setLastUsedQueryAction(QUERY_ACTIONS.execute); @@ -163,7 +159,6 @@ function QueryEditor(props: QueryEditorProps) { query, database: tenantName, querySettings, - schema, enableTracingLevel, queryId, }); diff --git a/src/containers/UserSettings/i18n/en.json b/src/containers/UserSettings/i18n/en.json index 775a1c660b..3e43579a3e 100644 --- a/src/containers/UserSettings/i18n/en.json +++ b/src/containers/UserSettings/i18n/en.json @@ -35,9 +35,6 @@ "settings.showDomainDatabase.title": "Show domain database", - "settings.queryUseMultiSchema.title": "Allow queries with multiple result sets", - "settings.queryUseMultiSchema.description": "Use 'multi' schema for queries. It enables queries with multiple result sets. It returns nothing on versions 23-3 and older", - "settings.useClusterBalancerAsBackend.title": "Use cluster balancer as backend", "settings.useClusterBalancerAsBackend.description": "By default random cluster node is used as backend. It causes saved links to become invalid after some time, when node is restarted. Using balancer as backend fixes it", diff --git a/src/containers/UserSettings/settings.tsx b/src/containers/UserSettings/settings.tsx index d110827af5..e29528e3e6 100644 --- a/src/containers/UserSettings/settings.tsx +++ b/src/containers/UserSettings/settings.tsx @@ -8,7 +8,6 @@ import { ENABLE_AUTOCOMPLETE, INVERTED_DISKS_KEY, LANGUAGE_KEY, - QUERY_USE_MULTI_SCHEMA_KEY, SHOW_DOMAIN_DATABASE_KEY, THEME_KEY, USE_CLUSTER_BALANCER_AS_BACKEND_KEY, @@ -100,12 +99,6 @@ export const showDomainDatabase: SettingProps = { title: i18n('settings.showDomainDatabase.title'), }; -export const queryUseMultiSchemaSetting: SettingProps = { - settingKey: QUERY_USE_MULTI_SCHEMA_KEY, - title: i18n('settings.queryUseMultiSchema.title'), - description: i18n('settings.queryUseMultiSchema.description'), -}; - export const useClusterBalancerAsBackendSetting: SettingProps = { settingKey: USE_CLUSTER_BALANCER_AS_BACKEND_KEY, title: i18n('settings.useClusterBalancerAsBackend.title'), @@ -143,7 +136,7 @@ export const appearanceSection: SettingsSection = { export const experimentsSection: SettingsSection = { id: 'experimentsSection', title: i18n('section.experiments'), - settings: [usePaginatedTables, queryUseMultiSchemaSetting], + settings: [usePaginatedTables], }; export const devSettingsSection: SettingsSection = { id: 'devSettingsSection', diff --git a/src/services/api.ts b/src/services/api.ts index ba235bb46a..2a16def959 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -28,7 +28,6 @@ import type { Actions, ErrorResponse, QueryAPIResponse, - Schemas, Stats, Timeout, TracingLevel, @@ -529,12 +528,11 @@ export class YdbEmbeddedAPI extends AxiosWrapper { }, ); } - sendQuery( + sendQuery( params: { query?: string; database?: string; action?: Action; - schema?: Schema; syntax?: QuerySyntax; stats?: Stats; tracingLevel?: TracingLevel; @@ -553,14 +551,12 @@ export class YdbEmbeddedAPI extends AxiosWrapper { BINARY_DATA_IN_PLAIN_TEXT_DISPLAY, true, ); - // FIXME: after backend fix - const {schema, ...rest} = params; // FIXME: base64 is passed both to params and body to work on versions before and after 24-3 - return this.post | ErrorResponse | null>( + return this.post | ErrorResponse | null>( this.getPath('/viewer/json/query'), - {...rest, base64}, - {schema, base64}, + {...params, base64}, + {schema: 'multi', base64}, { concurrentId, timeout: params.timeout, diff --git a/src/services/settings.ts b/src/services/settings.ts index db276e4fcf..978a26f52a 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -13,7 +13,6 @@ import { PARTITIONS_HIDDEN_COLUMNS_KEY, QUERY_EXECUTION_SETTINGS_KEY, QUERY_SETTINGS_BANNER_LAST_CLOSED_KEY, - QUERY_USE_MULTI_SCHEMA_KEY, SAVED_QUERIES_KEY, SHOW_DOMAIN_DATABASE_KEY, TENANT_INITIAL_PAGE_KEY, @@ -31,7 +30,6 @@ export const DEFAULT_USER_SETTINGS = { [THEME_KEY]: 'system', [LANGUAGE_KEY]: undefined, [INVERTED_DISKS_KEY]: false, - [QUERY_USE_MULTI_SCHEMA_KEY]: true, [BINARY_DATA_IN_PLAIN_TEXT_DISPLAY]: true, [SAVED_QUERIES_KEY]: [], [TENANT_INITIAL_PAGE_KEY]: TENANT_PAGES_IDS.query, diff --git a/src/store/reducers/cluster/__test__/parseGroupsStatsQueryResponse.test.ts b/src/store/reducers/cluster/__test__/parseGroupsStatsQueryResponse.test.ts index f2c8b18f19..da0fe2ffbf 100644 --- a/src/store/reducers/cluster/__test__/parseGroupsStatsQueryResponse.test.ts +++ b/src/store/reducers/cluster/__test__/parseGroupsStatsQueryResponse.test.ts @@ -30,64 +30,86 @@ describe('parseGroupsStatsQueryResponse', () => { // 2 disk types and 2 erasure types const dataSet1 = { - columns, result: [ - ['Type:SSD', 'block-4-2', '1000', '2000', 100, 50], - ['Type:ROT', 'block-4-2', '2000', '1000', 50, 0], - ['Type:ROT', 'mirror-3of4', '1000', '0', 15, 0], - ['Type:SSD', 'mirror-3of4', '1000', '0', 5, 50], - ['Type:ROT', 'mirror-3-dc', null, null, null, 0], - ['Type:SSD', 'mirror-3-dc', null, null, null, 0], + { + columns, + rows: [ + ['Type:SSD', 'block-4-2', '1000', '2000', 100, 50], + ['Type:ROT', 'block-4-2', '2000', '1000', 50, 0], + ['Type:ROT', 'mirror-3of4', '1000', '0', 15, 0], + ['Type:SSD', 'mirror-3of4', '1000', '0', 5, 50], + ['Type:ROT', 'mirror-3-dc', null, null, null, 0], + ['Type:SSD', 'mirror-3-dc', null, null, null, 0], + ], + }, ], }; // 2 disk types and 1 erasure types, but with additional disks params const dataSet2 = { - columns, result: [ - ['Type:ROT,SharedWithOs:0,ReadCentric:0,Kind:0', 'mirror-3-dc', '1000', '500', 16, 16], - ['Type:ROT,SharedWithOs:1,ReadCentric:0,Kind:0', 'mirror-3-dc', '2000', '1000', 8, 24], - ['Type:SSD', 'mirror-3-dc', '3000', '400', 2, 10], - ['Type:ROT', 'mirror-3-dc', null, null, null, 32], - ['Type:ROT', 'block-4-2', null, null, null, 20], - ['Type:SSD', 'block-4-2', null, null, null, 0], + { + columns, + rows: [ + [ + 'Type:ROT,SharedWithOs:0,ReadCentric:0,Kind:0', + 'mirror-3-dc', + '1000', + '500', + 16, + 16, + ], + [ + 'Type:ROT,SharedWithOs:1,ReadCentric:0,Kind:0', + 'mirror-3-dc', + '2000', + '1000', + 8, + 24, + ], + ['Type:SSD', 'mirror-3-dc', '3000', '400', 2, 10], + ['Type:ROT', 'mirror-3-dc', null, null, null, 32], + ['Type:ROT', 'block-4-2', null, null, null, 20], + ['Type:SSD', 'block-4-2', null, null, null, 0], + ], + }, ], }; const parsedDataSet1 = { - SSD: { + HDD: { 'block-4-2': { - diskType: 'SSD', + allocatedSize: 1000, + availableSize: 2000, + createdGroups: 50, + diskType: 'HDD', erasure: 'block-4-2', - createdGroups: 100, - totalGroups: 150, - allocatedSize: 2000, - availableSize: 1000, + totalGroups: 50, }, 'mirror-3of4': { - diskType: 'SSD', - erasure: 'mirror-3of4', - createdGroups: 5, - totalGroups: 55, allocatedSize: 0, availableSize: 1000, + createdGroups: 15, + diskType: 'HDD', + erasure: 'mirror-3of4', + totalGroups: 15, }, }, - HDD: { + SSD: { 'block-4-2': { - diskType: 'HDD', + allocatedSize: 2000, + availableSize: 1000, + createdGroups: 100, + diskType: 'SSD', erasure: 'block-4-2', - createdGroups: 50, - totalGroups: 50, - allocatedSize: 1000, - availableSize: 2000, + totalGroups: 150, }, 'mirror-3of4': { - diskType: 'HDD', - erasure: 'mirror-3of4', - createdGroups: 15, - totalGroups: 15, allocatedSize: 0, availableSize: 1000, + createdGroups: 5, + diskType: 'SSD', + erasure: 'mirror-3of4', + totalGroups: 55, }, }, }; @@ -95,22 +117,22 @@ describe('parseGroupsStatsQueryResponse', () => { const parsedDataSet2 = { HDD: { 'mirror-3-dc': { + allocatedSize: 1500, + availableSize: 3000, + createdGroups: 24, diskType: 'HDD', erasure: 'mirror-3-dc', - createdGroups: 24, totalGroups: 64, - allocatedSize: 1500, - availableSize: 3000, }, }, SSD: { 'mirror-3-dc': { + allocatedSize: 400, + availableSize: 3000, + createdGroups: 2, diskType: 'SSD', erasure: 'mirror-3-dc', - createdGroups: 2, totalGroups: 12, - allocatedSize: 400, - availableSize: 3000, }, }, }; diff --git a/src/store/reducers/cluster/cluster.ts b/src/store/reducers/cluster/cluster.ts index 90b70d8508..94147ac30c 100644 --- a/src/store/reducers/cluster/cluster.ts +++ b/src/store/reducers/cluster/cluster.ts @@ -89,7 +89,6 @@ export const clusterApi = api.injectEndpoints({ // Table with stats is supposed to be very small (less than 10 rows) // So we batch this request with cluster request to prevent possible layout shifts, if data is missing const groupsStatsResponse = await window.api.sendQuery({ - schema: 'modern', query: query, database: clusterRoot, action: 'execute-scan', diff --git a/src/store/reducers/cluster/utils.ts b/src/store/reducers/cluster/utils.ts index c0e54c5dd2..7d513d5717 100644 --- a/src/store/reducers/cluster/utils.ts +++ b/src/store/reducers/cluster/utils.ts @@ -86,9 +86,9 @@ function getGroupStats(data?: KeyValueRow[] | TStorageStats[]) { } export const parseGroupsStatsQueryResponse = ( - data: ExecuteQueryResponse<'modern'> | null, + data: ExecuteQueryResponse | null, ): ClusterGroupsStats => { - const parsedData = parseQueryAPIExecuteResponse(data).result; + const parsedData = parseQueryAPIExecuteResponse(data).resultSets?.[0]?.result; return getGroupStats(parsedData); }; diff --git a/src/store/reducers/executeQuery.ts b/src/store/reducers/executeQuery.ts index c197ae858a..5fc88271a3 100644 --- a/src/store/reducers/executeQuery.ts +++ b/src/store/reducers/executeQuery.ts @@ -2,7 +2,7 @@ import type {Reducer} from '@reduxjs/toolkit'; import {settingsManager} from '../../services/settings'; import {TracingLevelNumber} from '../../types/api/query'; -import type {ExecuteActions, Schemas} from '../../types/api/query'; +import type {ExecuteActions} from '../../types/api/query'; import {ResultType} from '../../types/store/executeQuery'; import type { ExecuteQueryAction, @@ -182,7 +182,6 @@ const executeQuery: Reducer = ( interface SendQueryParams extends QueryRequestParams { queryId: string; querySettings?: Partial; - schema?: Schemas; // flag whether to send new tracing header or not // default: not send enableTracingLevel?: boolean; @@ -197,14 +196,7 @@ export const executeQueryApi = api.injectEndpoints({ endpoints: (build) => ({ executeQuery: build.mutation({ queryFn: async ( - { - query, - database, - querySettings = {}, - schema = 'modern', - enableTracingLevel, - queryId, - }, + {query, database, querySettings = {}, enableTracingLevel, queryId}, {signal, dispatch}, ) => { let action: ExecuteActions = 'execute'; @@ -223,7 +215,6 @@ export const executeQueryApi = api.injectEndpoints({ const timeStart = Date.now(); const response = await window.api.sendQuery( { - schema, query, database, action, diff --git a/src/store/reducers/executeTopQueries/executeTopQueries.ts b/src/store/reducers/executeTopQueries/executeTopQueries.ts index 5dceeb70da..72a0bcea20 100644 --- a/src/store/reducers/executeTopQueries/executeTopQueries.ts +++ b/src/store/reducers/executeTopQueries/executeTopQueries.ts @@ -57,7 +57,6 @@ export const topQueriesApi = api.injectEndpoints({ try { const response = await window.api.sendQuery( { - schema: 'modern', query: getQueryText(database, preparedFilters), database, action: 'execute-scan', @@ -96,7 +95,9 @@ export const topQueriesApi = api.injectEndpoints({ ) => { try { const filterConditions = filters?.text ? `Query ILIKE '%${filters.text}%'` : ''; - const queryText = `SELECT UserSID, QueryStartAt, Query as QueryText, ApplicationName from \`.sys/query_sessions\` WHERE ${filterConditions || 'true'} ORDER BY SessionStartAt limit 100`; + const commonQueryPart = `SELECT UserSID, QueryStartAt, Query as QueryText, ApplicationName from \`.sys/query_sessions\` WHERE ${filterConditions || 'true'}`; + + const queryText = `${commonQueryPart} AND Query NOT LIKE '${commonQueryPart}%' ORDER BY SessionStartAt limit 100`; const response = await window.api.sendQuery( { @@ -111,7 +112,9 @@ export const topQueriesApi = api.injectEndpoints({ throw response; } - return {data: response?.result?.filter((item) => item.QueryText !== queryText)}; + const data = parseQueryAPIExecuteResponse(response); + + return {data}; } catch (error) { return {error}; } diff --git a/src/store/reducers/preview.ts b/src/store/reducers/preview.ts index 76b72c4b03..8a2841e904 100644 --- a/src/store/reducers/preview.ts +++ b/src/store/reducers/preview.ts @@ -15,7 +15,7 @@ export const previewApi = api.injectEndpoints({ queryFn: async ({query, database, action}: SendQueryParams, {signal}) => { try { const response = await window.api.sendQuery( - {schema: 'modern', query, database, action}, + {query, database, action}, {signal, withRetries: true}, ); diff --git a/src/store/reducers/shardsWorkload/shardsWorkload.ts b/src/store/reducers/shardsWorkload/shardsWorkload.ts index dcf6606826..43a10debd2 100644 --- a/src/store/reducers/shardsWorkload/shardsWorkload.ts +++ b/src/store/reducers/shardsWorkload/shardsWorkload.ts @@ -133,7 +133,6 @@ export const shardApi = api.injectEndpoints({ try { const response = await window.api.sendQuery( { - schema: 'modern', query: filters?.mode === EShardsWorkloadMode.Immediate ? createShardQueryImmediate(path, sortOrder, database) diff --git a/src/store/reducers/tenantOverview/executeTopTables/executeTopTables.ts b/src/store/reducers/tenantOverview/executeTopTables/executeTopTables.ts index e208f5fb18..ba3b1ab6a1 100644 --- a/src/store/reducers/tenantOverview/executeTopTables/executeTopTables.ts +++ b/src/store/reducers/tenantOverview/executeTopTables/executeTopTables.ts @@ -20,7 +20,6 @@ export const topTablesApi = api.injectEndpoints({ try { const response = await window.api.sendQuery( { - schema: 'modern', query: getQueryText(path), database: path, action: 'execute-scan', diff --git a/src/store/reducers/tenantOverview/topShards/tenantOverviewTopShards.ts b/src/store/reducers/tenantOverview/topShards/tenantOverviewTopShards.ts index f2b2ff8c1a..a2374a9940 100644 --- a/src/store/reducers/tenantOverview/topShards/tenantOverviewTopShards.ts +++ b/src/store/reducers/tenantOverview/topShards/tenantOverviewTopShards.ts @@ -28,7 +28,6 @@ export const topShardsApi = api.injectEndpoints({ try { const response = await window.api.sendQuery( { - schema: 'modern', query: createShardQuery(path, database), database, action: queryAction, diff --git a/src/store/reducers/viewSchema/viewSchema.ts b/src/store/reducers/viewSchema/viewSchema.ts index a888fd2efe..eba8c3d117 100644 --- a/src/store/reducers/viewSchema/viewSchema.ts +++ b/src/store/reducers/viewSchema/viewSchema.ts @@ -12,7 +12,6 @@ export const viewSchemaApi = api.injectEndpoints({ try { const response = await window.api.sendQuery( { - schema: 'modern', query: createViewSchemaQuery(path), database, action: 'execute-scan', @@ -24,7 +23,7 @@ export const viewSchemaApi = api.injectEndpoints({ return {error: response}; } - return {data: response?.columns || []}; + return {data: response?.result?.[0]?.columns || []}; } catch (error) { return {error: error}; } diff --git a/src/types/api/query.ts b/src/types/api/query.ts index f5d50af804..5cc98bd33c 100644 --- a/src/types/api/query.ts +++ b/src/types/api/query.ts @@ -221,9 +221,6 @@ export type Timeout = number; /** undefined = '15' */ export type TracingLevel = number; -/** undefined = 'classic' */ -export type Schemas = 'classic' | 'modern' | 'ydb' | 'multi' | undefined; - /** undefined = 'execute' */ export type ExecuteActions = | 'execute' @@ -271,76 +268,58 @@ export interface ExplainQueryResponse { plan?: QueryPlan; } -export type ExplainResponse = Action extends 'explain-script' +export type GenericExplainResponse = Action extends 'explain-script' ? ExplainScriptResponse : ExplainQueryResponse; -// ==== Execute Results ==== +// ==== Execute Result ==== -interface ModernSchemaResult { - result?: ArrayRow[]; +type SchemaResult = { + rows?: ArrayRow[] | null; columns?: ColumnType[]; -} -interface MultiSchemaResult { - result?: { - rows?: ArrayRow[] | null; - columns?: ColumnType[]; - truncated?: boolean; - }[]; -} -interface DefaultSchemaResult { - result?: KeyValueRow[]; -} - -// ==== Execute Responses ==== - -type ResultFields = Schema extends 'modern' - ? ModernSchemaResult - : Schema extends 'multi' - ? MultiSchemaResult - : DefaultSchemaResult; + truncated?: boolean; +}[]; /** * meta.type = 'query' * * execute-scan, execute-data, execute-query */ -export type ExecuteQueryResponse = { +export type ExecuteQueryResponse = { plan?: QueryPlan; ast?: string; stats?: TKqpStatsQuery; -} & ResultFields; + result?: SchemaResult; +}; /** * meta.type = 'script' * * execute, execute-script */ -export type ExecuteScriptResponse = { +export type ExecuteScriptResponse = { plan?: ScriptPlan; ast?: string; stats?: TKqpStatsQuery; -} & ResultFields; + result?: SchemaResult; +}; -export type ExecuteResponse = Action extends +export type GenericExecuteResponse = Action extends | 'execute-scan' | 'execute-data' | 'execute-query' - ? ExecuteQueryResponse - : ExecuteScriptResponse; + ? ExecuteQueryResponse + : ExecuteScriptResponse; export type CancelResponse = { stats?: TKqpStatsQuery; }; // ==== Combined API response ==== -export type QueryAPIResponseByAction< - Action extends Actions, - Schema extends Schemas, -> = Action extends ExplainActions - ? ExplainResponse +export type QueryAPIResponseByAction = Action extends ExplainActions + ? GenericExplainResponse : Action extends ExecuteActions - ? ExecuteResponse + ? GenericExecuteResponse : Action extends CancelActions ? CancelResponse : never; @@ -351,25 +330,10 @@ type QueryAPIResponseMeta = { }; }; -export type QueryAPIResponse< - Action extends Actions, - Schema extends Schemas, -> = QueryAPIResponseByAction & QueryAPIResponseMeta; +export type QueryAPIResponse = QueryAPIResponseByAction & + QueryAPIResponseMeta; // ==== types to use in query result preparation ==== -export type AnyExplainResponse = ExplainQueryResponse | ExplainScriptResponse; - -export type ExecuteModernResponse = - | ExecuteQueryResponse<'modern'> - | ExecuteScriptResponse<'modern'>; -export type ExecuteMultiResponse = ExecuteQueryResponse<'multi'> | ExecuteScriptResponse<'multi'>; -export type ExecuteClassicResponse = - | ExecuteQueryResponse<'classic'> - | ExecuteScriptResponse<'classic'>; -export type ExecuteYdbResponse = ExecuteQueryResponse<'ydb'> | ExecuteScriptResponse<'ydb'>; - -export type AnyExecuteResponse = - | ExecuteModernResponse - | ExecuteMultiResponse - | ExecuteClassicResponse - | ExecuteYdbResponse; +export type ExplainResponse = ExplainQueryResponse | ExplainScriptResponse; + +export type ExecuteResponse = ExecuteQueryResponse | ExecuteScriptResponse; diff --git a/src/types/store/query.ts b/src/types/store/query.ts index 31a7312ff5..bca59a38c3 100644 --- a/src/types/store/query.ts +++ b/src/types/store/query.ts @@ -28,7 +28,6 @@ export interface ParsedResultSet { export interface IQueryResult { resultSets?: ParsedResultSet[]; - result?: KeyValueRow[]; columns?: ColumnType[]; stats?: TKqpStatsQuery; plan?: ScriptPlan | QueryPlan; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 6c4df62041..c591eeff9b 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -137,9 +137,6 @@ export const USE_PAGINATED_TABLES_KEY = 'useBackendParamsForTables'; // Setting to hide domain in database list export const SHOW_DOMAIN_DATABASE_KEY = 'showDomainDatabase'; -// Enable schema that supports multiple resultsets -export const QUERY_USE_MULTI_SCHEMA_KEY = 'queryUseMultiSchema'; - export const USE_CLUSTER_BALANCER_AS_BACKEND_KEY = 'useClusterBalancerAsBacked'; export const ENABLE_AUTOCOMPLETE = 'enableAutocomplete'; diff --git a/src/utils/query.test.ts b/src/utils/query.test.ts index 58ded49f9b..44a812ae6c 100644 --- a/src/utils/query.test.ts +++ b/src/utils/query.test.ts @@ -39,78 +39,259 @@ describe('API utils', () => { }); }); describe('should correctly parse data', () => { - it('should parse modern schema result to KeyValueRow', () => { - const response = { - result: [['42', 'hello world']], - columns: [ + it('should accept stats without a result', () => { + const stats = {metric: 'good'} as TKqpStatsQuery; + const response = {stats}; + const actual = parseQueryAPIExecuteResponse(response); + expect(actual.resultSets?.[0]?.result).toBeUndefined(); + expect(actual.columns).toBeUndefined(); + expect(actual.stats).toEqual(response.stats); + }); + + const mockColumns = [ + {name: 'PDiskFilter', type: 'Utf8?'}, + {name: 'ErasureSpecies', type: 'Utf8?'}, + {name: 'CurrentAvailableSize', type: 'Uint64?'}, + {name: 'CurrentAllocatedSize', type: 'Uint64?'}, + {name: 'CurrentGroupsCreated', type: 'Uint32?'}, + {name: 'AvailableGroupsToCreate', type: 'Uint32?'}, + ]; + + const mockRows = [ + ['Type:SSD', 'block-4-2', '1000', '2000', 100, 50], + ['Type:ROT', 'block-4-2', '2000', '1000', 50, 0], + ]; + + it('should parse a valid ExecuteResponse correctly', () => { + const input = { + result: [ { - name: 'id', - type: 'Uint64?', + columns: mockColumns, + rows: mockRows, + truncated: false, }, + ], + stats: {DurationUs: '1000'}, + }; + + const expected = { + resultSets: [ { - name: 'value', - type: 'Utf8?', + columns: mockColumns, + result: [ + { + PDiskFilter: 'Type:SSD', + ErasureSpecies: 'block-4-2', + CurrentAvailableSize: '1000', + CurrentAllocatedSize: '2000', + CurrentGroupsCreated: 100, + AvailableGroupsToCreate: 50, + }, + { + PDiskFilter: 'Type:ROT', + ErasureSpecies: 'block-4-2', + CurrentAvailableSize: '2000', + CurrentAllocatedSize: '1000', + CurrentGroupsCreated: 50, + AvailableGroupsToCreate: 0, + }, + ], + truncated: false, }, ], + stats: {DurationUs: '1000'}, }; - const parsedResponse = parseQueryAPIExecuteResponse(response); - expect(parsedResponse.result).toEqual([ - { - id: '42', - value: 'hello world', - }, - ]); - expect(parsedResponse.columns).toEqual(response.columns); + expect(parseQueryAPIExecuteResponse(input)).toEqual(expected); }); - it('should return KeyValueRow result for ydb and classic schemas unchanged', () => { - const response = {result: [{foo: 'bar'}]}; - expect(parseQueryAPIExecuteResponse(response).result).toEqual(response.result); + + it('should handle empty result array', () => { + const input = { + result: [], + stats: {DurationUs: '1000'}, + }; + + const expected = { + result: [], + stats: {DurationUs: '1000'}, + }; + + expect(parseQueryAPIExecuteResponse(input)).toEqual(expected); }); - it('shoudl return stats for modern schema', () => { - const result = [['42', 'hello world']]; - const columns = [ - { - name: 'id', - type: 'Uint64?', - }, - { - name: 'value', - type: 'Utf8?', - }, - ]; - const stats = {metric: 'good'} as TKqpStatsQuery; + it('should handle result with columns but no rows', () => { + const input = { + result: [ + { + columns: mockColumns, + rows: [], + truncated: false, + }, + ], + stats: {DurationUs: '1000'}, + }; - const response = {result, columns, stats}; - const parsedResponse = parseQueryAPIExecuteResponse(response); + const expected = { + resultSets: [ + { + columns: mockColumns, + result: [], + truncated: false, + }, + ], + stats: {DurationUs: '1000'}, + }; - expect(parsedResponse.result).toEqual([ - { - id: '42', - value: 'hello world', - }, - ]); - expect(parsedResponse.columns).toEqual(response.columns); - expect(parsedResponse.stats).toEqual(response.stats); + expect(parseQueryAPIExecuteResponse(input)).toEqual(expected); }); - it('shoudl return stats for ydb and classic schemas', () => { - const result = [{foo: 'bar'}]; - const stats = {metric: 'good'} as TKqpStatsQuery; - const response = {result, stats}; - const parsedResponse = parseQueryAPIExecuteResponse(response); + it('should return empty object for unsupported format', () => { + const input = { + result: 'unsupported', + }; - expect(parsedResponse.result).toEqual(response.result); - expect(parsedResponse.stats).toEqual(response.stats); + expect(parseQueryAPIExecuteResponse(input)).toEqual({}); }); - it('should accept stats without a result', () => { - const stats = {metric: 'good'} as TKqpStatsQuery; - const response = {stats}; - const actual = parseQueryAPIExecuteResponse(response); - expect(actual.result).toBeUndefined(); - expect(actual.columns).toBeUndefined(); - expect(actual.stats).toEqual(response.stats); + + it('should handle multiple result sets', () => { + const input = { + result: [ + { + columns: mockColumns, + rows: mockRows, + truncated: false, + }, + { + columns: [{name: 'Count', type: 'Uint32?'}], + rows: [[2]], + truncated: false, + }, + ], + stats: {DurationUs: '1500'}, + }; + + const expected = { + resultSets: [ + { + columns: mockColumns, + result: [ + { + PDiskFilter: 'Type:SSD', + ErasureSpecies: 'block-4-2', + CurrentAvailableSize: '1000', + CurrentAllocatedSize: '2000', + CurrentGroupsCreated: 100, + AvailableGroupsToCreate: 50, + }, + { + PDiskFilter: 'Type:ROT', + ErasureSpecies: 'block-4-2', + CurrentAvailableSize: '2000', + CurrentAllocatedSize: '1000', + CurrentGroupsCreated: 50, + AvailableGroupsToCreate: 0, + }, + ], + truncated: false, + }, + { + columns: [{name: 'Count', type: 'Uint32?'}], + result: [{Count: 2}], + truncated: false, + }, + ], + stats: {DurationUs: '1500'}, + }; + + expect(parseQueryAPIExecuteResponse(input)).toEqual(expected); + }); + + it('should handle null values in rows', () => { + const input = { + result: [ + { + columns: mockColumns, + rows: [ + ['Type:SSD', null, '1000', null, 100, 50], + [null, 'block-4-2', null, '1000', null, 0], + ], + truncated: false, + }, + ], + stats: {DurationUs: '1000'}, + }; + + const expected = { + resultSets: [ + { + columns: mockColumns, + result: [ + { + PDiskFilter: 'Type:SSD', + ErasureSpecies: null, + CurrentAvailableSize: '1000', + CurrentAllocatedSize: null, + CurrentGroupsCreated: 100, + AvailableGroupsToCreate: 50, + }, + { + PDiskFilter: null, + ErasureSpecies: 'block-4-2', + CurrentAvailableSize: null, + CurrentAllocatedSize: '1000', + CurrentGroupsCreated: null, + AvailableGroupsToCreate: 0, + }, + ], + truncated: false, + }, + ], + stats: {DurationUs: '1000'}, + }; + + expect(parseQueryAPIExecuteResponse(input)).toEqual(expected); + }); + + it('should handle truncated results', () => { + const input = { + result: [ + { + columns: mockColumns, + rows: mockRows, + truncated: true, + }, + ], + stats: {DurationUs: '1000'}, + }; + + const result = parseQueryAPIExecuteResponse(input); + expect(result.resultSets?.[0].truncated).toBe(true); + }); + + it('should handle empty columns and rows', () => { + const input = { + result: [ + { + columns: [], + rows: [], + truncated: false, + }, + ], + stats: {DurationUs: '1000'}, + }; + + const expected = { + resultSets: [ + { + columns: [], + result: [], + truncated: false, + }, + ], + stats: {DurationUs: '1000'}, + }; + + expect(parseQueryAPIExecuteResponse(input)).toEqual(expected); }); }); }); diff --git a/src/utils/query.ts b/src/utils/query.ts index 4da0f40441..82a0dfcbdd 100644 --- a/src/utils/query.ts +++ b/src/utils/query.ts @@ -2,13 +2,11 @@ import {z} from 'zod'; import {YQLType} from '../types'; import type { - AnyExecuteResponse, - AnyExplainResponse, ArrayRow, ColumnType, ErrorResponse, - ExecuteModernResponse, - ExecuteMultiResponse, + ExecuteResponse, + ExplainResponse, KeyValueRow, QueryPlan, ScriptPlan, @@ -135,7 +133,7 @@ export const getColumnType = (type: string) => { }; /** parse response result from ArrayRow to KeyValueRow */ -const parseModernResult = (rows: ArrayRow[], columns: ColumnType[]) => { +const parseResult = (rows: ArrayRow[], columns: ColumnType[]) => { return rows.map((row) => { return row.reduce((newRow, cellData, columnIndex) => { const {name} = columns[columnIndex]; @@ -145,17 +143,7 @@ const parseModernResult = (rows: ArrayRow[], columns: ColumnType[]) => { }); }; -const parseExecuteModernResponse = (data: ExecuteModernResponse): IQueryResult => { - const {result, columns, ...restData} = data; - - return { - result: result && columns && parseModernResult(result, columns), - columns, - ...restData, - }; -}; - -const parseExecuteMultiResponse = (data: ExecuteMultiResponse): IQueryResult => { +const parseExecuteResponse = (data: ExecuteResponse): IQueryResult => { const {result, ...restData} = data; const parsedResult = result?.map((resultSet) => { @@ -169,7 +157,7 @@ const parseExecuteMultiResponse = (data: ExecuteMultiResponse): IQueryResult => } if (rows && columns) { - parsedRows = parseModernResult(rows, columns); + parsedRows = parseResult(rows, columns); } return { @@ -185,15 +173,7 @@ const parseExecuteMultiResponse = (data: ExecuteMultiResponse): IQueryResult => }; }; -const isModern = (response: AnyExecuteResponse): response is ExecuteModernResponse => - Boolean( - response && - !Array.isArray(response) && - Array.isArray(response.result) && - Array.isArray((response as ExecuteModernResponse).columns), - ); - -const isMulti = (response: AnyExecuteResponse): response is ExecuteMultiResponse => +const isSupportedType = (response: ExecuteResponse): response is ExecuteResponse => Boolean( response && !Array.isArray(response) && @@ -211,7 +191,7 @@ type UnsupportedQueryResponseFormat = | {result: string | Record}; const isUnsupportedType = ( - data: AnyExecuteResponse | AnyExplainResponse | UnsupportedQueryResponseFormat, + data: ExecuteResponse | ExplainResponse | UnsupportedQueryResponseFormat, ): data is UnsupportedQueryResponseFormat => { return Boolean( !data || @@ -228,24 +208,21 @@ export function isQueryErrorResponse(data: unknown): data is ErrorResponse { // Although schema is set in request, if schema is not supported default schema for the version will be used // So we should additionally parse response export const parseQueryAPIExecuteResponse = ( - data: AnyExecuteResponse | UnsupportedQueryResponseFormat, + data: ExecuteResponse | UnsupportedQueryResponseFormat, ): IQueryResult => { if (isUnsupportedType(data)) { return {}; } - if (isMulti(data)) { - return parseExecuteMultiResponse(data); - } - if (isModern(data)) { - return parseExecuteModernResponse(data); + if (isSupportedType(data)) { + return parseExecuteResponse(data); } return data; }; export const parseQueryAPIExplainResponse = ( - data: AnyExplainResponse | UnsupportedQueryResponseFormat, + data: ExplainResponse | UnsupportedQueryResponseFormat, ): IQueryResult => { if (isUnsupportedType(data)) { return {}; diff --git a/tests/suites/tenant/queryEditor/constants.ts b/tests/suites/tenant/constants.ts similarity index 100% rename from tests/suites/tenant/queryEditor/constants.ts rename to tests/suites/tenant/constants.ts diff --git a/tests/suites/tenant/diagnostics/Diagnostics.ts b/tests/suites/tenant/diagnostics/Diagnostics.ts index 6a3c6ee90e..bc41b17098 100644 --- a/tests/suites/tenant/diagnostics/Diagnostics.ts +++ b/tests/suites/tenant/diagnostics/Diagnostics.ts @@ -1,11 +1,13 @@ import type {Locator, Page} from '@playwright/test'; +import {retryAction} from '../../../utils/retryAction'; import {VISIBILITY_TIMEOUT} from '../TenantPage'; export enum DiagnosticsTab { Info = 'Info', Schema = 'Schema', TopShards = 'Top shards', + Queries = 'Queries', Nodes = 'Nodes', Graph = 'Graph', Tablets = 'Tablets', @@ -13,9 +15,109 @@ export enum DiagnosticsTab { Describe = 'Describe', } +export class Table { + private table: Locator; + + constructor(selector: Locator) { + this.table = selector.locator('.ydb-resizeable-data-table'); + } + + async isVisible() { + await this.table.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + return true; + } + + async isHidden() { + await this.table.waitFor({state: 'hidden', timeout: VISIBILITY_TIMEOUT}); + return true; + } + + async hasNoData() { + const noDataCell = this.table.locator('td.data-table__no-data'); + return ( + (await noDataCell.isVisible()) && + (await this.getRowCount()) === 1 && + (await noDataCell.innerText()) === 'No data' + ); + } + + async getRowCount() { + const rows = this.table.locator('tr.data-table__row'); + return rows.count(); + } + + async getCellValue(row: number, col: number) { + const cell = this.table.locator(`tr:nth-child(${row}) td:nth-child(${col})`); + return cell.innerText(); + } + + async waitForCellValue(row: number, col: number, value: string) { + const cell = this.table.locator(`tr:nth-child(${row}) td:nth-child(${col})`); + await retryAction(async () => { + const cellValue = (await cell.innerText()).trim(); + if (cellValue === value) { + return true; + } + + throw new Error(`Cell value ${cellValue} did not match expected ${value}`); + }); + + return true; + } + + async getHeaders() { + const headers = this.table.locator('th.data-table__th'); + const headerCount = await headers.count(); + const headerNames = []; + for (let i = 0; i < headerCount; i++) { + headerNames.push(await headers.nth(i).innerText()); + } + return headerNames; + } + + async getCellValueByHeader(row: number, header: string) { + const headers = await this.getHeaders(); + const colIndex = headers.indexOf(header); + if (colIndex === -1) { + throw new Error(`Header "${header}" not found`); + } + const cell = this.table.locator( + `tr.data-table__row:nth-child(${row}) td:nth-child(${colIndex + 1})`, + ); + return cell.innerText(); + } + + async waitForCellValueByHeader(row: number, header: string, value: string) { + const headers = await this.getHeaders(); + const colIndex = headers.indexOf(header); + if (colIndex === -1) { + throw new Error(`Header "${header}" not found`); + } + const cell = this.table.locator( + `tr.data-table__row:nth-child(${row}) td:nth-child(${colIndex + 1})`, + ); + await retryAction(async () => { + const cellValue = (await cell.innerText()).trim(); + if (cellValue === value) { + return true; + } + throw new Error(`Cell value ${cellValue} did not match expected ${value}`); + }); + return true; + } +} + +export enum QueriesSwitch { + Top = 'Top', + Running = 'Running', +} + export class Diagnostics { + table: Table; + private tabs: Locator; private schemaViewer: Locator; + private tableControls: Locator; private dataTable: Locator; private primaryKeys: Locator; private refreshButton: Locator; @@ -23,11 +125,13 @@ export class Diagnostics { constructor(page: Page) { this.tabs = page.locator('.kv-tenant-diagnostics__tabs'); + this.tableControls = page.locator('.ydb-table-with-controls-layout__controls'); this.schemaViewer = page.locator('.schema-viewer'); this.dataTable = page.locator('.data-table__table'); this.primaryKeys = page.locator('.schema-viewer__keys_type_primary'); this.refreshButton = page.locator('button[aria-label="Refresh"]'); this.autoRefreshSelect = page.locator('.g-select'); + this.table = new Table(page.locator('.object-general')); } async isSchemaViewerVisible() { @@ -45,6 +149,14 @@ export class Diagnostics { await tab.click(); } + async clickRadioSwitch(radioName: QueriesSwitch): Promise { + const option = this.tableControls.locator( + `.g-radio-button__option:has-text("${radioName}")`, + ); + + await option.evaluate((el) => (el as HTMLElement).click()); + } + async getPrimaryKeys(): Promise { const keysElement = this.primaryKeys.locator('.schema-viewer__keys-values'); const keysText = (await keysElement.textContent()) || ''; diff --git a/tests/suites/tenant/diagnostics/diagnostics.test.ts b/tests/suites/tenant/diagnostics/diagnostics.test.ts index df553bb41d..908415d544 100644 --- a/tests/suites/tenant/diagnostics/diagnostics.test.ts +++ b/tests/suites/tenant/diagnostics/diagnostics.test.ts @@ -1,12 +1,14 @@ import {expect, test} from '@playwright/test'; import {dsVslotsSchema, tenantName} from '../../../utils/constants'; -import {TenantPage} from '../TenantPage'; +import {NavigationTabs, TenantPage} from '../TenantPage'; +import {longRunningQuery} from '../constants'; +import {QueryEditor} from '../queryEditor/QueryEditor'; -import {Diagnostics, DiagnosticsTab} from './Diagnostics'; +import {Diagnostics, DiagnosticsTab, QueriesSwitch} from './Diagnostics'; test.describe('Diagnostics tab', async () => { - test.beforeEach(async ({page}) => { + test('Primary keys header is visible in Schema tab', async ({page}) => { const pageQueryParams = { schema: dsVslotsSchema, name: tenantName, @@ -14,9 +16,7 @@ test.describe('Diagnostics tab', async () => { }; const tenantPage = new TenantPage(page); await tenantPage.goto(pageQueryParams); - }); - test('Primary keys header is visible in Schema tab', async ({page}) => { const objectSummary = new Diagnostics(page); await objectSummary.clickTab(DiagnosticsTab.Schema); @@ -28,4 +28,46 @@ test.describe('Diagnostics tab', async () => { 'VSlotId', ]); }); + + test('No runnning queries in Queries if no queries are running', async ({page}) => { + const pageQueryParams = { + schema: tenantName, + name: tenantName, + tenantPage: 'diagnostics', + }; + const tenantPage = new TenantPage(page); + await tenantPage.goto(pageQueryParams); + + const diagnostics = new Diagnostics(page); + await diagnostics.clickTab(DiagnosticsTab.Queries); + await diagnostics.clickRadioSwitch(QueriesSwitch.Running); + await diagnostics.table.hasNoData(); + }); + + test('Running query is shown if query is running', async ({page}) => { + const pageQueryParams = { + schema: tenantName, + name: tenantName, + tenantPage: 'query', + }; + const tenantPage = new TenantPage(page); + await tenantPage.goto(pageQueryParams); + + const queryEditor = new QueryEditor(page); + + await queryEditor.setQuery(longRunningQuery); + await queryEditor.clickRunButton(); + await page.waitForTimeout(500); + const statusElement = await queryEditor.getExecutionStatus(); + await expect(statusElement).toBe('Running'); + await tenantPage.selectNavigationTab(NavigationTabs.Diagnostics); + + const diagnostics = new Diagnostics(page); + await diagnostics.clickTab(DiagnosticsTab.Queries); + await diagnostics.clickRadioSwitch(QueriesSwitch.Running); + expect(await diagnostics.table.getRowCount()).toBe(1); + expect( + await diagnostics.table.waitForCellValueByHeader(1, 'QueryText', longRunningQuery), + ).toBe(true); + }); }); diff --git a/tests/suites/tenant/queryEditor/QueryEditor.ts b/tests/suites/tenant/queryEditor/QueryEditor.ts index cb1502dd24..2083335bbf 100644 --- a/tests/suites/tenant/queryEditor/QueryEditor.ts +++ b/tests/suites/tenant/queryEditor/QueryEditor.ts @@ -372,18 +372,4 @@ export class QueryEditor { await this.indicatorIcon.waitFor({state: 'hidden', timeout: VISIBILITY_TIMEOUT}); return true; } - - async retry(action: () => Promise, maxAttempts = 3, delay = 1000): Promise { - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - try { - return await action(); - } catch (error) { - if (attempt === maxAttempts) { - throw error; - } - await new Promise((resolve) => setTimeout(resolve, delay)); - } - } - throw new Error('Max attempts reached'); - } } diff --git a/tests/suites/tenant/queryEditor/queryEditor.test.ts b/tests/suites/tenant/queryEditor/queryEditor.test.ts index 019df5001e..5c3f404d81 100644 --- a/tests/suites/tenant/queryEditor/queryEditor.test.ts +++ b/tests/suites/tenant/queryEditor/queryEditor.test.ts @@ -2,6 +2,7 @@ import {expect, test} from '@playwright/test'; import {tenantName} from '../../../utils/constants'; import {NavigationTabs, TenantPage, VISIBILITY_TIMEOUT} from '../TenantPage'; +import {longRunningQuery} from '../constants'; import { ButtonNames, @@ -11,7 +12,6 @@ import { QueryTabs, ResultTabNames, } from './QueryEditor'; -import {longRunningQuery} from './constants'; test.describe('Test Query Editor', async () => { const testQuery = 'SELECT 1, 2, 3, 4, 5;'; diff --git a/tests/utils/retryAction.ts b/tests/utils/retryAction.ts new file mode 100644 index 0000000000..02e8b9aa49 --- /dev/null +++ b/tests/utils/retryAction.ts @@ -0,0 +1,17 @@ +export const retryAction = async ( + action: () => Promise, + maxAttempts = 3, + delay = 1000, +): Promise => { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await action(); + } catch (error) { + if (attempt === maxAttempts) { + throw error; + } + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + throw new Error('Max attempts reached'); +};