diff --git a/package-lock.json b/package-lock.json
index 0ad7f8379c..5e2424fe35 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 2aa4c5cd11..96553e1346 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 0000000000..44fce835c0
--- /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 0000000000..0b4f47e689
--- /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 (
+
+ );
+}
+
+export const ConfirmationDialogNiceModal = NiceModal.create(
+ (props: ConfirmationDialogNiceModalProps) => {
+ const modal = NiceModal.useModal();
+
+ const handleClose = () => {
+ modal.hide();
+ modal.remove();
+ };
+
+ return (
+ {
+ 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 0000000000..88c90714ec
--- /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 0000000000..423b490754
--- /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 3193b76c76..400d1fd182 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({
-
- {children}
-
+
+
+ {children}
+
+
diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx
index c72044c30c..be722ce394 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 65e90ca349..9d5fd19ad2 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 01cc42db9c..5a7214a709 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 {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';
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 input = useTypedSelector(selectUserInput);
const [getTableSchemaDataMutation] = tableSchemaDataApi.useGetTableSchemaDataMutation();
const getTableSchemaDataPromise = React.useCallback(
@@ -144,6 +151,7 @@ export function SchemaTree(props: SchemaTreeProps) {
? handleOpenCreateDirectoryDialog
: undefined,
getTableSchemaDataPromise,
+ getConfirmation: input ? getConfirmation : undefined,
},
rootPath,
)}
diff --git a/src/containers/Tenant/Query/NewSQL/NewSQL.tsx b/src/containers/Tenant/Query/NewSQL/NewSQL.tsx
index cfc12c154c..51963b2453 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 6b611a2bb1..fdf7d32717 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 eb188451e4..2e365bf6d9 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) => void;
- goToPreviousQuery: (...args: Parameters) => void;
- setTenantPath: (...args: Parameters) => void;
- setQueryAction: (...args: Parameters) => void;
- setQueryResult: (...args: Parameters) => void;
- executeQuery: ExecuteQueryState;
theme: string;
type?: EPathType;
- showPreview: boolean;
- setShowPreview: (...args: Parameters) => void;
- saveQueryToHistory: (...args: Parameters) => 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) {
);
@@ -340,7 +328,7 @@ function QueryEditor(props: QueryEditorProps) {
{
- 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 b641ca842b..6f91e8746d 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={(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 3898160ca2..0eb21fd0a6 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) => {
+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 8fd4f2f156..40c948d3c8 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;
+ getConfirmation?: () => Promise;
}
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 b58376567c..4c92b919d3 100644
--- a/src/store/reducers/executeQuery.ts
+++ b/src/store/reducers/executeQuery.ts
@@ -1,16 +1,11 @@
-import type {Reducer} from '@reduxjs/toolkit';
+import {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 {
- ExecuteQueryAction,
- ExecuteQueryState,
- ExecuteQueryStateSlice,
- QueryInHistory,
- QueryResult,
-} from '../../types/store/executeQuery';
+import type {ExecuteQueryState, QueryInHistory, QueryResult} from '../../types/store/executeQuery';
import type {QueryRequestParams, QuerySettings, QuerySyntax} from '../../types/store/query';
import {QUERIES_HISTORY_KEY} from '../../utils/constants';
import {QUERY_SYNTAX, isQueryErrorResponse, parseQueryAPIExecuteResponse} from '../../utils/query';
@@ -20,16 +15,6 @@ import {api} from './api';
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,8 +22,7 @@ const queriesHistoryInitial = settingsManager.readUserSettingsValue(
const sliceLimit = queriesHistoryInitial.length - MAXIMUM_QUERIES_IN_HISTORY;
-const initialState = {
- loading: false,
+const initialState: ExecuteQueryState = {
input: '',
history: {
queries: queriesHistoryInitial
@@ -52,41 +36,26 @@ const initialState = {
},
};
-const executeQuery: Reducer = (
- 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;
+ },
+ 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) => {
+ 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 +63,25 @@ const executeQuery: Reducer = (
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 +94,72 @@ const executeQuery: Reducer = (
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) => {
+ state.tenantPath = action.payload;
+ },
+ setQueryHistoryFilter: (state, action: PayloadAction) => {
+ 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,
+ setQueryTraceReady,
+ setQueryResult,
+ saveQueryToHistory,
+ updateQueryInHistory,
+ goToPreviousQuery,
+ goToNextQuery,
+ setTenantPath,
+ setQueryHistoryFilter,
+} = slice.actions;
+export const {
+ selectQueriesHistoryFilter,
+ selectQueriesHistoryCurrentIndex,
+ selectQueriesHistory,
+ selectTenantPath,
+ selectResult,
+ selectUserInput,
+} = slice.selectors;
interface SendQueryParams extends QueryRequestParams {
queryId: string;
@@ -280,7 +247,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 +274,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 +282,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 4dea0013be..dd661f8b23 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 1b07c12e5c..d67f771745 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 = (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) => {
+ 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 391a20ae30..b8c3a0f632 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,7 @@ export type QueryResult = ExecuteQueryResult | ExplainQueryResult;
export interface ExecuteQueryState {
input: string;
- result?: QueryResult;
+ result?: QueryResult & {isTraceReady?: boolean};
history: {
// String type for backward compatibility
queries: QueryInHistory[];
@@ -57,18 +46,3 @@ export interface ExecuteQueryState {
};
tenantPath?: string;
}
-
-export type ExecuteQueryAction =
- | ReturnType
- | ReturnType
- | ReturnType
- | ReturnType
- | ReturnType
- | ReturnType
- | ReturnType
- | ReturnType
- | ReturnType;
-
-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 0000000000..266e9abde2
--- /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 query editor. 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 0000000000..f5e8522833
--- /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 0000000000..8ac958b89d
--- /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 {selectUserInput} from '../../../store/reducers/executeQuery';
+
+import i18n from './i18n';
+
+export async function getConfirmation(): Promise {
+ return await NiceModal.show(CONFIRMATION_DIALOG, {
+ id: CONFIRMATION_DIALOG,
+ caption: i18n('context_unsaved-changes-warning'),
+ textButtonApply: i18n('action_apply'),
+ });
+}
+
+export function changeInputWithConfirmation(callback: (args: T) => void) {
+ return async (args: T) => {
+ const confirmed = await getConfirmation();
+ if (!confirmed) {
+ return;
+ }
+ callback(args);
+ };
+}
+
+export function useChangeInputWithConfirmation(callback: (args: T) => void) {
+ const userInput = useTypedSelector(selectUserInput);
+ const callbackWithConfirmation = React.useMemo(
+ () => changeInputWithConfirmation(callback),
+ [callback],
+ );
+ if (!userInput) {
+ return callback;
+ }
+ return callbackWithConfirmation;
+}