diff --git a/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.scss b/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.scss index 1d985ec89..13447ab35 100644 --- a/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.scss +++ b/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.scss @@ -9,11 +9,14 @@ @include flex-container(); @include table-styles; + &__table-row { + cursor: pointer; + } + &__query { overflow: hidden; flex-grow: 1; - cursor: pointer; white-space: pre; text-overflow: ellipsis; } diff --git a/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx b/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx index a10ad9ca2..46e0b0df8 100644 --- a/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx +++ b/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx @@ -3,10 +3,13 @@ import block from 'bem-cn-lite'; import DataTable, {Column} from '@gravity-ui/react-data-table'; +import type {QueryInHistory} from '../../../../types/store/executeQuery'; import {TruncatedQuery} from '../../../../components/TruncatedQuery/TruncatedQuery'; import {setQueryTab} from '../../../../store/reducers/tenant/tenant'; +import {selectQueriesHistory} from '../../../../store/reducers/executeQuery'; import {TENANT_QUERY_TABS_ID} from '../../../../store/reducers/tenant/constants'; -import {useTypedSelector} from '../../../../utils/hooks'; +import {useQueryModes, useTypedSelector} from '../../../../utils/hooks'; +import {QUERY_MODES, QUERY_MODES_TITLES, QUERY_SYNTAX} from '../../../../utils/query'; import {MAX_QUERY_HEIGHT, QUERY_TABLE_SETTINGS} from '../../utils/constants'; import i18n from '../i18n'; @@ -22,24 +25,51 @@ interface QueriesHistoryProps { function QueriesHistory({changeUserInput}: QueriesHistoryProps) { const dispatch = useDispatch(); - const queriesHistory = useTypedSelector((state) => state.executeQuery.history.queries) ?? []; + const [queryMode, setQueryMode] = useQueryModes(); + + const queriesHistory = useTypedSelector(selectQueriesHistory); const reversedHistory = [...queriesHistory].reverse(); - const onQueryClick = (queryText: string) => { - changeUserInput({input: queryText}); - dispatch(setQueryTab(TENANT_QUERY_TABS_ID.newQuery)); + const onQueryClick = (query: QueryInHistory) => { + let isQueryModeSet = true; + + if (query.syntax === QUERY_SYNTAX.pg && queryMode !== QUERY_MODES.pg) { + isQueryModeSet = setQueryMode( + QUERY_MODES.pg, + i18n('history.cannot-set-mode', {mode: QUERY_MODES_TITLES[QUERY_MODES.pg]}), + ); + } else if (query.syntax !== QUERY_SYNTAX.pg && queryMode === QUERY_MODES.pg) { + // Set query mode for queries with yql syntax + isQueryModeSet = setQueryMode(QUERY_MODES.script); + } + + if (isQueryModeSet) { + changeUserInput({input: query.queryText}); + dispatch(setQueryTab(TENANT_QUERY_TABS_ID.newQuery)); + } }; - const columns: Column[] = [ + const columns: Column[] = [ { name: 'queryText', header: 'Query Text', - render: ({row: query}) => ( -
- -
- ), + render: ({row}) => { + return ( +
+ +
+ ); + }, + sortable: false, + }, + { + name: 'syntax', + header: 'Syntax', + render: ({row}) => { + return row.syntax === QUERY_SYNTAX.pg ? 'PostgreSQL' : 'YQL'; + }, sortable: false, + width: 200, }, ]; @@ -52,6 +82,7 @@ function QueriesHistory({changeUserInput}: QueriesHistoryProps) { settings={QUERY_TABLE_SETTINGS} emptyDataMessage={i18n('history.empty')} onRowClick={(row) => onQueryClick(row)} + rowClassName={() => b('table-row')} /> ); diff --git a/src/containers/Tenant/Query/QueryEditor/QueryEditor.js b/src/containers/Tenant/Query/QueryEditor/QueryEditor.js index c8606bfac..6b2166f24 100644 --- a/src/containers/Tenant/Query/QueryEditor/QueryEditor.js +++ b/src/containers/Tenant/Query/QueryEditor/QueryEditor.js @@ -264,7 +264,7 @@ function QueryEditor(props) { const {queries, currentIndex} = history; if (input !== queries[currentIndex]) { - saveQueryToHistory(input); + saveQueryToHistory(input, mode); } dispatchResultVisibilityState(PaneVisibilityActionTypes.triggerExpand); }; diff --git a/src/containers/Tenant/Query/QueryEditorControls/QueryEditorControls.tsx b/src/containers/Tenant/Query/QueryEditorControls/QueryEditorControls.tsx index 4a1457c21..52584d8f0 100644 --- a/src/containers/Tenant/Query/QueryEditorControls/QueryEditorControls.tsx +++ b/src/containers/Tenant/Query/QueryEditorControls/QueryEditorControls.tsx @@ -4,7 +4,7 @@ import {Button, ButtonView, DropdownMenu} from '@gravity-ui/uikit'; import {useMemo} from 'react'; import type {QueryAction, QueryMode} from '../../../../types/store/query'; -import {QUERY_MODES} from '../../../../utils/query'; +import {QUERY_MODES, QUERY_MODES_TITLES} from '../../../../utils/query'; import {Icon} from '../../../../components/Icon'; import {LabelWithPopover} from '../../../../components/LabelWithPopover'; @@ -21,32 +21,36 @@ const b = block('ydb-query-editor-controls'); const OldQueryModeSelectorOptions = { [QUERY_MODES.script]: { - title: 'YQL Script', + title: QUERY_MODES_TITLES[QUERY_MODES.script], description: i18n('method-description.script'), }, [QUERY_MODES.scan]: { - title: 'Scan', + title: QUERY_MODES_TITLES[QUERY_MODES.scan], description: i18n('method-description.scan'), }, } as const; const QueryModeSelectorOptions = { [QUERY_MODES.script]: { - title: 'YQL Script', + title: QUERY_MODES_TITLES[QUERY_MODES.script], description: i18n('method-description.script'), }, [QUERY_MODES.scan]: { - title: 'Scan', + title: QUERY_MODES_TITLES[QUERY_MODES.scan], description: i18n('method-description.scan'), }, [QUERY_MODES.data]: { - title: 'Data', + title: QUERY_MODES_TITLES[QUERY_MODES.data], description: i18n('method-description.data'), }, [QUERY_MODES.query]: { - title: 'YQL - QueryService', + title: QUERY_MODES_TITLES[QUERY_MODES.query], description: i18n('method-description.query'), }, + [QUERY_MODES.pg]: { + title: QUERY_MODES_TITLES[QUERY_MODES.pg], + description: i18n('method-description.pg'), + }, } as const; interface QueryEditorControlsProps { diff --git a/src/containers/Tenant/Query/i18n/en.json b/src/containers/Tenant/Query/i18n/en.json index 6c94862fa..dc77f6046 100644 --- a/src/containers/Tenant/Query/i18n/en.json +++ b/src/containers/Tenant/Query/i18n/en.json @@ -8,6 +8,8 @@ "history.empty": "History is empty", "saved.empty": "There are no saved queries", + "history.cannot-set-mode": "This query is available only with '{{mode}}' query mode. You need to turn in additional query modes in settings to enable it", + "delete-dialog.header": "Delete query", "delete-dialog.question": "Are you sure you want to delete query", "delete-dialog.delete": "Delete", @@ -21,6 +23,7 @@ "method-description.scan": "Read-only queries, potentially reading a lot of data.\nAPI call: table.ExecuteScan", "method-description.data": "DML queries for changing and fetching data in serialization mode.\nAPI call: table.executeDataQuery", "method-description.query": "Any query. An experimental API call supposed to replace all existing methods.\nAPI Call: query.ExecuteScript", + "method-description.pg": "Queries in postgresql syntax.\nAPI call: query.ExecuteScript", "query-duration.description": "Duration of server-side query execution" } diff --git a/src/containers/Tenant/Query/i18n/ru.json b/src/containers/Tenant/Query/i18n/ru.json index cabc626aa..2d6cc66c8 100644 --- a/src/containers/Tenant/Query/i18n/ru.json +++ b/src/containers/Tenant/Query/i18n/ru.json @@ -8,6 +8,8 @@ "history.empty": "История пуста", "saved.empty": "Нет сохраненных запросов", + "history.cannot-set-mode": "Этот запрос доступен только в режиме '{{mode}}'. Вам необходимо включить дополнительные режимы выполнения запросов в настройках", + "delete-dialog.header": "Удалить запрос", "delete-dialog.question": "Вы уверены что хотите удалить запрос", "delete-dialog.delete": "Удалить", @@ -21,6 +23,7 @@ "method-description.scan": "Только читающие запросы, потенциально читающие много данных.\nAPI call: table.ExecuteScan", "method-description.data": "DML-запросы для изменения и выборки данных в режиме изоляции Serializable.\nAPI call: table.executeDataQuery", "method-description.query": "Любые запросы. Экспериментальный перспективный метод, который в будущем заменит все остальные.\nAPI call: query.ExecuteScript", + "method-description.pg": "Запросы в синтаксисе postgresql.\nAPI call: query.ExecuteScript", "query-duration.description": "Время выполнения запроса на стороне сервера" } diff --git a/src/services/api.ts b/src/services/api.ts index b7114f14c..9db3fc849 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -28,6 +28,7 @@ import type {DescribeTopicResult} from '../types/api/topic'; import type {TEvPDiskStateResponse} from '../types/api/pdisk'; import type {TEvVDiskStateResponse} from '../types/api/vdisk'; import type {TUserToken} from '../types/api/whoami'; +import type {QuerySyntax} from '../types/store/query'; import type {ComputeApiRequestParams, NodesApiRequestParams} from '../store/reducers/nodes/types'; import type {StorageApiRequestParams} from '../store/reducers/storage/types'; @@ -281,17 +282,15 @@ export class YdbEmbeddedAPI extends AxiosWrapper { } sendQuery( { - query, - database, - action, - stats, schema, + ...params }: { query?: string; database?: string; action?: Action; stats?: string; schema?: Schema; + syntax?: QuerySyntax; }, {concurrentId}: AxiosOptions = {}, ) { @@ -303,12 +302,7 @@ export class YdbEmbeddedAPI extends AxiosWrapper { this.getPath( `/viewer/json/query?timeout=${backendTimeout}${schema ? `&schema=${schema}` : ''}`, ), - { - query, - database, - action, - stats, - }, + params, {}, { concurrentId, @@ -320,6 +314,7 @@ export class YdbEmbeddedAPI extends AxiosWrapper { query: string, database: string, action: Action, + syntax?: QuerySyntax, ) { return this.post>( this.getPath('/viewer/json/query'), @@ -327,6 +322,7 @@ export class YdbEmbeddedAPI extends AxiosWrapper { query, database, action: action || 'explain', + syntax, timeout: 600000, }, {}, diff --git a/src/store/reducers/executeQuery.ts b/src/store/reducers/executeQuery.ts index 1ff0e6876..f0c224b7a 100644 --- a/src/store/reducers/executeQuery.ts +++ b/src/store/reducers/executeQuery.ts @@ -4,12 +4,14 @@ import type {ExecuteActions} from '../../types/api/query'; import type { ExecuteQueryAction, ExecuteQueryState, + ExecuteQueryStateSlice, MonacoHotKeyAction, + QueryInHistory, } from '../../types/store/executeQuery'; -import type {QueryRequestParams, QueryMode} from '../../types/store/query'; +import type {QueryRequestParams, QueryMode, QuerySyntax} from '../../types/store/query'; import {getValueFromLS, parseJson} from '../../utils/utils'; import {QUERIES_HISTORY_KEY} from '../../utils/constants'; -import {parseQueryAPIExecuteResponse} from '../../utils/query'; +import {QUERY_MODES, QUERY_SYNTAX, parseQueryAPIExecuteResponse} from '../../utils/query'; import {parseQueryError} from '../../utils/error'; import '../../services/api'; @@ -87,8 +89,12 @@ const executeQuery: Reducer = ( } case SAVE_QUERY_TO_HISTORY: { - const query = action.data; - const newQueries = [...state.history.queries, query].slice( + const queryText = action.data.queryText; + + // Do not save explicit yql syntax value for easier further support (use yql by default) + const syntax = action.data.mode === QUERY_MODES.pg ? QUERY_SYNTAX.pg : undefined; + + const newQueries = [...state.history.queries, {queryText, syntax}].slice( state.history.queries.length >= MAXIMUM_QUERIES_IN_HISTORY ? 1 : 0, ); window.localStorage.setItem(QUERIES_HISTORY_KEY, JSON.stringify(newQueries)); @@ -151,7 +157,15 @@ interface SendQueryParams extends QueryRequestParams { } export const sendExecuteQuery = ({query, database, mode}: SendQueryParams) => { - const action: ExecuteActions = mode ? `execute-${mode}` : 'execute'; + let action: ExecuteActions = 'execute'; + let syntax: QuerySyntax = QUERY_SYNTAX.yql; + + if (mode === 'pg') { + action = 'execute-query'; + syntax = QUERY_SYNTAX.pg; + } else if (mode) { + action = `execute-${mode}`; + } return createApiRequest({ request: window.api.sendQuery({ @@ -159,6 +173,7 @@ export const sendExecuteQuery = ({query, database, mode}: SendQueryParams) => { query, database, action, + syntax, stats: 'profile', }), actions: SEND_QUERY, @@ -166,10 +181,10 @@ export const sendExecuteQuery = ({query, database, mode}: SendQueryParams) => { }); }; -export const saveQueryToHistory = (query: string) => { +export const saveQueryToHistory = (queryText: string, mode: QueryMode) => { return { type: SAVE_QUERY_TO_HISTORY, - data: query, + data: {queryText, mode}, } as const; }; @@ -206,4 +221,15 @@ export const setTenantPath = (value: string) => { } as const; }; +export const selectQueriesHistory = (state: ExecuteQueryStateSlice): QueryInHistory[] => { + return state.executeQuery.history.queries.map((rawQuery) => { + if (typeof rawQuery === 'string') { + return { + queryText: rawQuery, + }; + } + return rawQuery; + }); +}; + export default executeQuery; diff --git a/src/store/reducers/explainQuery.ts b/src/store/reducers/explainQuery.ts index b566df6a6..6c629e95e 100644 --- a/src/store/reducers/explainQuery.ts +++ b/src/store/reducers/explainQuery.ts @@ -9,10 +9,10 @@ import type { ExplainQueryState, PreparedExplainResponse, } from '../../types/store/explainQuery'; -import type {QueryRequestParams, QueryMode} from '../../types/store/query'; +import type {QueryRequestParams, QueryMode, QuerySyntax} from '../../types/store/query'; import {preparePlan} from '../../utils/prepareQueryExplain'; -import {parseQueryAPIExplainResponse, parseQueryExplainPlan} from '../../utils/query'; +import {QUERY_SYNTAX, parseQueryAPIExplainResponse, parseQueryExplainPlan} from '../../utils/query'; import {parseQueryError} from '../../utils/error'; import {createRequestActionTypes, createApiRequest} from '../utils'; @@ -103,10 +103,18 @@ interface ExplainQueryParams extends QueryRequestParams { } export const getExplainQuery = ({query, database, mode}: ExplainQueryParams) => { - const action: ExplainActions = mode ? `explain-${mode}` : 'explain'; + let action: ExplainActions = 'explain'; + let syntax: QuerySyntax = QUERY_SYNTAX.yql; + + if (mode === 'pg') { + action = 'explain-query'; + syntax = QUERY_SYNTAX.pg; + } else if (mode) { + action = `explain-${mode}`; + } return createApiRequest({ - request: window.api.getExplainQuery(query, database, action), + request: window.api.getExplainQuery(query, database, action, syntax), actions: GET_EXPLAIN_QUERY, dataHandler: (response): PreparedExplainResponse => { const {plan: rawPlan, ast} = parseQueryAPIExplainResponse(response); diff --git a/src/types/store/executeQuery.ts b/src/types/store/executeQuery.ts index cda0d96ce..b65ba4b5b 100644 --- a/src/types/store/executeQuery.ts +++ b/src/types/store/executeQuery.ts @@ -14,11 +14,17 @@ import type {IQueryResult, QueryError, QueryErrorResponse} from './query'; export type MonacoHotKeyAction = ValueOf; +export interface QueryInHistory { + queryText: string; + syntax?: string; +} + export interface ExecuteQueryState { loading: boolean; input: string; history: { - queries: string[]; + // String type for backward compatibility + queries: (QueryInHistory | string)[]; currentIndex: number; }; monacoHotKey: null | MonacoHotKeyAction; @@ -38,3 +44,7 @@ export type ExecuteQueryAction = | ReturnType | ReturnType | ReturnType; + +export interface ExecuteQueryStateSlice { + executeQuery: ExecuteQueryState; +} diff --git a/src/types/store/query.ts b/src/types/store/query.ts index 166a4537a..63e96dd0e 100644 --- a/src/types/store/query.ts +++ b/src/types/store/query.ts @@ -1,4 +1,4 @@ -import {QUERY_ACTIONS, QUERY_MODES} from '../../utils/query'; +import {QUERY_ACTIONS, QUERY_MODES, QUERY_SYNTAX} from '../../utils/query'; import type {IResponseError, NetworkError} from '../api/error'; import type { @@ -29,6 +29,7 @@ export type QueryError = NetworkError | QueryErrorResponse; export type QueryAction = ValueOf; export type QueryMode = ValueOf; +export type QuerySyntax = ValueOf; export interface SavedQuery { name: string; diff --git a/src/utils/hooks/i18n/en.json b/src/utils/hooks/i18n/en.json index d7533f405..b01e65e1d 100644 --- a/src/utils/hooks/i18n/en.json +++ b/src/utils/hooks/i18n/en.json @@ -1,3 +1,3 @@ { - "useQueryModes.queryModeCannotBeSet": "Query mode \"{{mode}}\" cannot be set. You need to turn in additional query modes in settings to enable it" + "useQueryModes.queryModeCannotBeSet": "Query mode '{{mode}}' cannot be set. You need to turn in additional query modes in settings to enable it" } diff --git a/src/utils/hooks/i18n/ru.json b/src/utils/hooks/i18n/ru.json index a2a5fdfb9..fb852a9bd 100644 --- a/src/utils/hooks/i18n/ru.json +++ b/src/utils/hooks/i18n/ru.json @@ -1,3 +1,3 @@ { - "useQueryModes.queryModeCannotBeSet": "Режим выполнения запроса \"{{mode}}\" недоступен. Вам необходимо включить дополнительные режимы выполнения запросов в настройках" + "useQueryModes.queryModeCannotBeSet": "Режим выполнения запроса '{{mode}}' недоступен. Вам необходимо включить дополнительные режимы выполнения запросов в настройках" } diff --git a/src/utils/hooks/useQueryModes.ts b/src/utils/hooks/useQueryModes.ts index 57680e7b2..cc5e0a26d 100644 --- a/src/utils/hooks/useQueryModes.ts +++ b/src/utils/hooks/useQueryModes.ts @@ -1,6 +1,6 @@ import type {QueryMode} from '../../types/store/query'; import {ENABLE_ADDITIONAL_QUERY_MODES, QUERY_INITIAL_MODE_KEY} from '../constants'; -import {isNewQueryMode} from '../query'; +import {QUERY_MODES_TITLES, isNewQueryMode} from '../query'; import createToast from '../createToast'; import {useSetting} from './useSetting'; import i18n from './i18n'; @@ -18,7 +18,9 @@ export const useQueryModes = (): [QueryMode, SetQueryModeIfAvailable] => { if (isNewQueryMode(value) && !enableAdditionalQueryModes) { createToast({ name: 'QueryModeCannotBeSet', - title: errorMessage ?? i18n('useQueryModes.queryModeCannotBeSet', {mode: value}), + title: + errorMessage ?? + i18n('useQueryModes.queryModeCannotBeSet', {mode: QUERY_MODES_TITLES[value]}), type: 'error', }); diff --git a/src/utils/query.ts b/src/utils/query.ts index b02b457f3..7ff3a19bc 100644 --- a/src/utils/query.ts +++ b/src/utils/query.ts @@ -19,6 +19,20 @@ export const QUERY_MODES = { script: 'script', data: 'data', query: 'query', + pg: 'pg', +} as const; + +export const QUERY_MODES_TITLES: Record = { + scan: 'Scan', + script: 'YQL Script', + data: 'Data', + query: 'YQL - QueryService', + pg: 'PostgreSQL', +} as const; + +export const QUERY_SYNTAX = { + yql: 'yql_v1', + pg: 'pg', } as const; export const isNewQueryMode = (value: QueryMode) => {