From 7df144054b2ef7ffd988436c2a9273626e285448 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 10 Apr 2025 18:29:36 +0300 Subject: [PATCH 01/21] feat: side panel aka refrigerator for query text in top queries --- .../Tenant/Diagnostics/Diagnostics.scss | 6 ++ .../Tenant/Diagnostics/Diagnostics.tsx | 6 +- .../Diagnostics/TopQueries/QueryDetails.scss | 81 ++++++++++++++++ .../Diagnostics/TopQueries/QueryDetails.tsx | 94 +++++++++++++++++++ .../TopQueries/RunningQueriesData.tsx | 8 +- .../Diagnostics/TopQueries/TopQueries.scss | 8 ++ .../Diagnostics/TopQueries/TopQueries.tsx | 91 ++++++++++++++---- .../Diagnostics/TopQueries/TopQueriesData.tsx | 8 +- .../Diagnostics/TopQueries/i18n/en.json | 16 +++- 9 files changed, 286 insertions(+), 32 deletions(-) create mode 100644 src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss create mode 100644 src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.tsx diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.scss b/src/containers/Tenant/Diagnostics/Diagnostics.scss index 070d3d16c..9a7409b72 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.scss +++ b/src/containers/Tenant/Diagnostics/Diagnostics.scss @@ -34,6 +34,12 @@ } } + &__drawer-container { + position: relative; + + height: 100%; + } + &__page-wrapper { overflow: auto; flex-grow: 1; diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.tsx b/src/containers/Tenant/Diagnostics/Diagnostics.tsx index 5c69d3f28..82ff8285e 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.tsx +++ b/src/containers/Tenant/Diagnostics/Diagnostics.tsx @@ -194,8 +194,10 @@ function Diagnostics(props: DiagnosticsProps) { ) : null} {renderTabs()} -
- {renderTabContent()} +
+
+ {renderTabContent()} +
); diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss new file mode 100644 index 000000000..4172c013f --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss @@ -0,0 +1,81 @@ +@import '../../../../styles/mixins.scss'; + +.kv-query-details { + display: flex; + flex-direction: column; + height: 100%; + background-color: var(--g-color-base-background-dark); + color: var(--g-color-text-primary-invert); + padding: var(--g-spacing-5) var(--g-spacing-6); + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + } + + &__title { + margin: 0; + font-size: 16px; + font-weight: 500; + } + + &__actions { + display: flex; + gap: 8px; + } + + &__content { + flex: 1; + padding-top: var(--g-spacing-5); + overflow: auto; + } + + &__query-section { + margin-top: 20px; + } + + &__query-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + } + + &__query-title { + font-size: 14px; + font-weight: 500; + } + + &__query-content { + background-color: #1e1e1e; + border-radius: 4px; + overflow: hidden; + position: relative; + + pre { + margin: 0 !important; + max-height: 100%; + background-color: transparent !important; + } + } + + &__close-button { + color: var(--g-color-text-secondary-invert); + + &:hover { + color: var(--g-color-text-primary-invert); + } + } + + &__editor-button { + display: flex; + align-items: center; + gap: 6px; + color: var(--g-color-text-secondary-invert); + + &:hover { + color: var(--g-color-text-primary-invert); + } + } +} diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.tsx new file mode 100644 index 000000000..535f17616 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.tsx @@ -0,0 +1,94 @@ +import React from 'react'; + +import {Button, Icon} from '@gravity-ui/uikit'; + +import type {InfoViewerItem} from '../../../../components/InfoViewer'; +import {InfoViewer} from '../../../../components/InfoViewer'; +import {YDBSyntaxHighlighter} from '../../../../components/SyntaxHighlighter/YDBSyntaxHighlighter'; +import type {KeyValueRow} from '../../../../types/api/query'; +import {cn} from '../../../../utils/cn'; +import {formatDateTime, formatNumber} from '../../../../utils/dataFormatters/dataFormatters'; +import {generateHash} from '../../../../utils/generateHash'; +import {formatToMs, parseUsToMs} from '../../../../utils/timeParsers'; + +import i18n from './i18n'; + +import './QueryDetails.scss'; + +const b = cn('kv-query-details'); + +interface QueryDetailsProps { + row: KeyValueRow; + onClose: () => void; + onOpenInEditor: () => void; +} + +export const QueryDetails = ({row, onClose, onOpenInEditor}: QueryDetailsProps) => { + const query = row.QueryText as string; + // Create info items for the InfoViewer with formatting matching the columns + const infoItems: InfoViewerItem[] = React.useMemo(() => { + return [ + {label: i18n('query-details.query-hash'), value: generateHash(String(row.QueryText))}, + { + label: i18n('query-details.cpu-time'), + value: formatToMs(parseUsToMs(row.CPUTimeUs ?? undefined)), + }, + { + label: i18n('query-details.duration'), + value: formatToMs(parseUsToMs(row.Duration ?? undefined)), + }, + {label: i18n('query-details.read-bytes'), value: formatNumber(row.ReadBytes)}, + {label: i18n('query-details.request-units'), value: formatNumber(row.RequestUnits)}, + { + label: i18n('query-details.end-time'), + value: row.EndTime + ? formatDateTime(new Date(row.EndTime as string).getTime()) + : '–', + }, + {label: i18n('query-details.read-rows'), value: formatNumber(row.ReadRows)}, + {label: i18n('query-details.user-sid'), value: row.UserSID || '–'}, + {label: i18n('query-details.application-name'), value: row.ApplicationName || '–'}, + { + label: i18n('query-details.query-start-at'), + value: row.QueryStartAt + ? formatDateTime(new Date(row.QueryStartAt as string).getTime()) + : '–', + }, + ]; + }, [row]); + + return ( +
+
+
Query
+
+ +
+
+ +
+ + +
+
+
{i18n('query-details.query.title')}
+ +
+
+ +
+
+
+
+ ); +}; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx index e63a8dbb0..d60f52e95 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx @@ -30,14 +30,14 @@ const b = cn('kv-top-queries'); interface RunningQueriesDataProps { tenantName: string; renderQueryModeControl: () => React.ReactNode; - onRowClick: (query: string) => void; + handleRowClick: (row: KeyValueRow) => void; handleTextSearchUpdate: (text: string) => void; } export const RunningQueriesData = ({ tenantName, renderQueryModeControl, - onRowClick, + handleRowClick, handleTextSearchUpdate, }: RunningQueriesDataProps) => { const [autoRefreshInterval] = useAutoRefreshInterval(); @@ -69,10 +69,6 @@ export const RunningQueriesData = ({ {pollingInterval: autoRefreshInterval}, ); - const handleRowClick = (row: KeyValueRow) => { - return onRowClick(row.QueryText as string); - }; - return ( diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss index 7f7d2b2fa..c4b71b577 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss @@ -32,4 +32,12 @@ text-overflow: ellipsis; } + + &__drawer-item { + z-index: 4; + // Because of tabs padding it is needed to move drawer item a little higher + // to make it stick to bottom of tabs + margin-top: calc(-1 * var(--g-spacing-4)); + height: calc(100% + var(--g-spacing-4)); + } } diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx index 1f430c473..bd04b110a 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx @@ -1,5 +1,6 @@ import React from 'react'; +import {Drawer, DrawerItem} from '@gravity-ui/navigation'; import type {RadioButtonOption} from '@gravity-ui/uikit'; import {RadioButton} from '@gravity-ui/uikit'; import {useHistory, useLocation} from 'react-router-dom'; @@ -16,10 +17,13 @@ import { 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 {useChangeInputWithConfirmation} from '../../../../utils/hooks/withConfirmation/useChangeInputWithConfirmation'; import {TenantTabsGroups, getTenantPath} from '../../TenantPages'; +import {QueryDetails} from './QueryDetails'; import {RunningQueriesData} from './RunningQueriesData'; import {TopQueriesData} from './TopQueriesData'; import {TimeFrameIds} from './constants'; @@ -54,6 +58,9 @@ interface TopQueriesProps { tenantName: string; } +const DRAWER_WIDTH_KEY = 'kv-top-queries-drawer-width'; +const b = cn('kv-top-queries'); + export const TopQueries = ({tenantName}: TopQueriesProps) => { const dispatch = useTypedDispatch(); const location = useLocation(); @@ -66,6 +73,13 @@ export const TopQueries = ({tenantName}: TopQueriesProps) => { const isTopQueries = queryMode === QueryModeIds.top; + const [selectedRow, setSelectedRow] = React.useState(null); + const [isDrawerVisible, setIsDrawerVisible] = React.useState(false); + const [drawerWidth, setDrawerWidth] = React.useState(() => { + const savedWidth = localStorage.getItem(DRAWER_WIDTH_KEY); + return savedWidth ? Number(savedWidth) : 400; + }); + const applyRowClick = React.useCallback( (input: string) => { dispatch(changeUserInput({input})); @@ -104,22 +118,65 @@ export const TopQueries = ({tenantName}: TopQueriesProps) => { ); }, [queryMode, setQueryMode]); - return isTopQueries ? ( - - ) : ( - + const handleRowClick = (row: KeyValueRow) => { + setSelectedRow(row); + setIsDrawerVisible(true); + }; + + const handleOpenInEditor = () => { + if (selectedRow) { + onRowClick(selectedRow.QueryText as string); + } + }; + + const handleCloseDetails = () => { + setIsDrawerVisible(false); + }; + + const handleResizeDrawer = (width: number) => { + setDrawerWidth(width); + localStorage.setItem(DRAWER_WIDTH_KEY, width.toString()); + }; + + return ( + + {isTopQueries ? ( + + ) : ( + + )} + + + {selectedRow && ( + + )} + + + ); }; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx index debd299a6..8f460318f 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx @@ -35,7 +35,7 @@ interface TopQueriesDataProps { tenantName: string; timeFrame: TimeFrame; renderQueryModeControl: () => React.ReactNode; - onRowClick: (query: string) => void; + handleRowClick: (row: KeyValueRow) => void; handleTimeFrameChange: (value: string[]) => void; handleDateRangeChange: (value: DateRangeValues) => void; handleTextSearchUpdate: (text: string) => void; @@ -45,7 +45,7 @@ export const TopQueriesData = ({ tenantName, timeFrame, renderQueryModeControl, - onRowClick, + handleRowClick, handleTimeFrameChange, handleDateRangeChange, handleTextSearchUpdate, @@ -79,10 +79,6 @@ export const TopQueriesData = ({ {pollingInterval: autoRefreshInterval}, ); - const handleRowClick = (row: KeyValueRow) => { - return onRowClick(row.QueryText as string); - }; - return ( diff --git a/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json b/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json index 5dbcec47d..4142e13da 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json @@ -4,5 +4,19 @@ "mode_top": "Top", "mode_running": "Running", "timeframe_hour": "Per hour", - "timeframe_minute": "Per minute" + "timeframe_minute": "Per minute", + "query-details.title": "Query Details", + "query-details.open-in-editor": "Open in Editor", + "query-details.close": "Close", + "query-details.query-hash": "Query Hash", + "query-details.cpu-time": "CPU Time", + "query-details.duration": "Duration", + "query-details.read-bytes": "Read Bytes", + "query-details.request-units": "Request Units", + "query-details.end-time": "End Time", + "query-details.read-rows": "Read Rows", + "query-details.user-sid": "User SID", + "query-details.application-name": "Application Name", + "query-details.query-start-at": "Query Start Time", + "query-details.query.title": "Query Text" } From d93d67bee599c1e5e10a1149495a7eac935bcfb2 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 10 Apr 2025 18:31:56 +0300 Subject: [PATCH 02/21] fix: fix lint --- .../Diagnostics/TopQueries/QueryDetails.scss | 25 +++++++++++++------ .../Diagnostics/TopQueries/TopQueries.scss | 3 ++- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss index 4172c013f..62a42d9ec 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss @@ -3,19 +3,22 @@ .kv-query-details { display: flex; flex-direction: column; + height: 100%; - background-color: var(--g-color-base-background-dark); - color: var(--g-color-text-primary-invert); padding: var(--g-spacing-5) var(--g-spacing-6); + color: var(--g-color-text-primary-invert); + background-color: var(--g-color-base-background-dark); + &__header { display: flex; - align-items: center; justify-content: space-between; + align-items: center; } &__title { margin: 0; + font-size: 16px; font-weight: 500; } @@ -26,9 +29,10 @@ } &__content { + overflow: auto; flex: 1; + padding-top: var(--g-spacing-5); - overflow: auto; } &__query-section { @@ -39,6 +43,7 @@ display: flex; justify-content: space-between; align-items: center; + margin-bottom: 12px; } @@ -48,14 +53,17 @@ } &__query-content { - background-color: #1e1e1e; - border-radius: 4px; - overflow: hidden; position: relative; + overflow: hidden; + + border-radius: 4px; + background-color: var(--code-background-color); + pre { - margin: 0 !important; max-height: 100%; + margin: 0 !important; + background-color: transparent !important; } } @@ -72,6 +80,7 @@ display: flex; align-items: center; gap: 6px; + color: var(--g-color-text-secondary-invert); &:hover { diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss index c4b71b577..c8c03a389 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss @@ -35,9 +35,10 @@ &__drawer-item { z-index: 4; + + height: calc(100% + var(--g-spacing-4)); // Because of tabs padding it is needed to move drawer item a little higher // to make it stick to bottom of tabs margin-top: calc(-1 * var(--g-spacing-4)); - height: calc(100% + var(--g-spacing-4)); } } From 147dcd767d839e3b87dca29c7c1156abac124684 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Tue, 15 Apr 2025 19:54:55 +0300 Subject: [PATCH 03/21] feat: refrigerator --- .../Tenant/Diagnostics/Diagnostics.scss | 7 +- .../Diagnostics/TopQueries/QueryDetails.scss | 37 ++---- .../Diagnostics/TopQueries/QueryDetails.tsx | 100 ++++++--------- .../TopQueries/QueryDetailsDrawer.tsx | 103 +++++++++++++++ .../TopQueries/RunningQueriesData.tsx | 81 +++++++----- .../Diagnostics/TopQueries/TopQueries.scss | 11 +- .../Diagnostics/TopQueries/TopQueries.tsx | 121 +++--------------- .../Diagnostics/TopQueries/TopQueriesData.tsx | 103 ++++++++------- .../Tenant/Diagnostics/TopQueries/utils.ts | 82 ++++++++++++ 9 files changed, 363 insertions(+), 282 deletions(-) create mode 100644 src/containers/Tenant/Diagnostics/TopQueries/QueryDetailsDrawer.tsx diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.scss b/src/containers/Tenant/Diagnostics/Diagnostics.scss index 9a7409b72..874515235 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.scss +++ b/src/containers/Tenant/Diagnostics/Diagnostics.scss @@ -8,7 +8,7 @@ height: 100%; &__header-wrapper { - padding: 0 20px 16px; + padding: 0 20px; background-color: var(--g-color-base-background); } @@ -37,7 +37,7 @@ &__drawer-container { position: relative; - height: 100%; + overflow: hidden; } &__page-wrapper { @@ -45,7 +45,8 @@ flex-grow: 1; width: 100%; - padding: 0 20px; + height: 100%; + margin: var(--g-spacing-4) var(--g-spacing-5) 0 var(--g-spacing-5); .ydb-table-with-controls-layout { &__controls { diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss index 62a42d9ec..3e9a3f593 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss @@ -5,15 +5,16 @@ flex-direction: column; height: 100%; - padding: var(--g-spacing-5) var(--g-spacing-6); - color: var(--g-color-text-primary-invert); + 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 { @@ -32,11 +33,7 @@ overflow: auto; flex: 1; - padding-top: var(--g-spacing-5); - } - - &__query-section { - margin-top: 20px; + padding: var(--g-spacing-5) var(--g-spacing-4) var(--g-spacing-5) var(--g-spacing-6); } &__query-header { @@ -44,7 +41,9 @@ justify-content: space-between; align-items: center; - margin-bottom: 12px; + padding: var(--g-spacing-2) var(--g-spacing-3); + + border-bottom: 1px solid var(--g-color-line-generic); } &__query-title { @@ -57,6 +56,8 @@ overflow: hidden; + margin-top: 20px; + border-radius: 4px; background-color: var(--code-background-color); @@ -67,24 +68,4 @@ background-color: transparent !important; } } - - &__close-button { - color: var(--g-color-text-secondary-invert); - - &:hover { - color: var(--g-color-text-primary-invert); - } - } - - &__editor-button { - display: flex; - align-items: center; - gap: 6px; - - color: var(--g-color-text-secondary-invert); - - &:hover { - color: var(--g-color-text-primary-invert); - } - } } diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.tsx index 535f17616..4892bc54a 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.tsx @@ -1,15 +1,12 @@ -import React from 'react'; - +import {Code, Link, Xmark} from '@gravity-ui/icons'; import {Button, Icon} from '@gravity-ui/uikit'; +import EnableFullscreenButton from '../../../../components/EnableFullscreenButton/EnableFullscreenButton'; +import Fullscreen from '../../../../components/Fullscreen/Fullscreen'; import type {InfoViewerItem} from '../../../../components/InfoViewer'; import {InfoViewer} from '../../../../components/InfoViewer'; import {YDBSyntaxHighlighter} from '../../../../components/SyntaxHighlighter/YDBSyntaxHighlighter'; -import type {KeyValueRow} from '../../../../types/api/query'; import {cn} from '../../../../utils/cn'; -import {formatDateTime, formatNumber} from '../../../../utils/dataFormatters/dataFormatters'; -import {generateHash} from '../../../../utils/generateHash'; -import {formatToMs, parseUsToMs} from '../../../../utils/timeParsers'; import i18n from './i18n'; @@ -18,77 +15,60 @@ import './QueryDetails.scss'; const b = cn('kv-query-details'); interface QueryDetailsProps { - row: KeyValueRow; + queryText: string; + infoItems: InfoViewerItem[]; onClose: () => void; onOpenInEditor: () => void; } -export const QueryDetails = ({row, onClose, onOpenInEditor}: QueryDetailsProps) => { - const query = row.QueryText as string; - // Create info items for the InfoViewer with formatting matching the columns - const infoItems: InfoViewerItem[] = React.useMemo(() => { - return [ - {label: i18n('query-details.query-hash'), value: generateHash(String(row.QueryText))}, - { - label: i18n('query-details.cpu-time'), - value: formatToMs(parseUsToMs(row.CPUTimeUs ?? undefined)), - }, - { - label: i18n('query-details.duration'), - value: formatToMs(parseUsToMs(row.Duration ?? undefined)), - }, - {label: i18n('query-details.read-bytes'), value: formatNumber(row.ReadBytes)}, - {label: i18n('query-details.request-units'), value: formatNumber(row.RequestUnits)}, - { - label: i18n('query-details.end-time'), - value: row.EndTime - ? formatDateTime(new Date(row.EndTime as string).getTime()) - : '–', - }, - {label: i18n('query-details.read-rows'), value: formatNumber(row.ReadRows)}, - {label: i18n('query-details.user-sid'), value: row.UserSID || '–'}, - {label: i18n('query-details.application-name'), value: row.ApplicationName || '–'}, - { - label: i18n('query-details.query-start-at'), - value: row.QueryStartAt - ? formatDateTime(new Date(row.QueryStartAt as string).getTime()) - : '–', - }, - ]; - }, [row]); - +export const QueryDetails = ({ + queryText, + infoItems, + onClose, + onOpenInEditor, +}: QueryDetailsProps) => { return (
Query
- + +
-
- + +
+ -
-
-
{i18n('query-details.query.title')}
- -
- +
+
+ {i18n('query-details.query.title')} +
+ +
+
-
+
); }; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetailsDrawer.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetailsDrawer.tsx new file mode 100644 index 000000000..0d2458046 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetailsDrawer.tsx @@ -0,0 +1,103 @@ +import React from 'react'; + +import {Drawer, DrawerItem} from '@gravity-ui/navigation'; +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 {QueryDetails} from './QueryDetails'; +import {createQueryInfoItems} from './utils'; + +const DEFAULT_DRAWER_WIDTH = 600; +const DRAWER_WIDTH_KEY = 'kv-top-queries-drawer-width'; +const b = cn('kv-top-queries'); + +interface QueryDetailsDrawerProps { + row: KeyValueRow | null; + onClose: () => void; +} + +export const QueryDetailsDrawer = ({row, onClose}: QueryDetailsDrawerProps) => { + const [isVisible, setIsVisible] = React.useState(false); + const [drawerWidth, setDrawerWidth] = React.useState(() => { + const savedWidth = localStorage.getItem(DRAWER_WIDTH_KEY); + return savedWidth ? Number(savedWidth) : DEFAULT_DRAWER_WIDTH; + }); + + // Effect to open drawer when row changes + React.useEffect(() => { + if (row) { + setIsVisible(true); + } + }, [row]); + + const handleClose = () => { + setIsVisible(false); + onClose(); + }; + + 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]); + + const handleResizeDrawer = (width: number) => { + setDrawerWidth(width); + localStorage.setItem(DRAWER_WIDTH_KEY, width.toString()); + }; + + return ( + + + {row && ( + + )} + + + ); +}; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx index d60f52e95..5bc375f7a 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx @@ -14,6 +14,7 @@ import {useAutoRefreshInterval, useTypedSelector} from '../../../../utils/hooks' import {useSelectedColumns} from '../../../../utils/hooks/useSelectedColumns'; import {parseQueryErrorToString} from '../../../../utils/query'; +import {QueryDetailsDrawer} from './QueryDetailsDrawer'; import {getRunningQueriesColumns} from './columns/columns'; import { DEFAULT_RUNNING_QUERIES_COLUMNS, @@ -30,16 +31,15 @@ const b = cn('kv-top-queries'); interface RunningQueriesDataProps { tenantName: string; renderQueryModeControl: () => React.ReactNode; - handleRowClick: (row: KeyValueRow) => void; handleTextSearchUpdate: (text: string) => void; } export const RunningQueriesData = ({ tenantName, renderQueryModeControl, - handleRowClick, handleTextSearchUpdate, }: RunningQueriesDataProps) => { + const [selectedRow, setSelectedRow] = React.useState(null); const [autoRefreshInterval] = useAutoRefreshInterval(); const filters = useTypedSelector((state) => state.executeTopQueries); @@ -69,40 +69,51 @@ export const RunningQueriesData = ({ {pollingInterval: autoRefreshInterval}, ); + const handleRowClick = (row: KeyValueRow) => { + setSelectedRow(row); + }; + + const handleCloseDetails = () => { + setSelectedRow(null); + }; + return ( - - - {renderQueryModeControl()} - - - + + + + {renderQueryModeControl()} + + + - {error ? : null} - - b('row')} - sortOrder={tableSort} - onSort={handleTableSort} - /> - - + {error ? : null} + + b('row')} + sortOrder={tableSort} + onSort={handleTableSort} + /> + + + + ); }; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss index c8c03a389..ff6e56ada 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.scss @@ -33,12 +33,15 @@ text-overflow: ellipsis; } - &__drawer-item { + &__drawer-container { z-index: 4; - height: calc(100% + var(--g-spacing-4)); - // Because of tabs padding it is needed to move drawer item a little higher - // to make it stick to bottom of tabs margin-top: calc(-1 * var(--g-spacing-4)); } + + &__drawer-item { + z-index: 4; + + height: 100%; + } } diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx index bd04b110a..a91cce888 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx @@ -1,29 +1,15 @@ import React from 'react'; -import {Drawer, DrawerItem} from '@gravity-ui/navigation'; 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 type {KeyValueRow} from '../../../../types/api/query'; -import {cn} from '../../../../utils/cn'; import {useTypedDispatch} from '../../../../utils/hooks'; -import {useChangeInputWithConfirmation} from '../../../../utils/hooks/withConfirmation/useChangeInputWithConfirmation'; -import {TenantTabsGroups, getTenantPath} from '../../TenantPages'; -import {QueryDetails} from './QueryDetails'; import {RunningQueriesData} from './RunningQueriesData'; import {TopQueriesData} from './TopQueriesData'; import {TimeFrameIds} from './constants'; @@ -58,13 +44,8 @@ interface TopQueriesProps { tenantName: string; } -const DRAWER_WIDTH_KEY = 'kv-top-queries-drawer-width'; -const b = cn('kv-top-queries'); - 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); @@ -73,33 +54,6 @@ export const TopQueries = ({tenantName}: TopQueriesProps) => { const isTopQueries = queryMode === QueryModeIds.top; - const [selectedRow, setSelectedRow] = React.useState(null); - const [isDrawerVisible, setIsDrawerVisible] = React.useState(false); - const [drawerWidth, setDrawerWidth] = React.useState(() => { - const savedWidth = localStorage.getItem(DRAWER_WIDTH_KEY); - return savedWidth ? Number(savedWidth) : 400; - }); - - 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})); }; @@ -118,65 +72,20 @@ export const TopQueries = ({tenantName}: TopQueriesProps) => { ); }, [queryMode, setQueryMode]); - const handleRowClick = (row: KeyValueRow) => { - setSelectedRow(row); - setIsDrawerVisible(true); - }; - - const handleOpenInEditor = () => { - if (selectedRow) { - onRowClick(selectedRow.QueryText as string); - } - }; - - const handleCloseDetails = () => { - setIsDrawerVisible(false); - }; - - const handleResizeDrawer = (width: number) => { - setDrawerWidth(width); - localStorage.setItem(DRAWER_WIDTH_KEY, width.toString()); - }; - - return ( - - {isTopQueries ? ( - - ) : ( - - )} - - - {selectedRow && ( - - )} - - - + return isTopQueries ? ( + + ) : ( + ); }; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx index 8f460318f..71eca7628 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx @@ -17,6 +17,7 @@ import {useAutoRefreshInterval, useTypedSelector} from '../../../../utils/hooks' import {useSelectedColumns} from '../../../../utils/hooks/useSelectedColumns'; import {parseQueryErrorToString} from '../../../../utils/query'; +import {QueryDetailsDrawer} from './QueryDetailsDrawer'; import {getTopQueriesColumns} from './columns/columns'; import { DEFAULT_TOP_QUERIES_COLUMNS, @@ -35,7 +36,6 @@ interface TopQueriesDataProps { tenantName: string; timeFrame: TimeFrame; renderQueryModeControl: () => React.ReactNode; - handleRowClick: (row: KeyValueRow) => void; handleTimeFrameChange: (value: string[]) => void; handleDateRangeChange: (value: DateRangeValues) => void; handleTextSearchUpdate: (text: string) => void; @@ -45,11 +45,11 @@ export const TopQueriesData = ({ tenantName, timeFrame, renderQueryModeControl, - handleRowClick, handleTimeFrameChange, handleDateRangeChange, handleTextSearchUpdate, }: TopQueriesDataProps) => { + const [selectedRow, setSelectedRow] = React.useState(null); const [autoRefreshInterval] = useAutoRefreshInterval(); const filters = useTypedSelector((state) => state.executeTopQueries); @@ -79,51 +79,62 @@ export const TopQueriesData = ({ {pollingInterval: autoRefreshInterval}, ); + const handleRowClick = (row: KeyValueRow) => { + setSelectedRow(row); + }; + + const handleCloseDetails = () => { + setSelectedRow(null); + }; + return ( - - - {renderQueryModeControl()} - + + + + - {error ? : null} - - b('row')} - sortOrder={tableSort} - onSort={handleTableSort} - /> - - + {error ? : null} + + b('row')} + sortOrder={tableSort} + onSort={handleTableSort} + /> + + + + ); }; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/utils.ts b/src/containers/Tenant/Diagnostics/TopQueries/utils.ts index 35660e3b7..9c5773fe5 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/utils.ts +++ b/src/containers/Tenant/Diagnostics/TopQueries/utils.ts @@ -3,7 +3,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 { @@ -11,6 +16,7 @@ import { getRunningQueriesColumnSortField, getTopQueriesColumnSortField, } from './columns/constants'; +import i18n from './i18n'; export const TOP_QUERIES_TABLE_SETTINGS: Settings = { ...QUERY_TABLE_SETTINGS, @@ -35,6 +41,82 @@ export function useTopQueriesSort() { }; } +export function createQueryInfoItems(data: KeyValueRow): InfoViewerItem[] { + const items: InfoViewerItem[] = []; + + if (data.QueryText) { + items.push({ + label: i18n('query-details.query-hash'), + value: generateHash(String(data.QueryText)), + }); + } + + if (data.CPUTimeUs !== undefined) { + items.push({ + label: i18n('query-details.cpu-time'), + value: formatToMs(parseUsToMs(data.CPUTimeUs ?? undefined)), + }); + } + + if (data.Duration !== undefined) { + items.push({ + label: i18n('query-details.duration'), + value: formatToMs(parseUsToMs(data.Duration ?? undefined)), + }); + } + + if (data.ReadBytes !== undefined) { + items.push({ + label: i18n('query-details.read-bytes'), + value: formatNumber(data.ReadBytes), + }); + } + + if (data.RequestUnits !== undefined) { + items.push({ + label: i18n('query-details.request-units'), + value: formatNumber(data.RequestUnits), + }); + } + + if (data.EndTime) { + items.push({ + label: i18n('query-details.end-time'), + value: formatDateTime(new Date(data.EndTime as string).getTime()), + }); + } + + if (data.ReadRows !== undefined) { + items.push({ + label: i18n('query-details.read-rows'), + value: formatNumber(data.ReadRows), + }); + } + + if (data.UserSID) { + items.push({ + label: i18n('query-details.user-sid'), + value: data.UserSID, + }); + } + + if (data.ApplicationName) { + items.push({ + label: i18n('query-details.application-name'), + value: data.ApplicationName, + }); + } + + if (data.QueryStartAt) { + items.push({ + label: i18n('query-details.query-start-at'), + value: formatDateTime(new Date(data.QueryStartAt as string).getTime()), + }); + } + + return items; +} + export function useRunningQueriesSort() { const [tableSort, handleTableSort] = useTableSort({ initialSortColumn: QUERIES_COLUMNS_IDS.QueryStartAt, From 9444b576138dcd3671e8304615a0222427f04aeb Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Wed, 16 Apr 2025 15:43:02 +0300 Subject: [PATCH 04/21] fix: style fixes --- .../Tenant/Diagnostics/Diagnostics.scss | 2 +- .../Diagnostics/TopQueries/QueryDetails.scss | 11 ++-------- .../Diagnostics/TopQueries/i18n/en.json | 10 --------- .../Tenant/Diagnostics/TopQueries/utils.ts | 22 +++++++++---------- 4 files changed, 14 insertions(+), 31 deletions(-) diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.scss b/src/containers/Tenant/Diagnostics/Diagnostics.scss index 874515235..1b93fca85 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.scss +++ b/src/containers/Tenant/Diagnostics/Diagnostics.scss @@ -8,7 +8,7 @@ height: 100%; &__header-wrapper { - padding: 0 20px; + padding: 0 var(--g-spacing-5); background-color: var(--g-color-base-background); } diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss index 3e9a3f593..8c558affb 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss @@ -26,7 +26,7 @@ &__actions { display: flex; - gap: 8px; + gap: var(--g-spacing-2); } &__content { @@ -56,16 +56,9 @@ overflow: hidden; - margin-top: 20px; + margin-top: var(--g-spacing-5); border-radius: 4px; background-color: var(--code-background-color); - - pre { - max-height: 100%; - margin: 0 !important; - - background-color: transparent !important; - } } } diff --git a/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json b/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json index 4142e13da..38b18acab 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json @@ -8,15 +8,5 @@ "query-details.title": "Query Details", "query-details.open-in-editor": "Open in Editor", "query-details.close": "Close", - "query-details.query-hash": "Query Hash", - "query-details.cpu-time": "CPU Time", - "query-details.duration": "Duration", - "query-details.read-bytes": "Read Bytes", - "query-details.request-units": "Request Units", - "query-details.end-time": "End Time", - "query-details.read-rows": "Read Rows", - "query-details.user-sid": "User SID", - "query-details.application-name": "Application Name", - "query-details.query-start-at": "Query Start Time", "query-details.query.title": "Query Text" } diff --git a/src/containers/Tenant/Diagnostics/TopQueries/utils.ts b/src/containers/Tenant/Diagnostics/TopQueries/utils.ts index 9c5773fe5..23e5d1715 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/utils.ts +++ b/src/containers/Tenant/Diagnostics/TopQueries/utils.ts @@ -16,7 +16,7 @@ import { getRunningQueriesColumnSortField, getTopQueriesColumnSortField, } from './columns/constants'; -import i18n from './i18n'; +import columnsI18n from './columns/i18n'; export const TOP_QUERIES_TABLE_SETTINGS: Settings = { ...QUERY_TABLE_SETTINGS, @@ -46,70 +46,70 @@ export function createQueryInfoItems(data: KeyValueRow): InfoViewerItem[] { if (data.QueryText) { items.push({ - label: i18n('query-details.query-hash'), + label: columnsI18n('query-hash'), value: generateHash(String(data.QueryText)), }); } if (data.CPUTimeUs !== undefined) { items.push({ - label: i18n('query-details.cpu-time'), + label: columnsI18n('cpu-time'), value: formatToMs(parseUsToMs(data.CPUTimeUs ?? undefined)), }); } if (data.Duration !== undefined) { items.push({ - label: i18n('query-details.duration'), + label: columnsI18n('duration'), value: formatToMs(parseUsToMs(data.Duration ?? undefined)), }); } if (data.ReadBytes !== undefined) { items.push({ - label: i18n('query-details.read-bytes'), + label: columnsI18n('read-bytes'), value: formatNumber(data.ReadBytes), }); } if (data.RequestUnits !== undefined) { items.push({ - label: i18n('query-details.request-units'), + label: columnsI18n('request-units'), value: formatNumber(data.RequestUnits), }); } if (data.EndTime) { items.push({ - label: i18n('query-details.end-time'), + label: columnsI18n('end-time'), value: formatDateTime(new Date(data.EndTime as string).getTime()), }); } if (data.ReadRows !== undefined) { items.push({ - label: i18n('query-details.read-rows'), + label: columnsI18n('read-rows'), value: formatNumber(data.ReadRows), }); } if (data.UserSID) { items.push({ - label: i18n('query-details.user-sid'), + label: columnsI18n('user'), value: data.UserSID, }); } if (data.ApplicationName) { items.push({ - label: i18n('query-details.application-name'), + label: columnsI18n('application'), value: data.ApplicationName, }); } if (data.QueryStartAt) { items.push({ - label: i18n('query-details.query-start-at'), + label: columnsI18n('start-time'), value: formatDateTime(new Date(data.QueryStartAt as string).getTime()), }); } From 4aa0f7c757ca039f3ae4302f5dc81cf464b7307b Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 17 Apr 2025 16:06:56 +0300 Subject: [PATCH 05/21] fix: use query params --- src/assets/icons/cry-cat.svg | 14 ++ .../DrawerWrapper/DrawerWrapper.scss | 6 + .../DrawerWrapper/DrawerWrapper.tsx | 134 ++++++++++++++++++ src/containers/DrawerWrapper/index.ts | 1 + .../Tenant/Diagnostics/Diagnostics.scss | 2 + .../TopQueries/QueryDetailsDrawer.tsx | 103 -------------- .../TopQueries/QueryDetailsDrawerContent.tsx | 79 +++++++++++ .../TopQueries/RunningQueriesData.tsx | 42 ++++-- .../Diagnostics/TopQueries/TopQueries.scss | 32 ++++- .../Diagnostics/TopQueries/TopQueriesData.tsx | 44 ++++-- .../TopQueries/columns/columns.tsx | 8 +- .../TopQueries/columns/constants.ts | 1 + .../hooks/useRunningQueriesRowSelection.ts | 88 ++++++++++++ .../hooks/useTopQueriesRowSelection.ts | 93 ++++++++++++ .../Diagnostics/TopQueries/i18n/en.json | 6 +- .../utils/getRunningQueryRowQueryParams.ts | 24 ++++ .../utils/getTopQueryRowQueryParams.ts | 19 +++ .../executeTopQueries/executeTopQueries.ts | 6 +- 18 files changed, 564 insertions(+), 138 deletions(-) create mode 100644 src/assets/icons/cry-cat.svg create mode 100644 src/containers/DrawerWrapper/DrawerWrapper.scss create mode 100644 src/containers/DrawerWrapper/DrawerWrapper.tsx create mode 100644 src/containers/DrawerWrapper/index.ts delete mode 100644 src/containers/Tenant/Diagnostics/TopQueries/QueryDetailsDrawer.tsx create mode 100644 src/containers/Tenant/Diagnostics/TopQueries/QueryDetailsDrawerContent.tsx create mode 100644 src/containers/Tenant/Diagnostics/TopQueries/hooks/useRunningQueriesRowSelection.ts create mode 100644 src/containers/Tenant/Diagnostics/TopQueries/hooks/useTopQueriesRowSelection.ts create mode 100644 src/containers/Tenant/Diagnostics/TopQueries/utils/getRunningQueryRowQueryParams.ts create mode 100644 src/containers/Tenant/Diagnostics/TopQueries/utils/getTopQueryRowQueryParams.ts diff --git a/src/assets/icons/cry-cat.svg b/src/assets/icons/cry-cat.svg new file mode 100644 index 000000000..4bff3ab5b --- /dev/null +++ b/src/assets/icons/cry-cat.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/containers/DrawerWrapper/DrawerWrapper.scss b/src/containers/DrawerWrapper/DrawerWrapper.scss new file mode 100644 index 000000000..1c3faf2d9 --- /dev/null +++ b/src/containers/DrawerWrapper/DrawerWrapper.scss @@ -0,0 +1,6 @@ +.ydb-drawer-wrapper { + &__item { + z-index: 4; + height: 100%; + } +} diff --git a/src/containers/DrawerWrapper/DrawerWrapper.tsx b/src/containers/DrawerWrapper/DrawerWrapper.tsx new file mode 100644 index 000000000..32fcd12da --- /dev/null +++ b/src/containers/DrawerWrapper/DrawerWrapper.tsx @@ -0,0 +1,134 @@ +import React from 'react'; + +import {Drawer, DrawerItem} from '@gravity-ui/navigation'; + +import {cn} from '../../utils/cn'; + +const DEFAULT_DRAWER_WIDTH = 600; +const DRAWER_WIDTH_KEY = 'drawer-width'; +const b = cn('ydb-drawer-wrapper'); + +import './DrawerWrapper.scss'; + +interface DrawerContentWrapperProps { + isVisible: boolean; + onClose: () => void; + children: React.ReactNode; + drawerId?: string; + storageKey?: string; + defaultWidth?: number; + direction?: 'left' | 'right'; + className?: string; + detectClickOutside?: boolean; +} + +export const DrawerContentWrapper = ({ + isVisible, + onClose, + children, + drawerId = 'drawer', + storageKey = DRAWER_WIDTH_KEY, + defaultWidth = DEFAULT_DRAWER_WIDTH, + direction = 'right', + className, + detectClickOutside = false, +}: DrawerContentWrapperProps) => { + const [drawerWidth, setDrawerWidth] = React.useState(() => { + const savedWidth = localStorage.getItem(storageKey); + return savedWidth ? Number(savedWidth) : defaultWidth; + }); + + const drawerRef = React.useRef(null); + + React.useEffect(() => { + if (!detectClickOutside) { + return undefined; + } + + const handleClickOutside = (event: MouseEvent) => { + if ( + isVisible && + drawerRef.current && + !drawerRef.current.contains(event.target as Node) + ) { + onClose(); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isVisible, onClose, detectClickOutside]); + + const handleResizeDrawer = (width: number) => { + setDrawerWidth(width); + localStorage.setItem(storageKey, width.toString()); + }; + + return ( + + + {children} + + + ); +}; + +interface DrawerWrapperProps { + children: React.ReactNode; + renderDrawerContent: () => React.ReactNode; + isDrawerVisible: boolean; + onCloseDrawer: () => void; + drawerId?: string; + storageKey?: string; + defaultWidth?: number; + direction?: 'left' | 'right'; + className?: string; + detectClickOutside?: boolean; +} + +export const DrawerWrapper = ({ + children, + renderDrawerContent, + isDrawerVisible, + onCloseDrawer, + drawerId, + storageKey, + defaultWidth, + direction, + className, + detectClickOutside, +}: DrawerWrapperProps) => { + return ( + + {children} + + {renderDrawerContent()} + + + ); +}; diff --git a/src/containers/DrawerWrapper/index.ts b/src/containers/DrawerWrapper/index.ts new file mode 100644 index 000000000..281470b73 --- /dev/null +++ b/src/containers/DrawerWrapper/index.ts @@ -0,0 +1 @@ +export {DrawerWrapper, DrawerContentWrapper} from './DrawerWrapper'; diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.scss b/src/containers/Tenant/Diagnostics/Diagnostics.scss index 1b93fca85..c402eb18e 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.scss +++ b/src/containers/Tenant/Diagnostics/Diagnostics.scss @@ -38,6 +38,8 @@ position: relative; overflow: hidden; + + height: 100%; } &__page-wrapper { diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetailsDrawer.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetailsDrawer.tsx deleted file mode 100644 index 0d2458046..000000000 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetailsDrawer.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React from 'react'; - -import {Drawer, DrawerItem} from '@gravity-ui/navigation'; -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 {QueryDetails} from './QueryDetails'; -import {createQueryInfoItems} from './utils'; - -const DEFAULT_DRAWER_WIDTH = 600; -const DRAWER_WIDTH_KEY = 'kv-top-queries-drawer-width'; -const b = cn('kv-top-queries'); - -interface QueryDetailsDrawerProps { - row: KeyValueRow | null; - onClose: () => void; -} - -export const QueryDetailsDrawer = ({row, onClose}: QueryDetailsDrawerProps) => { - const [isVisible, setIsVisible] = React.useState(false); - const [drawerWidth, setDrawerWidth] = React.useState(() => { - const savedWidth = localStorage.getItem(DRAWER_WIDTH_KEY); - return savedWidth ? Number(savedWidth) : DEFAULT_DRAWER_WIDTH; - }); - - // Effect to open drawer when row changes - React.useEffect(() => { - if (row) { - setIsVisible(true); - } - }, [row]); - - const handleClose = () => { - setIsVisible(false); - onClose(); - }; - - 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]); - - const handleResizeDrawer = (width: number) => { - setDrawerWidth(width); - localStorage.setItem(DRAWER_WIDTH_KEY, width.toString()); - }; - - return ( - - - {row && ( - - )} - - - ); -}; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetailsDrawerContent.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetailsDrawerContent.tsx new file mode 100644 index 000000000..773dfca74 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetailsDrawerContent.tsx @@ -0,0 +1,79 @@ +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 {QueryDetails} from './QueryDetails'; +import i18n from './i18n'; +import {createQueryInfoItems} from './utils'; + +import CryCatIcon from '../../../../assets/icons/cry-cat.svg'; + +const b = cn('kv-top-queries'); + +interface QueryDetailsDrawerContentProps { + 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 ( +
+ + + {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 5bc375f7a..11f16b82c 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx @@ -7,6 +7,7 @@ import {ResponseError} from '../../../../components/Errors/ResponseError'; import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; import {Search} from '../../../../components/Search'; import {TableWithControlsLayout} from '../../../../components/TableWithControlsLayout/TableWithControlsLayout'; +import {DrawerWrapper} from '../../../../containers/DrawerWrapper'; import {topQueriesApi} from '../../../../store/reducers/executeTopQueries/executeTopQueries'; import type {KeyValueRow} from '../../../../types/api/query'; import {cn} from '../../../../utils/cn'; @@ -14,7 +15,7 @@ import {useAutoRefreshInterval, useTypedSelector} from '../../../../utils/hooks' import {useSelectedColumns} from '../../../../utils/hooks/useSelectedColumns'; import {parseQueryErrorToString} from '../../../../utils/query'; -import {QueryDetailsDrawer} from './QueryDetailsDrawer'; +import {QueryDetailsDrawerContent} from './QueryDetailsDrawerContent'; import {getRunningQueriesColumns} from './columns/columns'; import { DEFAULT_RUNNING_QUERIES_COLUMNS, @@ -23,6 +24,7 @@ import { RUNNING_QUERIES_COLUMNS_WIDTH_LS_KEY, RUNNING_QUERIES_SELECTED_COLUMNS_LS_KEY, } from './columns/constants'; +import {useRunningQueriesRowSelection} from './hooks/useRunningQueriesRowSelection'; import i18n from './i18n'; import {TOP_QUERIES_TABLE_SETTINGS, useRunningQueriesSort} from './utils'; @@ -39,7 +41,6 @@ export const RunningQueriesData = ({ renderQueryModeControl, handleTextSearchUpdate, }: RunningQueriesDataProps) => { - const [selectedRow, setSelectedRow] = React.useState(null); const [autoRefreshInterval] = useAutoRefreshInterval(); const filters = useTypedSelector((state) => state.executeTopQueries); @@ -69,16 +70,36 @@ export const RunningQueriesData = ({ {pollingInterval: autoRefreshInterval}, ); + const rows = data?.resultSets?.[0]?.result; + const {handleRowSelect, selectedRow, hasSearchParams} = useRunningQueriesRowSelection(rows); + const handleRowClick = (row: KeyValueRow) => { - setSelectedRow(row); + // Simply pass the row to handleRowSelect + handleRowSelect(row); }; - const handleCloseDetails = () => { - setSelectedRow(null); - }; + const handleCloseDetails = React.useCallback(() => { + handleRowSelect(null); + }, [handleRowSelect]); + + const renderDrawerContent = React.useCallback(() => { + if (!hasSearchParams) { + return null; + } + return ; + }, [hasSearchParams, selectedRow, handleCloseDetails]); return ( - + {renderQueryModeControl()} @@ -103,17 +124,16 @@ export const RunningQueriesData = ({ emptyDataMessage={i18n('no-data')} columnsWidthLSKey={RUNNING_QUERIES_COLUMNS_WIDTH_LS_KEY} columns={columnsToShow} - data={data?.resultSets?.[0].result || []} + data={rows || []} loading={isFetching && currentData === undefined} settings={TOP_QUERIES_TABLE_SETTINGS} onRowClick={handleRowClick} - rowClassName={() => b('row')} + rowClassName={(row) => b('row', {active: 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 ff6e56ada..90cc7f78a 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 { @@ -33,15 +41,29 @@ text-overflow: ellipsis; } - &__drawer-container { - z-index: 4; - + &__drawer { margin-top: calc(-1 * var(--g-spacing-4)); } - &__drawer-item { - z-index: 4; + &__empty-state-icon { + color: var(--g-color-text-primary); + } + &__not-found-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: 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/TopQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx index 71eca7628..cc1661859 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx @@ -9,6 +9,7 @@ import {ResponseError} from '../../../../components/Errors/ResponseError'; import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; import {Search} from '../../../../components/Search'; import {TableWithControlsLayout} from '../../../../components/TableWithControlsLayout/TableWithControlsLayout'; +import {DrawerWrapper} from '../../../../containers/DrawerWrapper'; import {topQueriesApi} from '../../../../store/reducers/executeTopQueries/executeTopQueries'; import type {TimeFrame} from '../../../../store/reducers/executeTopQueries/types'; import type {KeyValueRow} from '../../../../types/api/query'; @@ -17,7 +18,7 @@ import {useAutoRefreshInterval, useTypedSelector} from '../../../../utils/hooks' import {useSelectedColumns} from '../../../../utils/hooks/useSelectedColumns'; import {parseQueryErrorToString} from '../../../../utils/query'; -import {QueryDetailsDrawer} from './QueryDetailsDrawer'; +import {QueryDetailsDrawerContent} from './QueryDetailsDrawerContent'; import {getTopQueriesColumns} from './columns/columns'; import { DEFAULT_TOP_QUERIES_COLUMNS, @@ -27,6 +28,7 @@ import { TOP_QUERIES_SELECTED_COLUMNS_LS_KEY, } from './columns/constants'; import {DEFAULT_TIME_FILTER_VALUE, TIME_FRAME_OPTIONS} from './constants'; +import {useTopQueriesRowSelection} from './hooks/useTopQueriesRowSelection'; import i18n from './i18n'; import {TOP_QUERIES_TABLE_SETTINGS, useTopQueriesSort} from './utils'; @@ -49,7 +51,6 @@ export const TopQueriesData = ({ handleDateRangeChange, handleTextSearchUpdate, }: TopQueriesDataProps) => { - const [selectedRow, setSelectedRow] = React.useState(null); const [autoRefreshInterval] = useAutoRefreshInterval(); const filters = useTypedSelector((state) => state.executeTopQueries); @@ -68,7 +69,6 @@ export const TopQueriesData = ({ ); const {tableSort, handleTableSort, backendSort} = useTopQueriesSort(); - const {currentData, data, isFetching, isLoading, error} = topQueriesApi.useGetTopQueriesQuery( { database: tenantName, @@ -79,16 +79,31 @@ export const TopQueriesData = ({ {pollingInterval: autoRefreshInterval}, ); - const handleRowClick = (row: KeyValueRow) => { - setSelectedRow(row); - }; + const rows = data?.resultSets?.[0]?.result; + const {handleRowSelect, selectedRow, hasSearchParams} = useTopQueriesRowSelection(rows); + + const handleCloseDetails = React.useCallback(() => { + handleRowSelect(null); + }, [handleRowSelect]); - const handleCloseDetails = () => { - setSelectedRow(null); - }; + const renderDrawerContent = React.useCallback(() => { + if (!hasSearchParams) { + return null; + } + return ; + }, [hasSearchParams, selectedRow, handleCloseDetails]); return ( - + {renderQueryModeControl()} @@ -124,17 +139,16 @@ export const TopQueriesData = ({ emptyDataMessage={i18n('no-data')} columnsWidthLSKey={TOP_QUERIES_COLUMNS_WIDTH_LS_KEY} columns={columnsToShow} - data={data?.resultSets?.[0].result || []} + data={rows || []} loading={isFetching && currentData === undefined} settings={TOP_QUERIES_TABLE_SETTINGS} - onRowClick={handleRowClick} - rowClassName={() => b('row')} + onRowClick={handleRowSelect} + rowClassName={(row) => b('row', {active: row === selectedRow})} sortOrder={tableSort} onSort={handleTableSort} /> - - + ); }; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/columns/columns.tsx b/src/containers/Tenant/Diagnostics/TopQueries/columns/columns.tsx index b33825d71..760a0790b 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/columns/columns.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/columns/columns.tsx @@ -152,7 +152,13 @@ export function getTenantOverviewTopQueriesColumns() { return [queryHashColumn, oneLineQueryTextColumn, cpuTimeUsColumn]; } export function getRunningQueriesColumns() { - const columns = [userSIDColumn, queryStartColumn, queryTextColumn, applicationColumn]; + const columns = [ + queryHashColumn, + userSIDColumn, + queryStartColumn, + queryTextColumn, + applicationColumn, + ]; return columns.map((column) => ({ ...column, diff --git a/src/containers/Tenant/Diagnostics/TopQueries/columns/constants.ts b/src/containers/Tenant/Diagnostics/TopQueries/columns/constants.ts index eef617545..b191b9869 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/columns/constants.ts +++ b/src/containers/Tenant/Diagnostics/TopQueries/columns/constants.ts @@ -37,6 +37,7 @@ export const DEFAULT_TOP_QUERIES_COLUMNS: QueriesColumnId[] = [ export const REQUIRED_TOP_QUERIES_COLUMNS: QueriesColumnId[] = ['CPUTime', 'QueryText']; export const DEFAULT_RUNNING_QUERIES_COLUMNS: QueriesColumnId[] = [ + 'QueryHash', 'UserSID', 'QueryStartAt', 'QueryText', diff --git a/src/containers/Tenant/Diagnostics/TopQueries/hooks/useRunningQueriesRowSelection.ts b/src/containers/Tenant/Diagnostics/TopQueries/hooks/useRunningQueriesRowSelection.ts new file mode 100644 index 000000000..bf5f0b646 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopQueries/hooks/useRunningQueriesRowSelection.ts @@ -0,0 +1,88 @@ +import React from 'react'; + +import {ObjectParam, useQueryParams} from 'use-query-params'; + +import type {KeyValueRow} from '../../../../../types/api/query'; +import {getRunningQueryRowQueryParams} from '../utils/getRunningQueryRowQueryParams'; + +export interface RunningQuerySearchParams { + queryHash?: string; + sessionIdHash?: string; + queryStart?: string; +} + +export function useRunningQueriesRowSelection(rows?: KeyValueRow[] | null) { + const [queryParams, setQueryParams] = useQueryParams({ + runningQuery: ObjectParam, + }); + + const searchParamsQuery: RunningQuerySearchParams = { + queryHash: queryParams.runningQuery?.queryHash ?? undefined, + sessionIdHash: queryParams.runningQuery?.sessionIdHash ?? undefined, + queryStart: queryParams.runningQuery?.queryStart ?? undefined, + }; + + const findMatchedQueryRow = React.useCallback(() => { + if ( + !rows || + !searchParamsQuery.queryHash || + !searchParamsQuery.sessionIdHash || + !searchParamsQuery.queryStart + ) { + return null; + } + + return rows.find((row) => { + const params = getRunningQueryRowQueryParams(row); + return ( + params.queryHash === searchParamsQuery.queryHash && + params.sessionIdHash === searchParamsQuery.sessionIdHash && + params.queryStart === searchParamsQuery.queryStart + ); + }); + }, [ + rows, + searchParamsQuery.queryHash, + searchParamsQuery.sessionIdHash, + searchParamsQuery.queryStart, + ]); + + const matchedRow = findMatchedQueryRow(); + + const handleRowSelect = React.useCallback( + (row: KeyValueRow | null) => { + if (!row) { + setQueryParams( + { + runningQuery: undefined, + }, + 'replaceIn', + ); + return; + } + + // Extract params using the utility function + const params = getRunningQueryRowQueryParams(row); + setQueryParams( + { + runningQuery: { + queryHash: params.queryHash || undefined, + sessionIdHash: params.sessionIdHash || undefined, + queryStart: params.queryStart || undefined, + }, + }, + 'replaceIn', + ); + }, + [setQueryParams], + ); + + return { + hasSearchParams: + Boolean(searchParamsQuery.queryHash) && + Boolean(searchParamsQuery.sessionIdHash) && + Boolean(searchParamsQuery.queryStart), + selectedRow: matchedRow || null, + handleRowSelect, + }; +} diff --git a/src/containers/Tenant/Diagnostics/TopQueries/hooks/useTopQueriesRowSelection.ts b/src/containers/Tenant/Diagnostics/TopQueries/hooks/useTopQueriesRowSelection.ts new file mode 100644 index 000000000..79be16cbd --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopQueries/hooks/useTopQueriesRowSelection.ts @@ -0,0 +1,93 @@ +import React from 'react'; + +import {ObjectParam, 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 useTopQueriesRowSelection(rows?: KeyValueRow[] | null) { + const [queryParams, setQueryParams] = useQueryParams({ + topQuery: ObjectParam, + }); + + const searchParamsQuery: SearchParamsQueryParams = { + rank: queryParams.topQuery?.rank ?? undefined, + intervalEnd: queryParams.topQuery?.intervalEnd ?? undefined, + endTime: queryParams.topQuery?.endTime ?? undefined, + queryHash: queryParams.topQuery?.queryHash ?? undefined, + }; + + const findMatchedQueryRow = React.useCallback(() => { + if ( + !rows || + !searchParamsQuery.rank || + !searchParamsQuery.intervalEnd || + !searchParamsQuery.endTime + ) { + return null; + } + + return rows.find((row) => { + const params = getTopQueryRowQueryParams(row); + return ( + params.rank === searchParamsQuery.rank && + params.intervalEnd === searchParamsQuery.intervalEnd && + params.endTime === searchParamsQuery.endTime && + searchParamsQuery.queryHash === params.queryHash + ); + }); + }, [ + rows, + searchParamsQuery.rank, + searchParamsQuery.intervalEnd, + searchParamsQuery.endTime, + searchParamsQuery.queryHash, + ]); + + const matchedRow = findMatchedQueryRow(); + + const handleRowSelect = React.useCallback( + (row: KeyValueRow | null) => { + if (!row) { + setQueryParams( + { + topQuery: undefined, + }, + 'replaceIn', + ); + return; + } + + const params = getTopQueryRowQueryParams(row); + setQueryParams( + { + topQuery: { + rank: params.rank || undefined, + intervalEnd: params.intervalEnd || undefined, + endTime: params.endTime || undefined, + queryHash: params.queryHash || undefined, + }, + }, + 'replaceIn', + ); + }, + [setQueryParams], + ); + + return { + hasSearchParams: + Boolean(searchParamsQuery.rank) && + Boolean(searchParamsQuery.intervalEnd) && + Boolean(searchParamsQuery.endTime) && + Boolean(searchParamsQuery.queryHash), + selectedRow: matchedRow || null, + handleRowSelect, + }; +} diff --git a/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json b/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json index 38b18acab..a791cddd7 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/TopQueries/i18n/en.json @@ -8,5 +8,9 @@ "query-details.title": "Query Details", "query-details.open-in-editor": "Open in Editor", "query-details.close": "Close", - "query-details.query.title": "Query Text" + "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" } diff --git a/src/containers/Tenant/Diagnostics/TopQueries/utils/getRunningQueryRowQueryParams.ts b/src/containers/Tenant/Diagnostics/TopQueries/utils/getRunningQueryRowQueryParams.ts new file mode 100644 index 000000000..30212d620 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopQueries/utils/getRunningQueryRowQueryParams.ts @@ -0,0 +1,24 @@ +import type {KeyValueRow} from '../../../../../types/api/query'; +import {generateHash} from '../../../../../utils/generateHash'; + +/** + * Extract query parameters from a running query row for use in URL search params + * @param row The running query row data + * @returns Parameters for URL search params + */ +/** + * Generates query parameters for a running query row. + * + * @param row - The KeyValueRow object containing query details. + * @returns An object containing the query hash, session ID, and query start time. + */ +export function getRunningQueryRowQueryParams(row: KeyValueRow) { + const queryHash = generateHash(String(row.QueryText)); + const sessionIdHash = generateHash(String(row.SessionId || '')); + + return { + queryHash, + sessionIdHash, + queryStart: String(row.QueryStartAt || ''), + }; +} 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..b765a7bf8 --- /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/useTopQueriesRowSelection'; + +/** + * 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, + }; +} diff --git a/src/store/reducers/executeTopQueries/executeTopQueries.ts b/src/store/reducers/executeTopQueries/executeTopQueries.ts index aa50b3568..f1f2bfddc 100644 --- a/src/store/reducers/executeTopQueries/executeTopQueries.ts +++ b/src/store/reducers/executeTopQueries/executeTopQueries.ts @@ -66,7 +66,8 @@ const getQueryText = ( ReadBytes, UserSID, Duration, - RequestUnits + RequestUnits, + Rank FROM \`${tableName}\` WHERE ${filterConditions || 'true'} AND QueryText NOT LIKE '%${QUERY_TECHNICAL_MARK}%' ${orderBy} @@ -90,7 +91,8 @@ SELECT UserSID, QueryStartAt, Query as QueryText, - ApplicationName + ApplicationName, + SessionId FROM \`.sys/query_sessions\` WHERE ${filterConditions || 'true'} AND Query NOT LIKE '%${QUERY_TECHNICAL_MARK}%' AND QueryStartAt is not null ${orderBy} From e981717667324a5c482bc63d11ef1ac6f0a0277c Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 17 Apr 2025 16:23:37 +0300 Subject: [PATCH 06/21] fix: better event handling --- src/containers/DrawerWrapper/DrawerWrapper.tsx | 9 +++++++-- .../Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx | 7 +------ .../TopQueries/hooks/useRunningQueriesRowSelection.ts | 8 +++++++- .../TopQueries/hooks/useTopQueriesRowSelection.ts | 8 +++++++- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/containers/DrawerWrapper/DrawerWrapper.tsx b/src/containers/DrawerWrapper/DrawerWrapper.tsx index 32fcd12da..ea6bef7a5 100644 --- a/src/containers/DrawerWrapper/DrawerWrapper.tsx +++ b/src/containers/DrawerWrapper/DrawerWrapper.tsx @@ -55,9 +55,9 @@ export const DrawerContentWrapper = ({ } }; - document.addEventListener('mousedown', handleClickOutside); + document.addEventListener('click', handleClickOutside); return () => { - document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('click', handleClickOutside); }; }, [isVisible, onClose, detectClickOutside]); @@ -114,6 +114,11 @@ export const DrawerWrapper = ({ className, detectClickOutside, }: DrawerWrapperProps) => { + React.useEffect(() => { + return () => { + onCloseDrawer(); + }; + }, [onCloseDrawer]); return ( {children} diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx index 11f16b82c..313006432 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx @@ -73,11 +73,6 @@ export const RunningQueriesData = ({ const rows = data?.resultSets?.[0]?.result; const {handleRowSelect, selectedRow, hasSearchParams} = useRunningQueriesRowSelection(rows); - const handleRowClick = (row: KeyValueRow) => { - // Simply pass the row to handleRowSelect - handleRowSelect(row); - }; - const handleCloseDetails = React.useCallback(() => { handleRowSelect(null); }, [handleRowSelect]); @@ -127,7 +122,7 @@ export const RunningQueriesData = ({ data={rows || []} loading={isFetching && currentData === undefined} settings={TOP_QUERIES_TABLE_SETTINGS} - onRowClick={handleRowClick} + onRowClick={handleRowSelect} rowClassName={(row) => b('row', {active: row === selectedRow})} sortOrder={tableSort} onSort={handleTableSort} diff --git a/src/containers/Tenant/Diagnostics/TopQueries/hooks/useRunningQueriesRowSelection.ts b/src/containers/Tenant/Diagnostics/TopQueries/hooks/useRunningQueriesRowSelection.ts index bf5f0b646..73eff96c5 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/hooks/useRunningQueriesRowSelection.ts +++ b/src/containers/Tenant/Diagnostics/TopQueries/hooks/useRunningQueriesRowSelection.ts @@ -50,7 +50,13 @@ export function useRunningQueriesRowSelection(rows?: KeyValueRow[] | null) { const matchedRow = findMatchedQueryRow(); const handleRowSelect = React.useCallback( - (row: KeyValueRow | null) => { + ( + row: KeyValueRow | null, + _index?: number, + event?: React.MouseEvent, + ) => { + event?.stopPropagation(); + if (!row) { setQueryParams( { diff --git a/src/containers/Tenant/Diagnostics/TopQueries/hooks/useTopQueriesRowSelection.ts b/src/containers/Tenant/Diagnostics/TopQueries/hooks/useTopQueriesRowSelection.ts index 79be16cbd..2ccc62540 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/hooks/useTopQueriesRowSelection.ts +++ b/src/containers/Tenant/Diagnostics/TopQueries/hooks/useTopQueriesRowSelection.ts @@ -54,7 +54,13 @@ export function useTopQueriesRowSelection(rows?: KeyValueRow[] | null) { const matchedRow = findMatchedQueryRow(); const handleRowSelect = React.useCallback( - (row: KeyValueRow | null) => { + ( + row: KeyValueRow | null, + _index?: number, + event?: React.MouseEvent, + ) => { + event?.stopPropagation(); + if (!row) { setQueryParams( { From 47dd47e0fbe590812806ef62abaec51e180302ce Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Thu, 17 Apr 2025 18:54:13 +0300 Subject: [PATCH 07/21] fix: simplier approach --- .../Diagnostics/TopQueries/QueryDetails.tsx | 25 ++++- .../TopQueries/QueryDetailsDrawerContent.tsx | 8 +- .../TopQueries/RunningQueriesData.tsx | 31 ++++-- .../Diagnostics/TopQueries/TopQueriesData.tsx | 56 ++++++++--- .../hooks/useRunningQueriesRowSelection.ts | 94 ------------------ .../useSetSelectedTopQueryRowFromParams.ts | 51 ++++++++++ .../hooks/useTopQueriesRowSelection.ts | 99 ------------------- .../Diagnostics/TopQueries/i18n/en.json | 3 +- .../TopQueries/utils/generateShareableUrl.ts | 35 +++++++ .../utils/getRunningQueryRowQueryParams.ts | 24 ----- .../utils/getTopQueryRowQueryParams.ts | 2 +- 11 files changed, 185 insertions(+), 243 deletions(-) delete mode 100644 src/containers/Tenant/Diagnostics/TopQueries/hooks/useRunningQueriesRowSelection.ts create mode 100644 src/containers/Tenant/Diagnostics/TopQueries/hooks/useSetSelectedTopQueryRowFromParams.ts delete mode 100644 src/containers/Tenant/Diagnostics/TopQueries/hooks/useTopQueriesRowSelection.ts create mode 100644 src/containers/Tenant/Diagnostics/TopQueries/utils/generateShareableUrl.ts delete mode 100644 src/containers/Tenant/Diagnostics/TopQueries/utils/getRunningQueryRowQueryParams.ts diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.tsx index 4892bc54a..2f6b04b7e 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.tsx @@ -19,6 +19,7 @@ interface QueryDetailsProps { infoItems: InfoViewerItem[]; onClose: () => void; onOpenInEditor: () => void; + onCopyLink?: () => void; } export const QueryDetails = ({ @@ -26,15 +27,33 @@ export const QueryDetails = ({ infoItems, onClose, onOpenInEditor, + onCopyLink, }: QueryDetailsProps) => { + // Function to copy current URL to clipboard + const copyLinkToClipboard = (e: React.MouseEvent) => { + e.stopPropagation(); + + // If onCopyLink is provided, call it to generate and copy a shareable URL + // The actual copy to clipboard is handled in the parent component + if (onCopyLink) { + onCopyLink(); + } + }; + return (
Query
- + {onCopyLink && ( + + )} + + )} + + ); +}; + +export function CopyLinkButton(props: ClipboardButtonProps) { + const {text, ...buttonProps} = props; + + const timerIdRef = React.useRef(); + const [tooltipCloseDelay, setTooltipCloseDelay] = React.useState(undefined); + const [tooltipDisabled, setTooltipDisabled] = React.useState(false); + const timeout = DEFAULT_TIMEOUT; + + React.useEffect(() => window.clearTimeout(timerIdRef.current), []); + + const handleCopy = React.useCallback(() => { + setTooltipDisabled(false); + setTooltipCloseDelay(timeout); + + window.clearTimeout(timerIdRef.current); + + timerIdRef.current = window.setTimeout(() => { + setTooltipDisabled(true); + }, timeout - TOOLTIP_ANIMATION); + }, [timeout]); + + return ( + + {(status) => ( + + )} + + ); +} diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss index 8c558affb..2c6049ab3 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss @@ -61,4 +61,9 @@ 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.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.tsx index 80f92a644..eb4c755bb 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.tsx @@ -1,15 +1,15 @@ import React from 'react'; -import {Code, Link, Xmark} from '@gravity-ui/icons'; -import {ActionTooltip, Button, Icon} from '@gravity-ui/uikit'; +import {Code, Xmark} from '@gravity-ui/icons'; +import {Button, Icon} from '@gravity-ui/uikit'; -import EnableFullscreenButton from '../../../../components/EnableFullscreenButton/EnableFullscreenButton'; import Fullscreen from '../../../../components/Fullscreen/Fullscreen'; import type {InfoViewerItem} from '../../../../components/InfoViewer'; import {InfoViewer} from '../../../../components/InfoViewer'; import {YDBSyntaxHighlighter} from '../../../../components/SyntaxHighlighter/YDBSyntaxHighlighter'; import {cn} from '../../../../utils/cn'; +import {CopyLinkButton} from './CopyLinkButton'; import i18n from './i18n'; import './QueryDetails.scss'; @@ -21,7 +21,7 @@ interface QueryDetailsProps { infoItems: InfoViewerItem[]; onClose: () => void; onOpenInEditor: () => void; - onCopyLink?: () => void; + getTopQueryUrl?: () => string; } export const QueryDetails = ({ @@ -29,47 +29,16 @@ export const QueryDetails = ({ infoItems, onClose, onOpenInEditor, - onCopyLink, + getTopQueryUrl, }: QueryDetailsProps) => { - const [isTooltipOpen, setIsTooltipOpen] = React.useState(false); - - // Function to copy current URL to clipboard - const copyLinkToClipboard = (e: React.MouseEvent) => { - e.stopPropagation(); - - setIsTooltipOpen(true); - // If onCopyLink is provided, call it to generate and copy a shareable URL - // The actual copy to clipboard is handled in the parent component - if (onCopyLink) { - onCopyLink(); - } - - setTimeout(() => { - setIsTooltipOpen(false); - }, 1000); - }; + const topQueryUrl = React.useMemo(() => getTopQueryUrl?.(), [getTopQueryUrl]); return (
Query
- {onCopyLink && ( - - - - )} - + {topQueryUrl && } diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetailsDrawerContent.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetailsDrawerContent.tsx index d051a4a23..50328f831 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetailsDrawerContent.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetailsDrawerContent.tsx @@ -26,13 +26,13 @@ const b = cn('kv-top-queries'); interface QueryDetailsDrawerContentProps { row: KeyValueRow | null; onClose: () => void; - onCopyLink?: () => void; + getTopQueryUrl?: () => string; } export const QueryDetailsDrawerContent = ({ row, onClose, - onCopyLink, + getTopQueryUrl, }: QueryDetailsDrawerContentProps) => { const dispatch = useTypedDispatch(); const location = useLocation(); @@ -63,7 +63,7 @@ export const QueryDetailsDrawerContent = ({ infoItems={createQueryInfoItems(row)} onClose={onClose} onOpenInEditor={handleOpenInEditor} - onCopyLink={onCopyLink} + getTopQueryUrl={getTopQueryUrl} /> ); } diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx index e3e0a922d..12ccd657f 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx @@ -77,7 +77,7 @@ export const RunningQueriesData = ({ const isDrawerVisible = selectedRow !== undefined; const handleCloseDetails = React.useCallback(() => { - setSelectedRow(null); + setSelectedRow(undefined); }, [setSelectedRow]); const renderDrawerContent = React.useCallback(() => { diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx index f4e687896..a4d672e7f 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx @@ -95,11 +95,11 @@ export const TopQueriesData = ({ const isDrawerVisible = selectedRow !== undefined; - const onCopyLink = React.useCallback(() => { + const getTopQueryUrl = React.useCallback(() => { if (selectedRow) { - const shareableUrl = generateShareableUrl(selectedRow, tableSort); - navigator.clipboard.writeText(shareableUrl); + return generateShareableUrl(selectedRow, tableSort); } + return ''; }, [selectedRow, tableSort]); const renderDrawerContent = React.useCallback(() => { @@ -110,10 +110,10 @@ export const TopQueriesData = ({ ); - }, [isDrawerVisible, selectedRow, handleCloseDetails, onCopyLink]); + }, [isDrawerVisible, selectedRow, handleCloseDetails, getTopQueryUrl]); const onRowClick = React.useCallback( ( From b7e678075c85c3913f6462c4f125297ad7c2c08c Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Fri, 18 Apr 2025 17:54:51 +0300 Subject: [PATCH 13/21] fix: better code --- src/containers/Drawer/Drawer.scss | 12 ++ .../DrawerWrapper.tsx => Drawer/Drawer.tsx} | 119 ++++++++++-------- .../DrawerWrapper/DrawerWrapper.scss | 7 -- src/containers/DrawerWrapper/index.ts | 1 - .../Tenant/Diagnostics/Diagnostics.scss | 8 -- .../Tenant/Diagnostics/Diagnostics.tsx | 5 +- .../TopQueries/RunningQueriesData.tsx | 7 +- .../Diagnostics/TopQueries/TopQueriesData.tsx | 7 +- 8 files changed, 86 insertions(+), 80 deletions(-) create mode 100644 src/containers/Drawer/Drawer.scss rename src/containers/{DrawerWrapper/DrawerWrapper.tsx => Drawer/Drawer.tsx} (61%) delete mode 100644 src/containers/DrawerWrapper/DrawerWrapper.scss delete mode 100644 src/containers/DrawerWrapper/index.ts diff --git a/src/containers/Drawer/Drawer.scss b/src/containers/Drawer/Drawer.scss new file mode 100644 index 000000000..aa67448c7 --- /dev/null +++ b/src/containers/Drawer/Drawer.scss @@ -0,0 +1,12 @@ +.ydb-drawer { + &__drawer-container { + position: relative; + overflow: hidden; + height: 100%; + } + + &__item { + z-index: 4; + height: 100%; + } +} diff --git a/src/containers/DrawerWrapper/DrawerWrapper.tsx b/src/containers/Drawer/Drawer.tsx similarity index 61% rename from src/containers/DrawerWrapper/DrawerWrapper.tsx rename to src/containers/Drawer/Drawer.tsx index ea6bef7a5..2a21fac65 100644 --- a/src/containers/DrawerWrapper/DrawerWrapper.tsx +++ b/src/containers/Drawer/Drawer.tsx @@ -1,16 +1,34 @@ import React from 'react'; -import {Drawer, DrawerItem} from '@gravity-ui/navigation'; +import {DrawerItem, Drawer as GravityDrawer} from '@gravity-ui/navigation'; import {cn} from '../../utils/cn'; const DEFAULT_DRAWER_WIDTH = 600; const DRAWER_WIDTH_KEY = 'drawer-width'; -const b = cn('ydb-drawer-wrapper'); +const b = cn('ydb-drawer'); -import './DrawerWrapper.scss'; +import './Drawer.scss'; -interface DrawerContentWrapperProps { +interface ContainerProps { + children: React.ReactNode; + className?: string; +} + +interface WrapperProps { + children: React.ReactNode; + renderDrawerContent: () => React.ReactNode; + isDrawerVisible: boolean; + onCloseDrawer: () => void; + drawerId?: string; + storageKey?: string; + defaultWidth?: number; + direction?: 'left' | 'right'; + className?: string; + detectClickOutside?: boolean; +} + +interface ContentWrapperProps { isVisible: boolean; onClose: () => void; children: React.ReactNode; @@ -22,7 +40,7 @@ interface DrawerContentWrapperProps { detectClickOutside?: boolean; } -export const DrawerContentWrapper = ({ +const ContentWrapper = ({ isVisible, onClose, children, @@ -32,7 +50,7 @@ export const DrawerContentWrapper = ({ direction = 'right', className, detectClickOutside = false, -}: DrawerContentWrapperProps) => { +}: ContentWrapperProps) => { const [drawerWidth, setDrawerWidth] = React.useState(() => { const savedWidth = localStorage.getItem(storageKey); return savedWidth ? Number(savedWidth) : defaultWidth; @@ -67,7 +85,7 @@ export const DrawerContentWrapper = ({ }; return ( - {children} - + ); }; -interface DrawerWrapperProps { - children: React.ReactNode; - renderDrawerContent: () => React.ReactNode; - isDrawerVisible: boolean; - onCloseDrawer: () => void; - drawerId?: string; - storageKey?: string; - defaultWidth?: number; - direction?: 'left' | 'right'; - className?: string; - detectClickOutside?: boolean; -} +export const Drawer = { + Container: ({children, className}: ContainerProps) => { + return
{children}
; + }, -export const DrawerWrapper = ({ - children, - renderDrawerContent, - isDrawerVisible, - onCloseDrawer, - drawerId, - storageKey, - defaultWidth, - direction, - className, - detectClickOutside, -}: DrawerWrapperProps) => { - React.useEffect(() => { - return () => { - onCloseDrawer(); - }; - }, [onCloseDrawer]); - return ( - - {children} - - {renderDrawerContent()} - - - ); + Wrapper: ({ + children, + renderDrawerContent, + isDrawerVisible, + onCloseDrawer, + drawerId, + storageKey, + defaultWidth, + direction, + className, + detectClickOutside, + }: WrapperProps) => { + React.useEffect(() => { + return () => { + onCloseDrawer(); + }; + }, [onCloseDrawer]); + return ( + + {children} + + {renderDrawerContent()} + + + ); + }, }; diff --git a/src/containers/DrawerWrapper/DrawerWrapper.scss b/src/containers/DrawerWrapper/DrawerWrapper.scss deleted file mode 100644 index e0f5d6447..000000000 --- a/src/containers/DrawerWrapper/DrawerWrapper.scss +++ /dev/null @@ -1,7 +0,0 @@ -.ydb-drawer-wrapper { - &__item { - z-index: 4; - - height: 100%; - } -} diff --git a/src/containers/DrawerWrapper/index.ts b/src/containers/DrawerWrapper/index.ts deleted file mode 100644 index 281470b73..000000000 --- a/src/containers/DrawerWrapper/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {DrawerWrapper, DrawerContentWrapper} from './DrawerWrapper'; diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.scss b/src/containers/Tenant/Diagnostics/Diagnostics.scss index c402eb18e..2d296ddb0 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.scss +++ b/src/containers/Tenant/Diagnostics/Diagnostics.scss @@ -34,14 +34,6 @@ } } - &__drawer-container { - position: relative; - - overflow: hidden; - - height: 100%; - } - &__page-wrapper { overflow: auto; flex-grow: 1; diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.tsx b/src/containers/Tenant/Diagnostics/Diagnostics.tsx index 82ff8285e..72815d950 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.tsx +++ b/src/containers/Tenant/Diagnostics/Diagnostics.tsx @@ -13,6 +13,7 @@ import type {AdditionalNodesProps, AdditionalTenantsProps} from '../../../types/ import type {EPathType} from '../../../types/api/schema'; import {cn} from '../../../utils/cn'; import {useTypedDispatch, useTypedSelector} from '../../../utils/hooks'; +import {Drawer} from '../../Drawer/Drawer'; import {Heatmap} from '../../Heatmap'; import {Nodes} from '../../Nodes/Nodes'; import {Operations} from '../../Operations'; @@ -194,11 +195,11 @@ function Diagnostics(props: DiagnosticsProps) { ) : null} {renderTabs()} -
+
{renderTabContent()}
-
+
); } diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx index 12ccd657f..002a5df12 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx @@ -7,13 +7,13 @@ import {ResponseError} from '../../../../components/Errors/ResponseError'; import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; import {Search} from '../../../../components/Search'; import {TableWithControlsLayout} from '../../../../components/TableWithControlsLayout/TableWithControlsLayout'; -import {DrawerWrapper} from '../../../../containers/DrawerWrapper'; import {topQueriesApi} from '../../../../store/reducers/executeTopQueries/executeTopQueries'; import type {KeyValueRow} from '../../../../types/api/query'; import {cn} from '../../../../utils/cn'; import {useAutoRefreshInterval, useTypedSelector} from '../../../../utils/hooks'; import {useSelectedColumns} from '../../../../utils/hooks/useSelectedColumns'; import {parseQueryErrorToString} from '../../../../utils/query'; +import {Drawer} from '../../../Drawer/Drawer'; import {QueryDetailsDrawerContent} from './QueryDetailsDrawerContent'; import {getRunningQueriesColumns} from './columns/columns'; @@ -100,7 +100,7 @@ export const RunningQueriesData = ({ ); return ( - @@ -144,6 +143,6 @@ export const RunningQueriesData = ({ /> - + ); }; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx index a4d672e7f..d056994a0 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx @@ -10,7 +10,6 @@ import {ResponseError} from '../../../../components/Errors/ResponseError'; import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; import {Search} from '../../../../components/Search'; import {TableWithControlsLayout} from '../../../../components/TableWithControlsLayout/TableWithControlsLayout'; -import {DrawerWrapper} from '../../../../containers/DrawerWrapper'; import {topQueriesApi} from '../../../../store/reducers/executeTopQueries/executeTopQueries'; import type {TimeFrame} from '../../../../store/reducers/executeTopQueries/types'; import type {KeyValueRow} from '../../../../types/api/query'; @@ -18,6 +17,7 @@ import {cn} from '../../../../utils/cn'; import {useAutoRefreshInterval, useTypedSelector} from '../../../../utils/hooks'; import {useSelectedColumns} from '../../../../utils/hooks/useSelectedColumns'; import {parseQueryErrorToString} from '../../../../utils/query'; +import {Drawer} from '../../../Drawer/Drawer'; import {QueryDetailsDrawerContent} from './QueryDetailsDrawerContent'; import {getTopQueriesColumns} from './columns/columns'; @@ -128,7 +128,7 @@ export const TopQueriesData = ({ ); return ( - @@ -183,6 +182,6 @@ export const TopQueriesData = ({ /> - + ); }; From ac185c97e16867aebb5eac573ecf53a1162790ac Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Fri, 18 Apr 2025 18:15:57 +0300 Subject: [PATCH 14/21] fix: use encode-decode --- .../hooks/useGetSelectedRowTableSort.ts | 2 +- .../hooks/useSetSelectedTopQueryRowFromParams.ts | 4 +++- .../TopQueries/utils/generateShareableUrl.ts | 16 +++++++++------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/containers/Tenant/Diagnostics/TopQueries/hooks/useGetSelectedRowTableSort.ts b/src/containers/Tenant/Diagnostics/TopQueries/hooks/useGetSelectedRowTableSort.ts index 3632288b5..5c91927f7 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/hooks/useGetSelectedRowTableSort.ts +++ b/src/containers/Tenant/Diagnostics/TopQueries/hooks/useGetSelectedRowTableSort.ts @@ -6,7 +6,7 @@ export function useGetSelectedRowTableSort(): SortOrder[] | undefined { selectedRow: StringParam, }); const searchParamsQuery: {tableSort?: SortOrder[]} = queryParams.selectedRow - ? JSON.parse(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 index 78b69a3f9..a1c20cf0b 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/hooks/useSetSelectedTopQueryRowFromParams.ts +++ b/src/containers/Tenant/Diagnostics/TopQueries/hooks/useSetSelectedTopQueryRowFromParams.ts @@ -23,7 +23,9 @@ export function useSetSelectedTopQueryRowFromParams( // Handle initialization from URL params React.useEffect(() => { if (rows && queryParams.selectedRow) { - const searchParamsQuery: SearchParamsQueryParams = JSON.parse(queryParams.selectedRow); + const searchParamsQuery: SearchParamsQueryParams = JSON.parse( + decodeURIComponent(queryParams.selectedRow), + ); const matchedRow = rows.find((row) => { const params = getTopQueryRowQueryParams(row); return ( diff --git a/src/containers/Tenant/Diagnostics/TopQueries/utils/generateShareableUrl.ts b/src/containers/Tenant/Diagnostics/TopQueries/utils/generateShareableUrl.ts index 1f19ff930..2a4c52761 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/utils/generateShareableUrl.ts +++ b/src/containers/Tenant/Diagnostics/TopQueries/utils/generateShareableUrl.ts @@ -23,13 +23,15 @@ export function generateShareableUrl(row: KeyValueRow, tableSort?: SortOrder[]): // Set a single selectedRow parameter with all query parameters searchParams.set( 'selectedRow', - 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 - }), + 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 From a080d85175c87a151b0d7331bbe009860fa049ef Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Fri, 18 Apr 2025 18:17:24 +0300 Subject: [PATCH 15/21] fix: stylelint --- src/containers/Drawer/Drawer.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/containers/Drawer/Drawer.scss b/src/containers/Drawer/Drawer.scss index aa67448c7..15952f720 100644 --- a/src/containers/Drawer/Drawer.scss +++ b/src/containers/Drawer/Drawer.scss @@ -1,12 +1,15 @@ .ydb-drawer { &__drawer-container { position: relative; + overflow: hidden; + height: 100%; } &__item { z-index: 4; + height: 100%; } } From 8d69beb2dd45b3817677fa70c6e24a349ad6ba72 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Fri, 18 Apr 2025 18:52:10 +0300 Subject: [PATCH 16/21] fix: only used props, move component --- .../{ => QueryDetails}/CopyLinkButton.tsx | 34 +++++++------------ .../{ => QueryDetails}/QueryDetails.scss | 2 +- .../{ => QueryDetails}/QueryDetails.tsx | 12 +++---- .../QueryDetailsDrawerContent.tsx | 20 +++++------ .../TopQueries/RunningQueriesData.tsx | 2 +- .../Diagnostics/TopQueries/TopQueriesData.tsx | 2 +- 6 files changed, 32 insertions(+), 40 deletions(-) rename src/containers/Tenant/Diagnostics/TopQueries/{ => QueryDetails}/CopyLinkButton.tsx (69%) rename src/containers/Tenant/Diagnostics/TopQueries/{ => QueryDetails}/QueryDetails.scss (96%) rename src/containers/Tenant/Diagnostics/TopQueries/{ => QueryDetails}/QueryDetails.tsx (85%) rename src/containers/Tenant/Diagnostics/TopQueries/{ => QueryDetails}/QueryDetailsDrawerContent.tsx (79%) diff --git a/src/containers/Tenant/Diagnostics/TopQueries/CopyLinkButton.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/CopyLinkButton.tsx similarity index 69% rename from src/containers/Tenant/Diagnostics/TopQueries/CopyLinkButton.tsx rename to src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/CopyLinkButton.tsx index c6abd82e3..87a247228 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/CopyLinkButton.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/CopyLinkButton.tsx @@ -1,39 +1,27 @@ import React from 'react'; import {Link} from '@gravity-ui/icons'; -import type {ButtonProps, CopyToClipboardProps, CopyToClipboardStatus} from '@gravity-ui/uikit'; +import type {ButtonProps, CopyToClipboardStatus} from '@gravity-ui/uikit'; import {ActionTooltip, Button, CopyToClipboard, Icon} from '@gravity-ui/uikit'; -import {cn} from '../../../../utils/cn'; - -import i18n from './i18n'; +import {cn} from '../../../../../utils/cn'; +import i18n from '../i18n'; import './QueryDetails.scss'; const b = cn('kv-query-details'); -export interface ClipboardButtonProps - extends Omit, - Omit {} - -interface ClipboardButtonComponentProps - extends Omit { - status: CopyToClipboardStatus; - closeDelay: number | undefined; - /** Disable tooltip. Tooltip won't be shown */ +interface LinkButtonComponentProps extends ButtonProps { + size?: ButtonProps['size']; hasTooltip?: boolean; - /** Text shown before copy */ - tooltipInitialText?: string; - /** Text shown after copy */ - tooltipSuccessText?: string; - /** Position of clipboard icon */ - iconPosition?: 'start' | 'end'; + status: CopyToClipboardStatus; + closeDelay?: number; } const DEFAULT_TIMEOUT = 1200; const TOOLTIP_ANIMATION = 200; -const LinkButtonComponent = (props: ClipboardButtonComponentProps) => { +const LinkButtonComponent = (props: LinkButtonComponentProps) => { const {size = 'm', hasTooltip = true, status, closeDelay, ...rest} = props; return ( @@ -55,7 +43,11 @@ const LinkButtonComponent = (props: ClipboardButtonComponentProps) => { ); }; -export function CopyLinkButton(props: ClipboardButtonProps) { +export interface CopyLinkButtonProps extends ButtonProps { + text: string; +} + +export function CopyLinkButton(props: CopyLinkButtonProps) { const {text, ...buttonProps} = props; const timerIdRef = React.useRef(); diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss similarity index 96% rename from src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss rename to src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss index 2c6049ab3..ea9c614fc 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.scss +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss @@ -1,4 +1,4 @@ -@import '../../../../styles/mixins.scss'; +@import '../../../../../styles/mixins.scss'; .kv-query-details { display: flex; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.tsx similarity index 85% rename from src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.tsx rename to src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.tsx index eb4c755bb..98a21ce54 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.tsx @@ -3,14 +3,14 @@ import React from 'react'; import {Code, Xmark} from '@gravity-ui/icons'; import {Button, Icon} from '@gravity-ui/uikit'; -import Fullscreen from '../../../../components/Fullscreen/Fullscreen'; -import type {InfoViewerItem} from '../../../../components/InfoViewer'; -import {InfoViewer} from '../../../../components/InfoViewer'; -import {YDBSyntaxHighlighter} from '../../../../components/SyntaxHighlighter/YDBSyntaxHighlighter'; -import {cn} from '../../../../utils/cn'; +import Fullscreen from '../../../../../components/Fullscreen/Fullscreen'; +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 {CopyLinkButton} from './CopyLinkButton'; -import i18n from './i18n'; import './QueryDetails.scss'; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetailsDrawerContent.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx similarity index 79% rename from src/containers/Tenant/Diagnostics/TopQueries/QueryDetailsDrawerContent.tsx rename to src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx index 50328f831..e7dc7e245 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetailsDrawerContent.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx @@ -3,23 +3,23 @@ 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 {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'; +} 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 i18n from './i18n'; -import {createQueryInfoItems} from './utils'; -import CryCatIcon from '../../../../assets/icons/cry-cat.svg'; +import CryCatIcon from '../../../../../assets/icons/cry-cat.svg'; const b = cn('kv-top-queries'); diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx index 002a5df12..9a115b8aa 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx @@ -15,7 +15,7 @@ import {useSelectedColumns} from '../../../../utils/hooks/useSelectedColumns'; import {parseQueryErrorToString} from '../../../../utils/query'; import {Drawer} from '../../../Drawer/Drawer'; -import {QueryDetailsDrawerContent} from './QueryDetailsDrawerContent'; +import {QueryDetailsDrawerContent} from './QueryDetails/QueryDetailsDrawerContent'; import {getRunningQueriesColumns} from './columns/columns'; import { DEFAULT_RUNNING_QUERIES_COLUMNS, diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx index d056994a0..2612475c0 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx @@ -19,7 +19,7 @@ import {useSelectedColumns} from '../../../../utils/hooks/useSelectedColumns'; import {parseQueryErrorToString} from '../../../../utils/query'; import {Drawer} from '../../../Drawer/Drawer'; -import {QueryDetailsDrawerContent} from './QueryDetailsDrawerContent'; +import {QueryDetailsDrawerContent} from './QueryDetails/QueryDetailsDrawerContent'; import {getTopQueriesColumns} from './columns/columns'; import { DEFAULT_TOP_QUERIES_COLUMNS, From c4446c2dc6e18fb24e91b214aeef35c4e2fabf19 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Mon, 21 Apr 2025 15:01:20 +0300 Subject: [PATCH 17/21] fix: relative width --- src/containers/Drawer/Drawer.tsx | 120 ++++++++++++++---- .../TopQueries/RunningQueriesData.tsx | 6 +- .../Diagnostics/TopQueries/TopQueriesData.tsx | 6 +- 3 files changed, 103 insertions(+), 29 deletions(-) diff --git a/src/containers/Drawer/Drawer.tsx b/src/containers/Drawer/Drawer.tsx index 2a21fac65..1a950bd56 100644 --- a/src/containers/Drawer/Drawer.tsx +++ b/src/containers/Drawer/Drawer.tsx @@ -4,29 +4,29 @@ import {DrawerItem, Drawer as GravityDrawer} from '@gravity-ui/navigation'; import {cn} from '../../utils/cn'; +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'; -interface ContainerProps { - children: React.ReactNode; - className?: string; +// Create a context for sharing container dimensions +interface DrawerContextType { + containerWidth: number; + setContainerWidth: React.Dispatch>; } -interface WrapperProps { - children: React.ReactNode; - renderDrawerContent: () => React.ReactNode; - isDrawerVisible: boolean; - onCloseDrawer: () => void; - drawerId?: string; - storageKey?: string; - defaultWidth?: number; - direction?: 'left' | 'right'; - className?: string; - detectClickOutside?: boolean; -} +const DrawerContext = React.createContext(undefined); + +// Custom hook to use the drawer context +const useDrawerContext = () => { + const context = React.useContext(DrawerContext); + if (context === undefined) { + return {containerWidth: 0, setContainerWidth: () => {}}; + } + return context; +}; interface ContentWrapperProps { isVisible: boolean; @@ -34,10 +34,11 @@ interface ContentWrapperProps { children: React.ReactNode; drawerId?: string; storageKey?: string; - defaultWidth?: number; direction?: 'left' | 'right'; className?: string; detectClickOutside?: boolean; + defaultWidth?: number; + isPercentageWidth?: boolean; } const ContentWrapper = ({ @@ -46,10 +47,11 @@ const ContentWrapper = ({ children, drawerId = 'drawer', storageKey = DRAWER_WIDTH_KEY, - defaultWidth = DEFAULT_DRAWER_WIDTH, + defaultWidth, direction = 'right', className, detectClickOutside = false, + isPercentageWidth, }: ContentWrapperProps) => { const [drawerWidth, setDrawerWidth] = React.useState(() => { const savedWidth = localStorage.getItem(storageKey); @@ -57,6 +59,16 @@ const ContentWrapper = ({ }); const drawerRef = React.useRef(null); + const {containerWidth} = useDrawerContext(); + // Calculate drawer width based on container width percentage if specified + const calculatedWidth = React.useMemo(() => { + if (isPercentageWidth && containerWidth > 0) { + return Math.round( + (containerWidth * (drawerWidth || DEFAULT_DRAWER_WIDTH_PERCENTS)) / 100, + ); + } + return drawerWidth || DEFAULT_DRAWER_WIDTH; + }, [containerWidth, isPercentageWidth, drawerWidth]); React.useEffect(() => { if (!detectClickOutside) { @@ -80,8 +92,14 @@ const ContentWrapper = ({ }, [isVisible, onClose, detectClickOutside]); const handleResizeDrawer = (width: number) => { - setDrawerWidth(width); - localStorage.setItem(storageKey, width.toString()); + if (isPercentageWidth && containerWidth > 0) { + const percentageWidth = Math.round((width / containerWidth) * 100); + setDrawerWidth(percentageWidth); + localStorage.setItem(storageKey, percentageWidth.toString()); + } else { + setDrawerWidth(width); + localStorage.setItem(storageKey, width.toString()); + } }; return ( @@ -95,7 +113,8 @@ const ContentWrapper = ({ id={drawerId} visible={isVisible} resizable - width={drawerWidth} + maxResizeWidth={containerWidth} + width={isPercentageWidth ? calculatedWidth : drawerWidth} onResize={handleResizeDrawer} direction={direction} className={b('item')} @@ -107,12 +126,65 @@ const ContentWrapper = ({ ); }; +interface ContainerProps { + children: React.ReactNode; + className?: string; +} + +interface ItemWrapperProps { + children: React.ReactNode; + renderDrawerContent: () => React.ReactNode; + isDrawerVisible: boolean; + onCloseDrawer: () => void; + drawerId?: string; + storageKey?: string; + defaultWidth?: number; + direction?: 'left' | 'right'; + className?: string; + detectClickOutside?: boolean; + isPercentageWidth?: boolean; +} + export const Drawer = { Container: ({children, className}: ContainerProps) => { - return
{children}
; + const [containerWidth, setContainerWidth] = React.useState(0); + const containerRef = React.useRef(null); + + React.useEffect(() => { + if (!containerRef.current) { + return undefined; + } + + const updateWidth = () => { + if (containerRef.current) { + setContainerWidth(containerRef.current.clientWidth); + } + }; + + // Set initial width + updateWidth(); + + // Update width on resize + const resizeObserver = new ResizeObserver(updateWidth); + resizeObserver.observe(containerRef.current); + + return () => { + if (containerRef.current) { + resizeObserver.disconnect(); + } + }; + }, []); + + return ( + +
+ {children} +
+
+ ); }, - Wrapper: ({ + ItemWrapper: ({ children, renderDrawerContent, isDrawerVisible, @@ -123,7 +195,8 @@ export const Drawer = { direction, className, detectClickOutside, - }: WrapperProps) => { + isPercentageWidth, + }: ItemWrapperProps) => { React.useEffect(() => { return () => { onCloseDrawer(); @@ -141,6 +214,7 @@ export const Drawer = { direction={direction} className={className} detectClickOutside={detectClickOutside} + isPercentageWidth={isPercentageWidth} > {renderDrawerContent()} diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx index 9a115b8aa..2bc0f0ffb 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx @@ -100,14 +100,14 @@ export const RunningQueriesData = ({ ); return ( - @@ -143,6 +143,6 @@ export const RunningQueriesData = ({ /> - + ); }; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx index 2612475c0..49d758126 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx @@ -128,14 +128,14 @@ export const TopQueriesData = ({ ); return ( - @@ -182,6 +182,6 @@ export const TopQueriesData = ({ /> - + ); }; From 466c07c4b37c676f86dd7071ca49d1309d10116d Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Mon, 21 Apr 2025 17:05:41 +0300 Subject: [PATCH 18/21] fix: copy button padding --- .../SyntaxHighlighter/YDBSyntaxHighlighter.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.tsx b/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.tsx index b92fd2279..28b4b4700 100644 --- a/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.tsx +++ b/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.tsx @@ -88,6 +88,20 @@ export function YDBSyntaxHighlighter({ return null; }; + let paddingStyles = {}; + + if ( + withClipboardButton && + typeof withClipboardButton === 'object' && + withClipboardButton.alwaysVisible + ) { + if (withClipboardButton.withLabel) { + paddingStyles = {paddingRight: 80}; + } else { + paddingStyles = {paddingRight: 40}; + } + } + return (
{renderCopyButton()} @@ -96,7 +110,7 @@ export function YDBSyntaxHighlighter({ key={highlighterKey} language={language} style={style} - customStyle={{height: '100%'}} + customStyle={{height: '100%', ...paddingStyles}} > {text} From 7aa2e615dfacf4b4109c9bf204b91b804251ec1d Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Mon, 21 Apr 2025 17:26:36 +0300 Subject: [PATCH 19/21] fix: editor height --- .../Diagnostics/TopQueries/QueryDetails/QueryDetails.scss | 4 +++- .../Diagnostics/TopQueries/QueryDetails/QueryDetails.tsx | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss index ea9c614fc..23dfaa569 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.scss @@ -54,7 +54,9 @@ &__query-content { position: relative; - overflow: hidden; + display: flex; + flex: 1; + flex-direction: column; margin-top: var(--g-spacing-5); diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.tsx index 98a21ce54..9870bd5f8 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetails.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {Code, Xmark} from '@gravity-ui/icons'; -import {Button, Icon} from '@gravity-ui/uikit'; +import {Button, Flex, Icon} from '@gravity-ui/uikit'; import Fullscreen from '../../../../../components/Fullscreen/Fullscreen'; import type {InfoViewerItem} from '../../../../../components/InfoViewer'; @@ -46,7 +46,7 @@ export const QueryDetails = ({
-
+
@@ -70,7 +70,7 @@ export const QueryDetails = ({ withClipboardButton={{alwaysVisible: true, withLabel: false}} />
-
+
); From 145e145bf866ed04b6ff359cb9cd156a44d052c0 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Mon, 21 Apr 2025 17:48:42 +0300 Subject: [PATCH 20/21] fix: blur on drawer open --- src/components/Search/Search.tsx | 3 +++ .../Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx | 9 +++++++++ .../Tenant/Diagnostics/TopQueries/TopQueriesData.tsx | 9 +++++++++ 3 files changed, 21 insertions(+) diff --git a/src/components/Search/Search.tsx b/src/components/Search/Search.tsx index fb259ae14..d880013dc 100644 --- a/src/components/Search/Search.tsx +++ b/src/components/Search/Search.tsx @@ -15,6 +15,7 @@ interface SearchProps { className?: string; debounce?: number; placeholder?: string; + inputRef?: React.RefObject; } export const Search = ({ @@ -24,6 +25,7 @@ export const Search = ({ className, debounce = 200, placeholder, + inputRef, }: SearchProps) => { const [searchValue, setSearchValue] = React.useState(value); @@ -52,6 +54,7 @@ export const Search = ({ (null); + + React.useEffect(() => { + if (isDrawerVisible) { + inputRef.current?.blur(); + } + }, [isDrawerVisible]); + return ( (null); + + React.useEffect(() => { + if (isDrawerVisible) { + inputRef.current?.blur(); + } + }, [isDrawerVisible]); + return ( Date: Mon, 21 Apr 2025 17:52:46 +0300 Subject: [PATCH 21/21] fix: isEqual --- .../Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx index abf1c27dd..4a9868a67 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 {ResponseError} from '../../../../components/Errors/ResponseError'; import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; @@ -146,7 +147,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} />