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', {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, + }; +}