diff --git a/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx b/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx index ac433dc81..4f9b8070f 100644 --- a/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx +++ b/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx @@ -48,7 +48,7 @@ export function SchemaTree(props: SchemaTreeProps) { {currentData: actionsSchemaData, isFetching: isActionsDataFetching}, ] = tableSchemaDataApi.useLazyGetTableSchemaDataQuery(); - const [querySettings, setQueryExecutionSettings] = useQueryExecutionSettings(); + const [querySettings] = useQueryExecutionSettings(); const [createDirectoryOpen, setCreateDirectoryOpen] = React.useState(false); const [parentPath, setParentPath] = React.useState(''); const setSchemaTreeKey = useDispatchTreeKey(); @@ -128,8 +128,6 @@ export function SchemaTree(props: SchemaTreeProps) { dispatch, { setActivePath: onActivePathUpdate, - updateQueryExecutionSettings: (settings) => - setQueryExecutionSettings({...querySettings, ...settings}), showCreateDirectoryDialog: createDirectoryFeatureAvailable ? handleOpenCreateDirectoryDialog : undefined, @@ -149,7 +147,6 @@ export function SchemaTree(props: SchemaTreeProps) { onActivePathUpdate, querySettings, rootPath, - setQueryExecutionSettings, ]); return ( diff --git a/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.scss b/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.scss index 7a2b9ddfe..5e6fec53a 100644 --- a/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.scss +++ b/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.scss @@ -28,24 +28,13 @@ flex: 6; } - &__limit-rows, - &__timeout { - width: 33.3%; + &__limit-rows { + width: 50%; margin-right: var(--g-spacing-2); } - &__timeout-suffix { - display: flex; - align-items: center; - - color: var(--g-color-text-secondary); - } - - &__documentation-link { - display: flex; - align-items: center; - - margin-left: var(--g-spacing-4); + &__postfix { + margin-right: var(--g-spacing-2); color: var(--g-color-text-secondary); } diff --git a/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx b/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx index 3a1d83671..f96fc6e03 100644 --- a/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx +++ b/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import {Dialog, Link as ExternalLink, Flex, TextInput, Tooltip} from '@gravity-ui/uikit'; +import {Button, Dialog, Flex, TextInput, Tooltip} from '@gravity-ui/uikit'; import {zodResolver} from '@hookform/resolvers/zod'; import {Controller, useForm} from 'react-hook-form'; @@ -18,9 +18,10 @@ import { useTypedDispatch, useTypedSelector, } from '../../../../utils/hooks'; -import {querySettingsValidationSchema} from '../../../../utils/query'; +import {QUERY_MODES, querySettingsValidationSchema} from '../../../../utils/query'; import {QuerySettingsSelect} from './QuerySettingsSelect'; +import {QuerySettingsTimeout} from './QuerySettingsTimeout'; import {QUERY_SETTINGS_FIELD_SETTINGS} from './constants'; import i18n from './i18n'; @@ -73,6 +74,8 @@ function QuerySettingsForm({initialValues, onSubmit, onClose}: QuerySettingsForm const { control, handleSubmit, + setValue, + watch, formState: {errors}, } = useForm({ defaultValues: initialValues, @@ -82,6 +85,9 @@ function QuerySettingsForm({initialValues, onSubmit, onClose}: QuerySettingsForm const [useShowPlanToSvg] = useSetting(USE_SHOW_PLAN_SVG_KEY); const enableTracingLevel = useTracingLevelOptionAvailable(); + const timeout = watch('timeout'); + const queryMode = watch('queryMode'); + return (
@@ -97,42 +103,21 @@ function QuerySettingsForm({initialValues, onSubmit, onClose}: QuerySettingsForm { + field.onChange(mode); + + if (mode !== 'query' && timeout === null) { + setValue('timeout', ''); + } else if (mode === 'query') { + setValue('timeout', null); + } + }} settingOptions={QUERY_SETTINGS_FIELD_SETTINGS.queryMode.options} /> )} /> - - -
- ( - - - - {i18n('form.timeout.seconds')} - - - )} - /> -
-
{enableTracingLevel && ( + + ( + field.onChange(enabled ? '' : null)} + validationState={errors.timeout ? 'invalid' : undefined} + errorMessage={errors.timeout?.message} + isDisabled={queryMode !== QUERY_MODES.query} + /> + )} + /> +
(
- {i18n('docs')} - +
{buttonCancel} {buttonApply} diff --git a/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsTimeout.scss b/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsTimeout.scss new file mode 100644 index 000000000..d04c6ad82 --- /dev/null +++ b/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsTimeout.scss @@ -0,0 +1,17 @@ +.ydb-query-settings-timeout { + &__control-wrapper { + display: flex; + flex: 6; + align-items: center; + } + + &__input { + width: 50%; + } + + &__postfix { + margin-right: var(--g-spacing-2); + + color: var(--g-color-text-secondary); + } +} diff --git a/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsTimeout.tsx b/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsTimeout.tsx new file mode 100644 index 000000000..1af8491ca --- /dev/null +++ b/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsTimeout.tsx @@ -0,0 +1,66 @@ +import React from 'react'; + +import {TextInput} from '@gravity-ui/uikit'; + +import {cn} from '../../../../utils/cn'; + +import {TimeoutLabel} from './TimeoutLabel'; +import i18n from './i18n'; + +import './QuerySettingsTimeout.scss'; + +const b = cn('ydb-query-settings-timeout'); + +interface QuerySettingsTimeoutProps { + id?: string; + value: number | null | undefined; + onChange: (value: number | undefined) => void; + onToggle: (enabled: boolean) => void; + validationState?: 'invalid'; + errorMessage?: string; + isDisabled?: boolean; +} + +export function QuerySettingsTimeout({ + id, + value, + onChange, + onToggle, + validationState, + errorMessage, + isDisabled, +}: QuerySettingsTimeoutProps) { + const handleValueChange = React.useCallback( + (event: React.ChangeEvent) => { + const newValue = event.target.value ? Number(event.target.value) : undefined; + onChange(newValue); + }, + [onChange], + ); + + const isChecked = value !== null; + + return ( + + + {isChecked && ( +
+ {i18n('form.timeout.seconds')} + } + /> +
+ )} +
+ ); +} diff --git a/src/containers/Tenant/Query/QuerySettingsDialog/TimeoutLabel.scss b/src/containers/Tenant/Query/QuerySettingsDialog/TimeoutLabel.scss new file mode 100644 index 000000000..0a450b39b --- /dev/null +++ b/src/containers/Tenant/Query/QuerySettingsDialog/TimeoutLabel.scss @@ -0,0 +1,23 @@ +.ydb-timeout-label { + &__switch { + align-items: center; + + height: var(--g-text-header-2-line-height); + margin-right: var(--g-spacing-1); + } + + &__label-title, + &__switch-title { + flex: 4; + align-items: center; + + margin-right: var(--g-spacing-3); + + font-weight: 500; + white-space: nowrap; + } + + &__label-title { + line-height: var(--g-text-header-2-line-height); + } +} diff --git a/src/containers/Tenant/Query/QuerySettingsDialog/TimeoutLabel.tsx b/src/containers/Tenant/Query/QuerySettingsDialog/TimeoutLabel.tsx new file mode 100644 index 000000000..4be744402 --- /dev/null +++ b/src/containers/Tenant/Query/QuerySettingsDialog/TimeoutLabel.tsx @@ -0,0 +1,47 @@ +import {HelpMark, Switch} from '@gravity-ui/uikit'; + +import {cn} from '../../../../utils/cn'; +import {ENABLE_QUERY_STREAMING} from '../../../../utils/constants'; +import {useSetting} from '../../../../utils/hooks'; + +import {QUERY_SETTINGS_FIELD_SETTINGS} from './constants'; +import i18n from './i18n'; + +import './TimeoutLabel.scss'; + +const b = cn('ydb-timeout-label'); + +interface TimeoutLabelProps { + isDisabled?: boolean; + isChecked: boolean; + onToggle: (enabled: boolean) => void; +} + +export function TimeoutLabel({isDisabled, isChecked, onToggle}: TimeoutLabelProps) { + const [isQueryStreamingEnabled] = useSetting(ENABLE_QUERY_STREAMING); + + if (isQueryStreamingEnabled) { + return ( +
+ + {isDisabled && ( + + {i18n('form.timeout.disabled')} + + )} +
+ ); + } + + return ( + + ); +} diff --git a/src/containers/Tenant/Query/QuerySettingsDialog/i18n/en.json b/src/containers/Tenant/Query/QuerySettingsDialog/i18n/en.json index ef1698ab4..4b5791198 100644 --- a/src/containers/Tenant/Query/QuerySettingsDialog/i18n/en.json +++ b/src/containers/Tenant/Query/QuerySettingsDialog/i18n/en.json @@ -10,6 +10,8 @@ "tooltip_plan-to-svg-statistics": "Statistics option is set to \"Full\" due to the enabled \"Execution plan\" experiment.\n To disable it, go to the \"Experiments\" section in the user settings.", "button-cancel": "Cancel", "form.timeout.seconds": "sec", + "form.limit.rows": "rows", + "form.timeout.disabled": "Not available to turn off in this query type", "form.validation.timeout": "Must be positive", "form.validation.limitRows": "Must be between 1 and 100000", "description.default": " (default)", diff --git a/src/containers/Tenant/Query/QuerySettingsDialog/i18n/ru.json b/src/containers/Tenant/Query/QuerySettingsDialog/i18n/ru.json index fb2b4e8a7..a2749657a 100644 --- a/src/containers/Tenant/Query/QuerySettingsDialog/i18n/ru.json +++ b/src/containers/Tenant/Query/QuerySettingsDialog/i18n/ru.json @@ -10,6 +10,8 @@ "button-done": "Готово", "button-cancel": "Отменить", "form.timeout.seconds": "сек", + "form.limit.rows": "строк", + "form.timeout.disabled": "Невозможно выключить для текущего типа запроса", "form.validation.timeout": "Таймаут должен быть положительным", "form.validation.limitRows": "Лимит строк должен быть между 1 и 100000", "description.default": " (default)", diff --git a/src/containers/Tenant/utils/schemaActions.tsx b/src/containers/Tenant/utils/schemaActions.tsx index 52857ff2d..7d9128bcd 100644 --- a/src/containers/Tenant/utils/schemaActions.tsx +++ b/src/containers/Tenant/utils/schemaActions.tsx @@ -7,7 +7,6 @@ import type {SnippetParams} from '../../../components/ConnectToDB/types'; import type {AppDispatch} from '../../../store'; 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'; import createToast from '../../../utils/createToast'; import {insertSnippetToEditor} from '../../../utils/monaco/insertSnippet'; import {transformPath} from '../ObjectSummary/transformPath'; @@ -42,7 +41,6 @@ import { } from './schemaQueryTemplates'; interface ActionsAdditionalParams { - updateQueryExecutionSettings: (settings?: Partial) => void; setActivePath: (path: string) => void; showCreateDirectoryDialog?: (path: string) => void; getConfirmation?: () => Promise; diff --git a/src/services/api/streaming.ts b/src/services/api/streaming.ts index f972516f6..1d7657b1b 100644 --- a/src/services/api/streaming.ts +++ b/src/services/api/streaming.ts @@ -2,6 +2,7 @@ import {parseMultipart} from '@mjackson/multipart-parser'; import qs from 'qs'; import { + isKeepAliveChunk, isQueryResponseChunk, isSessionChunk, isStreamDataChunk, @@ -93,6 +94,9 @@ export class StreamingAPI extends BaseYdbAPI { options.onStreamDataChunk(chunk); } else if (isQueryResponseChunk(chunk)) { options.onQueryResponseChunk(chunk); + } else if (isKeepAliveChunk(chunk)) { + // Logging for debug purposes + console.log('Received keep alive chunk'); } } catch (e) { throw new Error(`Error parsing chunk: ${e}`); diff --git a/src/services/settings.ts b/src/services/settings.ts index 594ebf70f..cca3cfb14 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -46,7 +46,7 @@ export const DEFAULT_USER_SETTINGS = { [USE_CLUSTER_BALANCER_AS_BACKEND_KEY]: true, [ENABLE_AUTOCOMPLETE]: true, [ENABLE_CODE_ASSISTANT]: true, - [ENABLE_QUERY_STREAMING]: false, + [ENABLE_QUERY_STREAMING]: true, [AUTOCOMPLETE_ON_ENTER]: true, [IS_HOTKEYS_HELP_HIDDEN_KEY]: false, [AUTO_REFRESH_INTERVAL]: 0, diff --git a/src/store/reducers/capabilities/hooks.ts b/src/store/reducers/capabilities/hooks.ts index 80609f301..defdb3eef 100644 --- a/src/store/reducers/capabilities/hooks.ts +++ b/src/store/reducers/capabilities/hooks.ts @@ -71,7 +71,7 @@ export const useClusterDashboardAvailable = () => { }; export const useStreamingAvailable = () => { - return useGetFeatureVersion('/viewer/query') >= 7; + return useGetFeatureVersion('/viewer/query') >= 8; }; const useGetSecuritySetting = (feature: SecuritySetting) => { diff --git a/src/store/reducers/query/query.ts b/src/store/reducers/query/query.ts index a01b976db..4c220725d 100644 --- a/src/store/reducers/query/query.ts +++ b/src/store/reducers/query/query.ts @@ -1,4 +1,4 @@ -import {createSlice} from '@reduxjs/toolkit'; +import {createSelector, createSlice} from '@reduxjs/toolkit'; import type {PayloadAction} from '@reduxjs/toolkit'; import {settingsManager} from '../../../services/settings'; @@ -135,11 +135,9 @@ const slice = createSlice({ selectors: { selectQueriesHistoryFilter: (state) => state.history.filter || '', selectTenantPath: (state) => state.tenantPath, - selectQueryDuration: (state) => ({ - startTime: state.result?.startTime, - endTime: state.result?.endTime, - }), selectResult: (state) => state.result, + selectStartTime: (state) => state.result?.startTime, + selectEndTime: (state) => state.result?.endTime, selectQueriesHistory: (state) => { const items = state.history.queries; const filter = state.history.filter?.toLowerCase(); @@ -154,6 +152,17 @@ const slice = createSlice({ }, }); +export const selectQueryDuration = createSelector( + slice.selectors.selectStartTime, + slice.selectors.selectEndTime, + (startTime, endTime) => { + return { + startTime, + endTime, + }; + }, +); + export default slice.reducer; export const { changeUserInput, @@ -177,7 +186,6 @@ export const { selectTenantPath, selectResult, selectUserInput, - selectQueryDuration, selectIsDirty, } = slice.selectors; diff --git a/src/store/reducers/query/utils.ts b/src/store/reducers/query/utils.ts index 073ff5f92..9d4a9b2d0 100644 --- a/src/store/reducers/query/utils.ts +++ b/src/store/reducers/query/utils.ts @@ -46,3 +46,7 @@ export function isStreamDataChunk(content: StreamingChunk): content is StreamDat export function isQueryResponseChunk(content: StreamingChunk): content is QueryResponseChunk { return content?.meta?.event === 'QueryResponse'; } + +export function isKeepAliveChunk(content: StreamingChunk): content is SessionChunk { + return content?.meta?.event === 'KeepAlive'; +} diff --git a/src/types/store/streaming.ts b/src/types/store/streaming.ts index 78228350a..48d5b148a 100644 --- a/src/types/store/streaming.ts +++ b/src/types/store/streaming.ts @@ -19,6 +19,12 @@ export interface SessionChunk { }; } +export interface KeepAliveChunk { + meta: { + event: 'KeepAlive'; + }; +} + export interface StreamDataChunk { meta: { event: 'StreamData'; @@ -51,4 +57,4 @@ export interface BaseQueryResponseChunk { export type QueryResponseChunk = BaseQueryResponseChunk & (SuccessQueryResponseData | ErrorQueryResponseData); -export type StreamingChunk = SessionChunk | StreamDataChunk | QueryResponseChunk; +export type StreamingChunk = SessionChunk | StreamDataChunk | QueryResponseChunk | KeepAliveChunk; diff --git a/src/utils/hooks/useQueryExecutionSettings.ts b/src/utils/hooks/useQueryExecutionSettings.ts index 042293dbb..769f6a7e2 100644 --- a/src/utils/hooks/useQueryExecutionSettings.ts +++ b/src/utils/hooks/useQueryExecutionSettings.ts @@ -2,8 +2,17 @@ import React from 'react'; import {useTracingLevelOptionAvailable} from '../../store/reducers/capabilities/hooks'; import type {QuerySettings} from '../../types/store/query'; -import {QUERY_EXECUTION_SETTINGS_KEY, USE_SHOW_PLAN_SVG_KEY} from '../constants'; -import {DEFAULT_QUERY_SETTINGS, STATISTICS_MODES, querySettingsRestoreSchema} from '../query'; +import { + ENABLE_QUERY_STREAMING, + QUERY_EXECUTION_SETTINGS_KEY, + USE_SHOW_PLAN_SVG_KEY, +} from '../constants'; +import { + DEFAULT_QUERY_SETTINGS, + QUERY_MODES, + STATISTICS_MODES, + querySettingsRestoreSchema, +} from '../query'; import {useSetting} from './useSetting'; @@ -13,6 +22,7 @@ export const useQueryExecutionSettings = () => { const validatedSettings = querySettingsRestoreSchema.parse(storageSettings); const [useShowPlanToSvg] = useSetting(USE_SHOW_PLAN_SVG_KEY); + const [enableQueryStreaming] = useSetting(ENABLE_QUERY_STREAMING); const setQueryExecutionSettings = React.useCallback( (settings: QuerySettings) => { @@ -28,6 +38,10 @@ export const useQueryExecutionSettings = () => { const settings: QuerySettings = { ...validatedSettings, + timeout: + enableQueryStreaming && validatedSettings.queryMode === QUERY_MODES.query + ? validatedSettings.timeout || null + : validatedSettings.timeout || undefined, statisticsMode: useShowPlanToSvg ? STATISTICS_MODES.full : validatedSettings.statisticsMode, tracingLevel: enableTracingLevel ? validatedSettings.tracingLevel diff --git a/src/utils/query.ts b/src/utils/query.ts index 692c975be..027c3687c 100644 --- a/src/utils/query.ts +++ b/src/utils/query.ts @@ -300,7 +300,7 @@ export const parseQueryErrorToString = (error: unknown) => { export const DEFAULT_QUERY_SETTINGS = { queryMode: QUERY_MODES.query, transactionMode: TRANSACTION_MODES.implicit, - timeout: 60, + timeout: null, limitRows: 10000, statisticsMode: STATISTICS_MODES.none, tracingLevel: TRACING_LEVELS.off, @@ -310,11 +310,15 @@ export const queryModeSchema = z.nativeEnum(QUERY_MODES); export const transactionModeSchema = z.nativeEnum(TRANSACTION_MODES); export const statisticsModeSchema = z.nativeEnum(STATISTICS_MODES); export const tracingLevelSchema = z.nativeEnum(TRACING_LEVELS); + +// timeout = null is for timeout switched off state export const querySettingsValidationSchema = z.object({ - timeout: z.preprocess( - (val) => (val === '' ? undefined : val), - z.coerce.number().positive().or(z.undefined()), - ), + timeout: z + .preprocess( + (val) => (val === '' ? undefined : val), + z.coerce.number().positive().or(z.undefined()).or(z.null()), + ) + .or(z.literal('')), limitRows: z.preprocess( (val) => (val === '' ? undefined : val), z.coerce.number().gt(0).lte(100_000).or(z.undefined()), @@ -329,7 +333,7 @@ export const querySettingsRestoreSchema = z .object({ timeout: z.preprocess( (val) => (val === '' ? undefined : val), - z.coerce.number().positive().optional().catch(DEFAULT_QUERY_SETTINGS.timeout), + z.coerce.number().positive().or(z.null()).optional(), ), limitRows: z.preprocess( (val) => (val === '' ? undefined : val), diff --git a/tests/suites/tenant/queryEditor/models/SettingsDialog.ts b/tests/suites/tenant/queryEditor/models/SettingsDialog.ts index 13f61d309..14b86f011 100644 --- a/tests/suites/tenant/queryEditor/models/SettingsDialog.ts +++ b/tests/suites/tenant/queryEditor/models/SettingsDialog.ts @@ -16,6 +16,11 @@ export class SettingsDialog { private limitRowsInput: Locator; private limitRowsErrorIcon: Locator; private limitRowsErrorPopover: Locator; + private timeoutInput: Locator; + private timeoutSwitch: Locator; + private timeoutSwitchHint: Locator; + private timeoutHintPopover: Locator; + private timeoutLabel: Locator; private queryModeSelect: Locator; private transactionModeSelect: Locator; @@ -32,6 +37,11 @@ export class SettingsDialog { ); this.limitRowsErrorPopover = this.page.locator('.g-popover__tooltip-content'); this.selectPopup = page.locator('.ydb-query-settings-select__popup'); + this.timeoutInput = this.dialog.locator('.ydb-query-settings-timeout__input'); + this.timeoutSwitch = this.dialog.locator('.ydb-timeout-label__switch'); + this.timeoutSwitchHint = this.dialog.locator('.ydb-timeout-label__question-icon'); + this.timeoutHintPopover = this.page.locator('.g-popover__tooltip-content'); + this.timeoutLabel = this.dialog.locator('.ydb-timeout-label__label-title'); // Define distinct locators for selects this.queryModeSelect = this.dialog.locator( @@ -125,6 +135,38 @@ export class SettingsDialog { return true; } + async isTimeoutInputVisible() { + return await this.timeoutInput.isVisible(); + } + + async clickTimeoutSwitch() { + await this.timeoutSwitch.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + await this.timeoutSwitch.click(); + await this.page.waitForTimeout(500); + } + + async isTimeoutSwitchChecked() { + return await this.timeoutSwitch.locator('input[type="checkbox"]').isChecked(); + } + + async isTimeoutSwitchDisabled() { + return await this.timeoutSwitch.locator('input[type="checkbox"][disabled]').isVisible(); + } + + async isTimeoutHintVisible() { + return await this.timeoutSwitchHint.isVisible(); + } + + async getTimeoutHintText() { + await this.timeoutSwitchHint.hover(); + await this.timeoutHintPopover.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); + return await this.timeoutHintPopover.textContent(); + } + + async isTimeoutLabelVisible() { + return await this.timeoutLabel.isVisible(); + } + async isStatisticsSelectDisabled() { await this.statisticsModeSelect.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); return this.statisticsModeSelect.locator('.g-select-control_disabled').isVisible(); diff --git a/tests/suites/tenant/queryEditor/querySettings.test.ts b/tests/suites/tenant/queryEditor/querySettings.test.ts index e95060230..660208d6e 100644 --- a/tests/suites/tenant/queryEditor/querySettings.test.ts +++ b/tests/suites/tenant/queryEditor/querySettings.test.ts @@ -2,6 +2,7 @@ import {expect, test} from '@playwright/test'; import {QUERY_MODES, TRANSACTION_MODES} from '../../../../src/utils/query'; import {tenantName} from '../../../utils/constants'; +import {toggleExperiment} from '../../../utils/toggleExperiment'; import {TenantPage, VISIBILITY_TIMEOUT} from '../TenantPage'; import {longRunningQuery} from '../constants'; @@ -152,4 +153,103 @@ test.describe('Test Query Settings', async () => { await expect(queryEditor.settingsDialog.isHidden()).resolves.toBe(true); }); + + test('Timeout input is invisible by default', async ({page}) => { + const queryEditor = new QueryEditor(page); + + // Open settings dialog + await queryEditor.clickGearButton(); + await expect(queryEditor.settingsDialog.isVisible()).resolves.toBe(true); + + // Check that timeout input is invisible + await expect(queryEditor.settingsDialog.isTimeoutInputVisible()).resolves.toBe(false); + + // Close dialog + await queryEditor.settingsDialog.clickButton(ButtonNames.Cancel); + await expect(queryEditor.settingsDialog.isHidden()).resolves.toBe(true); + }); + + test('Clicking timeout switch makes timeout input visible', async ({page}) => { + const queryEditor = new QueryEditor(page); + + // Open settings dialog + await queryEditor.clickGearButton(); + await expect(queryEditor.settingsDialog.isVisible()).resolves.toBe(true); + + // Initially timeout input should be invisible + await expect(queryEditor.settingsDialog.isTimeoutInputVisible()).resolves.toBe(false); + + // Click the timeout switch + await queryEditor.settingsDialog.clickTimeoutSwitch(); + + // Check that timeout input is now visible + await expect(queryEditor.settingsDialog.isTimeoutInputVisible()).resolves.toBe(true); + await expect(queryEditor.settingsDialog.isTimeoutSwitchChecked()).resolves.toBe(true); + + // Close dialog + await queryEditor.settingsDialog.clickButton(ButtonNames.Cancel); + await expect(queryEditor.settingsDialog.isHidden()).resolves.toBe(true); + }); + + test('Timeout switch is checked, disabled, and has hint when non-query mode is selected', async ({ + page, + }) => { + const queryEditor = new QueryEditor(page); + + // Open settings dialog + await queryEditor.clickGearButton(); + await expect(queryEditor.settingsDialog.isVisible()).resolves.toBe(true); + + // Initially timeout switch should be enabled and unchecked + await expect(queryEditor.settingsDialog.isTimeoutSwitchDisabled()).resolves.toBe(false); + await expect(queryEditor.settingsDialog.isTimeoutSwitchChecked()).resolves.toBe(false); + + // Change to a non-query mode + await queryEditor.settingsDialog.changeQueryMode(QUERY_MODES.scan); + + // Verify timeout switch is checked and disabled + await expect(queryEditor.settingsDialog.isTimeoutSwitchChecked()).resolves.toBe(true); + await expect(queryEditor.settingsDialog.isTimeoutSwitchDisabled()).resolves.toBe(true); + + // Verify hint is visible and has correct text + await expect(queryEditor.settingsDialog.isTimeoutHintVisible()).resolves.toBe(true); + + // Verify the hint text content + const hintText = await queryEditor.settingsDialog.getTimeoutHintText(); + expect(hintText).toBeTruthy(); // Should have some text content + + // Hover some other input to remove the hint + await queryEditor.settingsDialog.hoverStatisticsSelect(); + await page.waitForTimeout(500); + + // Close dialog + await queryEditor.settingsDialog.clickButton(ButtonNames.Cancel); + await expect(queryEditor.settingsDialog.isHidden()).resolves.toBe(true); + }); + + test('When Query Streaming is off, timeout has label and input is visible by default', async ({ + page, + }) => { + const queryEditor = new QueryEditor(page); + + // Turn off Query Streaming experiment + await toggleExperiment(page, 'off', 'Query Streaming'); + + // Open settings dialog + await queryEditor.clickGearButton(); + await expect(queryEditor.settingsDialog.isVisible()).resolves.toBe(true); + + // Verify there's a label instead of a switch + await expect(queryEditor.settingsDialog.isTimeoutLabelVisible()).resolves.toBe(true); + + // Verify timeout input is visible by default + await expect(queryEditor.settingsDialog.isTimeoutInputVisible()).resolves.toBe(true); + + // Close dialog + await queryEditor.settingsDialog.clickButton(ButtonNames.Cancel); + await expect(queryEditor.settingsDialog.isHidden()).resolves.toBe(true); + + // Restore Query Streaming experiment + await toggleExperiment(page, 'on', 'Query Streaming'); + }); });