From 672c68a31def44884db82b7e92fa45086d678a3f Mon Sep 17 00:00:00 2001 From: astandrik Date: Mon, 12 May 2025 17:41:34 +0300 Subject: [PATCH 01/11] feat: Drawer --- src/components/Drawer/Drawer.tsx | 12 +- .../TopQueries/QueryDetails/QueryDetails.scss | 71 +++++++ .../TopQueries/QueryDetails/QueryDetails.tsx | 48 +++++ .../QueryDetailsDrawerContent.tsx | 78 ++++++++ .../TopQueries/RunningQueriesData.tsx | 129 +++++++++---- .../Diagnostics/TopQueries/TopQueries.scss | 34 ++++ .../Diagnostics/TopQueries/TopQueries.tsx | 34 ---- .../Diagnostics/TopQueries/TopQueriesData.tsx | 178 ++++++++++++------ .../hooks/useGetSelectedRowTableSort.ts | 13 ++ .../useSetSelectedTopQueryRowFromParams.ts | 53 ++++++ .../Diagnostics/TopQueries/i18n/en.json | 12 +- .../Tenant/Diagnostics/TopQueries/utils.ts | 130 ++++++++++++- .../TopQueries/utils/generateShareableUrl.ts | 41 ++++ .../utils/getTopQueryRowQueryParams.ts | 19 ++ 14 files changed, 718 insertions(+), 134 deletions(-) create mode 100644 src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss create mode 100644 src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.tsx create mode 100644 src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx create mode 100644 src/containers/Tenant/Diagnostics/TopQueries/hooks/useGetSelectedRowTableSort.ts create mode 100644 src/containers/Tenant/Diagnostics/TopQueries/hooks/useSetSelectedTopQueryRowFromParams.ts create mode 100644 src/containers/Tenant/Diagnostics/TopQueries/utils/generateShareableUrl.ts create mode 100644 src/containers/Tenant/Diagnostics/TopQueries/utils/getTopQueryRowQueryParams.ts diff --git a/src/components/Drawer/Drawer.tsx b/src/components/Drawer/Drawer.tsx index 2688cb59c..0caf82f7a 100644 --- a/src/components/Drawer/Drawer.tsx +++ b/src/components/Drawer/Drawer.tsx @@ -129,10 +129,16 @@ const DrawerPaneContentWrapper = ({ ); }; +export enum DrawerControlType { + CLOSE = 'close', + COPY_LINK = 'copyLink', + CUSTOM = 'custom', +} + type DrawerControl = - | {type: 'close'} - | {type: 'copyLink'; link: string} - | {type: 'custom'; node: React.ReactNode; key: string}; + | {type: DrawerControlType.CLOSE} + | {type: DrawerControlType.COPY_LINK; link: string} + | {type: DrawerControlType.CUSTOM; node: React.ReactNode; key: string}; interface DrawerPaneProps { children: React.ReactNode; 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..23dfaa569 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss @@ -0,0 +1,71 @@ +@import '../../../../../styles/mixins.scss'; + +.kv-query-details { + display: flex; + flex-direction: column; + + height: 100%; + + color: var(--g-color-text-primary); + background-color: var(--g-color-base-background-dark); + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + + padding: var(--g-spacing-5) var(--g-spacing-6) 0 var(--g-spacing-6); + } + + &__title { + margin: 0; + + font-size: 16px; + font-weight: 500; + } + + &__actions { + display: flex; + gap: var(--g-spacing-2); + } + + &__content { + overflow: auto; + flex: 1; + + padding: var(--g-spacing-5) var(--g-spacing-4) var(--g-spacing-5) var(--g-spacing-6); + } + + &__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); + } + + &__icon { + // prevent button icon from firing onMouseEnter/onFocus through parent button's handler + pointer-events: none; + } +} 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..296960172 --- /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('kv-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..bfd0157df --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx @@ -0,0 +1,78 @@ +import React from 'react'; + +import {Button, Icon, Text} from '@gravity-ui/uikit'; +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 {cn} from '../../../../../utils/cn'; +import {useTypedDispatch} from '../../../../../utils/hooks'; +import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; +import i18n from '../i18n'; +import {createQueryInfoItems} from '../utils'; + +import {QueryDetails} from './QueryDetails'; + +import CryCatIcon from '../../../../../assets/icons/cry-cat.svg'; + +const b = cn('kv-top-queries'); + +interface QueryDetailsDrawerContentProps { + row: KeyValueRow | null; + onClose: () => void; // Needed for the "not found" case +} + +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 ( +
+ + + {i18n('query-details.not-found.title')} + + + {i18n('query-details.not-found.description')} + + +
+ ); +}; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx index 0916cb0e9..dc33775e7 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 {DrawerControlType} 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(() => { @@ -70,44 +75,90 @@ export const RunningQueriesData = ({ {pollingInterval: autoRefreshInterval}, ); - const handleRowClick = (row: KeyValueRow) => { - return onRowClick(row.QueryText as string); - }; + const rows = data?.resultSets?.[0]?.result; + + const isDrawerVisible = selectedRow !== undefined; + + const handleCloseDetails = React.useCallback(() => { + setSelectedRow(undefined); + }, [setSelectedRow]); + + const renderDrawerContent = React.useCallback(() => { + if (!isDrawerVisible) { + return null; + } + return ; + }, [isDrawerVisible, 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 = React.useMemo(() => [{type: DrawerControlType.CLOSE} as const], []); 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..7cc2a9334 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,30 @@ text-overflow: ellipsis; } + + &__drawer { + margin-top: calc(-1 * var(--g-spacing-4)); + } + + &__empty-state-icon { + color: var(--g-color-text-primary); + } + + &__not-found-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + 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..a91cce888 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,8 +46,6 @@ 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); @@ -66,26 +54,6 @@ export const TopQueries = ({tenantName}: TopQueriesProps) => { 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 +77,6 @@ export const TopQueries = ({tenantName}: TopQueriesProps) => { tenantName={tenantName} timeFrame={timeFrame} renderQueryModeControl={renderQueryModeControl} - onRowClick={onRowClick} handleTimeFrameChange={handleTimeFrameChange} handleDateRangeChange={handleDateRangeChange} handleTextSearchUpdate={handleTextSearchUpdate} @@ -118,7 +85,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..a092864c8 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 {DrawerControlType} 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 {useTopQueriesSort} from './hooks/useTopQueriesSort'; +import {useGetSelectedRowTableSort} from './hooks/useGetSelectedRowTableSort'; +import {useSetSelectedTopQueryRowFromParams} from './hooks/useSetSelectedTopQueryRowFromParams'; import i18n from './i18n'; -import {TOP_QUERIES_TABLE_SETTINGS} from './utils'; +import {TOP_QUERIES_TABLE_SETTINGS, useTopQueriesSort} 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,9 @@ 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 initialTableSort = useGetSelectedRowTableSort(); + const {tableSort, handleTableSort, backendSort} = useTopQueriesSort(initialTableSort); + const {currentData, isFetching, isLoading, error} = topQueriesApi.useGetTopQueriesQuery( { database: tenantName, filters, @@ -81,55 +87,115 @@ 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, tableSort); + } + return ''; + }, [selectedRow, tableSort]); + + const renderDrawerContent = React.useCallback(() => { + if (!isDrawerVisible) { + return null; + } + return ; + }, [isDrawerVisible, 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 = React.useMemo( + () => [ + {type: DrawerControlType.COPY_LINK, link: getTopQueryUrl()} as const, + {type: DrawerControlType.CLOSE} as const, + ], + [getTopQueryUrl], + ); return ( - - - {renderQueryModeControl()} - + + + + + + {error ? : null} + + b('row', {active: isEqual(row, selectedRow)})} + sortOrder={tableSort} + onSort={handleTableSort} + /> + + + ); }; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/hooks/useGetSelectedRowTableSort.ts b/src/containers/Tenant/Diagnostics/TopQueries/hooks/useGetSelectedRowTableSort.ts new file mode 100644 index 000000000..5c91927f7 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopQueries/hooks/useGetSelectedRowTableSort.ts @@ -0,0 +1,13 @@ +import type {SortOrder} from '@gravity-ui/react-data-table'; +import {StringParam, useQueryParams} from 'use-query-params'; + +export function useGetSelectedRowTableSort(): SortOrder[] | undefined { + const [queryParams] = useQueryParams({ + selectedRow: StringParam, + }); + const searchParamsQuery: {tableSort?: SortOrder[]} = queryParams.selectedRow + ? JSON.parse(decodeURIComponent(queryParams.selectedRow)) + : {}; + + return searchParamsQuery.tableSort; +} 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..23e59175f 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json @@ -4,5 +4,15 @@ "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", + "query-not-found": "Query Not Found", + "query-not-found.description": "The selected query is no longer available in the current dataset", + "query-details.copy-link": "Copy link to query", + "query-details.link-copied": "Copied to clipboard" } diff --git a/src/containers/Tenant/Diagnostics/TopQueries/utils.ts b/src/containers/Tenant/Diagnostics/TopQueries/utils.ts index 05b229abd..04f593bce 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/utils.ts +++ b/src/containers/Tenant/Diagnostics/TopQueries/utils.ts @@ -1,9 +1,137 @@ -import type {Settings} from '@gravity-ui/react-data-table'; +import React from 'react'; +import type {Settings, SortOrder} from '@gravity-ui/react-data-table'; +import DataTable 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 {prepareBackendSortFieldsFromTableSort, useTableSort} from '../../../../utils/hooks'; +import {formatToMs, parseUsToMs} from '../../../../utils/timeParsers'; import {QUERY_TABLE_SETTINGS} from '../../utils/constants'; +import { + QUERIES_COLUMNS_IDS, + getRunningQueriesColumnSortField, + getTopQueriesColumnSortField, +} from './columns/constants'; +import columnsI18n from './columns/i18n'; + export const TOP_QUERIES_TABLE_SETTINGS: Settings = { ...QUERY_TABLE_SETTINGS, disableSortReset: true, externalSort: true, }; + +export function useTopQueriesSort(initialSort?: SortOrder[]) { + const [tableSort, handleTableSort] = useTableSort({ + initialSortColumn: initialSort?.[0]?.columnId || QUERIES_COLUMNS_IDS.CPUTime, + initialSortOrder: initialSort?.[0]?.order || DataTable.DESCENDING, + multiple: true, + fixedOrderType: DataTable.DESCENDING, + }); + + return { + tableSort, + handleTableSort, + backendSort: React.useMemo( + () => prepareBackendSortFieldsFromTableSort(tableSort, getTopQueriesColumnSortField), + [tableSort], + ), + }; +} + +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; +} + +export function useRunningQueriesSort() { + const [tableSort, handleTableSort] = useTableSort({ + initialSortColumn: QUERIES_COLUMNS_IDS.QueryStartAt, + initialSortOrder: DataTable.DESCENDING, + multiple: true, + }); + + return { + tableSort, + handleTableSort, + backendSort: React.useMemo( + () => + prepareBackendSortFieldsFromTableSort(tableSort, getRunningQueriesColumnSortField), + [tableSort], + ), + }; +} 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..2a4c52761 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopQueries/utils/generateShareableUrl.ts @@ -0,0 +1,41 @@ +import type {SortOrder} from '@gravity-ui/react-data-table'; + +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 + * @param tableSort Optional sort configuration to include in the URL + * @returns A shareable URL string with the row's parameters and sort order encoded in the URL + */ +export function generateShareableUrl(row: KeyValueRow, tableSort?: SortOrder[]): string { + const params = getTopQueryRowQueryParams(row); + + // Get current URL without query parameters + const url = new URL(window.location.href); + + // Create URLSearchParams object from current search params + const searchParams = new URLSearchParams(url.search); + + // Add our parameters + // Set a single selectedRow parameter with all query parameters + searchParams.set( + 'selectedRow', + encodeURIComponent( + JSON.stringify({ + rank: params.rank || undefined, + intervalEnd: params.intervalEnd || undefined, + endTime: params.endTime || undefined, + queryHash: params.queryHash || undefined, + tableSort: tableSort || undefined, // Include the table sort order + }), + ), + ); + + // Update URL search params + 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, + }; +} From c44f1cb06448be61ff003bb490ba30b1b0a006ba Mon Sep 17 00:00:00 2001 From: astandrik Date: Mon, 12 May 2025 17:53:04 +0300 Subject: [PATCH 02/11] fix: minor fixes --- src/components/Drawer/Drawer.tsx | 4 ++-- .../TopQueries/RunningQueriesData.tsx | 21 +++++++++---------- .../Diagnostics/TopQueries/TopQueries.scss | 4 ++-- .../Diagnostics/TopQueries/TopQueries.tsx | 11 ++++++---- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/components/Drawer/Drawer.tsx b/src/components/Drawer/Drawer.tsx index 0caf82f7a..e989370b1 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; }; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx index dc33775e7..25406f95e 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx @@ -65,17 +65,16 @@ export const RunningQueriesData = ({ const {tableSort, handleTableSort, backendSort} = useRunningQueriesSort(); - const {currentData, data, isFetching, isLoading, error} = - topQueriesApi.useGetRunningQueriesQuery( - { - database: tenantName, - filters, - sortOrder: backendSort, - }, - {pollingInterval: autoRefreshInterval}, - ); - - const rows = data?.resultSets?.[0]?.result; + const {currentData, isFetching, isLoading, error} = topQueriesApi.useGetRunningQueriesQuery( + { + database: tenantName, + filters, + sortOrder: backendSort, + }, + {pollingInterval: autoRefreshInterval}, + ); + + const rows = currentData?.resultSets?.[0]?.result; const isDrawerVisible = selectedRow !== undefined; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss index 7cc2a9334..e61dc5d8f 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss @@ -18,8 +18,8 @@ &_active { background-color: var(--g-color-base-selection); - &:hover { - background: var(--g-color-base-selection-hover) !important; + &:hover.kv-top-queries__row_active { + background: var(--g-color-base-selection-hover); } } } diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx index a91cce888..8e12614f2 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx @@ -46,11 +46,14 @@ interface TopQueriesProps { export const TopQueries = ({tenantName}: TopQueriesProps) => { const dispatch = useTypedDispatch(); - 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; From 361535751508bdf5c14b6f4e486bf13bb4c8069c Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Mon, 12 May 2025 18:02:39 +0300 Subject: [PATCH 03/11] fix: nanofixes --- .../TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx | 2 +- .../Tenant/Diagnostics/TopQueries/TopQueries.scss | 4 ++-- .../Diagnostics/TopQueries/utils/generateShareableUrl.ts | 7 +------ 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx index bfd0157df..034409f52 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx @@ -25,7 +25,7 @@ const b = cn('kv-top-queries'); interface QueryDetailsDrawerContentProps { row: KeyValueRow | null; - onClose: () => void; // Needed for the "not found" case + onClose: () => void; } export const QueryDetailsDrawerContent = ({row, onClose}: QueryDetailsDrawerContentProps) => { diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss index e61dc5d8f..7cc2a9334 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss @@ -18,8 +18,8 @@ &_active { background-color: var(--g-color-base-selection); - &:hover.kv-top-queries__row_active { - background: var(--g-color-base-selection-hover); + &:hover { + background: var(--g-color-base-selection-hover) !important; } } } diff --git a/src/containers/Tenant/Diagnostics/TopQueries/utils/generateShareableUrl.ts b/src/containers/Tenant/Diagnostics/TopQueries/utils/generateShareableUrl.ts index 2a4c52761..31f0f72fd 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/utils/generateShareableUrl.ts +++ b/src/containers/Tenant/Diagnostics/TopQueries/utils/generateShareableUrl.ts @@ -13,14 +13,10 @@ import {getTopQueryRowQueryParams} from './getTopQueryRowQueryParams'; export function generateShareableUrl(row: KeyValueRow, tableSort?: SortOrder[]): string { const params = getTopQueryRowQueryParams(row); - // Get current URL without query parameters const url = new URL(window.location.href); - // Create URLSearchParams object from current search params const searchParams = new URLSearchParams(url.search); - // Add our parameters - // Set a single selectedRow parameter with all query parameters searchParams.set( 'selectedRow', encodeURIComponent( @@ -29,12 +25,11 @@ export function generateShareableUrl(row: KeyValueRow, tableSort?: SortOrder[]): intervalEnd: params.intervalEnd || undefined, endTime: params.endTime || undefined, queryHash: params.queryHash || undefined, - tableSort: tableSort || undefined, // Include the table sort order + tableSort: tableSort || undefined, }), ), ); - // Update URL search params url.search = searchParams.toString(); return url.toString(); From ba93521f48ef0e714d615fe16dc73372602b44b0 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Mon, 12 May 2025 18:17:29 +0300 Subject: [PATCH 04/11] fix: more fixes --- .../TopQueries/RunningQueriesData.tsx | 3 +-- .../Diagnostics/TopQueries/TopQueriesData.tsx | 14 +++++----- .../hooks/useGetSelectedRowTableSort.ts | 13 ---------- .../Tenant/Diagnostics/TopQueries/utils.ts | 26 ++----------------- .../TopQueries/utils/generateShareableUrl.ts | 8 ++---- 5 files changed, 11 insertions(+), 53 deletions(-) delete mode 100644 src/containers/Tenant/Diagnostics/TopQueries/hooks/useGetSelectedRowTableSort.ts diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx index 25406f95e..67970fd7a 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx @@ -2,7 +2,6 @@ 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 {DrawerControlType} from '../../../../components/Drawer/Drawer'; @@ -152,7 +151,7 @@ export const RunningQueriesData = ({ loading={isFetching && currentData === undefined} settings={TOP_QUERIES_TABLE_SETTINGS} onRowClick={onRowClick} - rowClassName={(row) => b('row', {active: isEqual(row, selectedRow)})} + rowClassName={(row) => b('row', {active: row === selectedRow})} sortOrder={tableSort} onSort={handleTableSort} /> diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx index a092864c8..0e39ac167 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx @@ -2,7 +2,6 @@ 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'; @@ -30,10 +29,10 @@ import { TOP_QUERIES_SELECTED_COLUMNS_LS_KEY, } from './columns/constants'; import {DEFAULT_TIME_FILTER_VALUE, TIME_FRAME_OPTIONS} from './constants'; -import {useGetSelectedRowTableSort} from './hooks/useGetSelectedRowTableSort'; import {useSetSelectedTopQueryRowFromParams} from './hooks/useSetSelectedTopQueryRowFromParams'; +import {useTopQueriesSort} from './hooks/useTopQueriesSort'; import i18n from './i18n'; -import {TOP_QUERIES_TABLE_SETTINGS, useTopQueriesSort} from './utils'; +import {TOP_QUERIES_TABLE_SETTINGS} from './utils'; import {generateShareableUrl} from './utils/generateShareableUrl'; const b = cn('kv-top-queries'); @@ -75,8 +74,7 @@ export const TopQueriesData = ({ REQUIRED_TOP_QUERIES_COLUMNS, ); - const initialTableSort = useGetSelectedRowTableSort(); - const {tableSort, handleTableSort, backendSort} = useTopQueriesSort(initialTableSort); + const {tableSort, handleTableSort, backendSort} = useTopQueriesSort(); const {currentData, isFetching, isLoading, error} = topQueriesApi.useGetTopQueriesQuery( { database: tenantName, @@ -98,10 +96,10 @@ export const TopQueriesData = ({ const getTopQueryUrl = React.useCallback(() => { if (selectedRow) { - return generateShareableUrl(selectedRow, tableSort); + return generateShareableUrl(selectedRow); } return ''; - }, [selectedRow, tableSort]); + }, [selectedRow]); const renderDrawerContent = React.useCallback(() => { if (!isDrawerVisible) { @@ -190,7 +188,7 @@ export const TopQueriesData = ({ loading={isFetching && currentData === undefined} settings={TOP_QUERIES_TABLE_SETTINGS} onRowClick={onRowClick} - rowClassName={(row) => b('row', {active: isEqual(row, selectedRow)})} + rowClassName={(row) => b('row', {active: row === selectedRow})} sortOrder={tableSort} onSort={handleTableSort} /> diff --git a/src/containers/Tenant/Diagnostics/TopQueries/hooks/useGetSelectedRowTableSort.ts b/src/containers/Tenant/Diagnostics/TopQueries/hooks/useGetSelectedRowTableSort.ts deleted file mode 100644 index 5c91927f7..000000000 --- a/src/containers/Tenant/Diagnostics/TopQueries/hooks/useGetSelectedRowTableSort.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type {SortOrder} from '@gravity-ui/react-data-table'; -import {StringParam, useQueryParams} from 'use-query-params'; - -export function useGetSelectedRowTableSort(): SortOrder[] | undefined { - const [queryParams] = useQueryParams({ - selectedRow: StringParam, - }); - const searchParamsQuery: {tableSort?: SortOrder[]} = queryParams.selectedRow - ? JSON.parse(decodeURIComponent(queryParams.selectedRow)) - : {}; - - return searchParamsQuery.tableSort; -} diff --git a/src/containers/Tenant/Diagnostics/TopQueries/utils.ts b/src/containers/Tenant/Diagnostics/TopQueries/utils.ts index 04f593bce..39cd99711 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/utils.ts +++ b/src/containers/Tenant/Diagnostics/TopQueries/utils.ts @@ -1,6 +1,6 @@ import React from 'react'; -import type {Settings, SortOrder} from '@gravity-ui/react-data-table'; +import type {Settings} from '@gravity-ui/react-data-table'; import DataTable from '@gravity-ui/react-data-table'; import type {InfoViewerItem} from '../../../../components/InfoViewer'; @@ -11,11 +11,7 @@ import {prepareBackendSortFieldsFromTableSort, useTableSort} from '../../../../u import {formatToMs, parseUsToMs} from '../../../../utils/timeParsers'; import {QUERY_TABLE_SETTINGS} from '../../utils/constants'; -import { - QUERIES_COLUMNS_IDS, - getRunningQueriesColumnSortField, - getTopQueriesColumnSortField, -} from './columns/constants'; +import {QUERIES_COLUMNS_IDS, getRunningQueriesColumnSortField} from './columns/constants'; import columnsI18n from './columns/i18n'; export const TOP_QUERIES_TABLE_SETTINGS: Settings = { @@ -24,24 +20,6 @@ export const TOP_QUERIES_TABLE_SETTINGS: Settings = { externalSort: true, }; -export function useTopQueriesSort(initialSort?: SortOrder[]) { - const [tableSort, handleTableSort] = useTableSort({ - initialSortColumn: initialSort?.[0]?.columnId || QUERIES_COLUMNS_IDS.CPUTime, - initialSortOrder: initialSort?.[0]?.order || DataTable.DESCENDING, - multiple: true, - fixedOrderType: DataTable.DESCENDING, - }); - - return { - tableSort, - handleTableSort, - backendSort: React.useMemo( - () => prepareBackendSortFieldsFromTableSort(tableSort, getTopQueriesColumnSortField), - [tableSort], - ), - }; -} - export function createQueryInfoItems(data: KeyValueRow): InfoViewerItem[] { const items: InfoViewerItem[] = []; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/utils/generateShareableUrl.ts b/src/containers/Tenant/Diagnostics/TopQueries/utils/generateShareableUrl.ts index 31f0f72fd..8dc9a2ff3 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/utils/generateShareableUrl.ts +++ b/src/containers/Tenant/Diagnostics/TopQueries/utils/generateShareableUrl.ts @@ -1,5 +1,3 @@ -import type {SortOrder} from '@gravity-ui/react-data-table'; - import type {KeyValueRow} from '../../../../../types/api/query'; import {getTopQueryRowQueryParams} from './getTopQueryRowQueryParams'; @@ -7,10 +5,9 @@ import {getTopQueryRowQueryParams} from './getTopQueryRowQueryParams'; /** * Generates a shareable URL with query parameters for a top query row * @param row The top query row data - * @param tableSort Optional sort configuration to include in the URL - * @returns A shareable URL string with the row's parameters and sort order encoded in the URL + * @returns A shareable URL string with the row's parameters encoded in the URL */ -export function generateShareableUrl(row: KeyValueRow, tableSort?: SortOrder[]): string { +export function generateShareableUrl(row: KeyValueRow): string { const params = getTopQueryRowQueryParams(row); const url = new URL(window.location.href); @@ -25,7 +22,6 @@ export function generateShareableUrl(row: KeyValueRow, tableSort?: SortOrder[]): intervalEnd: params.intervalEnd || undefined, endTime: params.endTime || undefined, queryHash: params.queryHash || undefined, - tableSort: tableSort || undefined, }), ), ); From eceba4200bb4bdae7a33d6ceba9512024f25fbdd Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Mon, 12 May 2025 20:08:29 +0300 Subject: [PATCH 05/11] fix: isEqual --- .../Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx | 3 ++- .../Tenant/Diagnostics/TopQueries/TopQueriesData.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx index 67970fd7a..25406f95e 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx @@ -2,6 +2,7 @@ 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 {DrawerControlType} from '../../../../components/Drawer/Drawer'; @@ -151,7 +152,7 @@ export const RunningQueriesData = ({ loading={isFetching && currentData === undefined} settings={TOP_QUERIES_TABLE_SETTINGS} onRowClick={onRowClick} - rowClassName={(row) => b('row', {active: row === selectedRow})} + rowClassName={(row) => b('row', {active: isEqual(row, selectedRow)})} sortOrder={tableSort} onSort={handleTableSort} /> diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx index 0e39ac167..bc16df01c 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx @@ -2,6 +2,7 @@ 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'; @@ -188,7 +189,7 @@ export const TopQueriesData = ({ loading={isFetching && currentData === undefined} settings={TOP_QUERIES_TABLE_SETTINGS} onRowClick={onRowClick} - rowClassName={(row) => b('row', {active: row === selectedRow})} + rowClassName={(row) => b('row', {active: isEqual(row, selectedRow)})} sortOrder={tableSort} onSort={handleTableSort} /> From dfd6964d69400817ad08c0fd5f553f0d70248603 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Mon, 12 May 2025 20:15:42 +0300 Subject: [PATCH 06/11] fix: move to separate container --- .../QueryDetails/NotFoundContainer.tsx | 29 +++++++++++++++++++ .../QueryDetailsDrawerContent.tsx | 23 ++------------- 2 files changed, 31 insertions(+), 21 deletions(-) create mode 100644 src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/NotFoundContainer.tsx 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..2360df3a6 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/NotFoundContainer.tsx @@ -0,0 +1,29 @@ +import {Button, 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/QueryDetailsDrawerContent.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx index 034409f52..b0cc374b9 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import {Button, Icon, Text} from '@gravity-ui/uikit'; import {useHistory, useLocation} from 'react-router-dom'; import {parseQuery} from '../../../../../routes'; @@ -11,18 +10,13 @@ import { TENANT_QUERY_TABS_ID, } from '../../../../../store/reducers/tenant/constants'; import type {KeyValueRow} from '../../../../../types/api/query'; -import {cn} from '../../../../../utils/cn'; import {useTypedDispatch} from '../../../../../utils/hooks'; import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; -import i18n from '../i18n'; import {createQueryInfoItems} from '../utils'; +import {NotFoundContainer} from './NotFoundContainer'; import {QueryDetails} from './QueryDetails'; -import CryCatIcon from '../../../../../assets/icons/cry-cat.svg'; - -const b = cn('kv-top-queries'); - interface QueryDetailsDrawerContentProps { row: KeyValueRow | null; onClose: () => void; @@ -61,18 +55,5 @@ export const QueryDetailsDrawerContent = ({row, onClose}: QueryDetailsDrawerCont ); } - return ( -
- - - {i18n('query-details.not-found.title')} - - - {i18n('query-details.not-found.description')} - - -
- ); + return ; }; From 523f8d051c435a065cf5600ed8611683a585d9c9 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Tue, 13 May 2025 01:00:33 +0300 Subject: [PATCH 07/11] fix: remove junk --- .../Tenant/Diagnostics/TopQueries/utils.ts | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/src/containers/Tenant/Diagnostics/TopQueries/utils.ts b/src/containers/Tenant/Diagnostics/TopQueries/utils.ts index 39cd99711..9a00c085d 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/utils.ts +++ b/src/containers/Tenant/Diagnostics/TopQueries/utils.ts @@ -1,17 +1,12 @@ -import React from 'react'; - import type {Settings} from '@gravity-ui/react-data-table'; -import DataTable 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 {prepareBackendSortFieldsFromTableSort, useTableSort} from '../../../../utils/hooks'; import {formatToMs, parseUsToMs} from '../../../../utils/timeParsers'; import {QUERY_TABLE_SETTINGS} from '../../utils/constants'; -import {QUERIES_COLUMNS_IDS, getRunningQueriesColumnSortField} from './columns/constants'; import columnsI18n from './columns/i18n'; export const TOP_QUERIES_TABLE_SETTINGS: Settings = { @@ -95,21 +90,3 @@ export function createQueryInfoItems(data: KeyValueRow): InfoViewerItem[] { return items; } - -export function useRunningQueriesSort() { - const [tableSort, handleTableSort] = useTableSort({ - initialSortColumn: QUERIES_COLUMNS_IDS.QueryStartAt, - initialSortOrder: DataTable.DESCENDING, - multiple: true, - }); - - return { - tableSort, - handleTableSort, - backendSort: React.useMemo( - () => - prepareBackendSortFieldsFromTableSort(tableSort, getRunningQueriesColumnSortField), - [tableSort], - ), - }; -} From 35528259dc37df65bbb6bab0a432d00d6f696ab0 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Tue, 13 May 2025 01:22:18 +0300 Subject: [PATCH 08/11] fix: panel height --- .../Diagnostics/TopQueries/QueryDetails/QueryDetails.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss index 23dfaa569..ab1f25614 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss @@ -2,10 +2,9 @@ .kv-query-details { display: flex; + flex: 1; flex-direction: column; - height: 100%; - color: var(--g-color-text-primary); background-color: var(--g-color-base-background-dark); From d9db6cc06a5dbf84971c369696fafd642db2a4d6 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Tue, 13 May 2025 01:30:57 +0300 Subject: [PATCH 09/11] fix: fix padding --- .../TopQueries/QueryDetails/QueryDetails.scss | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss index ab1f25614..16ba263cc 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss @@ -8,31 +8,11 @@ color: var(--g-color-text-primary); background-color: var(--g-color-base-background-dark); - &__header { - display: flex; - justify-content: space-between; - align-items: center; - - padding: var(--g-spacing-5) var(--g-spacing-6) 0 var(--g-spacing-6); - } - - &__title { - margin: 0; - - font-size: 16px; - font-weight: 500; - } - - &__actions { - display: flex; - gap: var(--g-spacing-2); - } - &__content { overflow: auto; flex: 1; - padding: var(--g-spacing-5) var(--g-spacing-4) var(--g-spacing-5) var(--g-spacing-6); + padding: var(--g-spacing-5) var(--g-spacing-4) var(--g-spacing-5) var(--g-spacing-4); } &__query-header { @@ -62,9 +42,4 @@ border-radius: 4px; background-color: var(--code-background-color); } - - &__icon { - // prevent button icon from firing onMouseEnter/onFocus through parent button's handler - pointer-events: none; - } } From 30a741f69b35381674f2f742d7e1a4ac7e5302e1 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Tue, 13 May 2025 12:04:34 +0300 Subject: [PATCH 10/11] fix: review fixes --- src/components/Drawer/Drawer.tsx | 14 ++++---------- .../QueryDetails/NotFoundContainer.tsx | 11 ++++++++--- .../TopQueries/QueryDetails/QueryDetails.scss | 4 +--- .../TopQueries/QueryDetails/QueryDetails.tsx | 6 +++--- .../QueryDetailsDrawerContent.tsx | 6 +++++- .../TopQueries/RunningQueriesData.tsx | 14 ++++++-------- .../Diagnostics/TopQueries/TopQueries.scss | 5 ----- .../Diagnostics/TopQueries/TopQueriesData.tsx | 19 +++++++------------ .../Diagnostics/TopQueries/i18n/en.json | 6 +----- 9 files changed, 35 insertions(+), 50 deletions(-) diff --git a/src/components/Drawer/Drawer.tsx b/src/components/Drawer/Drawer.tsx index e989370b1..8af990fe7 100644 --- a/src/components/Drawer/Drawer.tsx +++ b/src/components/Drawer/Drawer.tsx @@ -129,16 +129,10 @@ const DrawerPaneContentWrapper = ({ ); }; -export enum DrawerControlType { - CLOSE = 'close', - COPY_LINK = 'copyLink', - CUSTOM = 'custom', -} - -type DrawerControl = - | {type: DrawerControlType.CLOSE} - | {type: DrawerControlType.COPY_LINK; link: string} - | {type: DrawerControlType.CUSTOM; node: React.ReactNode; key: string}; +export type DrawerControl = + | {type: 'close'} + | {type: 'copyLink'; link: string} + | {type: 'custom'; node: React.ReactNode; key: string}; interface DrawerPaneProps { children: React.ReactNode; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/NotFoundContainer.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/NotFoundContainer.tsx index 2360df3a6..3ddb254d9 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/NotFoundContainer.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/NotFoundContainer.tsx @@ -1,4 +1,4 @@ -import {Button, Icon, Text} from '@gravity-ui/uikit'; +import {Button, Flex, Icon, Text} from '@gravity-ui/uikit'; import {cn} from '../../../../../utils/cn'; import i18n from '../i18n'; @@ -13,7 +13,12 @@ interface NotFoundContainerProps { export const NotFoundContainer = ({onClose}: NotFoundContainerProps) => { return ( -
+ {i18n('query-details.not-found.title')} @@ -24,6 +29,6 @@ export const NotFoundContainer = ({onClose}: NotFoundContainerProps) => { -
+ ); }; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss index 16ba263cc..85ab52c11 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss @@ -1,9 +1,7 @@ @import '../../../../../styles/mixins.scss'; -.kv-query-details { - display: flex; +.ydb-query-details { flex: 1; - flex-direction: column; color: var(--g-color-text-primary); background-color: var(--g-color-base-background-dark); diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.tsx index 296960172..cfdfd07c6 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.tsx @@ -9,7 +9,7 @@ import i18n from '../i18n'; import './QueryDetails.scss'; -const b = cn('kv-query-details'); +const b = cn('ydb-query-details'); interface QueryDetailsProps { queryText: string; @@ -19,7 +19,7 @@ interface QueryDetailsProps { export const QueryDetails = ({queryText, infoItems, onOpenInEditor}: QueryDetailsProps) => { return ( -
+ @@ -43,6 +43,6 @@ export const QueryDetails = ({queryText, infoItems, onOpenInEditor}: QueryDetail />
- + ); }; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx index b0cc374b9..e291059fd 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx @@ -18,7 +18,11 @@ import {NotFoundContainer} from './NotFoundContainer'; import {QueryDetails} from './QueryDetails'; interface QueryDetailsDrawerContentProps { - row: KeyValueRow | null; + // 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; } diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx index 25406f95e..bebf6d975 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx @@ -5,7 +5,7 @@ import {TableColumnSetup} from '@gravity-ui/uikit'; import {isEqual} from 'lodash'; import {DrawerWrapper} from '../../../../components/Drawer'; -import {DrawerControlType} from '../../../../components/Drawer/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'; @@ -82,12 +82,10 @@ export const RunningQueriesData = ({ setSelectedRow(undefined); }, [setSelectedRow]); - const renderDrawerContent = React.useCallback(() => { - if (!isDrawerVisible) { - return null; - } - return ; - }, [isDrawerVisible, selectedRow, handleCloseDetails]); + const renderDrawerContent = React.useCallback( + () => , + [selectedRow, handleCloseDetails], + ); const onRowClick = React.useCallback( ( @@ -109,7 +107,7 @@ export const RunningQueriesData = ({ } }, [isDrawerVisible]); - const drawerControls = React.useMemo(() => [{type: DrawerControlType.CLOSE} as const], []); + const drawerControls: DrawerControl[] = React.useMemo(() => [{type: 'close'}], []); return ( { - if (!isDrawerVisible) { - return null; - } - return ; - }, [isDrawerVisible, selectedRow, handleCloseDetails]); + const renderDrawerContent = React.useCallback( + () => , + [selectedRow, handleCloseDetails], + ); const onRowClick = React.useCallback( ( @@ -129,11 +127,8 @@ export const TopQueriesData = ({ } }, [isDrawerVisible]); - const drawerControls = React.useMemo( - () => [ - {type: DrawerControlType.COPY_LINK, link: getTopQueryUrl()} as const, - {type: DrawerControlType.CLOSE} as const, - ], + const drawerControls: DrawerControl[] = React.useMemo( + () => [{type: 'copyLink', link: getTopQueryUrl()}, {type: 'close'}], [getTopQueryUrl], ); diff --git a/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json b/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json index 23e59175f..06da07323 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json @@ -10,9 +10,5 @@ "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", - "query-not-found": "Query Not Found", - "query-not-found.description": "The selected query is no longer available in the current dataset", - "query-details.copy-link": "Copy link to query", - "query-details.link-copied": "Copied to clipboard" + "query-details.not-found.description": "This query no longer exists" } From 48aae892c82ec23d7083a403cd5d580c1b6654c5 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Tue, 13 May 2025 13:43:34 +0300 Subject: [PATCH 11/11] fix: move isDrawerVisible to inner --- src/components/Drawer/Drawer.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/Drawer/Drawer.tsx b/src/components/Drawer/Drawer.tsx index 8af990fe7..5db957419 100644 --- a/src/components/Drawer/Drawer.tsx +++ b/src/components/Drawer/Drawer.tsx @@ -223,10 +223,12 @@ export const DrawerWrapper = ({ detectClickOutside={detectClickOutside} isPercentageWidth={isPercentageWidth} > -
- {renderDrawerHeader()} - {renderDrawerContent()} -
+ {isDrawerVisible ? ( +
+ {renderDrawerHeader()} + {renderDrawerContent()} +
+ ) : null} );