diff --git a/src/components/Banner/Banner.tsx b/src/components/Banner/Banner.tsx new file mode 100644 index 000000000..c30945696 --- /dev/null +++ b/src/components/Banner/Banner.tsx @@ -0,0 +1,20 @@ +import {Alert} from '@gravity-ui/uikit'; + +interface BannerProps { + message?: React.ReactNode; + title?: string; + className?: string; + onClose?: () => void; +} + +export function Banner({message, title, className, onClose}: BannerProps) { + return ( + + ); +} diff --git a/src/components/QueryExecutionStatus/QueryExecutionStatus.scss b/src/components/QueryExecutionStatus/QueryExecutionStatus.scss index cc59d52a8..6acf9cb1f 100644 --- a/src/components/QueryExecutionStatus/QueryExecutionStatus.scss +++ b/src/components/QueryExecutionStatus/QueryExecutionStatus.scss @@ -10,4 +10,8 @@ color: var(--g-color-text-danger); } } + + &__query-settings-icon { + color: var(--g-color-text-hint); + } } diff --git a/src/components/QueryExecutionStatus/QueryExecutionStatus.tsx b/src/components/QueryExecutionStatus/QueryExecutionStatus.tsx index 359561159..673d26174 100644 --- a/src/components/QueryExecutionStatus/QueryExecutionStatus.tsx +++ b/src/components/QueryExecutionStatus/QueryExecutionStatus.tsx @@ -1,8 +1,15 @@ -import {CircleCheck, CircleQuestionFill, CircleXmark} from '@gravity-ui/icons'; -import {Icon} from '@gravity-ui/uikit'; +import React from 'react'; + +import {CircleCheck, CircleInfo, CircleQuestionFill, CircleXmark} from '@gravity-ui/icons'; +import {Icon, Tooltip} from '@gravity-ui/uikit'; import {isAxiosError} from 'axios'; +import getChangedQueryExecutionSettingsDescription from '../../containers/Tenant/Query/QueryEditorControls/utils/getChangedQueryExecutionSettingsDescription'; +import i18n from '../../containers/Tenant/Query/i18n'; +import {DEFAULT_QUERY_SETTINGS} from '../../lib'; import {cn} from '../../utils/cn'; +import {useIsQuerySettingsBannerHidden} from '../../utils/hooks/useIsQuerySettingsBannerHidden'; +import {useLastQueryExecutionSettings} from '../../utils/hooks/useLastQueryExecutionSettings'; import './QueryExecutionStatus.scss'; @@ -17,6 +24,17 @@ export const QueryExecutionStatus = ({className, error}: QueryExecutionStatusPro let icon: React.ReactNode; let label: string; + const isQuerySettingsDialogHidden = useIsQuerySettingsBannerHidden(); + const [querySettings] = useLastQueryExecutionSettings(); + const changedQuerySettingsDescription = React.useMemo(() => { + return querySettings + ? getChangedQueryExecutionSettingsDescription({ + currentSettings: querySettings, + defaultSettings: DEFAULT_QUERY_SETTINGS, + }) + : ''; + }, [querySettings]); + if (isAxiosError(error) && error.code === 'ECONNABORTED') { icon = ; label = 'Connection aborted'; @@ -35,6 +53,22 @@ export const QueryExecutionStatus = ({className, error}: QueryExecutionStatusPro
{icon} {label} + {isQuerySettingsDialogHidden && changedQuerySettingsDescription ? ( + + } + > + + + ) : null}
); }; diff --git a/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx b/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx index aafb6cc8f..4b6c01b34 100644 --- a/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx +++ b/src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx @@ -20,6 +20,7 @@ import {parseQueryError} from '../../../../utils/query'; import {PaneVisibilityToggleButtons} from '../../utils/paneVisibilityToggleHelpers'; import {ResultIssues} from '../Issues/Issues'; import {QueryDuration} from '../QueryDuration/QueryDuration'; +import {QuerySettingsBanner} from '../QuerySettingsBanner/QuerySettingsBanner'; import {getPreparedResult} from '../utils/getPreparedResult'; import './ExecuteResult.scss'; @@ -57,7 +58,6 @@ export function ExecuteResult({ }: ExecuteResultProps) { const [selectedResultSet, setSelectedResultSet] = React.useState(0); const [activeSection, setActiveSection] = React.useState(resultOptionsIds.result); - const isFullscreen = useTypedSelector((state) => state.fullscreen); const dispatch = useTypedDispatch(); @@ -203,7 +203,6 @@ export function ExecuteResult({
- {stats && !error && ( @@ -227,7 +226,7 @@ export function ExecuteResult({ />
- + {renderResultSection()} ); diff --git a/src/containers/Tenant/Query/ExplainResult/ExplainResult.js b/src/containers/Tenant/Query/ExplainResult/ExplainResult.js index f0fa4c8dc..f26363190 100644 --- a/src/containers/Tenant/Query/ExplainResult/ExplainResult.js +++ b/src/containers/Tenant/Query/ExplainResult/ExplainResult.js @@ -16,12 +16,12 @@ import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; import {S_EXPRESSION_LANGUAGE_ID} from '../../../../utils/monaco/constats'; import {parseQueryErrorToString} from '../../../../utils/query'; import {PaneVisibilityToggleButtons} from '../../utils/paneVisibilityToggleHelpers'; +import {QuerySettingsBanner} from '../QuerySettingsBanner/QuerySettingsBanner'; import {renderExplainNode} from './utils'; import './ExplainResult.scss'; import 'react-json-inspector/json-inspector.css'; - const b = cn('ydb-query-explain-result'); const EDITOR_OPTIONS = { @@ -77,7 +77,6 @@ function GraphRoot(props) { export function ExplainResult(props) { const dispatch = useTypedDispatch(); const [activeOption, setActiveOption] = React.useState(ExplainOptionIds.schema); - const isFullscreen = useTypedSelector((state) => state.fullscreen); React.useEffect(() => { @@ -258,6 +257,7 @@ export function ExplainResult(props) { )} +
{renderContent()}
); diff --git a/src/containers/Tenant/Query/ExplainResult/ExplainResult.scss b/src/containers/Tenant/Query/ExplainResult/ExplainResult.scss index de19d652b..c18d2ddbd 100644 --- a/src/containers/Tenant/Query/ExplainResult/ExplainResult.scss +++ b/src/containers/Tenant/Query/ExplainResult/ExplainResult.scss @@ -10,6 +10,7 @@ &__text-message { padding: 15px 20px; } + &__controls { position: sticky; z-index: 2; diff --git a/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx b/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx index e3a840e76..295dbf010 100644 --- a/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx +++ b/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx @@ -7,7 +7,11 @@ import {TENANT_QUERY_TABS_ID} from '../../../../store/reducers/tenant/constants' import {setQueryTab} from '../../../../store/reducers/tenant/tenant'; import type {QueryInHistory} from '../../../../types/store/executeQuery'; import {cn} from '../../../../utils/cn'; -import {useQueryModes, useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; +import { + useQueryExecutionSettings, + useTypedDispatch, + useTypedSelector, +} from '../../../../utils/hooks'; import {QUERY_MODES, QUERY_SYNTAX} from '../../../../utils/query'; import {MAX_QUERY_HEIGHT, QUERY_TABLE_SETTINGS} from '../../utils/constants'; import i18n from '../i18n'; @@ -25,17 +29,17 @@ interface QueriesHistoryProps { function QueriesHistory({changeUserInput}: QueriesHistoryProps) { const dispatch = useTypedDispatch(); - const [queryMode, setQueryMode] = useQueryModes(); + const [settings, setQuerySettings] = useQueryExecutionSettings(); const queriesHistory = useTypedSelector(selectQueriesHistory); const reversedHistory = [...queriesHistory].reverse(); const onQueryClick = (query: QueryInHistory) => { - if (query.syntax === QUERY_SYNTAX.pg && queryMode !== QUERY_MODES.pg) { - setQueryMode(QUERY_MODES.pg); - } else if (query.syntax !== QUERY_SYNTAX.pg && queryMode === QUERY_MODES.pg) { + if (query.syntax === QUERY_SYNTAX.pg && settings.queryMode !== QUERY_MODES.pg) { + setQuerySettings({...settings, queryMode: QUERY_MODES.pg}); + } else if (query.syntax !== QUERY_SYNTAX.pg && settings.queryMode === QUERY_MODES.pg) { // Set query mode for queries with yql syntax - setQueryMode(QUERY_MODES.script); + setQuerySettings({...settings, queryMode: QUERY_MODES.script}); } changeUserInput({input: query.queryText}); diff --git a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx index 37236f455..126f6f363 100644 --- a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx +++ b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx @@ -21,7 +21,7 @@ import {setShowPreview} from '../../../../store/reducers/schema/schema'; import type {EPathType} from '../../../../types/api/schema'; import type {ValueOf} from '../../../../types/common'; import type {ExecuteQueryState} from '../../../../types/store/executeQuery'; -import type {IQueryResult, QueryAction, QueryMode} from '../../../../types/store/query'; +import type {IQueryResult, QueryAction, QuerySettings} from '../../../../types/store/query'; import {cn} from '../../../../utils/cn'; import { DEFAULT_IS_QUERY_RESULT_COLLAPSED, @@ -29,7 +29,9 @@ import { LAST_USED_QUERY_ACTION_KEY, QUERY_USE_MULTI_SCHEMA_KEY, } from '../../../../utils/constants'; -import {useQueryModes, useSetting} from '../../../../utils/hooks'; +import {useQueryExecutionSettings, useSetting} from '../../../../utils/hooks'; +import {useLastQueryExecutionSettings} from '../../../../utils/hooks/useLastQueryExecutionSettings'; +import {useQuerySettingsBannerLastClosed} from '../../../../utils/hooks/useQuerySettingsBannerLastClosed'; import {YQL_LANGUAGE_ID} from '../../../../utils/monaco/constats'; import {QUERY_ACTIONS} from '../../../../utils/query'; import type {InitialPaneState} from '../../utils/paneVisibilityToggleHelpers'; @@ -41,6 +43,7 @@ import {ExecuteResult} from '../ExecuteResult/ExecuteResult'; import {ExplainResult} from '../ExplainResult/ExplainResult'; import {Preview} from '../Preview/Preview'; import {QueryEditorControls} from '../QueryEditorControls/QueryEditorControls'; +import getChangedQueryExecutionSettings from '../QueryEditorControls/utils/getChangedQueryExecutionSettings'; import {QuerySettingsDialog} from '../QuerySettingsDialog/QuerySettingsDialog'; import {SaveQueryDialog} from '../SaveQuery/SaveQuery'; import i18n from '../i18n'; @@ -102,7 +105,11 @@ function QueryEditor(props: QueryEditorProps) { const [resultType, setResultType] = React.useState(RESULT_TYPES.EXECUTE); const [isResultLoaded, setIsResultLoaded] = React.useState(false); - const [queryMode, setQueryMode] = useQueryModes(); + const [querySettings, setQuerySettings] = useQueryExecutionSettings(); + const [lastQueryExecutionSettings, setLastQueryExecutionSettings] = + useLastQueryExecutionSettings(); + const [_, setQuerySettingsBannerLastClosed] = useQuerySettingsBannerLastClosed(); + const [useMultiSchema] = useSetting(QUERY_USE_MULTI_SCHEMA_KEY); const [lastUsedQueryAction, setLastUsedQueryAction] = useSetting( LAST_USED_QUERY_ACTION_KEY, @@ -182,10 +189,7 @@ function QueryEditor(props: QueryEditorProps) { }; }, [executeQuery]); - const handleSendExecuteClick = (mode: QueryMode | undefined, text?: string) => { - if (!mode) { - return; - } + const handleSendExecuteClick = (settings: QuerySettings, text?: string) => { const {input, history} = executeQuery; const schema = useMultiSchema ? 'multi' : 'modern'; @@ -193,11 +197,23 @@ function QueryEditor(props: QueryEditorProps) { const query = text ?? input; setLastUsedQueryAction(QUERY_ACTIONS.execute); + + if (lastQueryExecutionSettings) { + const areSettingsChanged = + getChangedQueryExecutionSettings(querySettings, lastQueryExecutionSettings).length > + 0; + + if (areSettingsChanged) { + setQuerySettingsBannerLastClosed(undefined); + } + } + setLastQueryExecutionSettings(settings); + setResultType(RESULT_TYPES.EXECUTE); sendExecuteQuery({ query, database: tenantName, - mode, + querySettings: settings, schema, }); setIsResultLoaded(true); @@ -207,7 +223,7 @@ function QueryEditor(props: QueryEditorProps) { if (!text) { const {queries, currentIndex} = history; if (query !== queries[currentIndex]?.queryText) { - props.saveQueryToHistory(input, mode); + props.saveQueryToHistory(input, querySettings.queryMode); } } dispatchResultVisibilityState(PaneVisibilityActionTypes.triggerExpand); @@ -218,15 +234,27 @@ function QueryEditor(props: QueryEditorProps) { props.setShowPreview(false); }; - const handleGetExplainQueryClick = (mode: QueryMode | undefined) => { + const handleGetExplainQueryClick = (settings: QuerySettings) => { const {input} = executeQuery; setLastUsedQueryAction(QUERY_ACTIONS.explain); + + if (lastQueryExecutionSettings) { + const areSettingsChanged = + getChangedQueryExecutionSettings(querySettings, lastQueryExecutionSettings).length > + 0; + + if (areSettingsChanged) { + setQuerySettingsBannerLastClosed(undefined); + } + } + setLastQueryExecutionSettings(settings); + setResultType(RESULT_TYPES.EXPLAIN); sendExplainQuery({ query: input, database: tenantName, - mode: mode, + querySettings: settings, }); setIsResultLoaded(true); props.setShowPreview(false); @@ -241,9 +269,9 @@ function QueryEditor(props: QueryEditorProps) { switch (monacoHotKey) { case MONACO_HOT_KEY_ACTIONS.sendQuery: { if (lastUsedQueryAction === QUERY_ACTIONS.explain) { - handleGetExplainQueryClick(queryMode); + handleGetExplainQueryClick(querySettings); } else { - handleSendExecuteClick(queryMode); + handleSendExecuteClick(querySettings); } break; } @@ -257,7 +285,7 @@ function QueryEditor(props: QueryEditorProps) { endLineNumber: selection.getPosition().lineNumber, endColumn: selection.getPosition().column, }); - handleSendExecuteClick(queryMode, text); + handleSendExecuteClick(querySettings, text); } break; } @@ -356,8 +384,8 @@ function QueryEditor(props: QueryEditorProps) { onExplainButtonClick={handleGetExplainQueryClick} explainIsLoading={explainQueryResult.isLoading} disabled={!executeQuery.input} - onUpdateQueryMode={setQueryMode} - queryMode={queryMode} + onUpdateQueryMode={(queryMode) => setQuerySettings({...querySettings, queryMode})} + querySettings={querySettings} highlightedAction={lastUsedQueryAction} /> ); diff --git a/src/containers/Tenant/Query/QueryEditorControls/QueryEditorControls.tsx b/src/containers/Tenant/Query/QueryEditorControls/QueryEditorControls.tsx index e6295db7d..6521f2ba1 100644 --- a/src/containers/Tenant/Query/QueryEditorControls/QueryEditorControls.tsx +++ b/src/containers/Tenant/Query/QueryEditorControls/QueryEditorControls.tsx @@ -2,16 +2,20 @@ import React from 'react'; import {ChevronDown, Gear, PlayFill} from '@gravity-ui/icons'; import type {ButtonView} from '@gravity-ui/uikit'; -import {Button, DropdownMenu, Icon} from '@gravity-ui/uikit'; +import {Button, DropdownMenu, Icon, Tooltip} from '@gravity-ui/uikit'; import {LabelWithPopover} from '../../../../components/LabelWithPopover'; -import {QUERY_SETTINGS, useSetting} from '../../../../lib'; -import type {QueryAction, QueryMode} from '../../../../types/store/query'; +import {DEFAULT_QUERY_SETTINGS, QUERY_SETTINGS, useSetting} from '../../../../lib'; +import type {QueryAction, QueryMode, QuerySettings} from '../../../../types/store/query'; import {cn} from '../../../../utils/cn'; +import {useQueryExecutionSettings} from '../../../../utils/hooks'; import {QUERY_MODES, QUERY_MODES_TITLES} from '../../../../utils/query'; import {SaveQuery} from '../SaveQuery/SaveQuery'; import i18n from '../i18n'; +import getChangedQueryExecutionSettings from './utils/getChangedQueryExecutionSettings'; +import getChangedQueryExecutionSettingsDescription from './utils/getChangedQueryExecutionSettingsDescription'; + import './QueryEditorControls.scss'; const queryModeSelectorQa = 'query-mode-selector'; @@ -43,14 +47,14 @@ const QueryModeSelectorOptions = { } as const; interface QueryEditorControlsProps { - onRunButtonClick: (mode?: QueryMode) => void; + onRunButtonClick: (querySettings: QuerySettings) => void; onSettingsButtonClick: () => void; runIsLoading: boolean; - onExplainButtonClick: (mode?: QueryMode) => void; + onExplainButtonClick: (querySettings: QuerySettings) => void; explainIsLoading: boolean; disabled: boolean; onUpdateQueryMode: (mode: QueryMode) => void; - queryMode: QueryMode; + querySettings: QuerySettings; highlightedAction: QueryAction; } @@ -62,10 +66,16 @@ export const QueryEditorControls = ({ onExplainButtonClick, explainIsLoading, disabled, - queryMode, + querySettings, highlightedAction, }: QueryEditorControlsProps) => { const [useQuerySettings] = useSetting(QUERY_SETTINGS); + const [queryExecutionSettings] = useQueryExecutionSettings(); + const changedQuerySettings = React.useMemo( + () => getChangedQueryExecutionSettings(queryExecutionSettings, DEFAULT_QUERY_SETTINGS), + [queryExecutionSettings], + ); + const runView: ButtonView | undefined = highlightedAction === 'execute' ? 'action' : undefined; const explainView: ButtonView | undefined = highlightedAction === 'explain' ? 'action' : undefined; @@ -88,12 +98,20 @@ export const QueryEditorControls = ({ }); }, [onUpdateQueryMode]); + const extraGearProps = + changedQuerySettings.length > 0 + ? ({ + view: 'outlined-info', + selected: true, + } as const) + : {}; + return (
{useQuerySettings ? ( - + + ) : (
{`${i18n('controls.query-mode-selector_type')} ${ - QueryModeSelectorOptions[queryMode].title + QueryModeSelectorOptions[querySettings.queryMode].title }`} diff --git a/src/containers/Tenant/Query/QueryEditorControls/utils/getChangedQueryExecutionSettings.test.ts b/src/containers/Tenant/Query/QueryEditorControls/utils/getChangedQueryExecutionSettings.test.ts new file mode 100644 index 000000000..38f2d9994 --- /dev/null +++ b/src/containers/Tenant/Query/QueryEditorControls/utils/getChangedQueryExecutionSettings.test.ts @@ -0,0 +1,53 @@ +import type {QuerySettings} from '../../../../../types/store/query'; +import { + ISOLATION_LEVELS, + QUERY_MODES, + STATISTICS_MODES, + TRACING_LEVELS, +} from '../../../../../utils/query'; + +const DEFAULT_QUERY_SETTINGS: QuerySettings = { + queryMode: QUERY_MODES.script, + isolationLevel: ISOLATION_LEVELS.serializable, + timeout: '60', + statisticsMode: STATISTICS_MODES.none, + tracingLevel: TRACING_LEVELS.detailed, +}; + +import getChangedQueryExecutionSettings from './getChangedQueryExecutionSettings'; + +describe('getChangedQueryExecutionSettings', () => { + it('should return an empty array if no settings have changed', () => { + const currentSettings: QuerySettings = {...DEFAULT_QUERY_SETTINGS}; + const result = getChangedQueryExecutionSettings(currentSettings, DEFAULT_QUERY_SETTINGS); + expect(result).toEqual([]); + }); + + it('should return the keys of settings that have changed', () => { + const currentSettings: QuerySettings = { + ...DEFAULT_QUERY_SETTINGS, + queryMode: QUERY_MODES.data, + timeout: '30', + }; + const result = getChangedQueryExecutionSettings(currentSettings, DEFAULT_QUERY_SETTINGS); + expect(result).toEqual(['queryMode', 'timeout']); + }); + + it('should return all keys if all settings have changed', () => { + const currentSettings: QuerySettings = { + queryMode: QUERY_MODES.data, + isolationLevel: ISOLATION_LEVELS.onlinero, + timeout: '90', + statisticsMode: STATISTICS_MODES.basic, + tracingLevel: TRACING_LEVELS.basic, + }; + const result = getChangedQueryExecutionSettings(currentSettings, DEFAULT_QUERY_SETTINGS); + expect(result).toEqual([ + 'queryMode', + 'isolationLevel', + 'timeout', + 'statisticsMode', + 'tracingLevel', + ]); + }); +}); diff --git a/src/containers/Tenant/Query/QueryEditorControls/utils/getChangedQueryExecutionSettings.ts b/src/containers/Tenant/Query/QueryEditorControls/utils/getChangedQueryExecutionSettings.ts new file mode 100644 index 000000000..541deab86 --- /dev/null +++ b/src/containers/Tenant/Query/QueryEditorControls/utils/getChangedQueryExecutionSettings.ts @@ -0,0 +1,10 @@ +import type {QuerySettings} from '../../../../../types/store/query'; + +export default function getChangedQueryExecutionSettings( + currentSettings: QuerySettings, + defaultSettings: QuerySettings, +) { + return (Object.keys(currentSettings) as (keyof QuerySettings)[]).filter((key) => { + return currentSettings[key] !== defaultSettings[key]; + }); +} diff --git a/src/containers/Tenant/Query/QueryEditorControls/utils/getChangedQueryExecutionSettingsDescription.test.ts b/src/containers/Tenant/Query/QueryEditorControls/utils/getChangedQueryExecutionSettingsDescription.test.ts new file mode 100644 index 000000000..67c9a5913 --- /dev/null +++ b/src/containers/Tenant/Query/QueryEditorControls/utils/getChangedQueryExecutionSettingsDescription.test.ts @@ -0,0 +1,80 @@ +import type {QuerySettings} from '../../../../../types/store/query'; +import { + ISOLATION_LEVELS, + ISOLATION_LEVELS_TITLES, + QUERY_MODES, + QUERY_MODES_TITLES, + STATISTICS_MODES, + STATISTICS_MODES_TITLES, + TRACING_LEVELS, + TRACING_LEVELS_TITLES, +} from '../../../../../utils/query'; +import {QUERY_SETTINGS_FIELD_SETTINGS} from '../../QuerySettingsDialog/constants'; + +import getChangedQueryExecutionSettingsDescription from './getChangedQueryExecutionSettingsDescription'; + +const DEFAULT_QUERY_SETTINGS: QuerySettings = { + queryMode: QUERY_MODES.script, + isolationLevel: ISOLATION_LEVELS.serializable, + timeout: '60', + statisticsMode: STATISTICS_MODES.none, + tracingLevel: TRACING_LEVELS.detailed, +}; + +describe('getChangedQueryExecutionSettingsDescription', () => { + it('should return an empty string if no settings changed', () => { + const currentSettings: QuerySettings = DEFAULT_QUERY_SETTINGS; + + const result = getChangedQueryExecutionSettingsDescription({ + currentSettings, + defaultSettings: DEFAULT_QUERY_SETTINGS, + }); + expect(result).toBe(''); + }); + + it('should return the description for changed settings', () => { + const currentSettings: QuerySettings = { + queryMode: QUERY_MODES.pg, + isolationLevel: ISOLATION_LEVELS.serializable, + timeout: '63', + statisticsMode: STATISTICS_MODES.none, + tracingLevel: TRACING_LEVELS.detailed, + }; + + const result = getChangedQueryExecutionSettingsDescription({ + currentSettings, + defaultSettings: DEFAULT_QUERY_SETTINGS, + }); + expect(result).toBe( + `${QUERY_SETTINGS_FIELD_SETTINGS.queryMode.title}: ${QUERY_MODES_TITLES[QUERY_MODES.pg]}, ${QUERY_SETTINGS_FIELD_SETTINGS.timeout.title}: 63`, + ); + }); + + it('should return the correct description for all changed settings', () => { + const currentSettings: QuerySettings = { + queryMode: QUERY_MODES.data, + isolationLevel: ISOLATION_LEVELS.snapshot, + timeout: '120', + statisticsMode: STATISTICS_MODES.profile, + tracingLevel: TRACING_LEVELS.diagnostic, + }; + + const result = getChangedQueryExecutionSettingsDescription({ + currentSettings, + defaultSettings: DEFAULT_QUERY_SETTINGS, + }); + expect(result).toBe( + `${ + QUERY_SETTINGS_FIELD_SETTINGS.queryMode.title + }: ${QUERY_MODES_TITLES[QUERY_MODES.data]}, ${ + QUERY_SETTINGS_FIELD_SETTINGS.isolationLevel.title + }: ${ISOLATION_LEVELS_TITLES[ISOLATION_LEVELS.snapshot]}, ${ + QUERY_SETTINGS_FIELD_SETTINGS.timeout.title + }: 120, ${ + QUERY_SETTINGS_FIELD_SETTINGS.statisticsMode.title + }: ${STATISTICS_MODES_TITLES[STATISTICS_MODES.profile]}, ${ + QUERY_SETTINGS_FIELD_SETTINGS.tracingLevel.title + }: ${TRACING_LEVELS_TITLES[TRACING_LEVELS.diagnostic]}`, + ); + }); +}); diff --git a/src/containers/Tenant/Query/QueryEditorControls/utils/getChangedQueryExecutionSettingsDescription.ts b/src/containers/Tenant/Query/QueryEditorControls/utils/getChangedQueryExecutionSettingsDescription.ts new file mode 100644 index 000000000..4c8f77012 --- /dev/null +++ b/src/containers/Tenant/Query/QueryEditorControls/utils/getChangedQueryExecutionSettingsDescription.ts @@ -0,0 +1,30 @@ +import type {QuerySettings} from '../../../../../types/store/query'; +import {QUERY_SETTINGS_FIELD_SETTINGS} from '../../QuerySettingsDialog/constants'; + +import getChangedQueryExecutionSettings from './getChangedQueryExecutionSettings'; + +export default function getChangedQueryExecutionSettingsDescription({ + currentSettings, + defaultSettings, +}: { + currentSettings: QuerySettings; + defaultSettings: QuerySettings; +}): string { + const keys = getChangedQueryExecutionSettings(currentSettings, defaultSettings); + return keys + .map((key) => { + const settings = QUERY_SETTINGS_FIELD_SETTINGS[key]; + const currentValue = currentSettings[key]; + + if ('options' in settings) { + const content = settings.options.find( + (option) => option.value === currentValue, + )?.content; + + return `${settings.title}: ${content}`; + } + + return `${settings.title}: ${currentValue}`; + }) + .join(', '); +} diff --git a/src/containers/Tenant/Query/QuerySettingsBanner/QuerySettingsBanner.scss b/src/containers/Tenant/Query/QuerySettingsBanner/QuerySettingsBanner.scss new file mode 100644 index 000000000..a68b86c90 --- /dev/null +++ b/src/containers/Tenant/Query/QuerySettingsBanner/QuerySettingsBanner.scss @@ -0,0 +1,3 @@ +.ydb-query-settings-banner { + margin: var(--g-spacing-1) var(--g-spacing-5); +} diff --git a/src/containers/Tenant/Query/QuerySettingsBanner/QuerySettingsBanner.tsx b/src/containers/Tenant/Query/QuerySettingsBanner/QuerySettingsBanner.tsx new file mode 100644 index 000000000..91cc3e401 --- /dev/null +++ b/src/containers/Tenant/Query/QuerySettingsBanner/QuerySettingsBanner.tsx @@ -0,0 +1,50 @@ +import React from 'react'; + +import {Banner} from '../../../../components/Banner/Banner'; +import {DEFAULT_QUERY_SETTINGS, WEEK_IN_SECONDS} from '../../../../lib'; +import {cn} from '../../../../utils/cn'; +import {useLastQueryExecutionSettings} from '../../../../utils/hooks/useLastQueryExecutionSettings'; +import {useQuerySettingsBannerLastClosed} from '../../../../utils/hooks/useQuerySettingsBannerLastClosed'; +import getChangedQueryExecutionSettingsDescription from '../QueryEditorControls/utils/getChangedQueryExecutionSettingsDescription'; +import i18n from '../i18n'; +const b = cn('ydb-query-settings-banner'); + +import './QuerySettingsBanner.scss'; + +export function QuerySettingsBanner() { + const [lastQueryExecutionSettings] = useLastQueryExecutionSettings(); + const [querySettingsBannerLastClosed, setQuerySettingsBannerLastClosed] = + useQuerySettingsBannerLastClosed(); + + const changedQuerySettingsDescription = React.useMemo(() => { + return lastQueryExecutionSettings + ? getChangedQueryExecutionSettingsDescription({ + currentSettings: lastQueryExecutionSettings, + defaultSettings: DEFAULT_QUERY_SETTINGS, + }) + : undefined; + }, [lastQueryExecutionSettings]); + + const isShownLately = React.useMemo(() => { + return ( + querySettingsBannerLastClosed && + new Date().getTime() - querySettingsBannerLastClosed < WEEK_IN_SECONDS + ); + }, [querySettingsBannerLastClosed]); + + return changedQuerySettingsDescription && !isShownLately ? ( + setQuerySettingsBannerLastClosed(Date.now())} + message={ +
+ } + /> + ) : null; +} diff --git a/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx b/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx index 1bf556605..701d30a3d 100644 --- a/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx +++ b/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx @@ -7,65 +7,43 @@ import { selectQueryAction, setQueryAction, } from '../../../../store/reducers/queryActions/queryActions'; -import type { - IsolationLevel, - QueryMode, - StatisticsMode, - TracingLevel, -} from '../../../../types/store/query'; +import type {QuerySettings} from '../../../../types/store/query'; import {cn} from '../../../../utils/cn'; -import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; import { - ISOLATION_LEVELS, - QUERY_MODES, - STATISTICS_MODES, - TRACING_LEVELS, -} from '../../../../utils/query'; + useQueryExecutionSettings, + useTypedDispatch, + useTypedSelector, +} from '../../../../utils/hooks'; import {QuerySettingsSelect} from './QuerySettingsSelect'; -import { - ISOLATION_LEVEL_SELECT_OPTIONS, - QUERY_MODE_SELECT_OPTIONS, - STATISTICS_MODE_SELECT_OPTIONS, - TRACING_LEVEL_SELECT_OPTIONS, -} from './constants'; +import {QUERY_SETTINGS_FIELD_SETTINGS} from './constants'; import i18n from './i18n'; import './QuerySettingsDialog.scss'; const b = cn('ydb-query-settings-dialog'); -type FormValues = { - queryMode: QueryMode; - timeout: string; - isolationLevel: IsolationLevel; - statisticsMode: StatisticsMode; - tracingLevel: TracingLevel; -}; - export function QuerySettingsDialog() { const dispatch = useTypedDispatch(); const queryAction = useTypedSelector(selectQueryAction); - const {control, handleSubmit, reset} = useForm({ - defaultValues: { - queryMode: QUERY_MODES.script, - timeout: '60', - isolationLevel: ISOLATION_LEVELS.serializable, - statisticsMode: STATISTICS_MODES.none, - tracingLevel: TRACING_LEVELS.detailed, - }, + const [querySettings, setQuerySettings] = useQueryExecutionSettings(); + const {control, handleSubmit, reset} = useForm({ + defaultValues: querySettings, }); - const onCloseDialog = () => { + const onCloseDialog = React.useCallback(() => { dispatch(setQueryAction('idle')); reset(); - }; + }, [dispatch, reset]); - const onSaveClick = (data: FormValues) => { - console.log('Form Data:', data); - // dispatch(saveQuerySettings(data)); - onCloseDialog(); - }; + const onSaveClick = React.useCallback( + (data: QuerySettings) => { + setQuerySettings(data); + reset(data); + onCloseDialog(); + }, + [onCloseDialog, reset, setQuerySettings], + ); return (
)} /> @@ -98,7 +78,7 @@ export function QuerySettingsDialog() {
)} /> @@ -140,7 +122,7 @@ export function QuerySettingsDialog() {
)} /> @@ -158,7 +142,7 @@ export function QuerySettingsDialog() {
)} /> diff --git a/src/containers/Tenant/Query/QuerySettingsDialog/constants.ts b/src/containers/Tenant/Query/QuerySettingsDialog/constants.ts index ab9d89f33..296ed494f 100644 --- a/src/containers/Tenant/Query/QuerySettingsDialog/constants.ts +++ b/src/containers/Tenant/Query/QuerySettingsDialog/constants.ts @@ -10,6 +10,8 @@ import { } from '../../../../utils/query'; import i18n from '../i18n'; +import formI18n from './i18n'; + export const ISOLATION_LEVEL_SELECT_OPTIONS = [ { value: ISOLATION_LEVELS.serializable, @@ -88,6 +90,16 @@ export const STATISTICS_MODE_SELECT_OPTIONS = [ ]; export const TRACING_LEVEL_SELECT_OPTIONS = [ + { + value: TRACING_LEVELS.off, + content: TRACING_LEVELS_TITLES[TRACING_LEVELS.off], + text: i18n('tracing-level-description.off'), + }, + { + value: TRACING_LEVELS.toplevel, + content: TRACING_LEVELS_TITLES[TRACING_LEVELS.toplevel], + text: i18n('tracing-level-description.toplevel'), + }, { value: TRACING_LEVELS.basic, content: TRACING_LEVELS_TITLES[TRACING_LEVELS.basic], @@ -104,19 +116,31 @@ export const TRACING_LEVEL_SELECT_OPTIONS = [ content: TRACING_LEVELS_TITLES[TRACING_LEVELS.diagnostic], text: i18n('tracing-level-description.diagnostic'), }, - { - value: TRACING_LEVELS.off, - content: TRACING_LEVELS_TITLES[TRACING_LEVELS.off], - text: i18n('tracing-level-description.off'), - }, - { - value: TRACING_LEVELS.toplevel, - content: TRACING_LEVELS_TITLES[TRACING_LEVELS.toplevel], - text: i18n('tracing-level-description.toplevel'), - }, { value: TRACING_LEVELS.trace, content: TRACING_LEVELS_TITLES[TRACING_LEVELS.trace], text: i18n('tracing-level-description.trace'), }, ]; + +export const QUERY_SETTINGS_FIELD_SETTINGS = { + isolationLevel: { + title: formI18n('form.isolation-level'), + options: ISOLATION_LEVEL_SELECT_OPTIONS, + }, + queryMode: { + title: formI18n('form.query-mode'), + options: QUERY_MODE_SELECT_OPTIONS, + }, + statisticsMode: { + title: formI18n('form.statistics-mode'), + options: STATISTICS_MODE_SELECT_OPTIONS, + }, + tracingLevel: { + title: formI18n('form.tracing-level'), + options: TRACING_LEVEL_SELECT_OPTIONS, + }, + timeout: { + title: formI18n('form.timeout'), + }, +} as const; diff --git a/src/containers/Tenant/Query/i18n/en.json b/src/containers/Tenant/Query/i18n/en.json index 33ccc7d99..604c42b50 100644 --- a/src/containers/Tenant/Query/i18n/en.json +++ b/src/containers/Tenant/Query/i18n/en.json @@ -46,5 +46,8 @@ "action.send-selected-query": "Send selected query", "action.previous-query": "Previous query in history", "action.next-query": "Next query in history", - "action.save-query": "Save query" + "action.save-query": "Save query", + + "gear.tooltip": "Query execution settings have been changed for {{changesText}}", + "banner.query-settings.message": "Query results are displayed for {{message}}" } diff --git a/src/containers/Tenant/Schema/SchemaTree/SchemaTree.tsx b/src/containers/Tenant/Schema/SchemaTree/SchemaTree.tsx index bc0c20655..9cb2c12d5 100644 --- a/src/containers/Tenant/Schema/SchemaTree/SchemaTree.tsx +++ b/src/containers/Tenant/Schema/SchemaTree/SchemaTree.tsx @@ -8,7 +8,7 @@ import {NavigationTree} from 'ydb-ui-components'; import {USE_DIRECTORY_OPERATIONS} from '../../../../lib'; import {schemaApi} from '../../../../store/reducers/schema/schema'; import type {EPathType, TEvDescribeSchemeResult} from '../../../../types/api/schema'; -import {useQueryModes, useSetting, useTypedDispatch} from '../../../../utils/hooks'; +import {useQueryExecutionSettings, useSetting, useTypedDispatch} from '../../../../utils/hooks'; import {isChildlessPathType, mapPathTypeToNavigationTreeType} from '../../utils/schema'; import {getActions} from '../../utils/schemaActions'; import {getControls} from '../../utils/schemaControls'; @@ -27,7 +27,7 @@ export function SchemaTree(props: SchemaTreeProps) { const {rootPath, rootName, rootType, currentPath, onActivePathUpdate} = props; const dispatch = useTypedDispatch(); - const [_, setQueryMode] = useQueryModes(); + const [_, setQueryExecutionSettings] = useQueryExecutionSettings(); const [createDirectoryOpen, setCreateDirectoryOpen] = React.useState(false); const [parentPath, setParentPath] = React.useState(''); const [schemaTreeKey, setSchemaTreeKey] = React.useState(''); @@ -112,7 +112,7 @@ export function SchemaTree(props: SchemaTreeProps) { fetchPath={fetchPath} getActions={getActions(dispatch, { setActivePath: onActivePathUpdate, - setQueryMode, + setQueryExecutionSettings, showCreateDirectoryDialog: useDirectoryActions ? handleOpenCreateDirectoryDialog : undefined, diff --git a/src/containers/Tenant/utils/schemaActions.ts b/src/containers/Tenant/utils/schemaActions.ts index 35ccddb53..aeee7f192 100644 --- a/src/containers/Tenant/utils/schemaActions.ts +++ b/src/containers/Tenant/utils/schemaActions.ts @@ -1,10 +1,11 @@ import copy from 'copy-to-clipboard'; import type {NavigationTreeNodeType, NavigationTreeProps} from 'ydb-ui-components'; +import {DEFAULT_QUERY_SETTINGS} from '../../../lib'; import {changeUserInput} from '../../../store/reducers/executeQuery'; import {TENANT_PAGES_IDS, TENANT_QUERY_TABS_ID} from '../../../store/reducers/tenant/constants'; import {setQueryTab, setTenantPage} from '../../../store/reducers/tenant/tenant'; -import type {QueryMode} from '../../../types/store/query'; +import type {QueryMode, QuerySettings} from '../../../types/store/query'; import createToast from '../../../utils/createToast'; import i18n from '../i18n'; @@ -27,7 +28,7 @@ import { } from './queryTemplates'; interface ActionsAdditionalEffects { - setQueryMode: (mode: QueryMode) => void; + setQueryExecutionSettings: (settings: QuerySettings) => void; setActivePath: (path: string) => void; showCreateDirectoryDialog?: (path: string) => void; } @@ -37,11 +38,11 @@ const bindActions = ( dispatch: React.Dispatch, additionalEffects: ActionsAdditionalEffects, ) => { - const {setActivePath, setQueryMode, showCreateDirectoryDialog} = additionalEffects; + const {setActivePath, setQueryExecutionSettings, showCreateDirectoryDialog} = additionalEffects; const inputQuery = (tmpl: (path: string) => string, mode?: QueryMode) => () => { if (mode) { - setQueryMode(mode); + setQueryExecutionSettings({...DEFAULT_QUERY_SETTINGS, queryMode: mode}); } dispatch(changeUserInput({input: tmpl(path)})); diff --git a/src/services/api.ts b/src/services/api.ts index 51e77eb68..b25fda222 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -25,6 +25,10 @@ import type { ExplainResponse, QueryAPIResponse, Schemas, + Stats, + Timeout, + TracingLevel, + TransactionMode, } from '../types/api/query'; import type {JsonRenderRequestParams, JsonRenderResponse} from '../types/api/render'; import type {RestartPDiskResponse} from '../types/api/restartPDisk'; @@ -438,9 +442,12 @@ export class YdbEmbeddedAPI extends AxiosWrapper { query?: string; database?: string; action?: Action; - stats?: string; schema?: Schema; syntax?: QuerySyntax; + stats?: Stats; + tracingLevel?: TracingLevel; + transaction_mode?: TransactionMode; + timeout?: Timeout; }, {concurrentId, signal}: AxiosOptions = {}, ) { @@ -469,25 +476,45 @@ export class YdbEmbeddedAPI extends AxiosWrapper { concurrentId, timeout: uiTimeout, requestConfig: {signal}, - }, - ); - } - getExplainQuery( - query: string, - database: string, - action: Action, - syntax?: QuerySyntax, - ) { + headers: params.tracingLevel + ? { + 'X-Trace-Verbosity': params.tracingLevel, + } + : undefined, + }, + ); + } + getExplainQuery(params: { + query: string; + database: string; + action: Action; + syntax?: QuerySyntax; + stats?: Stats; + tracingLevel?: TracingLevel; + // eslint-disable-next-line camelcase + transaction_mode?: TransactionMode; + timeout?: Timeout; + }) { return this.post | ErrorResponse>( this.getPath('/viewer/json/query'), { - query, - database, - action: action || 'explain', - syntax, - timeout: 600000, + query: params.query, + database: params.database, + action: params.action || 'explain', + syntax: params.syntax, + timeout: params.timeout, + stats: params.stats, + // eslint-disable-next-line camelcase + transaction_mode: params.transaction_mode, }, {}, + { + headers: params.tracingLevel + ? { + 'X-Trace-Verbosity': params.tracingLevel, + } + : undefined, + }, ); } getExplainQueryAst(query: string, database: string) { diff --git a/src/services/settings.ts b/src/services/settings.ts index 62f803143..b1e487679 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -4,14 +4,17 @@ import { AUTOCOMPLETE_ON_ENTER, AUTO_REFRESH_INTERVAL, BINARY_DATA_IN_PLAIN_TEXT_DISPLAY, + DEFAULT_QUERY_SETTINGS, ENABLE_AUTOCOMPLETE, INVERTED_DISKS_KEY, IS_HOTKEYS_HELP_HIDDEN_KEY, LANGUAGE_KEY, + LAST_QUERY_EXECUTION_SETTINGS_KEY, LAST_USED_QUERY_ACTION_KEY, PARTITIONS_HIDDEN_COLUMNS_KEY, - QUERY_INITIAL_MODE_KEY, + QUERY_EXECUTION_SETTINGS_KEY, QUERY_SETTINGS, + QUERY_SETTINGS_BANNER_LAST_CLOSED_KEY, QUERY_USE_MULTI_SCHEMA_KEY, SAVED_QUERIES_KEY, TENANT_INITIAL_PAGE_KEY, @@ -22,7 +25,7 @@ import { USE_PAGINATED_TABLES_KEY, USE_SEPARATE_DISKS_PAGES_KEY, } from '../utils/constants'; -import {QUERY_ACTIONS, QUERY_MODES} from '../utils/query'; +import {QUERY_ACTIONS} from '../utils/query'; import {parseJson} from '../utils/utils'; export type SettingsObject = Record; @@ -37,7 +40,6 @@ export const DEFAULT_USER_SETTINGS = { [BINARY_DATA_IN_PLAIN_TEXT_DISPLAY]: true, [SAVED_QUERIES_KEY]: [], [TENANT_INITIAL_PAGE_KEY]: TENANT_PAGES_IDS.query, - [QUERY_INITIAL_MODE_KEY]: QUERY_MODES.script, [LAST_USED_QUERY_ACTION_KEY]: QUERY_ACTIONS.execute, [ASIDE_HEADER_COMPACT_KEY]: true, [PARTITIONS_HIDDEN_COLUMNS_KEY]: [], @@ -50,6 +52,9 @@ export const DEFAULT_USER_SETTINGS = { [AUTO_REFRESH_INTERVAL]: 0, [USE_DIRECTORY_OPERATIONS]: false, [QUERY_SETTINGS]: false, + [LAST_QUERY_EXECUTION_SETTINGS_KEY]: undefined, + [QUERY_SETTINGS_BANNER_LAST_CLOSED_KEY]: undefined, + [QUERY_EXECUTION_SETTINGS_KEY]: DEFAULT_QUERY_SETTINGS, } as const satisfies SettingsObject; class SettingsManager { diff --git a/src/store/reducers/executeQuery.ts b/src/store/reducers/executeQuery.ts index bb19f32ed..99e2083c3 100644 --- a/src/store/reducers/executeQuery.ts +++ b/src/store/reducers/executeQuery.ts @@ -1,6 +1,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 { ExecuteQueryAction, @@ -12,6 +13,7 @@ import type { IQueryResult, QueryMode, QueryRequestParams, + QuerySettings, QuerySyntax, } from '../../types/store/query'; import {QUERIES_HISTORY_KEY} from '../../utils/constants'; @@ -21,6 +23,7 @@ import { isQueryErrorResponse, parseQueryAPIExecuteResponse, } from '../../utils/query'; +import {isNumeric} from '../../utils/utils'; import {createRequestActionTypes} from '../utils'; import {api} from './api'; @@ -139,22 +142,22 @@ const executeQuery: Reducer = ( }; interface SendQueryParams extends QueryRequestParams { - mode?: QueryMode; + querySettings?: QuerySettings; schema?: Schemas; } export const executeQueryApi = api.injectEndpoints({ endpoints: (build) => ({ executeQuery: build.mutation({ - queryFn: async ({query, database, mode, schema = 'modern'}) => { + queryFn: async ({query, database, querySettings, schema = 'modern'}) => { let action: ExecuteActions = 'execute'; let syntax: QuerySyntax = QUERY_SYNTAX.yql; - if (mode === 'pg') { + if (querySettings?.queryMode === 'pg') { action = 'execute-query'; syntax = QUERY_SYNTAX.pg; - } else if (mode) { - action = `execute-${mode}`; + } else if (querySettings?.queryMode) { + action = `execute-${querySettings?.queryMode}`; } try { @@ -164,7 +167,14 @@ export const executeQueryApi = api.injectEndpoints({ database, action, syntax, - stats: 'full', + stats: querySettings?.statisticsMode, + tracingLevel: querySettings?.tracingLevel + ? TracingLevelNumber[querySettings?.tracingLevel] + : undefined, + transaction_mode: querySettings?.isolationLevel, + timeout: isNumeric(querySettings?.timeout) + ? Number(querySettings?.timeout) * 1000 + : undefined, }); if (isQueryErrorResponse(response)) { diff --git a/src/store/reducers/explainQuery/explainQuery.ts b/src/store/reducers/explainQuery/explainQuery.ts index 0c3dce80c..ebb0e81e9 100644 --- a/src/store/reducers/explainQuery/explainQuery.ts +++ b/src/store/reducers/explainQuery/explainQuery.ts @@ -1,36 +1,46 @@ +import {TracingLevelNumber} from '../../../types/api/query'; import type {ExplainActions} from '../../../types/api/query'; -import type {QueryMode, QueryRequestParams, QuerySyntax} from '../../../types/store/query'; +import type {QueryRequestParams, QuerySettings, QuerySyntax} from '../../../types/store/query'; import {QUERY_SYNTAX, isQueryErrorResponse} from '../../../utils/query'; +import {isNumeric} from '../../../utils/utils'; import {api} from '../api'; import type {PreparedExplainResponse} from './types'; import {prepareExplainResponse} from './utils'; interface ExplainQueryParams extends QueryRequestParams { - mode?: QueryMode; + querySettings?: QuerySettings; } export const explainQueryApi = api.injectEndpoints({ endpoints: (build) => ({ explainQuery: build.mutation({ - queryFn: async ({query, database, mode}) => { + queryFn: async ({query, database, querySettings}) => { let action: ExplainActions = 'explain'; let syntax: QuerySyntax = QUERY_SYNTAX.yql; - if (mode === 'pg') { + if (querySettings?.queryMode === 'pg') { action = 'explain-query'; syntax = QUERY_SYNTAX.pg; - } else if (mode) { - action = `explain-${mode}`; + } else if (querySettings?.queryMode) { + action = `explain-${querySettings?.queryMode}`; } try { - const response = await window.api.getExplainQuery( + const response = await window.api.getExplainQuery({ query, database, action, syntax, - ); + stats: querySettings?.statisticsMode, + tracingLevel: querySettings?.tracingLevel + ? TracingLevelNumber[querySettings?.tracingLevel] + : undefined, + transaction_mode: querySettings?.isolationLevel, + timeout: isNumeric(querySettings?.timeout) + ? Number(querySettings?.timeout) * 1000 + : undefined, + }); if (isQueryErrorResponse(response)) { return {error: response}; diff --git a/src/types/api/query.ts b/src/types/api/query.ts index ad07918aa..c0088037b 100644 --- a/src/types/api/query.ts +++ b/src/types/api/query.ts @@ -1,3 +1,6 @@ +import {TRACING_LEVELS} from '../../utils/query'; +import type {IsolationLevel, StatisticsMode} from '../store/query'; + // ==== types from backend protos ==== interface Position { row?: number; @@ -179,6 +182,27 @@ export interface ColumnType { type: string; } +export const TracingLevelNumber = { + [TRACING_LEVELS.off]: 0, + [TRACING_LEVELS.toplevel]: 4, + [TRACING_LEVELS.basic]: 9, + [TRACING_LEVELS.detailed]: 13, + [TRACING_LEVELS.diagnostic]: 14, + [TRACING_LEVELS.trace]: 15, +}; + +/** undefined = 'none' */ +export type Stats = StatisticsMode; + +/** undefined = '60000' */ +export type Timeout = number; + +/** undefined = 'serializable-read-write' */ +export type TransactionMode = IsolationLevel; + +/** undefined = '15' */ +export type TracingLevel = number; + /** undefined = 'classic' */ export type Schemas = 'classic' | 'modern' | 'ydb' | 'multi' | undefined; diff --git a/src/types/store/query.ts b/src/types/store/query.ts index 7279b795b..8325f45cd 100644 --- a/src/types/store/query.ts +++ b/src/types/store/query.ts @@ -36,6 +36,14 @@ export interface QueryRequestParams { query: string; } +export interface QuerySettings { + queryMode: QueryMode; + isolationLevel: IsolationLevel; + timeout?: string; + statisticsMode?: StatisticsMode; + tracingLevel?: TracingLevel; +} + export type QueryErrorResponse = IResponseError; export type QueryError = NetworkError | QueryErrorResponse; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index f67a1d16f..22dbfcff4 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -2,6 +2,9 @@ import DataTable from '@gravity-ui/react-data-table'; import type {Settings} from '@gravity-ui/react-data-table'; import {EType} from '../types/api/tablet'; +import type {QuerySettings} from '../types/store/query'; + +import {ISOLATION_LEVELS, QUERY_MODES, STATISTICS_MODES, TRACING_LEVELS} from './query'; const SECOND = 1000; @@ -18,6 +21,7 @@ export const TERABYTE = 1_000_000_000_000; export const MINUTE_IN_SECONDS = 60; export const HOUR_IN_SECONDS = 60 * MINUTE_IN_SECONDS; export const DAY_IN_SECONDS = 24 * HOUR_IN_SECONDS; +export const WEEK_IN_SECONDS = 7 * DAY_IN_SECONDS; export const MS_IN_NANOSECONDS = 1000000; @@ -118,7 +122,18 @@ export const TENANT_OVERVIEW_TABLES_SETTINGS = { dynamicRender: false, } as const; -export const QUERY_INITIAL_MODE_KEY = 'query_initial_mode'; +export const DEFAULT_QUERY_SETTINGS: QuerySettings = { + queryMode: QUERY_MODES.script, + isolationLevel: ISOLATION_LEVELS.serializable, + timeout: '60', + statisticsMode: STATISTICS_MODES.none, + tracingLevel: TRACING_LEVELS.detailed, +}; + +export const QUERY_EXECUTION_SETTINGS_KEY = 'queryExecutionSettings'; +export const LAST_QUERY_EXECUTION_SETTINGS_KEY = 'last_query_execution_settings'; +export const QUERY_SETTINGS_BANNER_LAST_CLOSED_KEY = 'querySettingsBannerLastClosed'; + export const LAST_USED_QUERY_ACTION_KEY = 'last_used_query_action'; export const PARTITIONS_HIDDEN_COLUMNS_KEY = 'partitionsHiddenColumns'; diff --git a/src/utils/hooks/index.ts b/src/utils/hooks/index.ts index 1f951947b..0aa4f3ab0 100644 --- a/src/utils/hooks/index.ts +++ b/src/utils/hooks/index.ts @@ -1,7 +1,7 @@ export * from './useTypedSelector'; export * from './useTypedDispatch'; export * from './useSetting'; -export * from './useQueryModes'; +export * from './useQueryExecutionSettings'; export * from './useTableSort'; export * from './useSearchQuery'; export * from './useAutoRefreshInterval'; diff --git a/src/utils/hooks/useIsQuerySettingsBannerHidden.ts b/src/utils/hooks/useIsQuerySettingsBannerHidden.ts new file mode 100644 index 000000000..0e873015c --- /dev/null +++ b/src/utils/hooks/useIsQuerySettingsBannerHidden.ts @@ -0,0 +1,30 @@ +import React from 'react'; + +import getChangedQueryExecutionSettingsDescription from '../../containers/Tenant/Query/QueryEditorControls/utils/getChangedQueryExecutionSettingsDescription'; +import {DEFAULT_QUERY_SETTINGS, WEEK_IN_SECONDS} from '../constants'; + +import {useLastQueryExecutionSettings} from './useLastQueryExecutionSettings'; +import {useQuerySettingsBannerLastClosed} from './useQuerySettingsBannerLastClosed'; + +export const useIsQuerySettingsBannerHidden = () => { + const [lastQueryExecutionSettings] = useLastQueryExecutionSettings(); + const [querySettingsBannerLastClosed] = useQuerySettingsBannerLastClosed(); + + const changedQuerySettingsDescription = React.useMemo(() => { + return lastQueryExecutionSettings + ? getChangedQueryExecutionSettingsDescription({ + currentSettings: lastQueryExecutionSettings, + defaultSettings: DEFAULT_QUERY_SETTINGS, + }) + : undefined; + }, [lastQueryExecutionSettings]); + + const isShownLately = React.useMemo(() => { + return ( + querySettingsBannerLastClosed && + new Date().getTime() - querySettingsBannerLastClosed < WEEK_IN_SECONDS + ); + }, [querySettingsBannerLastClosed]); + + return changedQuerySettingsDescription && isShownLately; +}; diff --git a/src/utils/hooks/useLastQueryExecutionSettings.ts b/src/utils/hooks/useLastQueryExecutionSettings.ts new file mode 100644 index 000000000..872441feb --- /dev/null +++ b/src/utils/hooks/useLastQueryExecutionSettings.ts @@ -0,0 +1,8 @@ +import type {QuerySettings} from '../../types/store/query'; +import {LAST_QUERY_EXECUTION_SETTINGS_KEY} from '../constants'; + +import {useSetting} from './useSetting'; + +export const useLastQueryExecutionSettings = () => { + return useSetting(LAST_QUERY_EXECUTION_SETTINGS_KEY); +}; diff --git a/src/utils/hooks/useQueryExecutionSettings.ts b/src/utils/hooks/useQueryExecutionSettings.ts new file mode 100644 index 000000000..107b529c5 --- /dev/null +++ b/src/utils/hooks/useQueryExecutionSettings.ts @@ -0,0 +1,8 @@ +import type {QuerySettings} from '../../types/store/query'; +import {QUERY_EXECUTION_SETTINGS_KEY} from '../constants'; + +import {useSetting} from './useSetting'; + +export const useQueryExecutionSettings = () => { + return useSetting(QUERY_EXECUTION_SETTINGS_KEY); +}; diff --git a/src/utils/hooks/useQueryModes.ts b/src/utils/hooks/useQueryModes.ts deleted file mode 100644 index 165c1a8d2..000000000 --- a/src/utils/hooks/useQueryModes.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type {QueryMode} from '../../types/store/query'; -import {QUERY_INITIAL_MODE_KEY} from '../constants'; - -import {useSetting} from './useSetting'; - -export const useQueryModes = () => { - return useSetting(QUERY_INITIAL_MODE_KEY); -}; diff --git a/src/utils/hooks/useQuerySettingsBannerLastClosed.ts b/src/utils/hooks/useQuerySettingsBannerLastClosed.ts new file mode 100644 index 000000000..5f0fdff0c --- /dev/null +++ b/src/utils/hooks/useQuerySettingsBannerLastClosed.ts @@ -0,0 +1,7 @@ +import {QUERY_SETTINGS_BANNER_LAST_CLOSED_KEY} from '../constants'; + +import {useSetting} from './useSetting'; + +export const useQuerySettingsBannerLastClosed = () => { + return useSetting(QUERY_SETTINGS_BANNER_LAST_CLOSED_KEY); +};