From d1997fb240b4e41deb1184e51660057601485f82 Mon Sep 17 00:00:00 2001
From: Elena Makarova <el-makarova@yandex-team.ru>
Date: Thu, 14 Nov 2024 13:28:40 +0300
Subject: [PATCH 1/4] feat: warn about unsaved changes in editor

---
 package-lock.json                             |  11 +
 package.json                                  |   1 +
 .../ConfirmationDialog.scss                   |   5 +
 .../ConfirmationDialog/ConfirmationDialog.tsx |  99 ++++++
 .../ConfirmationDialog/i18n/en.json           |   3 +
 .../ConfirmationDialog/i18n/index.ts          |   7 +
 src/containers/App/Providers.tsx              |   9 +-
 .../TenantOverview/TenantCpu/TopQueries.tsx   |   5 +-
 .../Diagnostics/TopQueries/TopQueries.tsx     |   5 +-
 .../ObjectSummary/SchemaTree/SchemaTree.tsx   |  10 +-
 src/containers/Tenant/Query/NewSQL/NewSQL.tsx |  16 +-
 .../Query/QueriesHistory/QueriesHistory.tsx   |   5 +-
 .../Tenant/Query/QueryEditor/QueryEditor.tsx  | 125 +++----
 .../Query/SavedQueries/SavedQueries.tsx       |  23 +-
 .../Tenant/utils/newSQLQueryActions.ts        |   6 +-
 src/containers/Tenant/utils/schemaActions.ts  |  54 +--
 src/store/reducers/executeQuery.ts            | 314 +++++++-----------
 .../reducers/queryActions/queryActions.ts     |   2 +-
 src/store/reducers/schema/schema.ts           |  42 +--
 src/types/store/executeQuery.ts               |  29 +-
 src/utils/hooks/withConfirmation/i18n/en.json |   4 +
 .../hooks/withConfirmation/i18n/index.ts      |   7 +
 .../useChangeInputWithConfirmation.ts         |  39 +++
 23 files changed, 455 insertions(+), 366 deletions(-)
 create mode 100644 src/components/ConfirmationDialog/ConfirmationDialog.scss
 create mode 100644 src/components/ConfirmationDialog/ConfirmationDialog.tsx
 create mode 100644 src/components/ConfirmationDialog/i18n/en.json
 create mode 100644 src/components/ConfirmationDialog/i18n/index.ts
 create mode 100644 src/utils/hooks/withConfirmation/i18n/en.json
 create mode 100644 src/utils/hooks/withConfirmation/i18n/index.ts
 create mode 100644 src/utils/hooks/withConfirmation/useChangeInputWithConfirmation.ts

diff --git a/package-lock.json b/package-lock.json
index 0ad7f8379..5e2424fe3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -56,6 +56,7 @@
       "devDependencies": {
         "@commitlint/cli": "^19.3.0",
         "@commitlint/config-conventional": "^19.2.2",
+        "@ebay/nice-modal-react": "^1.2.13",
         "@gravity-ui/browserslist-config": "^4.3.0",
         "@gravity-ui/eslint-config": "^3.2.0",
         "@gravity-ui/prettier-config": "^1.1.0",
@@ -3477,6 +3478,16 @@
         "react": ">=16.8.0"
       }
     },
+    "node_modules/@ebay/nice-modal-react": {
+      "version": "1.2.13",
+      "resolved": "https://registry.npmjs.org/@ebay/nice-modal-react/-/nice-modal-react-1.2.13.tgz",
+      "integrity": "sha512-jx8xIWe/Up4tpNuM02M+rbnLoxdngTGk3Y8LjJsLGXXcSoKd/+eZStZcAlIO/jwxyz/bhPZnpqPJZWAmhOofuA==",
+      "dev": true,
+      "peerDependencies": {
+        "react": ">16.8.0",
+        "react-dom": ">16.8.0"
+      }
+    },
     "node_modules/@eslint-community/eslint-utils": {
       "version": "4.4.0",
       "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
diff --git a/package.json b/package.json
index 2aa4c5cd1..96553e134 100644
--- a/package.json
+++ b/package.json
@@ -119,6 +119,7 @@
   "devDependencies": {
     "@commitlint/cli": "^19.3.0",
     "@commitlint/config-conventional": "^19.2.2",
+    "@ebay/nice-modal-react": "^1.2.13",
     "@gravity-ui/browserslist-config": "^4.3.0",
     "@gravity-ui/eslint-config": "^3.2.0",
     "@gravity-ui/prettier-config": "^1.1.0",
diff --git a/src/components/ConfirmationDialog/ConfirmationDialog.scss b/src/components/ConfirmationDialog/ConfirmationDialog.scss
new file mode 100644
index 000000000..44fce835c
--- /dev/null
+++ b/src/components/ConfirmationDialog/ConfirmationDialog.scss
@@ -0,0 +1,5 @@
+.confirmation-dialog {
+    &__message {
+        white-space: pre-wrap;
+    }
+}
diff --git a/src/components/ConfirmationDialog/ConfirmationDialog.tsx b/src/components/ConfirmationDialog/ConfirmationDialog.tsx
new file mode 100644
index 000000000..0b4f47e68
--- /dev/null
+++ b/src/components/ConfirmationDialog/ConfirmationDialog.tsx
@@ -0,0 +1,99 @@
+import * as NiceModal from '@ebay/nice-modal-react';
+import type {ButtonView} from '@gravity-ui/uikit';
+import {Dialog} from '@gravity-ui/uikit';
+
+import {cn} from '../../utils/cn';
+
+import {confirmationDialogKeyset} from './i18n';
+
+import './ConfirmationDialog.scss';
+
+const block = cn('confirmation-dialog');
+
+interface CommonDialogProps {
+    caption?: string;
+    message?: React.ReactNode;
+    body?: React.ReactNode;
+
+    progress?: boolean;
+    textButtonCancel?: string;
+    textButtonApply?: string;
+    buttonApplyView?: ButtonView;
+    className?: string;
+    onConfirm?: () => void;
+}
+
+interface ConfirmationDialogNiceModalProps extends CommonDialogProps {
+    onClose?: () => void;
+}
+
+interface ConfirmationDialogProps extends CommonDialogProps {
+    onClose: () => void;
+    open: boolean;
+    children?: React.ReactNode;
+}
+
+export const CONFIRMATION_DIALOG = 'confirmation-dialog';
+function ConfirmationDialog({
+    caption = '',
+    children,
+    onConfirm,
+    onClose,
+    progress,
+    textButtonApply,
+    textButtonCancel,
+    buttonApplyView = 'normal',
+    className,
+    open,
+}: ConfirmationDialogProps) {
+    return (
+        <Dialog
+            className={block(null, className)}
+            size="s"
+            onClose={onClose}
+            disableOutsideClick
+            open={open}
+        >
+            <Dialog.Header caption={caption} />
+            <Dialog.Body>{children}</Dialog.Body>
+            <Dialog.Footer
+                onClickButtonApply={onConfirm}
+                propsButtonApply={{view: buttonApplyView}}
+                textButtonApply={textButtonApply}
+                textButtonCancel={textButtonCancel ?? confirmationDialogKeyset('action_cancel')}
+                onClickButtonCancel={onClose}
+                loading={progress}
+            />
+        </Dialog>
+    );
+}
+
+export const ConfirmationDialogNiceModal = NiceModal.create(
+    (props: ConfirmationDialogNiceModalProps) => {
+        const modal = NiceModal.useModal();
+
+        const handleClose = () => {
+            modal.hide();
+            modal.remove();
+        };
+
+        return (
+            <ConfirmationDialog
+                {...props}
+                onConfirm={async () => {
+                    await props.onConfirm?.();
+                    modal.resolve(true);
+                    handleClose();
+                }}
+                onClose={() => {
+                    props.onClose?.();
+                    modal.resolve(false);
+                    handleClose();
+                }}
+                open={modal.visible}
+            />
+        );
+    },
+);
+
+NiceModal.register(CONFIRMATION_DIALOG, ConfirmationDialogNiceModal);
diff --git a/src/components/ConfirmationDialog/i18n/en.json b/src/components/ConfirmationDialog/i18n/en.json
new file mode 100644
index 000000000..88c90714e
--- /dev/null
+++ b/src/components/ConfirmationDialog/i18n/en.json
@@ -0,0 +1,3 @@
+{
+  "action_cancel": "Cancel"
+}
diff --git a/src/components/ConfirmationDialog/i18n/index.ts b/src/components/ConfirmationDialog/i18n/index.ts
new file mode 100644
index 000000000..423b49075
--- /dev/null
+++ b/src/components/ConfirmationDialog/i18n/index.ts
@@ -0,0 +1,7 @@
+import {registerKeysets} from '../../../utils/i18n';
+
+import en from './en.json';
+
+const COMPONENT = 'ydb-confirmation-dialog';
+
+export const confirmationDialogKeyset = registerKeysets(COMPONENT, {en});
diff --git a/src/containers/App/Providers.tsx b/src/containers/App/Providers.tsx
index 3193b76c7..400d1fd18 100644
--- a/src/containers/App/Providers.tsx
+++ b/src/containers/App/Providers.tsx
@@ -1,5 +1,6 @@
 import React from 'react';
 
+import * as NiceModal from '@ebay/nice-modal-react';
 import {ThemeProvider} from '@gravity-ui/uikit';
 import type {Store} from '@reduxjs/toolkit';
 import type {History} from 'history';
@@ -34,9 +35,11 @@ export function Providers({
                 <Router history={history}>
                     <QueryParamProvider adapter={ReactRouter5Adapter}>
                         <Theme>
-                            <ComponentsProvider registry={componentsRegistry}>
-                                {children}
-                            </ComponentsProvider>
+                            <NiceModal.Provider>
+                                <ComponentsProvider registry={componentsRegistry}>
+                                    {children}
+                                </ComponentsProvider>
+                            </NiceModal.Provider>
                         </Theme>
                     </QueryParamProvider>
                 </Router>
diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx
index c72044c30..be722ce39 100644
--- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx
+++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx
@@ -15,6 +15,7 @@ import {
     TENANT_QUERY_TABS_ID,
 } from '../../../../../store/reducers/tenant/constants';
 import {useAutoRefreshInterval, useTypedDispatch} from '../../../../../utils/hooks';
+import {useChangeInputWithConfirmation} from '../../../../../utils/hooks/withConfirmation/useChangeInputWithConfirmation';
 import {parseQueryErrorToString} from '../../../../../utils/query';
 import {TenantTabsGroups, getTenantPath} from '../../../TenantPages';
 import {
@@ -48,7 +49,7 @@ export function TopQueries({tenantName}: TopQueriesProps) {
     const loading = isFetching && currentData === undefined;
     const data = currentData?.resultSets?.[0]?.result || [];
 
-    const handleRowClick = React.useCallback(
+    const applyRowClick = React.useCallback(
         (row: any) => {
             const {QueryText: input} = row;
 
@@ -67,6 +68,8 @@ export function TopQueries({tenantName}: TopQueriesProps) {
         [dispatch, history, location],
     );
 
+    const handleRowClick = useChangeInputWithConfirmation(applyRowClick);
+
     const title = getSectionTitle({
         entity: i18n('queries'),
         postfix: i18n('by-cpu-time', {executionPeriod: i18n('executed-last-hour')}),
diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx
index 65e90ca34..9d5fd19ad 100644
--- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx
+++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx
@@ -20,6 +20,7 @@ import {
 } from '../../../../store/reducers/tenant/constants';
 import {cn} from '../../../../utils/cn';
 import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks';
+import {useChangeInputWithConfirmation} from '../../../../utils/hooks/withConfirmation/useChangeInputWithConfirmation';
 import {TenantTabsGroups, getTenantPath} from '../../TenantPages';
 
 import {RunningQueriesData} from './RunningQueriesData';
@@ -68,7 +69,7 @@ export const TopQueries = ({tenantName}: TopQueriesProps) => {
 
     const filters = useTypedSelector((state) => state.executeTopQueries);
 
-    const onRowClick = React.useCallback(
+    const applyRowClick = React.useCallback(
         (input: string) => {
             dispatch(changeUserInput({input}));
 
@@ -85,6 +86,8 @@ export const TopQueries = ({tenantName}: TopQueriesProps) => {
         [dispatch, history, location],
     );
 
+    const onRowClick = useChangeInputWithConfirmation(applyRowClick);
+
     const handleTextSearchUpdate = (text: string) => {
         dispatch(setTopQueriesFilters({text}));
     };
diff --git a/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx b/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx
index 01cc42db9..cabc3b3bb 100644
--- a/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx
+++ b/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx
@@ -6,13 +6,19 @@ import React from 'react';
 import {NavigationTree} from 'ydb-ui-components';
 
 import {useCreateDirectoryFeatureAvailable} from '../../../../store/reducers/capabilities/hooks';
+import {selectIsQuerySaved} from '../../../../store/reducers/executeQuery';
 import {schemaApi} from '../../../../store/reducers/schema/schema';
 import {tableSchemaDataApi} from '../../../../store/reducers/tableSchemaData';
 import type {GetTableSchemaDataParams} from '../../../../store/reducers/tableSchemaData';
 import type {EPathType, TEvDescribeSchemeResult} from '../../../../types/api/schema';
 import {wait} from '../../../../utils';
 import {SECOND_IN_MS} from '../../../../utils/constants';
-import {useQueryExecutionSettings, useTypedDispatch} from '../../../../utils/hooks';
+import {
+    useQueryExecutionSettings,
+    useTypedDispatch,
+    useTypedSelector,
+} from '../../../../utils/hooks';
+import {getConfirmation} from '../../../../utils/hooks/withConfirmation/useChangeInputWithConfirmation';
 import {getSchemaControls} from '../../utils/controls';
 import {isChildlessPathType, mapPathTypeToNavigationTreeType} from '../../utils/schema';
 import {getActions} from '../../utils/schemaActions';
@@ -33,6 +39,7 @@ export function SchemaTree(props: SchemaTreeProps) {
     const createDirectoryFeatureAvailable = useCreateDirectoryFeatureAvailable();
     const {rootPath, rootName, rootType, currentPath, onActivePathUpdate} = props;
     const dispatch = useTypedDispatch();
+    const isQuerySaved = useTypedSelector(selectIsQuerySaved);
     const [getTableSchemaDataMutation] = tableSchemaDataApi.useGetTableSchemaDataMutation();
 
     const getTableSchemaDataPromise = React.useCallback(
@@ -144,6 +151,7 @@ export function SchemaTree(props: SchemaTreeProps) {
                             ? handleOpenCreateDirectoryDialog
                             : undefined,
                         getTableSchemaDataPromise,
+                        getConfirmation: isQuerySaved ? undefined : getConfirmation,
                     },
                     rootPath,
                 )}
diff --git a/src/containers/Tenant/Query/NewSQL/NewSQL.tsx b/src/containers/Tenant/Query/NewSQL/NewSQL.tsx
index cfc12c154..51963b245 100644
--- a/src/containers/Tenant/Query/NewSQL/NewSQL.tsx
+++ b/src/containers/Tenant/Query/NewSQL/NewSQL.tsx
@@ -1,14 +1,28 @@
+import React from 'react';
+
 import {ChevronDown} from '@gravity-ui/icons';
 import {Button, DropdownMenu} from '@gravity-ui/uikit';
 
+import {changeUserInput} from '../../../../store/reducers/executeQuery';
 import {useTypedDispatch} from '../../../../utils/hooks';
+import {useChangeInputWithConfirmation} from '../../../../utils/hooks/withConfirmation/useChangeInputWithConfirmation';
 import {bindActions} from '../../utils/newSQLQueryActions';
 
 import i18n from './i18n';
 
 export function NewSQL() {
     const dispatch = useTypedDispatch();
-    const actions = bindActions(dispatch);
+
+    const insertTemplate = React.useCallback(
+        (input: string) => {
+            dispatch(changeUserInput({input}));
+        },
+        [dispatch],
+    );
+
+    const onTemplateClick = useChangeInputWithConfirmation(insertTemplate);
+
+    const actions = bindActions(onTemplateClick);
 
     const items = [
         {
diff --git a/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx b/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx
index 6b611a2bb..fdf7d3271 100644
--- a/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx
+++ b/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx
@@ -15,6 +15,7 @@ import type {QueryInHistory} from '../../../../types/store/executeQuery';
 import {cn} from '../../../../utils/cn';
 import {formatDateTime} from '../../../../utils/dataFormatters/dataFormatters';
 import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks';
+import {useChangeInputWithConfirmation} from '../../../../utils/hooks/withConfirmation/useChangeInputWithConfirmation';
 import {formatToMs, parseUsToMs} from '../../../../utils/timeParsers';
 import {MAX_QUERY_HEIGHT, QUERY_TABLE_SETTINGS} from '../../utils/constants';
 import i18n from '../i18n';
@@ -36,11 +37,13 @@ function QueriesHistory({changeUserInput}: QueriesHistoryProps) {
     const filter = useTypedSelector(selectQueriesHistoryFilter);
     const reversedHistory = [...queriesHistory].reverse();
 
-    const onQueryClick = (query: QueryInHistory) => {
+    const applyQueryClick = (query: QueryInHistory) => {
         changeUserInput({input: query.queryText});
         dispatch(setQueryTab(TENANT_QUERY_TABS_ID.newQuery));
     };
 
+    const onQueryClick = useChangeInputWithConfirmation(applyQueryClick);
+
     const onChangeFilter = (value: string) => {
         dispatch(setQueryHistoryFilter(value));
     };
diff --git a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx
index eb188451e..2e365bf6d 100644
--- a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx
+++ b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx
@@ -3,27 +3,29 @@ import React from 'react';
 import {isEqual} from 'lodash';
 import throttle from 'lodash/throttle';
 import type Monaco from 'monaco-editor';
-import {connect} from 'react-redux';
 import {v4 as uuidv4} from 'uuid';
 
 import {MonacoEditor} from '../../../../components/MonacoEditor/MonacoEditor';
 import SplitPane from '../../../../components/SplitPane';
-import type {RootState} from '../../../../store';
 import {useTracingLevelOptionAvailable} from '../../../../store/reducers/capabilities/hooks';
 import {
     executeQueryApi,
     goToNextQuery,
     goToPreviousQuery,
     saveQueryToHistory,
-    setQueryResult,
+    selectQueriesHistory,
+    selectQueriesHistoryCurrentIndex,
+    selectResult,
+    selectTenantPath,
+    selectUserInput,
     setTenantPath,
 } from '../../../../store/reducers/executeQuery';
 import {explainQueryApi} from '../../../../store/reducers/explainQuery/explainQuery';
 import {setQueryAction} from '../../../../store/reducers/queryActions/queryActions';
-import {setShowPreview} from '../../../../store/reducers/schema/schema';
+import {selectShowPreview, setShowPreview} from '../../../../store/reducers/schema/schema';
 import type {EPathType} from '../../../../types/api/schema';
 import {ResultType} from '../../../../types/store/executeQuery';
-import type {ExecuteQueryState, QueryResult} from '../../../../types/store/executeQuery';
+import type {QueryResult} from '../../../../types/store/executeQuery';
 import type {QueryAction} from '../../../../types/store/query';
 import {cn} from '../../../../utils/cn';
 import {
@@ -31,7 +33,13 @@ import {
     DEFAULT_SIZE_RESULT_PANE_KEY,
     LAST_USED_QUERY_ACTION_KEY,
 } from '../../../../utils/constants';
-import {useEventHandler, useQueryExecutionSettings, useSetting} from '../../../../utils/hooks';
+import {
+    useEventHandler,
+    useQueryExecutionSettings,
+    useSetting,
+    useTypedDispatch,
+    useTypedSelector,
+} from '../../../../utils/hooks';
 import {useChangedQuerySettings} from '../../../../utils/hooks/useChangedQuerySettings';
 import {useLastQueryExecutionSettings} from '../../../../utils/hooks/useLastQueryExecutionSettings';
 import {YQL_LANGUAGE_ID} from '../../../../utils/monaco/constats';
@@ -68,35 +76,22 @@ interface QueryEditorProps {
     tenantName: string;
     path: string;
     changeUserInput: (arg: {input: string}) => void;
-    goToNextQuery: (...args: Parameters<typeof goToNextQuery>) => void;
-    goToPreviousQuery: (...args: Parameters<typeof goToPreviousQuery>) => void;
-    setTenantPath: (...args: Parameters<typeof setTenantPath>) => void;
-    setQueryAction: (...args: Parameters<typeof setQueryAction>) => void;
-    setQueryResult: (...args: Parameters<typeof setQueryResult>) => void;
-    executeQuery: ExecuteQueryState;
     theme: string;
     type?: EPathType;
-    showPreview: boolean;
-    setShowPreview: (...args: Parameters<typeof setShowPreview>) => void;
-    saveQueryToHistory: (...args: Parameters<typeof saveQueryToHistory>) => void;
 }
 
-function QueryEditor(props: QueryEditorProps) {
+export default function QueryEditor(props: QueryEditorProps) {
     const editorOptions = useEditorOptions();
-    const {
-        tenantName,
-        path,
-        setTenantPath: setPath,
-        executeQuery,
-        type,
-        theme,
-        changeUserInput,
-        setQueryResult,
-        showPreview,
-    } = props;
-    const {tenantPath: savedPath} = executeQuery;
-
-    const isResultLoaded = Boolean(executeQuery.result);
+    const dispatch = useTypedDispatch();
+    const {tenantName, path, type, theme, changeUserInput} = props;
+    const savedPath = useTypedSelector(selectTenantPath);
+    const result = useTypedSelector(selectResult);
+    const historyQueries = useTypedSelector(selectQueriesHistory);
+    const historyCurrentIndex = useTypedSelector(selectQueriesHistoryCurrentIndex);
+    const input = useTypedSelector(selectUserInput);
+    const showPreview = useTypedSelector(selectShowPreview);
+
+    const isResultLoaded = Boolean(result);
 
     const [querySettings] = useQueryExecutionSettings();
     const enableTracingLevel = useTracingLevelOptionAvailable();
@@ -113,13 +108,9 @@ function QueryEditor(props: QueryEditorProps) {
 
     React.useEffect(() => {
         if (savedPath !== tenantName) {
-            if (savedPath) {
-                changeUserInput({input: ''});
-                setQueryResult();
-            }
-            setPath(tenantName);
+            dispatch(setTenantPath(tenantName));
         }
-    }, [changeUserInput, setPath, setQueryResult, tenantName, savedPath]);
+    }, [dispatch, tenantName, savedPath]);
 
     const [resultVisibilityState, dispatchResultVisibilityState] = React.useReducer(
         paneVisibilityToggleReducerCreator(DEFAULT_IS_QUERY_RESULT_COLLAPSED),
@@ -131,21 +122,21 @@ function QueryEditor(props: QueryEditorProps) {
     }, []);
 
     React.useEffect(() => {
-        if (props.showPreview || isResultLoaded) {
+        if (showPreview || isResultLoaded) {
             dispatchResultVisibilityState(PaneVisibilityActionTypes.triggerExpand);
         } else {
             dispatchResultVisibilityState(PaneVisibilityActionTypes.triggerCollapse);
         }
-    }, [props.showPreview, isResultLoaded]);
+    }, [showPreview, isResultLoaded]);
 
     const getLastQueryText = useEventHandler(() => {
-        const {history} = executeQuery;
-        return history.queries[history.queries.length - 1]?.queryText || '';
+        if (!historyQueries || historyQueries.length === 0) {
+            return '';
+        }
+        return historyQueries[historyQueries.length - 1].queryText;
     });
 
     const handleSendExecuteClick = useEventHandler((text?: string) => {
-        const {input, history} = executeQuery;
-
         const query = text ?? input;
 
         setLastUsedQueryAction(QUERY_ACTIONS.execute);
@@ -163,25 +154,22 @@ function QueryEditor(props: QueryEditorProps) {
             queryId,
         });
 
-        props.setShowPreview(false);
+        dispatch(setShowPreview(false));
 
         // Don't save partial queries in history
         if (!text) {
-            const {queries, currentIndex} = history;
-            if (query !== queries[currentIndex]?.queryText) {
-                props.saveQueryToHistory(input, queryId);
+            if (query !== historyQueries[historyCurrentIndex]?.queryText) {
+                dispatch(saveQueryToHistory({queryText: input, queryId}));
             }
         }
         dispatchResultVisibilityState(PaneVisibilityActionTypes.triggerExpand);
     });
 
     const handleSettingsClick = () => {
-        props.setQueryAction('settings');
+        dispatch(setQueryAction('settings'));
     };
 
     const handleGetExplainQueryClick = useEventHandler(() => {
-        const {input} = executeQuery;
-
         setLastUsedQueryAction(QUERY_ACTIONS.explain);
 
         if (!isEqual(lastQueryExecutionSettings, querySettings)) {
@@ -199,7 +187,7 @@ function QueryEditor(props: QueryEditorProps) {
             queryId,
         });
 
-        props.setShowPreview(false);
+        dispatch(setShowPreview(false));
 
         dispatchResultVisibilityState(PaneVisibilityActionTypes.triggerExpand);
     });
@@ -269,7 +257,7 @@ function QueryEditor(props: QueryEditorProps) {
             contextMenuGroupId: CONTEXT_MENU_GROUP_ID,
             contextMenuOrder: 2,
             run: () => {
-                props.goToPreviousQuery();
+                dispatch(goToPreviousQuery());
             },
         });
         editor.addAction({
@@ -279,7 +267,7 @@ function QueryEditor(props: QueryEditorProps) {
             contextMenuGroupId: CONTEXT_MENU_GROUP_ID,
             contextMenuOrder: 3,
             run: () => {
-                props.goToNextQuery();
+                dispatch(goToNextQuery());
             },
         });
         editor.addAction({
@@ -287,13 +275,13 @@ function QueryEditor(props: QueryEditorProps) {
             label: i18n('action.save-query'),
             keybindings: [keybindings.saveQuery],
             run: () => {
-                props.setQueryAction('save');
+                dispatch(setQueryAction('save'));
             },
         });
     };
 
     const onChange = (newValue: string) => {
-        props.changeUserInput({input: newValue});
+        changeUserInput({input: newValue});
     };
 
     const onCollapseResultHandler = () => {
@@ -312,9 +300,9 @@ function QueryEditor(props: QueryEditorProps) {
             <QueryEditorControls
                 handleSendExecuteClick={handleSendExecuteClick}
                 onSettingsButtonClick={handleSettingsClick}
-                isLoading={Boolean(executeQuery.result?.isLoading)}
+                isLoading={Boolean(result?.isLoading)}
                 handleGetExplainQueryClick={handleGetExplainQueryClick}
-                disabled={!executeQuery.input}
+                disabled={!input}
                 highlightedAction={lastUsedQueryAction}
             />
         );
@@ -340,7 +328,7 @@ function QueryEditor(props: QueryEditorProps) {
                         <div className={b('monaco')}>
                             <MonacoEditor
                                 language={YQL_LANGUAGE_ID}
-                                value={executeQuery.input}
+                                value={input}
                                 options={editorOptions}
                                 onChange={onChange}
                                 editorDidMount={editorDidMount}
@@ -357,8 +345,8 @@ function QueryEditor(props: QueryEditorProps) {
                         onCollapseResultHandler={onCollapseResultHandler}
                         type={type}
                         theme={theme}
-                        key={executeQuery.result?.queryId}
-                        result={executeQuery.result}
+                        key={result?.queryId}
+                        result={result}
                         tenantName={tenantName}
                         path={path}
                         showPreview={showPreview}
@@ -371,25 +359,6 @@ function QueryEditor(props: QueryEditorProps) {
     );
 }
 
-const mapStateToProps = (state: RootState) => {
-    return {
-        executeQuery: state.executeQuery,
-        showPreview: state.schema.showPreview,
-    };
-};
-
-const mapDispatchToProps = {
-    saveQueryToHistory,
-    goToPreviousQuery,
-    goToNextQuery,
-    setShowPreview,
-    setTenantPath,
-    setQueryAction,
-    setQueryResult,
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(QueryEditor);
-
 interface ResultProps {
     resultVisibilityState: InitialPaneState;
     onExpandResultHandler: VoidFunction;
diff --git a/src/containers/Tenant/Query/SavedQueries/SavedQueries.tsx b/src/containers/Tenant/Query/SavedQueries/SavedQueries.tsx
index b641ca842..928c15424 100644
--- a/src/containers/Tenant/Query/SavedQueries/SavedQueries.tsx
+++ b/src/containers/Tenant/Query/SavedQueries/SavedQueries.tsx
@@ -20,6 +20,7 @@ import {setQueryTab} from '../../../../store/reducers/tenant/tenant';
 import type {SavedQuery} from '../../../../types/store/query';
 import {cn} from '../../../../utils/cn';
 import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks';
+import {useChangeInputWithConfirmation} from '../../../../utils/hooks/withConfirmation/useChangeInputWithConfirmation';
 import {MAX_QUERY_HEIGHT, QUERY_TABLE_SETTINGS} from '../../utils/constants';
 import i18n from '../i18n';
 import {useSavedQueries} from '../utils/useSavedQueries';
@@ -88,11 +89,16 @@ export const SavedQueries = ({changeUserInput}: SavedQueriesProps) => {
         setQueryNameToDelete('');
     };
 
-    const onQueryClick = (queryText: string, queryName: string) => {
-        changeUserInput({input: queryText});
-        dispatch(setQueryNameToEdit(queryName));
-        dispatch(setQueryTab(TENANT_QUERY_TABS_ID.newQuery));
-    };
+    const applyQueryClick = React.useCallback(
+        ({queryText, queryName}: {queryText: string; queryName: string}) => {
+            changeUserInput({input: queryText});
+            dispatch(setQueryNameToEdit(queryName));
+            dispatch(setQueryTab(TENANT_QUERY_TABS_ID.newQuery));
+        },
+        [changeUserInput, dispatch],
+    );
+
+    const onQueryClick = useChangeInputWithConfirmation(applyQueryClick);
 
     const onDeleteQueryClick = (queryName: string) => {
         return (event: React.MouseEvent) => {
@@ -154,7 +160,12 @@ export const SavedQueries = ({changeUserInput}: SavedQueriesProps) => {
                         settings={QUERY_TABLE_SETTINGS}
                         emptyDataMessage={i18n(filter ? 'history.empty-search' : 'saved.empty')}
                         rowClassName={() => b('row')}
-                        onRowClick={(row) => onQueryClick(row.body, row.name)}
+                        onRowClick={async (row) =>
+                            onQueryClick({
+                                queryText: row.body,
+                                queryName: row.name,
+                            })
+                        }
                         initialSortOrder={{
                             columnId: 'name',
                             order: DataTable.ASCENDING,
diff --git a/src/containers/Tenant/utils/newSQLQueryActions.ts b/src/containers/Tenant/utils/newSQLQueryActions.ts
index 3898160ca..0eb21fd0a 100644
--- a/src/containers/Tenant/utils/newSQLQueryActions.ts
+++ b/src/containers/Tenant/utils/newSQLQueryActions.ts
@@ -1,5 +1,3 @@
-import {changeUserInput} from '../../../store/reducers/executeQuery';
-
 import {
     addTableIndex,
     alterAsyncReplicationTemplate,
@@ -29,9 +27,9 @@ import {
     upsertQueryTemplate,
 } from './schemaQueryTemplates';
 
-export const bindActions = (dispatch: React.Dispatch<any>) => {
+export const bindActions = (changeUserInput: (input: string) => void) => {
     const inputQuery = (query: () => string) => () => {
-        dispatch(changeUserInput({input: query()}));
+        changeUserInput(query());
     };
 
     return {
diff --git a/src/containers/Tenant/utils/schemaActions.ts b/src/containers/Tenant/utils/schemaActions.ts
index 8fd4f2f15..40c948d3c 100644
--- a/src/containers/Tenant/utils/schemaActions.ts
+++ b/src/containers/Tenant/utils/schemaActions.ts
@@ -43,6 +43,7 @@ interface ActionsAdditionalEffects {
     getTableSchemaDataPromise?: (
         params: GetTableSchemaDataParams,
     ) => Promise<SchemaData[] | undefined>;
+    getConfirmation?: () => Promise<boolean>;
 }
 
 interface BindActionParams {
@@ -57,28 +58,41 @@ const bindActions = (
     dispatch: AppDispatch,
     additionalEffects: ActionsAdditionalEffects,
 ) => {
-    const {setActivePath, showCreateDirectoryDialog, getTableSchemaDataPromise} = additionalEffects;
+    const {setActivePath, showCreateDirectoryDialog, getTableSchemaDataPromise, getConfirmation} =
+        additionalEffects;
 
     const inputQuery = (tmpl: TemplateFn) => () => {
-        const pathType = nodeTableTypeToPathType[params.type];
-        const withTableData = [selectQueryTemplate, upsertQueryTemplate].includes(tmpl);
-
-        const userInputDataPromise =
-            withTableData && pathType && getTableSchemaDataPromise
-                ? getTableSchemaDataPromise({
-                      path: params.path,
-                      tenantName: params.tenantName,
-                      type: pathType,
-                  })
-                : Promise.resolve(undefined);
-
-        userInputDataPromise.then((tableData) => {
-            dispatch(changeUserInput({input: tmpl({...params, tableData})}));
-        });
-
-        dispatch(setTenantPage(TENANT_PAGES_IDS.query));
-        dispatch(setQueryTab(TENANT_QUERY_TABS_ID.newQuery));
-        setActivePath(params.path);
+        const applyInsert = () => {
+            const pathType = nodeTableTypeToPathType[params.type];
+            const withTableData = [selectQueryTemplate, upsertQueryTemplate].includes(tmpl);
+
+            const userInputDataPromise =
+                withTableData && pathType && getTableSchemaDataPromise
+                    ? getTableSchemaDataPromise({
+                          path: params.path,
+                          tenantName: params.tenantName,
+                          type: pathType,
+                      })
+                    : Promise.resolve(undefined);
+
+            userInputDataPromise.then((tableData) => {
+                dispatch(changeUserInput({input: tmpl({...params, tableData})}));
+            });
+
+            dispatch(setTenantPage(TENANT_PAGES_IDS.query));
+            dispatch(setQueryTab(TENANT_QUERY_TABS_ID.newQuery));
+            setActivePath(params.path);
+        };
+        if (getConfirmation) {
+            const confirmedPromise = getConfirmation();
+            confirmedPromise.then((confirmed) => {
+                if (confirmed) {
+                    applyInsert();
+                }
+            });
+        } else {
+            applyInsert();
+        }
     };
 
     return {
diff --git a/src/store/reducers/executeQuery.ts b/src/store/reducers/executeQuery.ts
index b58376567..10f87f573 100644
--- a/src/store/reducers/executeQuery.ts
+++ b/src/store/reducers/executeQuery.ts
@@ -1,35 +1,27 @@
-import type {Reducer} from '@reduxjs/toolkit';
+import {createSelector, createSlice} from '@reduxjs/toolkit';
+import type {PayloadAction} from '@reduxjs/toolkit';
 
 import {settingsManager} from '../../services/settings';
 import {TracingLevelNumber} from '../../types/api/query';
 import type {ExecuteActions} from '../../types/api/query';
 import {ResultType} from '../../types/store/executeQuery';
+import type {ExecuteQueryState, QueryInHistory, QueryResult} from '../../types/store/executeQuery';
 import type {
-    ExecuteQueryAction,
-    ExecuteQueryState,
-    ExecuteQueryStateSlice,
-    QueryInHistory,
-    QueryResult,
-} from '../../types/store/executeQuery';
-import type {QueryRequestParams, QuerySettings, QuerySyntax} from '../../types/store/query';
-import {QUERIES_HISTORY_KEY} from '../../utils/constants';
+    QueryRequestParams,
+    QuerySettings,
+    QuerySyntax,
+    SavedQuery,
+} from '../../types/store/query';
+import {QUERIES_HISTORY_KEY, SAVED_QUERIES_KEY} from '../../utils/constants';
 import {QUERY_SYNTAX, isQueryErrorResponse, parseQueryAPIExecuteResponse} from '../../utils/query';
 import {isNumeric} from '../../utils/utils';
+import type {RootState} from '../defaultStore';
 
 import {api} from './api';
+import {getSettingValue} from './settings/settings';
 
 const MAXIMUM_QUERIES_IN_HISTORY = 20;
 
-const CHANGE_USER_INPUT = 'query/CHANGE_USER_INPUT';
-const SET_QUERY_RESULT = 'query/SET_QUERY_RESULT';
-const SET_QUERY_TRACE_READY = 'query/SET_QUERY_TRACE_READY';
-const SAVE_QUERY_TO_HISTORY = 'query/SAVE_QUERY_TO_HISTORY';
-const UPDATE_QUERY_IN_HISTORY = 'query/UPDATE_QUERY_IN_HISTORY';
-const SET_QUERY_HISTORY_FILTER = 'query/SET_QUERY_HISTORY_FILTER';
-const GO_TO_PREVIOUS_QUERY = 'query/GO_TO_PREVIOUS_QUERY';
-const GO_TO_NEXT_QUERY = 'query/GO_TO_NEXT_QUERY';
-const SET_TENANT_PATH = 'query/SET_TENANT_PATH';
-
 const queriesHistoryInitial = settingsManager.readUserSettingsValue(
     QUERIES_HISTORY_KEY,
     [],
@@ -37,9 +29,9 @@ const queriesHistoryInitial = settingsManager.readUserSettingsValue(
 
 const sliceLimit = queriesHistoryInitial.length - MAXIMUM_QUERIES_IN_HISTORY;
 
-const initialState = {
-    loading: false,
+const initialState: ExecuteQueryState = {
     input: '',
+    changed: false,
     history: {
         queries: queriesHistoryInitial
             .slice(sliceLimit < 0 ? 0 : sliceLimit)
@@ -52,41 +44,31 @@ const initialState = {
     },
 };
 
-const executeQuery: Reducer<ExecuteQueryState, ExecuteQueryAction> = (
-    state = initialState,
-    action,
-) => {
-    switch (action.type) {
-        case CHANGE_USER_INPUT: {
-            return {
-                ...state,
-                input: action.data.input,
-            };
-        }
-
-        case SET_QUERY_TRACE_READY: {
+const slice = createSlice({
+    name: 'executeQuery',
+    initialState,
+    reducers: {
+        changeUserInput: (state, action: PayloadAction<{input: string}>) => {
+            state.input = action.payload.input;
+            state.changed = state.input !== action.payload.input;
+        },
+
+        setQueryChanged: (state, action: PayloadAction<boolean>) => {
+            state.changed = action.payload;
+        },
+        setQueryTraceReady: (state) => {
             if (state.result) {
-                return {
-                    ...state,
-                    result: {
-                        ...state.result,
-                        isTraceReady: true,
-                    },
-                };
+                state.result.isTraceReady = true;
             }
-
-            return state;
-        }
-
-        case SET_QUERY_RESULT: {
-            return {
-                ...state,
-                result: action.data,
-            };
-        }
-
-        case SAVE_QUERY_TO_HISTORY: {
-            const {queryText, queryId} = action.data;
+        },
+        setQueryResult: (state, action: PayloadAction<QueryResult | undefined>) => {
+            state.result = action.payload;
+        },
+        saveQueryToHistory: (
+            state,
+            action: PayloadAction<{queryText: string; queryId: string}>,
+        ) => {
+            const {queryText, queryId} = action.payload;
 
             const newQueries = [...state.history.queries, {queryText, queryId}].slice(
                 state.history.queries.length >= MAXIMUM_QUERIES_IN_HISTORY ? 1 : 0,
@@ -94,26 +76,25 @@ const executeQuery: Reducer<ExecuteQueryState, ExecuteQueryAction> = (
             settingsManager.setUserSettingsValue(QUERIES_HISTORY_KEY, newQueries);
             const currentIndex = newQueries.length - 1;
 
-            return {
-                ...state,
-                history: {
-                    queries: newQueries,
-                    currentIndex,
-                },
+            state.history = {
+                queries: newQueries,
+                currentIndex,
             };
-        }
-
-        case UPDATE_QUERY_IN_HISTORY: {
-            const {queryId, stats} = action.data;
+        },
+        updateQueryInHistory: (
+            state,
+            action: PayloadAction<{queryId: string; stats: QueryStats}>,
+        ) => {
+            const {queryId, stats} = action.payload;
 
             if (!stats) {
-                return state;
+                return;
             }
 
             const index = state.history.queries.findIndex((item) => item.queryId === queryId);
 
             if (index === -1) {
-                return state;
+                return;
             }
 
             const newQueries = [...state.history.queries];
@@ -126,73 +107,85 @@ const executeQuery: Reducer<ExecuteQueryState, ExecuteQueryAction> = (
 
             settingsManager.setUserSettingsValue(QUERIES_HISTORY_KEY, newQueries);
 
-            return {
-                ...state,
-                history: {
-                    ...state.history,
-                    queries: newQueries,
-                },
-            };
-        }
-
-        case GO_TO_PREVIOUS_QUERY: {
+            state.history.queries = newQueries;
+        },
+        goToPreviousQuery: (state) => {
             const currentIndex = state.history.currentIndex;
             if (currentIndex <= 0) {
-                return state;
+                return;
             }
             const newCurrentIndex = currentIndex - 1;
             const query = state.history.queries[newCurrentIndex];
-
-            return {
-                ...state,
-                history: {
-                    ...state.history,
-                    currentIndex: newCurrentIndex,
-                },
-                input: query.queryText,
-            };
-        }
-
-        case GO_TO_NEXT_QUERY: {
-            const lastIndexInHistory = state.history.queries.length - 1;
+            state.input = query.queryText;
+            state.history.currentIndex = newCurrentIndex;
+        },
+        goToNextQuery: (state) => {
             const currentIndex = state.history.currentIndex;
-            if (currentIndex >= lastIndexInHistory) {
-                return state;
+            if (currentIndex >= state.history.queries.length - 1) {
+                return;
             }
             const newCurrentIndex = currentIndex + 1;
             const query = state.history.queries[newCurrentIndex];
+            state.input = query.queryText;
+            state.history.currentIndex = newCurrentIndex;
+        },
+        setTenantPath: (state, action: PayloadAction<string>) => {
+            state.tenantPath = action.payload;
+        },
+        setQueryHistoryFilter: (state, action: PayloadAction<string>) => {
+            state.history.filter = action.payload;
+        },
+    },
+    selectors: {
+        selectQueriesHistoryFilter: (state) => state.history.filter || '',
+        selectTenantPath: (state) => state.tenantPath,
+        selectResult: (state) => state.result,
+        selectQueriesHistory: (state) => {
+            const items = state.history.queries;
+            const filter = state.history.filter?.toLowerCase();
+
+            return filter
+                ? items.filter((item) => item.queryText.toLowerCase().includes(filter))
+                : items;
+        },
+        selectUserInput: (state) => state.input,
+        selectQueriesHistoryCurrentIndex: (state) => state.history?.currentIndex,
+    },
+});
 
-            return {
-                ...state,
-                history: {
-                    ...state.history,
-                    currentIndex: newCurrentIndex,
-                },
-                input: query.queryText,
-            };
-        }
-
-        case SET_TENANT_PATH: {
-            return {
-                ...state,
-                tenantPath: action.data,
-            };
-        }
-
-        case SET_QUERY_HISTORY_FILTER: {
-            return {
-                ...state,
-                history: {
-                    ...state.history,
-                    filter: action.data.filter,
-                },
-            };
-        }
-
-        default:
-            return state;
-    }
-};
+export default slice.reducer;
+export const {
+    changeUserInput,
+    setQueryChanged,
+    setQueryTraceReady,
+    setQueryResult,
+    saveQueryToHistory,
+    updateQueryInHistory,
+    goToPreviousQuery,
+    goToNextQuery,
+    setTenantPath,
+    setQueryHistoryFilter,
+} = slice.actions;
+export const {
+    selectQueriesHistoryFilter,
+    selectQueriesHistoryCurrentIndex,
+    selectQueriesHistory,
+    selectTenantPath,
+    selectResult,
+    selectUserInput,
+} = slice.selectors;
+
+export const selectIsQuerySaved = createSelector(
+    (state: RootState) => state,
+    (state: RootState) => {
+        const savedQueries = (getSettingValue(state, SAVED_QUERIES_KEY) as SavedQuery[]) ?? [];
+        return (
+            savedQueries.some((query) => query.body === state.executeQuery.input) ||
+            state.executeQuery.history.queries[state.executeQuery.history.queries.length - 1]
+                ?.queryText === state.executeQuery.input
+        );
+    },
+);
 
 interface SendQueryParams extends QueryRequestParams {
     queryId: string;
@@ -280,7 +273,7 @@ export const executeQueryApi = api.injectEndpoints({
                         queryStats.endTime = now;
                     }
 
-                    dispatch(updateQueryInHistory(queryStats, queryId));
+                    dispatch(updateQueryInHistory({stats: queryStats, queryId}));
                     dispatch(
                         setQueryResult({
                             type: ResultType.EXECUTE,
@@ -307,70 +300,6 @@ export const executeQueryApi = api.injectEndpoints({
     overrideExisting: 'throw',
 });
 
-export const saveQueryToHistory = (queryText: string, queryId: string) => {
-    return {
-        type: SAVE_QUERY_TO_HISTORY,
-        data: {queryText, queryId},
-    } as const;
-};
-
-export function updateQueryInHistory(stats: QueryStats, queryId: string) {
-    return {
-        type: UPDATE_QUERY_IN_HISTORY,
-        data: {queryId, stats},
-    } as const;
-}
-
-export function setQueryResult(data?: QueryResult) {
-    return {
-        type: SET_QUERY_RESULT,
-        data,
-    } as const;
-}
-
-export function setQueryTraceReady() {
-    return {
-        type: SET_QUERY_TRACE_READY,
-    } as const;
-}
-
-export const goToPreviousQuery = () => {
-    return {
-        type: GO_TO_PREVIOUS_QUERY,
-    } as const;
-};
-
-export const goToNextQuery = () => {
-    return {
-        type: GO_TO_NEXT_QUERY,
-    } as const;
-};
-
-export const changeUserInput = ({input}: {input: string}) => {
-    return {
-        type: CHANGE_USER_INPUT,
-        data: {input},
-    } as const;
-};
-
-export const setTenantPath = (value: string) => {
-    return {
-        type: SET_TENANT_PATH,
-        data: value,
-    } as const;
-};
-
-export const selectQueriesHistoryFilter = (state: ExecuteQueryStateSlice): string => {
-    return state.executeQuery.history.filter || '';
-};
-
-export const selectQueriesHistory = (state: ExecuteQueryStateSlice): QueryInHistory[] => {
-    const items = state.executeQuery.history.queries;
-    const filter = state.executeQuery.history.filter?.toLowerCase();
-
-    return filter ? items.filter((item) => item.queryText.toLowerCase().includes(filter)) : items;
-};
-
 function getQueryInHistory(rawQuery: string | QueryInHistory) {
     if (typeof rawQuery === 'string') {
         return {
@@ -379,12 +308,3 @@ function getQueryInHistory(rawQuery: string | QueryInHistory) {
     }
     return rawQuery;
 }
-
-export const setQueryHistoryFilter = (filter: string) => {
-    return {
-        type: SET_QUERY_HISTORY_FILTER,
-        data: {filter},
-    } as const;
-};
-
-export default executeQuery;
diff --git a/src/store/reducers/queryActions/queryActions.ts b/src/store/reducers/queryActions/queryActions.ts
index 4dea0013b..dd661f8b2 100644
--- a/src/store/reducers/queryActions/queryActions.ts
+++ b/src/store/reducers/queryActions/queryActions.ts
@@ -14,7 +14,7 @@ const initialState: QueryActionsState = {
     savedQueriesFilter: '',
 };
 
-export const slice = createSlice({
+const slice = createSlice({
     name: 'queryActions',
     initialState,
     reducers: {
diff --git a/src/store/reducers/schema/schema.ts b/src/store/reducers/schema/schema.ts
index 1b07c12e5..d67f77174 100644
--- a/src/store/reducers/schema/schema.ts
+++ b/src/store/reducers/schema/schema.ts
@@ -1,14 +1,11 @@
 import React from 'react';
 
-import type {Reducer} from '@reduxjs/toolkit';
+import {createSlice} from '@reduxjs/toolkit';
+import type {PayloadAction} from '@reduxjs/toolkit';
 
 import type {TEvDescribeSchemeResult} from '../../../types/api/schema';
 import {api} from '../api';
 
-import type {SchemaAction, SchemaState} from './types';
-
-const SET_SHOW_PREVIEW = 'schema/SET_SHOW_PREVIEW';
-
 export const initialState = {
     loading: true,
     data: {},
@@ -16,27 +13,22 @@ export const initialState = {
     showPreview: false,
 };
 
-const schema: Reducer<SchemaState, SchemaAction> = (state = initialState, action) => {
-    switch (action.type) {
-        case SET_SHOW_PREVIEW: {
-            return {
-                ...state,
-                showPreview: action.data,
-            };
-        }
-        default:
-            return state;
-    }
-};
-
-export function setShowPreview(value: boolean) {
-    return {
-        type: SET_SHOW_PREVIEW,
-        data: value,
-    } as const;
-}
+const slice = createSlice({
+    name: 'schema',
+    initialState,
+    reducers: {
+        setShowPreview: (state, action: PayloadAction<boolean>) => {
+            state.showPreview = action.payload;
+        },
+    },
+    selectors: {
+        selectShowPreview: (state) => state.showPreview,
+    },
+});
 
-export default schema;
+export default slice.reducer;
+export const {setShowPreview} = slice.actions;
+export const {selectShowPreview} = slice.selectors;
 
 export const schemaApi = api.injectEndpoints({
     endpoints: (builder) => ({
diff --git a/src/types/store/executeQuery.ts b/src/types/store/executeQuery.ts
index 391a20ae3..394a6c3db 100644
--- a/src/types/store/executeQuery.ts
+++ b/src/types/store/executeQuery.ts
@@ -1,14 +1,3 @@
-import type {
-    changeUserInput,
-    goToNextQuery,
-    goToPreviousQuery,
-    saveQueryToHistory,
-    setQueryHistoryFilter,
-    setQueryResult,
-    setQueryTraceReady,
-    setTenantPath,
-    updateQueryInHistory,
-} from '../../store/reducers/executeQuery';
 import type {PreparedExplainResponse} from '../../store/reducers/explainQuery/types';
 
 import type {IQueryResult} from './query';
@@ -48,7 +37,8 @@ export type QueryResult = ExecuteQueryResult | ExplainQueryResult;
 
 export interface ExecuteQueryState {
     input: string;
-    result?: QueryResult;
+    result?: QueryResult & {isTraceReady?: boolean};
+    changed?: boolean;
     history: {
         // String type for backward compatibility
         queries: QueryInHistory[];
@@ -57,18 +47,3 @@ export interface ExecuteQueryState {
     };
     tenantPath?: string;
 }
-
-export type ExecuteQueryAction =
-    | ReturnType<typeof goToNextQuery>
-    | ReturnType<typeof goToPreviousQuery>
-    | ReturnType<typeof changeUserInput>
-    | ReturnType<typeof setQueryResult>
-    | ReturnType<typeof saveQueryToHistory>
-    | ReturnType<typeof updateQueryInHistory>
-    | ReturnType<typeof setTenantPath>
-    | ReturnType<typeof setQueryHistoryFilter>
-    | ReturnType<typeof setQueryTraceReady>;
-
-export interface ExecuteQueryStateSlice {
-    executeQuery: ExecuteQueryState;
-}
diff --git a/src/utils/hooks/withConfirmation/i18n/en.json b/src/utils/hooks/withConfirmation/i18n/en.json
new file mode 100644
index 000000000..871408c8e
--- /dev/null
+++ b/src/utils/hooks/withConfirmation/i18n/en.json
@@ -0,0 +1,4 @@
+{
+  "action_apply": "Proceed",
+  "context_unsaved-changes-warning": "You have unsaved changes in this query. Do you want to proceed?"
+}
diff --git a/src/utils/hooks/withConfirmation/i18n/index.ts b/src/utils/hooks/withConfirmation/i18n/index.ts
new file mode 100644
index 000000000..f5e852283
--- /dev/null
+++ b/src/utils/hooks/withConfirmation/i18n/index.ts
@@ -0,0 +1,7 @@
+import {registerKeysets} from '../../../i18n';
+
+import en from './en.json';
+
+const COMPONENT = 'ydb-change-input-confirmation';
+
+export default registerKeysets(COMPONENT, {en});
diff --git a/src/utils/hooks/withConfirmation/useChangeInputWithConfirmation.ts b/src/utils/hooks/withConfirmation/useChangeInputWithConfirmation.ts
new file mode 100644
index 000000000..ce170d49b
--- /dev/null
+++ b/src/utils/hooks/withConfirmation/useChangeInputWithConfirmation.ts
@@ -0,0 +1,39 @@
+import React from 'react';
+
+import NiceModal from '@ebay/nice-modal-react';
+
+import {useTypedSelector} from '..';
+import {CONFIRMATION_DIALOG} from '../../../components/ConfirmationDialog/ConfirmationDialog';
+import {selectIsQuerySaved} from '../../../store/reducers/executeQuery';
+
+import i18n from './i18n';
+
+export async function getConfirmation(): Promise<boolean> {
+    return await NiceModal.show(CONFIRMATION_DIALOG, {
+        id: CONFIRMATION_DIALOG,
+        caption: i18n('context_unsaved-changes-warning'),
+        textButtonApply: i18n('action_apply'),
+    });
+}
+
+export function changeInputWithConfirmation<T>(callback: (args: T) => void) {
+    return async (args: T) => {
+        const confirmed = await getConfirmation();
+        if (!confirmed) {
+            return;
+        }
+        callback(args);
+    };
+}
+
+export function useChangeInputWithConfirmation<T>(callback: (args: T) => void) {
+    const isQuerySaved = useTypedSelector(selectIsQuerySaved);
+    const callbackWithConfirmation = React.useMemo(
+        () => changeInputWithConfirmation<T>(callback),
+        [callback],
+    );
+    if (isQuerySaved) {
+        return callback;
+    }
+    return callbackWithConfirmation;
+}

From 04bde5051ac6d04011c7a4f9537c95612984510d Mon Sep 17 00:00:00 2001
From: Elena Makarova <el-makarova@yandex-team.ru>
Date: Thu, 14 Nov 2024 15:15:04 +0300
Subject: [PATCH 2/4] fix: should always ask confirmation is user input is
 truthy

---
 .../ObjectSummary/SchemaTree/SchemaTree.tsx   |  6 ++--
 src/store/reducers/executeQuery.ts            | 32 ++-----------------
 src/types/store/executeQuery.ts               |  1 -
 .../useChangeInputWithConfirmation.ts         |  6 ++--
 4 files changed, 9 insertions(+), 36 deletions(-)

diff --git a/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx b/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx
index cabc3b3bb..5a7214a70 100644
--- a/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx
+++ b/src/containers/Tenant/ObjectSummary/SchemaTree/SchemaTree.tsx
@@ -6,7 +6,7 @@ import React from 'react';
 import {NavigationTree} from 'ydb-ui-components';
 
 import {useCreateDirectoryFeatureAvailable} from '../../../../store/reducers/capabilities/hooks';
-import {selectIsQuerySaved} from '../../../../store/reducers/executeQuery';
+import {selectUserInput} from '../../../../store/reducers/executeQuery';
 import {schemaApi} from '../../../../store/reducers/schema/schema';
 import {tableSchemaDataApi} from '../../../../store/reducers/tableSchemaData';
 import type {GetTableSchemaDataParams} from '../../../../store/reducers/tableSchemaData';
@@ -39,7 +39,7 @@ export function SchemaTree(props: SchemaTreeProps) {
     const createDirectoryFeatureAvailable = useCreateDirectoryFeatureAvailable();
     const {rootPath, rootName, rootType, currentPath, onActivePathUpdate} = props;
     const dispatch = useTypedDispatch();
-    const isQuerySaved = useTypedSelector(selectIsQuerySaved);
+    const input = useTypedSelector(selectUserInput);
     const [getTableSchemaDataMutation] = tableSchemaDataApi.useGetTableSchemaDataMutation();
 
     const getTableSchemaDataPromise = React.useCallback(
@@ -151,7 +151,7 @@ export function SchemaTree(props: SchemaTreeProps) {
                             ? handleOpenCreateDirectoryDialog
                             : undefined,
                         getTableSchemaDataPromise,
-                        getConfirmation: isQuerySaved ? undefined : getConfirmation,
+                        getConfirmation: input ? getConfirmation : undefined,
                     },
                     rootPath,
                 )}
diff --git a/src/store/reducers/executeQuery.ts b/src/store/reducers/executeQuery.ts
index 10f87f573..4c92b919d 100644
--- a/src/store/reducers/executeQuery.ts
+++ b/src/store/reducers/executeQuery.ts
@@ -1,4 +1,4 @@
-import {createSelector, createSlice} from '@reduxjs/toolkit';
+import {createSlice} from '@reduxjs/toolkit';
 import type {PayloadAction} from '@reduxjs/toolkit';
 
 import {settingsManager} from '../../services/settings';
@@ -6,19 +6,12 @@ import {TracingLevelNumber} from '../../types/api/query';
 import type {ExecuteActions} from '../../types/api/query';
 import {ResultType} from '../../types/store/executeQuery';
 import type {ExecuteQueryState, QueryInHistory, QueryResult} from '../../types/store/executeQuery';
-import type {
-    QueryRequestParams,
-    QuerySettings,
-    QuerySyntax,
-    SavedQuery,
-} from '../../types/store/query';
-import {QUERIES_HISTORY_KEY, SAVED_QUERIES_KEY} from '../../utils/constants';
+import type {QueryRequestParams, QuerySettings, QuerySyntax} from '../../types/store/query';
+import {QUERIES_HISTORY_KEY} from '../../utils/constants';
 import {QUERY_SYNTAX, isQueryErrorResponse, parseQueryAPIExecuteResponse} from '../../utils/query';
 import {isNumeric} from '../../utils/utils';
-import type {RootState} from '../defaultStore';
 
 import {api} from './api';
-import {getSettingValue} from './settings/settings';
 
 const MAXIMUM_QUERIES_IN_HISTORY = 20;
 
@@ -31,7 +24,6 @@ const sliceLimit = queriesHistoryInitial.length - MAXIMUM_QUERIES_IN_HISTORY;
 
 const initialState: ExecuteQueryState = {
     input: '',
-    changed: false,
     history: {
         queries: queriesHistoryInitial
             .slice(sliceLimit < 0 ? 0 : sliceLimit)
@@ -50,11 +42,6 @@ const slice = createSlice({
     reducers: {
         changeUserInput: (state, action: PayloadAction<{input: string}>) => {
             state.input = action.payload.input;
-            state.changed = state.input !== action.payload.input;
-        },
-
-        setQueryChanged: (state, action: PayloadAction<boolean>) => {
-            state.changed = action.payload;
         },
         setQueryTraceReady: (state) => {
             if (state.result) {
@@ -156,7 +143,6 @@ const slice = createSlice({
 export default slice.reducer;
 export const {
     changeUserInput,
-    setQueryChanged,
     setQueryTraceReady,
     setQueryResult,
     saveQueryToHistory,
@@ -175,18 +161,6 @@ export const {
     selectUserInput,
 } = slice.selectors;
 
-export const selectIsQuerySaved = createSelector(
-    (state: RootState) => state,
-    (state: RootState) => {
-        const savedQueries = (getSettingValue(state, SAVED_QUERIES_KEY) as SavedQuery[]) ?? [];
-        return (
-            savedQueries.some((query) => query.body === state.executeQuery.input) ||
-            state.executeQuery.history.queries[state.executeQuery.history.queries.length - 1]
-                ?.queryText === state.executeQuery.input
-        );
-    },
-);
-
 interface SendQueryParams extends QueryRequestParams {
     queryId: string;
     querySettings?: Partial<QuerySettings>;
diff --git a/src/types/store/executeQuery.ts b/src/types/store/executeQuery.ts
index 394a6c3db..b8c3a0f63 100644
--- a/src/types/store/executeQuery.ts
+++ b/src/types/store/executeQuery.ts
@@ -38,7 +38,6 @@ export type QueryResult = ExecuteQueryResult | ExplainQueryResult;
 export interface ExecuteQueryState {
     input: string;
     result?: QueryResult & {isTraceReady?: boolean};
-    changed?: boolean;
     history: {
         // String type for backward compatibility
         queries: QueryInHistory[];
diff --git a/src/utils/hooks/withConfirmation/useChangeInputWithConfirmation.ts b/src/utils/hooks/withConfirmation/useChangeInputWithConfirmation.ts
index ce170d49b..8ac958b89 100644
--- a/src/utils/hooks/withConfirmation/useChangeInputWithConfirmation.ts
+++ b/src/utils/hooks/withConfirmation/useChangeInputWithConfirmation.ts
@@ -4,7 +4,7 @@ import NiceModal from '@ebay/nice-modal-react';
 
 import {useTypedSelector} from '..';
 import {CONFIRMATION_DIALOG} from '../../../components/ConfirmationDialog/ConfirmationDialog';
-import {selectIsQuerySaved} from '../../../store/reducers/executeQuery';
+import {selectUserInput} from '../../../store/reducers/executeQuery';
 
 import i18n from './i18n';
 
@@ -27,12 +27,12 @@ export function changeInputWithConfirmation<T>(callback: (args: T) => void) {
 }
 
 export function useChangeInputWithConfirmation<T>(callback: (args: T) => void) {
-    const isQuerySaved = useTypedSelector(selectIsQuerySaved);
+    const userInput = useTypedSelector(selectUserInput);
     const callbackWithConfirmation = React.useMemo(
         () => changeInputWithConfirmation<T>(callback),
         [callback],
     );
-    if (isQuerySaved) {
+    if (!userInput) {
         return callback;
     }
     return callbackWithConfirmation;

From d291d86a61d0f8bd9b2a3ae6fd6c594165d7466d Mon Sep 17 00:00:00 2001
From: Elena Makarova <el-makarova@yandex-team.ru>
Date: Thu, 14 Nov 2024 15:37:21 +0300
Subject: [PATCH 3/4] fix: review

---
 src/containers/Tenant/Query/SavedQueries/SavedQueries.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/containers/Tenant/Query/SavedQueries/SavedQueries.tsx b/src/containers/Tenant/Query/SavedQueries/SavedQueries.tsx
index 928c15424..6f91e8746 100644
--- a/src/containers/Tenant/Query/SavedQueries/SavedQueries.tsx
+++ b/src/containers/Tenant/Query/SavedQueries/SavedQueries.tsx
@@ -160,7 +160,7 @@ export const SavedQueries = ({changeUserInput}: SavedQueriesProps) => {
                         settings={QUERY_TABLE_SETTINGS}
                         emptyDataMessage={i18n(filter ? 'history.empty-search' : 'saved.empty')}
                         rowClassName={() => b('row')}
-                        onRowClick={async (row) =>
+                        onRowClick={(row) =>
                             onQueryClick({
                                 queryText: row.body,
                                 queryName: row.name,

From f74ad023f68c2b4295bb263c773fdba7456d349c Mon Sep 17 00:00:00 2001
From: Elena Makarova <el-makarova@yandex-team.ru>
Date: Thu, 14 Nov 2024 16:20:42 +0300
Subject: [PATCH 4/4] fix: review

---
 src/utils/hooks/withConfirmation/i18n/en.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/utils/hooks/withConfirmation/i18n/en.json b/src/utils/hooks/withConfirmation/i18n/en.json
index 871408c8e..266e9abde 100644
--- a/src/utils/hooks/withConfirmation/i18n/en.json
+++ b/src/utils/hooks/withConfirmation/i18n/en.json
@@ -1,4 +1,4 @@
 {
   "action_apply": "Proceed",
-  "context_unsaved-changes-warning": "You have unsaved changes in this query. Do you want to proceed?"
+  "context_unsaved-changes-warning": "You have unsaved changes in query editor. Do you want to proceed?"
 }