Skip to content

Conversation

sareyu
Copy link
Collaborator

@sareyu sareyu commented Aug 20, 2025

CI Results

Test Status: ⚠️ FLAKY

📊 Full Report

Total Passed Failed Flaky Skipped
378 369 0 7 2
Test Changes Summary ⏭️2

⏭️ Skipped Tests (2)

  1. Scroll to row, get shareable link, navigate to URL and verify row is scrolled into view (tenant/diagnostics/tabs/queries.test.ts)
  2. Copy result button copies to clipboard (tenant/queryEditor/queryEditor.test.ts)

Bundle Size: ⚠️

Current: 0.00 KB | Main: 0.00 KB
Diff: 0.00 KB (N/A)

⚠️ Unable to calculate change.

ℹ️ CI Information
  • Test recordings for failed tests are available in the full report.
  • Bundle size is measured for the entire 'dist' directory.
  • 📊 indicates links to detailed reports.
  • 🔺 indicates increase, 🔽 decrease, and ✅ no change in bundle size.

Greptile Summary

Updated On: 2025-09-15 16:32:11 UTC

This PR migrates the Graph component from a custom implementation to the official @gravity-ui/graph library (version 1.1.4). The migration is part of a broader architectural strategy to leverage Gravity UI components throughout the codebase rather than maintaining custom implementations.

The core change replaces the existing YDBGraph component with a new GravityGraph component that uses web workers for layout calculations and provides better performance. The migration maintains backward compatibility by implementing a conditional rendering approach with a hardcoded true condition, allowing for easy rollback if issues arise during testing.

Key architectural changes include:

  • Component Structure: New block components (QueryBlockComponent, ResultBlockComponent, StageBlockComponent, ConnectionBlockComponent) that render different node types in query execution plans
  • Layout Engine: A complete tree layout algorithm (treeLayout.ts) that runs in a web worker to prevent UI blocking during graph calculations
  • Styling System: New SCSS styles (GravityGraph.scss) that use Gravity UI design tokens for consistent theming
  • Interactive Features: GraphControls component for zoom functionality and TooltipComponent for displaying statistical information
  • Data Transformation: Utility functions in utils.ts that bridge existing data structures with the new library's expected formats

The migration preserves all existing functionality while leveraging the official library's features like camera management, zoom controls, and built-in state handling. The implementation follows the project's established patterns with BEM naming conventions, TypeScript typing, and proper integration with the Gravity UI design system.

Important Files Changed

Changed Files
Filename Score Overview
src/containers/Tenant/Query/QueryResult/components/Graph/Graph.tsx 3/5 Implements conditional rendering to switch from YDBGraph to GravityGraph with hardcoded boolean
src/components/Graph/GravityGraph.scss 5/5 New stylesheet with comprehensive styling for graph elements using Gravity UI design tokens
package.json 4/5 Adds @gravity-ui/graph dependency version 1.1.4 for the migration
src/components/Graph/GraphControls.tsx 4/5 New component providing zoom controls for the graph with some performance considerations
src/components/Graph/BlockComponents/StageBlockComponent.tsx 4/5 Renders query execution stage blocks with operators, tables, and conditional tooltips
src/components/Graph/GravityGraph.tsx 4/5 Main graph component with web worker layout, theming support, and dynamic block rendering
src/components/Graph/colorsConfig.ts 5/5 Color configuration using CSS custom properties for consistent Gravity UI theming
src/components/Graph/utils.ts 4/5 Utility functions for data transformation and CSS property parsing with some type safety concerns
src/components/Graph/treeLayout.ts 4/5 Complete tree layout algorithm with web worker integration and Russian comments throughout
src/components/Graph/BlockComponents/ConnectionBlockComponent.tsx 4/5 Renders connection blocks with icons and conditional tooltips for query plan connections
src/containers/Tenant/Query/QueryResult/components/Graph/Graph.scss 5/5 Adds position: relative to canvas container for proper absolute positioning of zoom controls
src/components/Graph/types.ts 4/5 TypeScript interfaces extending TBlock for YDB-specific properties and graph structures
src/components/Graph/BlockComponents/ResultBlockComponent.tsx 4/5 Minimal component for result nodes with basic name display
src/components/Graph/NonSelectableConnection.tsx 4/5 Custom connection class that disables selection behavior for graph edges
src/components/Graph/BlockComponents/QueryBlockComponent.tsx 4/5 Minimal placeholder component for query nodes that may need enhancement
src/components/Graph/TooltipComponent.tsx 3/5 Popover component for displaying node statistics with performance concerns around memoization

Confidence score: 4/5

  • This PR is generally safe to merge with moderate attention to performance considerations
  • Score reflects comprehensive migration with good architectural patterns but missing performance optimizations and some hardcoded values
  • Pay close attention to GraphControls.tsx and TooltipComponent.tsx for missing memoization that could cause performance issues

Sequence Diagram

sequenceDiagram
    participant User
    participant Graph as Graph Component
    participant GravityGraph
    participant Worker as TreeLayout Worker
    participant GraphCanvas as @gravity-ui/graph Canvas
    participant TooltipComponent
    participant GraphControls

    User->>Graph: "Load query explain plan"
    Graph->>Graph: "Validate graph data (links, nodes)"
    alt Valid graph data
        Graph->>GravityGraph: "Render with data and theme"
        GravityGraph->>Worker: "Post message with nodes and links"
        Worker->>Worker: "Calculate tree layout and edges"
        Worker->>GravityGraph: "Return layout and edges"
        GravityGraph->>GraphCanvas: "Set entities (blocks, connections)"
        GravityGraph->>GraphCanvas: "Set colors from theme"
        GraphCanvas->>User: "Display interactive graph"
        
        par User interactions
            User->>GraphControls: "Click zoom in/out/reset"
            GraphControls->>GraphCanvas: "Update camera scale"
            GraphCanvas->>User: "Update graph view"
        and
            User->>GraphCanvas: "Click on block with stats"
            GraphCanvas->>TooltipComponent: "Show tooltip with block stats"
            TooltipComponent->>User: "Display stats in tabbed popup"
        end
    else Invalid graph data
        Graph->>User: "Show 'graph is not supported' message"
    end
Loading

Context used:

Style Guide - description of repository for agents (link)

@sareyu sareyu linked an issue Aug 20, 2025 that may be closed by this pull request
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

16 files reviewed, 8 comments

Edit Code Review Bot Settings | Greptile

Comment on lines +18 to +30
const onZoomInClick = () => {
const cameraScale = graph.cameraService.getCameraScale();
graph.zoom({scale: cameraScale * ZOOM_STEP});
};

const onZoomOutClick = () => {
const cameraScale = graph.cameraService.getCameraScale();
graph.zoom({scale: cameraScale / ZOOM_STEP});
};

const onResetZoomClick = () => {
graph.zoom({scale: 1});
};
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Event handlers should be memoized with useCallback to prevent unnecessary re-renders when this component is re-rendered

Suggested change
const onZoomInClick = () => {
const cameraScale = graph.cameraService.getCameraScale();
graph.zoom({scale: cameraScale * ZOOM_STEP});
};
const onZoomOutClick = () => {
const cameraScale = graph.cameraService.getCameraScale();
graph.zoom({scale: cameraScale / ZOOM_STEP});
};
const onResetZoomClick = () => {
graph.zoom({scale: 1});
};
import {useCallback} from 'react';
const onZoomInClick = useCallback(() => {
const cameraScale = graph.cameraService.getCameraScale();
graph.zoom({scale: cameraScale * ZOOM_STEP});
}, [graph]);
const onZoomOutClick = useCallback(() => {
const cameraScale = graph.cameraService.getCameraScale();
graph.zoom({scale: cameraScale / ZOOM_STEP});
}, [graph]);
const onResetZoomClick = useCallback(() => {
graph.zoom({scale: 1});
}, [graph]);

: block.name}
{block.tables ? (
<div>
<Text color="secondary">Tables: </Text>
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Consider using i18n for user-facing text instead of hardcoded 'Tables: '

Context Used: Style Guide - description of repository for agents (link)

Comment on lines +17 to +22
// Настройки отступов
this.options = {
horizontalSpacing: options.horizontalSpacing || 40, // расстояние между блоками по горизонтали
verticalSpacing: options.verticalSpacing || 20, // расстояние между уровнями
...options,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Russian comments violate the project's internationalization standards. All code comments should be in English.

Suggested change
// Настройки отступов
this.options = {
horizontalSpacing: options.horizontalSpacing || 40, // расстояние между блоками по горизонтали
verticalSpacing: options.verticalSpacing || 20, // расстояние между уровнями
...options,
};
// Spacing settings
this.options = {
horizontalSpacing: options.horizontalSpacing || 40, // horizontal distance between blocks
verticalSpacing: options.verticalSpacing || 20, // vertical distance between levels
...options,
};

Context Used: Style Guide - description of repository for agents (link)

Comment on lines +19 to +34
const getIcon = (name: string): IconData | undefined => {
switch (name) {
case 'Merge':
return DatabaseFill;
case 'UnionAll':
return GripHorizontal;
case 'HashShuffle':
return Shuffle;
case 'Map':
return MapPin;
case 'Broadcast':
return ArrowsExpandHorizontal;
default:
return undefined;
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Consider memoizing this function with useMemo or moving it outside the component to avoid recreation on every render

Context Used: Style Guide - description of repository for agents (link)

}
};

export const ConnectionBlockComponent = ({className, block}: Props) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Component should be memoized with React.memo since it receives object props that may not change between renders

Context Used: Style Guide - description of repository for agents (link)

Comment on lines +1 to +3
type Props = {
className: string;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Props interface is inconsistent with other block components. StageBlockComponent and ConnectionBlockComponent expect both block and className props, but this only accepts className.

Comment on lines +42 to +61
const useTooltipContent = (block: ExtendedTBlock) => {
const firstTab = block?.stats?.[0]?.group || '';
const [activeTab, setActiveTab] = useState(firstTab);

return (
<TabProvider value={activeTab} onUpdate={setActiveTab}>
<TabList className={b('tooltip-tabs')}>
{block?.stats?.map((item) => (
<Tab value={item.group} key={item.group}>
{item.group}
</Tab>
))}
</TabList>
{block?.stats?.map((item) => (
<TabPanel value={item.group} key={item.group}>
{item.stats?.map(getStatsContent)}
</TabPanel>
))}
</TabProvider>
);
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Performance issue: useTooltipContent creates new JSX on every render, causing unnecessary re-renders. Should use useMemo to memoize the JSX content.

Suggested change
const useTooltipContent = (block: ExtendedTBlock) => {
const firstTab = block?.stats?.[0]?.group || '';
const [activeTab, setActiveTab] = useState(firstTab);
return (
<TabProvider value={activeTab} onUpdate={setActiveTab}>
<TabList className={b('tooltip-tabs')}>
{block?.stats?.map((item) => (
<Tab value={item.group} key={item.group}>
{item.group}
</Tab>
))}
</TabList>
{block?.stats?.map((item) => (
<TabPanel value={item.group} key={item.group}>
{item.stats?.map(getStatsContent)}
</TabPanel>
))}
</TabProvider>
);
const useTooltipContent = (block: ExtendedTBlock) => {
const firstTab = block?.stats?.[0]?.group || '';
const [activeTab, setActiveTab] = useState(firstTab);
return useMemo(
() => (
<TabProvider value={activeTab} onUpdate={setActiveTab}>
<TabList className={b('tooltip-tabs')}>
{block?.stats?.map((item) => (
<Tab value={item.group} key={item.group}>
{item.group}
</Tab>
))}
</TabList>
{block?.stats?.map((item) => (
<TabPanel value={item.group} key={item.group}>
{item.stats?.map(getStatsContent)}
</TabPanel>
))}
</TabProvider>
),
[activeTab, block?.stats]
);

Comment on lines +64 to +80
export const TooltipComponent = ({block, children}: Props) => {
const content = useTooltipContent(block);

return (
<Popover
content={content}
hasArrow
trigger="click"
placement="right-start"
className="ydb-gravity-graph__tooltip-content"
disablePortal
strategy="fixed"
>
{children as React.ReactElement}
</Popover>
);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Component should be wrapped with React.memo for performance optimization, especially since it's likely to be rendered many times in graph nodes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Migrate from @gravity-ui/paranoid to @gravity-ui/graph
1 participant