diff --git a/src/components/Drawer/Drawer.tsx b/src/components/Drawer/Drawer.tsx
index 2688cb59c..5db957419 100644
--- a/src/components/Drawer/Drawer.tsx
+++ b/src/components/Drawer/Drawer.tsx
@@ -10,13 +10,13 @@ import {CopyLinkButton} from '../CopyLinkButton/CopyLinkButton';
import {useDrawerContext} from './DrawerContext';
+import './Drawer.scss';
+
const DEFAULT_DRAWER_WIDTH_PERCENTS = 60;
const DEFAULT_DRAWER_WIDTH = 600;
const DRAWER_WIDTH_KEY = 'drawer-width';
const b = cn('ydb-drawer');
-import './Drawer.scss';
-
type DrawerEvent = MouseEvent & {
_capturedInsideDrawer?: boolean;
};
@@ -129,7 +129,7 @@ const DrawerPaneContentWrapper = ({
);
};
-type DrawerControl =
+export type DrawerControl =
| {type: 'close'}
| {type: 'copyLink'; link: string}
| {type: 'custom'; node: React.ReactNode; key: string};
@@ -223,10 +223,12 @@ export const DrawerWrapper = ({
detectClickOutside={detectClickOutside}
isPercentageWidth={isPercentageWidth}
>
-
- {renderDrawerHeader()}
- {renderDrawerContent()}
-
+ {isDrawerVisible ? (
+
+ {renderDrawerHeader()}
+ {renderDrawerContent()}
+
+ ) : null}
);
diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/NotFoundContainer.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/NotFoundContainer.tsx
new file mode 100644
index 000000000..3ddb254d9
--- /dev/null
+++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/NotFoundContainer.tsx
@@ -0,0 +1,34 @@
+import {Button, Flex, Icon, Text} from '@gravity-ui/uikit';
+
+import {cn} from '../../../../../utils/cn';
+import i18n from '../i18n';
+
+import CryCatIcon from '../../../../../assets/icons/cry-cat.svg';
+
+const b = cn('kv-top-queries');
+
+interface NotFoundContainerProps {
+ onClose: () => void;
+}
+
+export const NotFoundContainer = ({onClose}: NotFoundContainerProps) => {
+ return (
+
+
+
+ {i18n('query-details.not-found.title')}
+
+
+ {i18n('query-details.not-found.description')}
+
+
+
+ );
+};
diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss
new file mode 100644
index 000000000..85ab52c11
--- /dev/null
+++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss
@@ -0,0 +1,43 @@
+@import '../../../../../styles/mixins.scss';
+
+.ydb-query-details {
+ flex: 1;
+
+ color: var(--g-color-text-primary);
+ background-color: var(--g-color-base-background-dark);
+
+ &__content {
+ overflow: auto;
+ flex: 1;
+
+ padding: var(--g-spacing-5) var(--g-spacing-4) var(--g-spacing-5) var(--g-spacing-4);
+ }
+
+ &__query-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ padding: var(--g-spacing-2) var(--g-spacing-3);
+
+ border-bottom: 1px solid var(--g-color-line-generic);
+ }
+
+ &__query-title {
+ font-size: 14px;
+ font-weight: 500;
+ }
+
+ &__query-content {
+ position: relative;
+
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+
+ margin-top: var(--g-spacing-5);
+
+ border-radius: 4px;
+ background-color: var(--code-background-color);
+ }
+}
diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.tsx
new file mode 100644
index 000000000..cfdfd07c6
--- /dev/null
+++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.tsx
@@ -0,0 +1,48 @@
+import {Code} from '@gravity-ui/icons';
+import {Button, Flex, Icon} from '@gravity-ui/uikit';
+
+import type {InfoViewerItem} from '../../../../../components/InfoViewer';
+import {InfoViewer} from '../../../../../components/InfoViewer';
+import {YDBSyntaxHighlighter} from '../../../../../components/SyntaxHighlighter/YDBSyntaxHighlighter';
+import {cn} from '../../../../../utils/cn';
+import i18n from '../i18n';
+
+import './QueryDetails.scss';
+
+const b = cn('ydb-query-details');
+
+interface QueryDetailsProps {
+ queryText: string;
+ infoItems: InfoViewerItem[];
+ onOpenInEditor: () => void;
+}
+
+export const QueryDetails = ({queryText, infoItems, onOpenInEditor}: QueryDetailsProps) => {
+ return (
+
+
+
+
+
+
+
{i18n('query-details.query.title')}
+
+
+
+
+
+
+ );
+};
diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx
new file mode 100644
index 000000000..e291059fd
--- /dev/null
+++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx
@@ -0,0 +1,63 @@
+import React from 'react';
+
+import {useHistory, useLocation} from 'react-router-dom';
+
+import {parseQuery} from '../../../../../routes';
+import {changeUserInput, setIsDirty} from '../../../../../store/reducers/query/query';
+import {
+ TENANT_PAGE,
+ TENANT_PAGES_IDS,
+ TENANT_QUERY_TABS_ID,
+} from '../../../../../store/reducers/tenant/constants';
+import type {KeyValueRow} from '../../../../../types/api/query';
+import {useTypedDispatch} from '../../../../../utils/hooks';
+import {TenantTabsGroups, getTenantPath} from '../../../TenantPages';
+import {createQueryInfoItems} from '../utils';
+
+import {NotFoundContainer} from './NotFoundContainer';
+import {QueryDetails} from './QueryDetails';
+
+interface QueryDetailsDrawerContentProps {
+ // Three cases:
+ // 1. row is KeyValueRow and we can show it.
+ // 2. row is null and we can show not found container.
+ // 3. row is undefined and we can show nothing.
+ row?: KeyValueRow | null;
+ onClose: () => void;
+}
+
+export const QueryDetailsDrawerContent = ({row, onClose}: QueryDetailsDrawerContentProps) => {
+ const dispatch = useTypedDispatch();
+ const location = useLocation();
+ const history = useHistory();
+
+ const handleOpenInEditor = React.useCallback(() => {
+ if (row) {
+ const input = row.QueryText as string;
+ dispatch(changeUserInput({input}));
+ dispatch(setIsDirty(false));
+
+ const queryParams = parseQuery(location);
+
+ const queryPath = getTenantPath({
+ ...queryParams,
+ [TENANT_PAGE]: TENANT_PAGES_IDS.query,
+ [TenantTabsGroups.queryTab]: TENANT_QUERY_TABS_ID.newQuery,
+ });
+
+ history.push(queryPath);
+ }
+ }, [dispatch, history, location, row]);
+
+ if (row) {
+ return (
+
+ );
+ }
+
+ return ;
+};
diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx
index 0916cb0e9..bebf6d975 100644
--- a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx
+++ b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx
@@ -2,7 +2,10 @@ import React from 'react';
import type {Column} from '@gravity-ui/react-data-table';
import {TableColumnSetup} from '@gravity-ui/uikit';
+import {isEqual} from 'lodash';
+import {DrawerWrapper} from '../../../../components/Drawer';
+import type {DrawerControl} from '../../../../components/Drawer/Drawer';
import {ResponseError} from '../../../../components/Errors/ResponseError';
import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable';
import {Search} from '../../../../components/Search';
@@ -14,6 +17,7 @@ import {useAutoRefreshInterval, useTypedSelector} from '../../../../utils/hooks'
import {useSelectedColumns} from '../../../../utils/hooks/useSelectedColumns';
import {parseQueryErrorToString} from '../../../../utils/query';
+import {QueryDetailsDrawerContent} from './QueryDetails/QueryDetailsDrawerContent';
import {getRunningQueriesColumns} from './columns/columns';
import {
DEFAULT_RUNNING_QUERIES_COLUMNS,
@@ -31,18 +35,19 @@ const b = cn('kv-top-queries');
interface RunningQueriesDataProps {
tenantName: string;
renderQueryModeControl: () => React.ReactNode;
- onRowClick: (query: string) => void;
handleTextSearchUpdate: (text: string) => void;
}
export const RunningQueriesData = ({
tenantName,
renderQueryModeControl,
- onRowClick,
handleTextSearchUpdate,
}: RunningQueriesDataProps) => {
const [autoRefreshInterval] = useAutoRefreshInterval();
const filters = useTypedSelector((state) => state.executeTopQueries);
+ // Internal state for selected row
+ // null is reserved for not found state
+ const [selectedRow, setSelectedRow] = React.useState(undefined);
// Get columns for running queries
const columns: Column[] = React.useMemo(() => {
@@ -60,54 +65,97 @@ export const RunningQueriesData = ({
const {tableSort, handleTableSort, backendSort} = useRunningQueriesSort();
- const {currentData, data, isFetching, isLoading, error} =
- topQueriesApi.useGetRunningQueriesQuery(
- {
- database: tenantName,
- filters,
- sortOrder: backendSort,
- },
- {pollingInterval: autoRefreshInterval},
- );
+ const {currentData, isFetching, isLoading, error} = topQueriesApi.useGetRunningQueriesQuery(
+ {
+ database: tenantName,
+ filters,
+ sortOrder: backendSort,
+ },
+ {pollingInterval: autoRefreshInterval},
+ );
+
+ const rows = currentData?.resultSets?.[0]?.result;
+
+ const isDrawerVisible = selectedRow !== undefined;
- const handleRowClick = (row: KeyValueRow) => {
- return onRowClick(row.QueryText as string);
- };
+ const handleCloseDetails = React.useCallback(() => {
+ setSelectedRow(undefined);
+ }, [setSelectedRow]);
+
+ const renderDrawerContent = React.useCallback(
+ () => ,
+ [selectedRow, handleCloseDetails],
+ );
+
+ const onRowClick = React.useCallback(
+ (
+ row: KeyValueRow | null,
+ _index?: number,
+ event?: React.MouseEvent,
+ ) => {
+ event?.stopPropagation();
+ setSelectedRow(row);
+ },
+ [setSelectedRow],
+ );
+
+ const inputRef = React.useRef(null);
+
+ React.useEffect(() => {
+ if (isDrawerVisible) {
+ inputRef.current?.blur();
+ }
+ }, [isDrawerVisible]);
+
+ const drawerControls: DrawerControl[] = React.useMemo(() => [{type: 'close'}], []);
return (
-
-
- {renderQueryModeControl()}
-
-
-
-
- {error ? : null}
-
- b('row')}
- sortOrder={tableSort}
- onSort={handleTableSort}
- />
-
-
+
+
+
+ {renderQueryModeControl()}
+
+
+
+
+ {error ? : null}
+
+ b('row', {active: isEqual(row, selectedRow)})}
+ sortOrder={tableSort}
+ onSort={handleTableSort}
+ />
+
+
+
);
};
diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss
index 7f7d2b2fa..622edfbab 100644
--- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss
+++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss
@@ -14,6 +14,14 @@
&__row {
cursor: pointer;
+
+ &_active {
+ background-color: var(--g-color-base-selection);
+
+ &:hover {
+ background: var(--g-color-base-selection-hover) !important;
+ }
+ }
}
&__query {
@@ -32,4 +40,25 @@
text-overflow: ellipsis;
}
+
+ &__drawer {
+ margin-top: calc(-1 * var(--g-spacing-4));
+ }
+
+ &__empty-state-icon {
+ color: var(--g-color-text-primary);
+ }
+
+ &__not-found-container {
+ height: 100%;
+ padding: var(--g-spacing-5) 0;
+ }
+
+ &__not-found-description {
+ margin-top: var(--g-spacing-2);
+ }
+
+ &__not-found-close {
+ margin-top: var(--g-spacing-5);
+ }
}
diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx
index 1f430c473..8e12614f2 100644
--- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx
+++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx
@@ -2,23 +2,13 @@ import React from 'react';
import type {RadioButtonOption} from '@gravity-ui/uikit';
import {RadioButton} from '@gravity-ui/uikit';
-import {useHistory, useLocation} from 'react-router-dom';
import {StringParam, useQueryParam} from 'use-query-params';
import {z} from 'zod';
import type {DateRangeValues} from '../../../../components/DateRange';
-import {parseQuery} from '../../../../routes';
import {setTopQueriesFilters} from '../../../../store/reducers/executeTopQueries/executeTopQueries';
import type {TimeFrame} from '../../../../store/reducers/executeTopQueries/types';
-import {changeUserInput, setIsDirty} from '../../../../store/reducers/query/query';
-import {
- TENANT_PAGE,
- TENANT_PAGES_IDS,
- TENANT_QUERY_TABS_ID,
-} from '../../../../store/reducers/tenant/constants';
import {useTypedDispatch} from '../../../../utils/hooks';
-import {useChangeInputWithConfirmation} from '../../../../utils/hooks/withConfirmation/useChangeInputWithConfirmation';
-import {TenantTabsGroups, getTenantPath} from '../../TenantPages';
import {RunningQueriesData} from './RunningQueriesData';
import {TopQueriesData} from './TopQueriesData';
@@ -56,36 +46,17 @@ interface TopQueriesProps {
export const TopQueries = ({tenantName}: TopQueriesProps) => {
const dispatch = useTypedDispatch();
- const location = useLocation();
- const history = useHistory();
- const [_queryMode = QueryModeIds.top, setQueryMode] = useQueryParam('queryMode', StringParam);
- const [_timeFrame = TimeFrameIds.hour, setTimeFrame] = useQueryParam('timeFrame', StringParam);
+ const [rawQueryMode = QueryModeIds.top, setQueryMode] = useQueryParam('queryMode', StringParam);
+ const [rawTimeFrame = TimeFrameIds.hour, setTimeFrame] = useQueryParam(
+ 'timeFrame',
+ StringParam,
+ );
- const queryMode = queryModeSchema.parse(_queryMode);
- const timeFrame = timeFrameSchema.parse(_timeFrame);
+ const queryMode = queryModeSchema.parse(rawQueryMode);
+ const timeFrame = timeFrameSchema.parse(rawTimeFrame);
const isTopQueries = queryMode === QueryModeIds.top;
- const applyRowClick = React.useCallback(
- (input: string) => {
- dispatch(changeUserInput({input}));
- dispatch(setIsDirty(false));
-
- const queryParams = parseQuery(location);
-
- const queryPath = getTenantPath({
- ...queryParams,
- [TENANT_PAGE]: TENANT_PAGES_IDS.query,
- [TenantTabsGroups.queryTab]: TENANT_QUERY_TABS_ID.newQuery,
- });
-
- history.push(queryPath);
- },
- [dispatch, history, location],
- );
-
- const onRowClick = useChangeInputWithConfirmation(applyRowClick);
-
const handleTextSearchUpdate = (text: string) => {
dispatch(setTopQueriesFilters({text}));
};
@@ -109,7 +80,6 @@ export const TopQueries = ({tenantName}: TopQueriesProps) => {
tenantName={tenantName}
timeFrame={timeFrame}
renderQueryModeControl={renderQueryModeControl}
- onRowClick={onRowClick}
handleTimeFrameChange={handleTimeFrameChange}
handleDateRangeChange={handleDateRangeChange}
handleTextSearchUpdate={handleTextSearchUpdate}
@@ -118,7 +88,6 @@ export const TopQueries = ({tenantName}: TopQueriesProps) => {
);
diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx
index cbd3a4a27..c734ffae3 100644
--- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx
+++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx
@@ -2,9 +2,12 @@ import React from 'react';
import type {Column} from '@gravity-ui/react-data-table';
import {Select, TableColumnSetup} from '@gravity-ui/uikit';
+import {isEqual} from 'lodash';
import type {DateRangeValues} from '../../../../components/DateRange';
import {DateRange} from '../../../../components/DateRange';
+import {DrawerWrapper} from '../../../../components/Drawer';
+import type {DrawerControl} from '../../../../components/Drawer/Drawer';
import {ResponseError} from '../../../../components/Errors/ResponseError';
import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable';
import {Search} from '../../../../components/Search';
@@ -17,6 +20,7 @@ import {useAutoRefreshInterval, useTypedSelector} from '../../../../utils/hooks'
import {useSelectedColumns} from '../../../../utils/hooks/useSelectedColumns';
import {parseQueryErrorToString} from '../../../../utils/query';
+import {QueryDetailsDrawerContent} from './QueryDetails/QueryDetailsDrawerContent';
import {getTopQueriesColumns} from './columns/columns';
import {
DEFAULT_TOP_QUERIES_COLUMNS,
@@ -26,9 +30,11 @@ import {
TOP_QUERIES_SELECTED_COLUMNS_LS_KEY,
} from './columns/constants';
import {DEFAULT_TIME_FILTER_VALUE, TIME_FRAME_OPTIONS} from './constants';
+import {useSetSelectedTopQueryRowFromParams} from './hooks/useSetSelectedTopQueryRowFromParams';
import {useTopQueriesSort} from './hooks/useTopQueriesSort';
import i18n from './i18n';
import {TOP_QUERIES_TABLE_SETTINGS} from './utils';
+import {generateShareableUrl} from './utils/generateShareableUrl';
const b = cn('kv-top-queries');
@@ -36,7 +42,6 @@ interface TopQueriesDataProps {
tenantName: string;
timeFrame: TimeFrame;
renderQueryModeControl: () => React.ReactNode;
- onRowClick: (query: string) => void;
handleTimeFrameChange: (value: string[]) => void;
handleDateRangeChange: (value: DateRangeValues) => void;
handleTextSearchUpdate: (text: string) => void;
@@ -46,13 +51,15 @@ export const TopQueriesData = ({
tenantName,
timeFrame,
renderQueryModeControl,
- onRowClick,
handleTimeFrameChange,
handleDateRangeChange,
handleTextSearchUpdate,
}: TopQueriesDataProps) => {
const [autoRefreshInterval] = useAutoRefreshInterval();
const filters = useTypedSelector((state) => state.executeTopQueries);
+ // Internal state for selected row
+ // null is reserved for not found state
+ const [selectedRow, setSelectedRow] = React.useState(undefined);
// Get columns for top queries
const columns: Column[] = React.useMemo(() => {
@@ -68,10 +75,8 @@ export const TopQueriesData = ({
REQUIRED_TOP_QUERIES_COLUMNS,
);
- // Use the sort params from URL in the hook
const {tableSort, handleTableSort, backendSort} = useTopQueriesSort();
-
- const {currentData, data, isFetching, isLoading, error} = topQueriesApi.useGetTopQueriesQuery(
+ const {currentData, isFetching, isLoading, error} = topQueriesApi.useGetTopQueriesQuery(
{
database: tenantName,
filters,
@@ -81,55 +86,110 @@ export const TopQueriesData = ({
{pollingInterval: autoRefreshInterval},
);
- const handleRowClick = (row: KeyValueRow) => {
- return onRowClick(row.QueryText as string);
- };
+ const rows = currentData?.resultSets?.[0]?.result;
+ useSetSelectedTopQueryRowFromParams(setSelectedRow, rows);
+
+ const handleCloseDetails = React.useCallback(() => {
+ setSelectedRow(undefined);
+ }, [setSelectedRow]);
+
+ const isDrawerVisible = selectedRow !== undefined;
+
+ const getTopQueryUrl = React.useCallback(() => {
+ if (selectedRow) {
+ return generateShareableUrl(selectedRow);
+ }
+ return '';
+ }, [selectedRow]);
+
+ const renderDrawerContent = React.useCallback(
+ () => ,
+ [selectedRow, handleCloseDetails],
+ );
+
+ const onRowClick = React.useCallback(
+ (
+ row: KeyValueRow | null,
+ _index?: number,
+ event?: React.MouseEvent,
+ ) => {
+ event?.stopPropagation();
+ setSelectedRow(row);
+ },
+ [setSelectedRow],
+ );
+
+ const inputRef = React.useRef(null);
+
+ React.useEffect(() => {
+ if (isDrawerVisible) {
+ inputRef.current?.blur();
+ }
+ }, [isDrawerVisible]);
+
+ const drawerControls: DrawerControl[] = React.useMemo(
+ () => [{type: 'copyLink', link: getTopQueryUrl()}, {type: 'close'}],
+ [getTopQueryUrl],
+ );
return (
-
-
- {renderQueryModeControl()}
-
-
-
-
-
-
- {error ? : null}
-
- b('row')}
- sortOrder={tableSort}
- onSort={handleTableSort}
- />
-
-
+
+
+
+ {renderQueryModeControl()}
+
+
+
+
+
+
+ {error ? : null}
+
+ b('row', {active: isEqual(row, selectedRow)})}
+ sortOrder={tableSort}
+ onSort={handleTableSort}
+ />
+
+
+
);
};
diff --git a/src/containers/Tenant/Diagnostics/TopQueries/hooks/useSetSelectedTopQueryRowFromParams.ts b/src/containers/Tenant/Diagnostics/TopQueries/hooks/useSetSelectedTopQueryRowFromParams.ts
new file mode 100644
index 000000000..a1c20cf0b
--- /dev/null
+++ b/src/containers/Tenant/Diagnostics/TopQueries/hooks/useSetSelectedTopQueryRowFromParams.ts
@@ -0,0 +1,53 @@
+import React from 'react';
+
+import {StringParam, useQueryParams} from 'use-query-params';
+
+import type {KeyValueRow} from '../../../../../types/api/query';
+import {getTopQueryRowQueryParams} from '../utils/getTopQueryRowQueryParams';
+
+export interface SearchParamsQueryParams {
+ rank?: string;
+ intervalEnd?: string;
+ endTime?: string;
+ queryHash?: string;
+}
+
+export function useSetSelectedTopQueryRowFromParams(
+ setSelectedRow: (row: KeyValueRow | null) => void,
+ rows?: KeyValueRow[] | null,
+) {
+ const [queryParams, setQueryParams] = useQueryParams({
+ selectedRow: StringParam,
+ });
+
+ // Handle initialization from URL params
+ React.useEffect(() => {
+ if (rows && queryParams.selectedRow) {
+ const searchParamsQuery: SearchParamsQueryParams = JSON.parse(
+ decodeURIComponent(queryParams.selectedRow),
+ );
+ const matchedRow = rows.find((row) => {
+ const params = getTopQueryRowQueryParams(row);
+ return (
+ params.rank === searchParamsQuery.rank &&
+ params.intervalEnd === searchParamsQuery.intervalEnd &&
+ params.endTime === searchParamsQuery.endTime &&
+ searchParamsQuery.queryHash === params.queryHash
+ );
+ });
+
+ if (matchedRow) {
+ setSelectedRow(matchedRow);
+ } else {
+ // If we had a selectedRow in URL but couldn't find a matching row,
+ // explicitly set selectedRow to null to indicate empty state
+ setSelectedRow(null);
+ }
+
+ // Clear URL params after using them
+ setQueryParams({selectedRow: undefined});
+ }
+ }, [queryParams.selectedRow, setQueryParams, rows, setSelectedRow]);
+
+ return null;
+}
diff --git a/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json b/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json
index 5dbcec47d..06da07323 100644
--- a/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json
+++ b/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json
@@ -4,5 +4,11 @@
"mode_top": "Top",
"mode_running": "Running",
"timeframe_hour": "Per hour",
- "timeframe_minute": "Per minute"
+ "timeframe_minute": "Per minute",
+ "query-details.title": "Query",
+ "query-details.open-in-editor": "Open in Editor",
+ "query-details.close": "Close",
+ "query-details.query.title": "Query Text",
+ "query-details.not-found.title": "Not found",
+ "query-details.not-found.description": "This query no longer exists"
}
diff --git a/src/containers/Tenant/Diagnostics/TopQueries/utils.ts b/src/containers/Tenant/Diagnostics/TopQueries/utils.ts
index 05b229abd..9a00c085d 100644
--- a/src/containers/Tenant/Diagnostics/TopQueries/utils.ts
+++ b/src/containers/Tenant/Diagnostics/TopQueries/utils.ts
@@ -1,9 +1,92 @@
import type {Settings} from '@gravity-ui/react-data-table';
+import type {InfoViewerItem} from '../../../../components/InfoViewer';
+import type {KeyValueRow} from '../../../../types/api/query';
+import {formatDateTime, formatNumber} from '../../../../utils/dataFormatters/dataFormatters';
+import {generateHash} from '../../../../utils/generateHash';
+import {formatToMs, parseUsToMs} from '../../../../utils/timeParsers';
import {QUERY_TABLE_SETTINGS} from '../../utils/constants';
+import columnsI18n from './columns/i18n';
+
export const TOP_QUERIES_TABLE_SETTINGS: Settings = {
...QUERY_TABLE_SETTINGS,
disableSortReset: true,
externalSort: true,
};
+
+export function createQueryInfoItems(data: KeyValueRow): InfoViewerItem[] {
+ const items: InfoViewerItem[] = [];
+
+ if (data.QueryText) {
+ items.push({
+ label: columnsI18n('query-hash'),
+ value: generateHash(String(data.QueryText)),
+ });
+ }
+
+ if (data.CPUTimeUs !== undefined) {
+ items.push({
+ label: columnsI18n('cpu-time'),
+ value: formatToMs(parseUsToMs(data.CPUTimeUs ?? undefined)),
+ });
+ }
+
+ if (data.Duration !== undefined) {
+ items.push({
+ label: columnsI18n('duration'),
+ value: formatToMs(parseUsToMs(data.Duration ?? undefined)),
+ });
+ }
+
+ if (data.ReadBytes !== undefined) {
+ items.push({
+ label: columnsI18n('read-bytes'),
+ value: formatNumber(data.ReadBytes),
+ });
+ }
+
+ if (data.RequestUnits !== undefined) {
+ items.push({
+ label: columnsI18n('request-units'),
+ value: formatNumber(data.RequestUnits),
+ });
+ }
+
+ if (data.EndTime) {
+ items.push({
+ label: columnsI18n('end-time'),
+ value: formatDateTime(new Date(data.EndTime as string).getTime()),
+ });
+ }
+
+ if (data.ReadRows !== undefined) {
+ items.push({
+ label: columnsI18n('read-rows'),
+ value: formatNumber(data.ReadRows),
+ });
+ }
+
+ if (data.UserSID) {
+ items.push({
+ label: columnsI18n('user'),
+ value: data.UserSID,
+ });
+ }
+
+ if (data.ApplicationName) {
+ items.push({
+ label: columnsI18n('application'),
+ value: data.ApplicationName,
+ });
+ }
+
+ if (data.QueryStartAt) {
+ items.push({
+ label: columnsI18n('start-time'),
+ value: formatDateTime(new Date(data.QueryStartAt as string).getTime()),
+ });
+ }
+
+ return items;
+}
diff --git a/src/containers/Tenant/Diagnostics/TopQueries/utils/generateShareableUrl.ts b/src/containers/Tenant/Diagnostics/TopQueries/utils/generateShareableUrl.ts
new file mode 100644
index 000000000..8dc9a2ff3
--- /dev/null
+++ b/src/containers/Tenant/Diagnostics/TopQueries/utils/generateShareableUrl.ts
@@ -0,0 +1,32 @@
+import type {KeyValueRow} from '../../../../../types/api/query';
+
+import {getTopQueryRowQueryParams} from './getTopQueryRowQueryParams';
+
+/**
+ * Generates a shareable URL with query parameters for a top query row
+ * @param row The top query row data
+ * @returns A shareable URL string with the row's parameters encoded in the URL
+ */
+export function generateShareableUrl(row: KeyValueRow): string {
+ const params = getTopQueryRowQueryParams(row);
+
+ const url = new URL(window.location.href);
+
+ const searchParams = new URLSearchParams(url.search);
+
+ searchParams.set(
+ 'selectedRow',
+ encodeURIComponent(
+ JSON.stringify({
+ rank: params.rank || undefined,
+ intervalEnd: params.intervalEnd || undefined,
+ endTime: params.endTime || undefined,
+ queryHash: params.queryHash || undefined,
+ }),
+ ),
+ );
+
+ url.search = searchParams.toString();
+
+ return url.toString();
+}
diff --git a/src/containers/Tenant/Diagnostics/TopQueries/utils/getTopQueryRowQueryParams.ts b/src/containers/Tenant/Diagnostics/TopQueries/utils/getTopQueryRowQueryParams.ts
new file mode 100644
index 000000000..c764985eb
--- /dev/null
+++ b/src/containers/Tenant/Diagnostics/TopQueries/utils/getTopQueryRowQueryParams.ts
@@ -0,0 +1,19 @@
+import type {KeyValueRow} from '../../../../../types/api/query';
+import {generateHash} from '../../../../../utils/generateHash';
+import type {SearchParamsQueryParams} from '../hooks/useSetSelectedTopQueryRowFromParams';
+
+/**
+ * Extract query parameters from a row for use in URL search params
+ * @param row The top query row data
+ * @returns Parameters for URL search params
+ */
+export function getTopQueryRowQueryParams(row: KeyValueRow): SearchParamsQueryParams {
+ const queryHash = generateHash(String(row.QueryText));
+
+ return {
+ rank: String(row.Rank),
+ intervalEnd: String(row.IntervalEnd),
+ endTime: String(row.EndTime),
+ queryHash,
+ };
+}