Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 26 additions & 9 deletions src/components/QueryResultTable/Cell/Cell.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';

import {showTooltip} from '../../../store/reducers/tooltip';
import {useTypedDispatch} from '../../../utils/hooks';
import {Popup} from '@gravity-ui/uikit';

import {b} from '../QueryResultTable';

interface CellProps {
Expand All @@ -12,14 +12,31 @@ interface CellProps {
export const Cell = React.memo(function Cell(props: CellProps) {
const {className, value} = props;

const dispatch = useTypedDispatch();
const [open, setOpen] = React.useState(false);
const anchorRef = React.useRef<HTMLSpanElement | null>(null);

const handleToggle = React.useCallback(() => {
setOpen((prevOpen) => !prevOpen);
}, []);

const handleClose = React.useCallback(() => {
setOpen(false);
}, []);

return (
<span
className={b('cell', className)}
onClick={(e) => dispatch(showTooltip(e.target, value, 'cell'))}
>
{value}
</span>
<React.Fragment>
<Popup
open={open}
hasArrow
placement={['top', 'bottom']}
anchorRef={anchorRef}
onOutsideClick={handleClose}
>
<div className={b('cell-popup')}>{value}</div>
</Popup>
<span ref={anchorRef} className={b('cell', className)} onClick={handleToggle}>
{value}
</span>
</React.Fragment>
);
});
7 changes: 7 additions & 0 deletions src/components/QueryResultTable/QueryResultTable.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
@include mixins.cell-container;
}

&__cell-popup {
max-width: 300px;
padding: 10px;

word-break: break-word;
}

&__message {
padding: 15px 10px;
}
Expand Down
6 changes: 3 additions & 3 deletions src/components/QueryResultTable/QueryResultTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const b = cn('ydb-query-result-table');

const WIDTH_PREDICTION_ROWS_COUNT = 100;

const prepareTypedColumns = (columns: ColumnType[], data?: KeyValueRow[]) => {
const prepareTypedColumns = (columns: ColumnType[], data: KeyValueRow[] | undefined) => {
if (!columns.length) {
return [];
}
Expand All @@ -49,7 +49,7 @@ const prepareTypedColumns = (columns: ColumnType[], data?: KeyValueRow[]) => {
});
};

const prepareGenericColumns = (data?: KeyValueRow[]) => {
const prepareGenericColumns = (data: KeyValueRow[] | undefined) => {
if (!data?.length) {
return [];
}
Expand Down Expand Up @@ -85,7 +85,7 @@ export const QueryResultTable = (props: QueryResultTableProps) => {

const preparedColumns = React.useMemo(() => {
return columns ? prepareTypedColumns(columns, data) : prepareGenericColumns(data);
}, [data, columns]);
}, [columns, data]);

const settings = React.useMemo(() => {
return {
Expand Down
2 changes: 0 additions & 2 deletions src/containers/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {Helmet} from 'react-helmet-async';
import {componentsRegistry} from '../../components/ComponentsProvider/componentsRegistry';
import {FullscreenProvider} from '../../components/Fullscreen/FullscreenContext';
import {useTypedSelector} from '../../utils/hooks';
import ReduxTooltip from '../ReduxTooltip/ReduxTooltip';
import type {YDBEmbeddedUISettings} from '../UserSettings/settings';

import {useAppTitle} from './AppTitleContext';
Expand All @@ -34,7 +33,6 @@ function App({store, history, children, userSettings, appTitle = defaultAppTitle
<Providers store={store} history={history} appTitle={appTitle}>
<AppContent userSettings={userSettings}>{children}</AppContent>
{ChatPanel && <ChatPanel />}
<ReduxTooltip />
</Providers>
);
}
Expand Down
15 changes: 15 additions & 0 deletions src/containers/Heatmap/Heatmap.scss
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
}

&__items {
position: relative;

overflow: auto;
}
&__canvas-container {
Expand All @@ -59,6 +61,19 @@
cursor: pointer;
}

&__tooltip-anchor {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add paddings
Screenshot 2025-12-11 at 17 53 55

position: absolute;

width: 1px;
height: 1px;

pointer-events: none;
}

&__tooltip {
padding: 10px;
}

&__filters {
display: flex;
align-items: center;
Expand Down
89 changes: 70 additions & 19 deletions src/containers/Heatmap/Heatmap.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from 'react';

import {Checkbox, Select} from '@gravity-ui/uikit';
import {Checkbox, Popup, Select} from '@gravity-ui/uikit';

import {ResponseError} from '../../components/Errors/ResponseError';
import {Loader} from '../../components/Loader';
import {TabletTooltipContent} from '../../components/TooltipsContent';
import {heatmapApi, setHeatmapOptions} from '../../store/reducers/heatmap';
import {hideTooltip, showTooltip} from '../../store/reducers/tooltip';
import type {IHeatmapMetricValue} from '../../types/store/heatmap';
import type {IHeatmapMetricValue, IHeatmapTabletData} from '../../types/store/heatmap';
import {cn} from '../../utils/cn';
import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants';
import {formatNumber} from '../../utils/dataFormatters/dataFormatters';
Expand All @@ -32,6 +32,14 @@ export const Heatmap = ({path, database, databaseFullPath}: HeatmapProps) => {

const itemsContainer = React.createRef<HTMLDivElement>();

const [tabletTooltip, setTabletTooltip] = React.useState<{
tablet: IHeatmapTabletData;
position: {left: number; top: number};
} | null>(null);
const [tabletTooltipAnchorElement, setTabletTooltipAnchorElement] =
React.useState<HTMLDivElement | null>(null);
const isTabletTooltipHoveredRef = React.useRef(false);

const [autoRefreshInterval] = useAutoRefreshInterval();

const {currentData, isFetching, error} = heatmapApi.useGetHeatmapTabletsInfoQuery(
Expand All @@ -44,13 +52,35 @@ export const Heatmap = ({path, database, databaseFullPath}: HeatmapProps) => {
const {tablets = [], metrics} = currentData || {};
const {sort, heatmap, currentMetric} = useTypedSelector((state) => state.heatmap);

const onShowTooltip = (...args: Parameters<typeof showTooltip>) => {
dispatch(showTooltip(...args));
};
const handleShowTabletTooltip = React.useCallback(
(tablet: IHeatmapTabletData, position: {left: number; top: number}) => {
setTabletTooltip({tablet, position});
},
[],
);

const onHideTooltip = () => {
dispatch(hideTooltip());
};
const handleHideTabletTooltip = React.useCallback(() => {
setTabletTooltip(null);
}, []);

const handleRequestHideTabletTooltip = React.useCallback(() => {
setTabletTooltip((prev) => {
if (!prev || isTabletTooltipHoveredRef.current) {
return prev;
}

return null;
});
}, []);

const handleTooltipMouseEnter = React.useCallback(() => {
isTabletTooltipHoveredRef.current = true;
}, []);

const handleTooltipMouseLeave = React.useCallback(() => {
isTabletTooltipHoveredRef.current = false;
handleHideTabletTooltip();
}, [handleHideTabletTooltip]);

const handleMetricChange = (value: string[]) => {
dispatch(
Expand All @@ -76,14 +106,7 @@ export const Heatmap = ({path, database, databaseFullPath}: HeatmapProps) => {
};

const renderHistogram = () => {
return (
<Histogram
tablets={tablets}
currentMetric={currentMetric}
showTooltip={onShowTooltip}
hideTooltip={onHideTooltip}
/>
);
return <Histogram tablets={tablets} currentMetric={currentMetric} />;
};

const renderHeatmapCanvas = () => {
Expand All @@ -108,11 +131,22 @@ export const Heatmap = ({path, database, databaseFullPath}: HeatmapProps) => {

return (
<div ref={itemsContainer} className={b('items')}>
{tabletTooltip ? (
<div
key={`${tabletTooltip.position.left}-${tabletTooltip.position.top}`}
ref={setTabletTooltipAnchorElement}
className={b('tooltip-anchor')}
style={{
left: tabletTooltip.position.left,
top: tabletTooltip.position.top,
}}
/>
) : null}
<HeatmapCanvas
tablets={sortedTablets}
parentRef={itemsContainer}
showTooltip={onShowTooltip}
hideTooltip={onHideTooltip}
onShowTabletTooltip={handleShowTabletTooltip}
onHideTabletTooltip={handleRequestHideTabletTooltip}
/>
</div>
);
Expand All @@ -128,6 +162,23 @@ export const Heatmap = ({path, database, databaseFullPath}: HeatmapProps) => {

return (
<div className={b()}>
{tabletTooltip ? (
<Popup
open
hasArrow
placement={['top', 'bottom', 'left', 'right']}
anchorElement={tabletTooltipAnchorElement}
onOutsideClick={handleHideTabletTooltip}
>
<div
className={b('tooltip')}
onMouseEnter={handleTooltipMouseEnter}
onMouseLeave={handleTooltipMouseLeave}
>
<TabletTooltipContent data={tabletTooltip.tablet} />
</div>
</Popup>
) : null}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Popup may render before anchor element is ready

The Popup component receives tabletTooltipAnchorElement which is null on the first render cycle when tabletTooltip becomes truthy. The anchor div is created with a callback ref (setTabletTooltipAnchorElement), but this state update only takes effect after the initial render. This means the Popup opens with a null anchor element initially, potentially causing it to appear in the wrong position or not at all until the second render.

Additional Locations (1)

Fix in Cursor Fix in Web

<div className={b('filters')}>
<Select
className={b('heatmap-select')}
Expand Down
Loading
Loading