diff --git a/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx b/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx index 03dd2a22a1..1b4caad160 100644 --- a/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx +++ b/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx @@ -1,3 +1,5 @@ +import React from 'react'; + import type {Column} from '@gravity-ui/react-data-table'; import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; @@ -5,12 +7,12 @@ import {Search} from '../../../../components/Search'; import {TableWithControlsLayout} from '../../../../components/TableWithControlsLayout/TableWithControlsLayout'; import {TruncatedQuery} from '../../../../components/TruncatedQuery/TruncatedQuery'; import { - selectQueriesHistory, selectQueriesHistoryFilter, setIsDirty, setQueryHistoryFilter, } from '../../../../store/reducers/query/query'; import type {QueryInHistory} from '../../../../store/reducers/query/types'; +import type {useQueriesHistory} from '../../../../store/reducers/query/useQueriesHistory'; import {TENANT_QUERY_TABS_ID} from '../../../../store/reducers/tenant/constants'; import {setQueryTab} from '../../../../store/reducers/tenant/tenant'; import {cn} from '../../../../utils/cn'; @@ -29,14 +31,17 @@ const QUERIES_HISTORY_COLUMNS_WIDTH_LS_KEY = 'queriesHistoryTableColumnsWidth'; interface QueriesHistoryProps { changeUserInput: (value: {input: string}) => void; + queriesHistory: ReturnType; } -function QueriesHistory({changeUserInput}: QueriesHistoryProps) { +function QueriesHistory({changeUserInput, queriesHistory}: QueriesHistoryProps) { const dispatch = useTypedDispatch(); - const queriesHistory = useTypedSelector(selectQueriesHistory); + const reversedHistory = React.useMemo(() => { + return queriesHistory.filteredHistoryQueries.toReversed(); + }, [queriesHistory.filteredHistoryQueries]); + const filter = useTypedSelector(selectQueriesHistoryFilter); - const reversedHistory = [...queriesHistory].reverse(); const applyQueryClick = (query: QueryInHistory) => { changeUserInput({input: query.queryText}); diff --git a/src/containers/Tenant/Query/Query.tsx b/src/containers/Tenant/Query/Query.tsx index d643fa946a..69e85e5845 100644 --- a/src/containers/Tenant/Query/Query.tsx +++ b/src/containers/Tenant/Query/Query.tsx @@ -3,6 +3,7 @@ import React from 'react'; import {Helmet} from 'react-helmet-async'; import {changeUserInput} from '../../../store/reducers/query/query'; +import {useQueriesHistory} from '../../../store/reducers/query/useQueriesHistory'; import {TENANT_QUERY_TABS_ID} from '../../../store/reducers/tenant/constants'; import {cn} from '../../../utils/cn'; import {useTypedDispatch, useTypedSelector} from '../../../utils/hooks'; @@ -25,6 +26,8 @@ export const Query = (props: QueryProps) => { const {queryTab = TENANT_QUERY_TABS_ID.newQuery} = useTypedSelector((state) => state.tenant); + const queriesHistory = useQueriesHistory(); + const handleUserInputChange = (value: {input: string}) => { dispatch(changeUserInput(value)); }; @@ -37,10 +40,21 @@ export const Query = (props: QueryProps) => { const renderContent = () => { switch (queryTab) { case TENANT_QUERY_TABS_ID.newQuery: { - return ; + return ( + + ); } case TENANT_QUERY_TABS_ID.history: { - return ; + return ( + + ); } case TENANT_QUERY_TABS_ID.saved: { return ; diff --git a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx index a49e344919..10a1bebb92 100644 --- a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx +++ b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx @@ -11,15 +11,13 @@ import { } from '../../../../store/reducers/capabilities/hooks'; import { queryApi, - saveQueryToHistory, - selectQueriesHistory, - selectQueriesHistoryCurrentIndex, selectResult, selectTenantPath, setIsDirty, setTenantPath, } from '../../../../store/reducers/query/query'; import type {QueryResult} from '../../../../store/reducers/query/types'; +import type {useQueriesHistory} from '../../../../store/reducers/query/useQueriesHistory'; import {setQueryAction} from '../../../../store/reducers/queryActions/queryActions'; import {selectShowPreview, setShowPreview} from '../../../../store/reducers/schema/schema'; import {SETTING_KEYS} from '../../../../store/reducers/settings/constants'; @@ -69,18 +67,25 @@ const initialTenantCommonInfoState = { interface QueryEditorProps { changeUserInput: (arg: {input: string}) => void; theme: string; + queriesHistory: ReturnType; } -export default function QueryEditor(props: QueryEditorProps) { +export default function QueryEditor({theme, changeUserInput, queriesHistory}: QueryEditorProps) { const dispatch = useTypedDispatch(); const {database, path, type, subType, databaseFullPath} = useCurrentSchema(); - const {theme, changeUserInput} = props; const savedPath = useTypedSelector(selectTenantPath); const result = useTypedSelector(selectResult); - const historyQueries = useTypedSelector(selectQueriesHistory); - const historyCurrentIndex = useTypedSelector(selectQueriesHistoryCurrentIndex); const showPreview = useTypedSelector(selectShowPreview); + const { + historyQueries, + historyCurrentQueryId, + saveQueryToHistory, + updateQueryInHistory, + goToPreviousQuery, + goToNextQuery, + } = queriesHistory; + const isResultLoaded = Boolean(result); const [querySettings] = useQueryExecutionSettings(); @@ -182,6 +187,17 @@ export default function QueryEditor(props: QueryEditorProps) { base64: encodeTextWithBase64, }); + query + .then(({data}) => { + if (data?.queryId) { + updateQueryInHistory(data.queryId, data?.queryStats); + } + }) + .catch((error) => { + // Do not add query stats for failed query + console.error('Failed to update query history:', error); + }); + queryManagerInstance.registerQuery(query); } @@ -189,8 +205,11 @@ export default function QueryEditor(props: QueryEditorProps) { // Don't save partial queries in history if (!partial) { - if (text !== historyQueries[historyCurrentIndex]?.queryText) { - dispatch(saveQueryToHistory({queryText: text, queryId})); + const currentQuery = historyCurrentQueryId + ? historyQueries.find((q) => q.queryId === historyCurrentQueryId) + : null; + if (text !== currentQuery?.queryText) { + saveQueryToHistory(text, queryId); } dispatch(setIsDirty(false)); } @@ -279,6 +298,9 @@ export default function QueryEditor(props: QueryEditorProps) { theme={theme} handleSendExecuteClick={handleSendExecuteClick} handleGetExplainQueryClick={handleGetExplainQueryClick} + historyQueries={historyQueries} + goToPreviousQuery={goToPreviousQuery} + goToNextQuery={goToNextQuery} /> diff --git a/src/containers/Tenant/Query/QueryEditor/YqlEditor.tsx b/src/containers/Tenant/Query/QueryEditor/YqlEditor.tsx index 2606f2b863..0954658ec6 100644 --- a/src/containers/Tenant/Query/QueryEditor/YqlEditor.tsx +++ b/src/containers/Tenant/Query/QueryEditor/YqlEditor.tsx @@ -6,13 +6,8 @@ import throttle from 'lodash/throttle'; import type Monaco from 'monaco-editor'; import {MonacoEditor} from '../../../../components/MonacoEditor/MonacoEditor'; -import { - goToNextQuery, - goToPreviousQuery, - selectQueriesHistory, - selectUserInput, - setIsDirty, -} from '../../../../store/reducers/query/query'; +import {selectUserInput, setIsDirty} from '../../../../store/reducers/query/query'; +import type {QueryInHistory} from '../../../../store/reducers/query/types'; import {SETTING_KEYS} from '../../../../store/reducers/settings/constants'; import type {QueryAction} from '../../../../types/store/query'; import { @@ -37,6 +32,9 @@ interface YqlEditorProps { theme: string; handleGetExplainQueryClick: (text: string) => void; handleSendExecuteClick: (text: string, partial?: boolean) => void; + historyQueries: QueryInHistory[]; + goToPreviousQuery: () => void; + goToNextQuery: () => void; } export function YqlEditor({ @@ -44,12 +42,14 @@ export function YqlEditor({ theme, handleSendExecuteClick, handleGetExplainQueryClick, + historyQueries, + goToPreviousQuery, + goToNextQuery, }: YqlEditorProps) { const input = useTypedSelector(selectUserInput); const dispatch = useTypedDispatch(); const [monacoGhostInstance, setMonacoGhostInstance] = React.useState>(); - const historyQueries = useTypedSelector(selectQueriesHistory); const [isCodeAssistEnabled] = useSetting(SETTING_KEYS.ENABLE_CODE_ASSISTANT); const editorOptions = useEditorOptions(); @@ -76,7 +76,7 @@ export function YqlEditor({ window.ydbEditor = undefined; }; - const {monacoGhostConfig, prepareUserQueriesCache} = useCodeAssistHelpers(); + const {monacoGhostConfig, prepareUserQueriesCache} = useCodeAssistHelpers(historyQueries); React.useEffect(() => { if (monacoGhostInstance && isCodeAssistEnabled) { @@ -160,7 +160,7 @@ export function YqlEditor({ contextMenuGroupId: CONTEXT_MENU_GROUP_ID, contextMenuOrder: 2, run: () => { - dispatch(goToPreviousQuery()); + goToPreviousQuery(); }, }); editor.addAction({ @@ -169,7 +169,7 @@ export function YqlEditor({ contextMenuGroupId: CONTEXT_MENU_GROUP_ID, contextMenuOrder: 3, run: () => { - dispatch(goToNextQuery()); + goToNextQuery(); }, }); editor.addAction({ diff --git a/src/containers/Tenant/Query/QueryEditor/helpers.ts b/src/containers/Tenant/Query/QueryEditor/helpers.ts index d9a23a2aec..3bb7efb8fb 100644 --- a/src/containers/Tenant/Query/QueryEditor/helpers.ts +++ b/src/containers/Tenant/Query/QueryEditor/helpers.ts @@ -4,10 +4,10 @@ import type {AcceptEvent, DeclineEvent, IgnoreEvent, PromptFile} from '@ydb-plat import type Monaco from 'monaco-editor'; import {codeAssistApi} from '../../../../store/reducers/codeAssist/codeAssist'; -import {selectQueriesHistory} from '../../../../store/reducers/query/query'; +import type {QueryInHistory} from '../../../../store/reducers/query/types'; import {SETTING_KEYS} from '../../../../store/reducers/settings/constants'; import type {TelemetryOpenTabs} from '../../../../types/api/codeAssist'; -import {useSetting, useTypedSelector} from '../../../../utils/hooks'; +import {useSetting} from '../../../../utils/hooks'; import {YQL_LANGUAGE_ID} from '../../../../utils/monaco/constats'; import {useSavedQueries} from '../utils/useSavedQueries'; @@ -39,13 +39,12 @@ export function useEditorOptions() { return options; } -export function useCodeAssistHelpers() { +export function useCodeAssistHelpers(historyQueries: QueryInHistory[]) { const [sendCodeAssistPrompt] = codeAssistApi.useLazyGetCodeAssistSuggestionsQuery(); const [acceptSuggestion] = codeAssistApi.useAcceptSuggestionMutation(); const [discardSuggestion] = codeAssistApi.useDiscardSuggestionMutation(); const [ignoreSuggestion] = codeAssistApi.useIgnoreSuggestionMutation(); const [sendUserQueriesData] = codeAssistApi.useSendUserQueriesDataMutation(); - const historyQueries = useTypedSelector(selectQueriesHistory); const {savedQueries} = useSavedQueries(); const getCodeAssistSuggestions = React.useCallback( diff --git a/src/store/reducers/query/__test__/tabPersistence.test.tsx b/src/store/reducers/query/__test__/tabPersistence.test.tsx index d04a1f3026..b8bb2829e6 100644 --- a/src/store/reducers/query/__test__/tabPersistence.test.tsx +++ b/src/store/reducers/query/__test__/tabPersistence.test.tsx @@ -4,10 +4,6 @@ import type {QueryState} from '../types'; describe('QueryResultViewer tab persistence integration', () => { const initialState: QueryState = { input: '', - history: { - queries: [], - currentIndex: -1, - }, }; test('should save and retrieve tab selection for explain queries', () => { diff --git a/src/store/reducers/query/query.ts b/src/store/reducers/query/query.ts index 6895c4a423..657310ee48 100644 --- a/src/store/reducers/query/query.ts +++ b/src/store/reducers/query/query.ts @@ -1,7 +1,6 @@ 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 {QueryAction, QueryRequestParams, QuerySettings} from '../../../types/store/query'; import type {StreamDataChunk} from '../../../types/store/streaming'; @@ -11,7 +10,6 @@ import {isQueryErrorResponse} from '../../../utils/query'; import {isNumeric} from '../../../utils/utils'; import type {RootState} from '../../defaultStore'; import {api} from '../api'; -import {SETTING_KEYS} from '../settings/constants'; import {prepareQueryData} from './prepareQueryData'; import { @@ -19,17 +17,8 @@ import { setStreamQueryResponse as setStreamQueryResponseReducer, setStreamSession as setStreamSessionReducer, } from './streamingReducers'; -import type {QueryResult, QueryState} from './types'; -import {getActionAndSyntaxFromQueryMode, getQueryInHistory, prepareQueryWithPragmas} from './utils'; - -const MAXIMUM_QUERIES_IN_HISTORY = 20; - -const queriesHistoryInitial = settingsManager.readUserSettingsValue( - SETTING_KEYS.QUERIES_HISTORY, - [], -) as string[]; - -const sliceLimit = queriesHistoryInitial.length - MAXIMUM_QUERIES_IN_HISTORY; +import type {QueryResult, QueryState, QueryStats} from './types'; +import {getActionAndSyntaxFromQueryMode, prepareQueryWithPragmas} from './utils'; const rawQuery = loadFromSessionStorage(QUERY_EDITOR_CURRENT_QUERY_KEY); const input = typeof rawQuery === 'string' ? rawQuery : ''; @@ -39,16 +28,9 @@ const isDirty = Boolean(loadFromSessionStorage(QUERY_EDITOR_DIRTY_KEY)); const initialState: QueryState = { input, isDirty, - history: { - queries: queriesHistoryInitial - .slice(sliceLimit < 0 ? 0 : sliceLimit) - .map(getQueryInHistory), - currentIndex: - queriesHistoryInitial.length > MAXIMUM_QUERIES_IN_HISTORY - ? MAXIMUM_QUERIES_IN_HISTORY - 1 - : queriesHistoryInitial.length - 1, - filter: '', - }, + + historyFilter: '', + historyCurrentQueryId: undefined, }; const slice = createSlice({ @@ -66,76 +48,14 @@ const slice = createSlice({ 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, - ); - settingsManager.setUserSettingsValue(SETTING_KEYS.QUERIES_HISTORY, newQueries); - const currentIndex = newQueries.length - 1; - - state.history = { - queries: newQueries, - currentIndex, - }; - }, - updateQueryInHistory: ( - state, - action: PayloadAction<{queryId: string; stats: QueryStats}>, - ) => { - const {queryId, stats} = action.payload; - - if (!stats) { - return; - } - - const index = state.history.queries.findIndex((item) => item.queryId === queryId); - - if (index === -1) { - return; - } - - const newQueries = [...state.history.queries]; - const {durationUs, endTime} = stats; - newQueries.splice(index, 1, { - ...state.history.queries[index], - durationUs, - endTime, - }); - - settingsManager.setUserSettingsValue(SETTING_KEYS.QUERIES_HISTORY, newQueries); - - state.history.queries = newQueries; - }, - goToPreviousQuery: (state) => { - const currentIndex = state.history.currentIndex; - if (currentIndex <= 0) { - return; - } - const newCurrentIndex = currentIndex - 1; - const query = state.history.queries[newCurrentIndex]; - state.input = query.queryText; - state.history.currentIndex = newCurrentIndex; - }, - goToNextQuery: (state) => { - const currentIndex = state.history.currentIndex; - 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; + state.historyFilter = action.payload; + }, + setHistoryCurrentQueryId: (state, action: PayloadAction) => { + state.historyCurrentQueryId = action.payload; }, setResultTab: ( state, @@ -152,14 +72,14 @@ const slice = createSlice({ setStreamQueryResponse: setStreamQueryResponseReducer, }, selectors: { - selectQueriesHistoryFilter: (state) => state.history.filter || '', + selectQueriesHistoryFilter: (state) => state.historyFilter || '', + selectHistoryCurrentQueryId: (state) => state.historyCurrentQueryId, selectTenantPath: (state) => state.tenantPath, selectResult: (state) => state.result, selectStartTime: (state) => state.result?.startTime, selectEndTime: (state) => state.result?.endTime, selectUserInput: (state) => state.input, selectIsDirty: (state) => state.isDirty, - selectQueriesHistoryCurrentIndex: (state) => state.history?.currentIndex, selectResultTab: (state) => state.selectedResultTab, }, }); @@ -175,29 +95,13 @@ export const selectQueryDuration = createSelector( }, ); -export const selectQueriesHistory = createSelector( - [ - (state: RootState) => state.query.history.queries, - (state: RootState) => state.query.history.filter, - ], - (queries, filter) => { - const normalizedFilter = filter?.toLowerCase(); - return normalizedFilter - ? queries.filter((item) => item.queryText.toLowerCase().includes(normalizedFilter)) - : queries; - }, -); - export default slice.reducer; export const { changeUserInput, setQueryResult, - saveQueryToHistory, - updateQueryInHistory, - goToPreviousQuery, - goToNextQuery, setTenantPath, setQueryHistoryFilter, + setHistoryCurrentQueryId, addStreamingChunks, setStreamQueryResponse, setStreamSession, @@ -207,7 +111,7 @@ export const { export const { selectQueriesHistoryFilter, - selectQueriesHistoryCurrentIndex, + selectHistoryCurrentQueryId, selectTenantPath, selectResult, selectUserInput, @@ -228,11 +132,6 @@ interface SendQueryParams extends QueryRequestParams { // Stream query receives queryId from session chunk. type StreamQueryParams = Omit; -interface QueryStats { - durationUs?: string | number; - endTime?: string | number; -} - const DEFAULT_STREAM_CHUNK_SIZE = 1000; const DEFAULT_CONCURRENT_RESULTS = false; @@ -345,7 +244,7 @@ export const queryApi = api.injectEndpoints({ } }, }), - useSendQuery: build.mutation({ + useSendQuery: build.mutation<{queryStats: QueryStats; queryId: string}, SendQueryParams>({ queryFn: async ( { actionType = 'execute', @@ -355,7 +254,7 @@ export const queryApi = api.injectEndpoints({ enableTracingLevel, queryId, base64, - }: SendQueryParams, + }, {signal, dispatch, getState}, ) => { const startTime = Date.now(); @@ -421,8 +320,9 @@ export const queryApi = api.injectEndpoints({ const data = prepareQueryData(response); data.traceId = response?._meta?.traceId; + const queryStats: QueryStats = {}; + if (actionType === 'execute') { - const queryStats: QueryStats = {}; if (data.stats) { const {DurationUs, Executions: [{FinishTimeMs}] = [{}]} = data.stats; queryStats.durationUs = DurationUs; @@ -432,8 +332,6 @@ export const queryApi = api.injectEndpoints({ queryStats.durationUs = (now - timeStart) * 1000; queryStats.endTime = now; } - - dispatch(updateQueryInHistory({stats: queryStats, queryId})); } dispatch( @@ -446,7 +344,7 @@ export const queryApi = api.injectEndpoints({ endTime: Date.now(), }), ); - return {data: null}; + return {data: {queryStats, queryId}}; } catch (error) { const state = getState() as RootState; if (state.query.result?.startTime !== startTime) { diff --git a/src/store/reducers/query/types.ts b/src/store/reducers/query/types.ts index 9d753b5210..c8a258cc4d 100644 --- a/src/store/reducers/query/types.ts +++ b/src/store/reducers/query/types.ts @@ -62,14 +62,18 @@ export interface QueryState { input: string; result?: QueryResult; isDirty?: boolean; - history: { - queries: QueryInHistory[]; - currentIndex: number; - filter?: string; - }; + + historyFilter?: string; + historyCurrentQueryId?: string; + tenantPath?: string; selectedResultTab?: { execute?: string; explain?: string; }; } + +export interface QueryStats { + durationUs?: string | number; + endTime?: string | number; +} diff --git a/src/store/reducers/query/useQueriesHistory.ts b/src/store/reducers/query/useQueriesHistory.ts new file mode 100644 index 0000000000..df5d402109 --- /dev/null +++ b/src/store/reducers/query/useQueriesHistory.ts @@ -0,0 +1,143 @@ +import React from 'react'; + +import { + useEventHandler, + useSetting, + useTypedDispatch, + useTypedSelector, +} from '../../../utils/hooks'; +import {SETTING_KEYS} from '../settings/constants'; + +import { + changeUserInput, + selectHistoryCurrentQueryId, + selectQueriesHistoryFilter, + setHistoryCurrentQueryId, +} from './query'; +import type {QueryInHistory, QueryStats} from './types'; +import {getQueryInHistory} from './utils'; + +const MAXIMUM_QUERIES_IN_HISTORY = 20; + +export function useQueriesHistory() { + const dispatch = useTypedDispatch(); + const queriesFilter = useTypedSelector(selectQueriesHistoryFilter); + const historyCurrentQueryId = useTypedSelector(selectHistoryCurrentQueryId); + + const [savedHistoryQueries, saveHistoryQueries] = useSetting( + SETTING_KEYS.QUERIES_HISTORY, + ); + + const preparedQueries = React.useMemo(() => { + const queries = savedHistoryQueries ?? []; + + return queries.map(getQueryInHistory); + }, [savedHistoryQueries]); + + const filteredHistoryQueries = React.useMemo(() => { + const normalizedFilter = queriesFilter?.toLowerCase(); + return normalizedFilter + ? preparedQueries.filter((item) => + item.queryText.toLowerCase().includes(normalizedFilter), + ) + : preparedQueries; + }, [preparedQueries, queriesFilter]); + + // These functions are used inside Monaco editorDidMount + // They should be stable to work properly + const goToPreviousQuery = useEventHandler(() => { + if (preparedQueries.length === 0) { + return; + } + + let query: QueryInHistory | undefined; + + const currentIndex = preparedQueries.findIndex((q) => q.queryId === historyCurrentQueryId); + + if (currentIndex === -1) { + // Query not found, start from last + query = preparedQueries.at(-1); + } else if (currentIndex === 0) { + // At first query, wrap to last + query = preparedQueries.at(-1); + } else { + // Go to previous query + query = preparedQueries[currentIndex - 1]; + } + + if (query) { + dispatch(changeUserInput({input: query.queryText})); + dispatch(setHistoryCurrentQueryId(query.queryId)); + } + }); + + const goToNextQuery = useEventHandler(() => { + if (preparedQueries.length === 0) { + return; + } + + let query: QueryInHistory | undefined; + + const currentIndex = preparedQueries.findIndex((q) => q.queryId === historyCurrentQueryId); + + if (currentIndex === -1) { + // Query not found, start from last + query = preparedQueries.at(-1); + } else if (currentIndex === preparedQueries.length - 1) { + // At last query, wrap to first + query = preparedQueries[0]; + } else { + // Go to next query + query = preparedQueries[currentIndex + 1]; + } + + if (query) { + dispatch(changeUserInput({input: query.queryText})); + dispatch(setHistoryCurrentQueryId(query.queryId)); + } + }); + + const saveQueryToHistory = useEventHandler((queryText: string, queryId: string) => { + if (!queryText) { + return; + } + // +1 to include new query + const sliceLimit = preparedQueries.length + 1 - MAXIMUM_QUERIES_IN_HISTORY; + const slicedQueries = preparedQueries.slice(sliceLimit < 0 ? 0 : sliceLimit); + const newQueries = [...slicedQueries, {queryText, queryId}]; + + saveHistoryQueries(newQueries); + + // Update currentQueryId to point to the newly added query + dispatch(setHistoryCurrentQueryId(queryId)); + }); + + const updateQueryInHistory = useEventHandler((queryId: string, stats: QueryStats) => { + if (!stats || !preparedQueries.length) { + return; + } + + const index = preparedQueries.findIndex((item) => item.queryId === queryId); + + if (index !== -1) { + const newQueries = [...preparedQueries]; + const {durationUs, endTime} = stats; + newQueries[index] = { + ...preparedQueries[index], + durationUs, + endTime, + }; + saveHistoryQueries(newQueries); + } + }); + + return { + historyQueries: preparedQueries, + historyCurrentQueryId, + filteredHistoryQueries, + goToPreviousQuery, + goToNextQuery, + saveQueryToHistory, + updateQueryInHistory, + }; +} diff --git a/src/store/reducers/query/utils.ts b/src/store/reducers/query/utils.ts index dc1f111d1c..e034286f2c 100644 --- a/src/store/reducers/query/utils.ts +++ b/src/store/reducers/query/utils.ts @@ -1,3 +1,5 @@ +import {v4 as uuidv4} from 'uuid'; + import type {Actions, ErrorResponse} from '../../../types/api/query'; import type {QueryAction, QueryMode, QuerySyntax} from '../../../types/store/query'; import type { @@ -30,9 +32,17 @@ export function getQueryInHistory(rawQuery: string | QueryInHistory) { if (typeof rawQuery === 'string') { return { queryText: rawQuery, + queryId: uuidv4(), }; } - return rawQuery; + + if (rawQuery.queryId) { + return rawQuery; + } + return { + ...rawQuery, + queryId: uuidv4(), + }; } export function isSessionChunk(content: StreamingChunk): content is SessionChunk {