Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
30 changes: 20 additions & 10 deletions src/components/QueryResultTable/Cell/Cell.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
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 {
className?: string;
value: string;
isActive: boolean;
onToggle: () => void;
}

export const Cell = React.memo(function Cell(props: CellProps) {
const {className, value} = props;
const {className, value, isActive, onToggle} = props;

const dispatch = useTypedDispatch();
const anchorRef = React.useRef<HTMLSpanElement | null>(null);

return (
<span
className={b('cell', className)}
onClick={(e) => dispatch(showTooltip(e.target, value, 'cell'))}
>
{value}
</span>
<React.Fragment>
<Popup
open={isActive}
hasArrow
placement={['top', 'bottom']}
anchorRef={anchorRef}
onOutsideClick={onToggle}
>
<div className={b('cell-popup')}>{value}</div>
</Popup>
<span ref={anchorRef} className={b('cell', className)} onClick={onToggle}>
{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
53 changes: 47 additions & 6 deletions src/components/QueryResultTable/QueryResultTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@ export const b = cn('ydb-query-result-table');

const WIDTH_PREDICTION_ROWS_COUNT = 100;

const prepareTypedColumns = (columns: ColumnType[], data?: KeyValueRow[]) => {
type RenderCellArgs = {row: KeyValueRow; columnName: string};
type RenderCell = (args: RenderCellArgs) => React.ReactNode;

const prepareTypedColumns = (
columns: ColumnType[],
data: KeyValueRow[] | undefined,
renderCell: RenderCell,
) => {
if (!columns.length) {
return [];
}
Expand All @@ -42,14 +49,14 @@ const prepareTypedColumns = (columns: ColumnType[], data?: KeyValueRow[]) => {
name,
width: getColumnWidth({data: dataSlice, name}),
align: columnType === 'number' ? DataTable.RIGHT : DataTable.LEFT,
render: ({row}) => <Cell value={String(row[name])} />,
render: ({row}) => renderCell({row, columnName: name}),
};

return column;
});
};

const prepareGenericColumns = (data?: KeyValueRow[]) => {
const prepareGenericColumns = (data: KeyValueRow[] | undefined, renderCell: RenderCell) => {
if (!data?.length) {
return [];
}
Expand All @@ -61,7 +68,7 @@ const prepareGenericColumns = (data?: KeyValueRow[]) => {
name,
width: getColumnWidth({data: dataSlice, name}),
align: isNumeric(data[0][name]) ? DataTable.RIGHT : DataTable.LEFT,
render: ({row}) => <Cell value={String(row[name])} />,
render: ({row}) => renderCell({row, columnName: name}),
};

return column;
Expand All @@ -83,9 +90,43 @@ interface QueryResultTableProps
export const QueryResultTable = (props: QueryResultTableProps) => {
const {columns, data, settings: propsSettings} = props;

const [activeCell, setActiveCell] = React.useState<{
row: KeyValueRow;
columnName: string;
} | null>(null);

const renderCell = React.useCallback(
({row, columnName}: RenderCellArgs) => {
const isActive = Boolean(
activeCell && activeCell.row === row && activeCell.columnName === columnName,
);

const value = row[columnName];

return (
<Cell
value={String(value)}
isActive={isActive}
onToggle={() => {
setActiveCell((prev) => {
if (prev && prev.row === row && prev.columnName === columnName) {
return null;
}

return {row, columnName};
});
}}
/>
);
},
[activeCell],
);

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

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
73 changes: 54 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, useVirtualElementRef} 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,16 @@ 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 [isTabletTooltipHovered, setIsTabletTooltipHovered] = React.useState(false);

const tabletTooltipAnchorRef = useVirtualElementRef({
rect: tabletTooltip?.position,
});

const [autoRefreshInterval] = useAutoRefreshInterval();

const {currentData, isFetching, error} = heatmapApi.useGetHeatmapTabletsInfoQuery(
Expand All @@ -44,13 +54,26 @@ 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 || isTabletTooltipHovered) {
return prev;
}

return null;
});
}, [isTabletTooltipHovered]);
Copy link

Choose a reason for hiding this comment

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

Bug: Stale closure causes tooltip to hide when hovered

The handleRequestHideTabletTooltip callback captures isTabletTooltipHovered from its closure, but when called from the setTimeout in HeatmapCanvas._onCanvasMouseLeave, it uses a stale value. When the user moves their mouse from the canvas to the tooltip, the setTimeout fires with the old function reference that has isTabletTooltipHovered = false captured, causing the tooltip to hide even though the user is hovering over it. The isTabletTooltipHovered check is effectively bypassed because the timeout callback was created before the hover state changed.

Additional Locations (1)

Fix in Cursor Fix in Web


const handleMetricChange = (value: string[]) => {
dispatch(
Expand All @@ -76,14 +99,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 Down Expand Up @@ -111,8 +127,8 @@ export const Heatmap = ({path, database, databaseFullPath}: HeatmapProps) => {
<HeatmapCanvas
tablets={sortedTablets}
parentRef={itemsContainer}
showTooltip={onShowTooltip}
hideTooltip={onHideTooltip}
onShowTabletTooltip={handleShowTabletTooltip}
onHideTabletTooltip={handleRequestHideTabletTooltip}
/>
</div>
);
Expand All @@ -128,6 +144,25 @@ export const Heatmap = ({path, database, databaseFullPath}: HeatmapProps) => {

return (
<div className={b()}>
{tabletTooltip ? (
<Popup
open
hasArrow
placement={['top', 'bottom', 'left', 'right']}
anchorRef={tabletTooltipAnchorRef}
onOutsideClick={handleHideTabletTooltip}
>
<div
onMouseEnter={() => setIsTabletTooltipHovered(true)}
onMouseLeave={() => {
setIsTabletTooltipHovered(false);
handleHideTabletTooltip();
}}
>
<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