Skip to content
Draft
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
54 changes: 54 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@gravity-ui/components": "^4.4.0",
"@gravity-ui/date-components": "^3.2.3",
"@gravity-ui/date-utils": "^2.5.6",
"@gravity-ui/graph": "^1.1.4",
"@gravity-ui/i18n": "^1.7.0",
"@gravity-ui/icons": "^2.13.0",
"@gravity-ui/illustrations": "^2.1.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {
ArrowsExpandHorizontal,
DatabaseFill,
GripHorizontal,
MapPin,
Shuffle,
} from '@gravity-ui/icons';
import {Icon} from '@gravity-ui/uikit';
import type {IconData} from '@gravity-ui/uikit';

import {TooltipComponent} from '../TooltipComponent';
import type {ExtendedTBlock} from '../types';

type Props = {
block: ExtendedTBlock;
className: string;
};

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;
}
};
Comment on lines +19 to +34
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)

const icon = getIcon(block.name);
const content = (
<div className={className}>
{icon && <Icon data={icon} />} {block.name}
</div>
);

if (!block.stats?.length) {
return content;
}

return <TooltipComponent block={block}>{content}</TooltipComponent>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type Props = {
className: string;
};
Comment on lines +1 to +3
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.


export const QueryBlockComponent = ({className}: Props) => {
return <div className={className} />;
};
10 changes: 10 additions & 0 deletions src/components/Graph/BlockComponents/ResultBlockComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type {TBlock} from '@gravity-ui/graph';

type Props = {
block: TBlock;
className: string;
};

export const ResultBlockComponent = ({className, block}: Props) => {
return <div className={className}>{block.name}</div>;
};
31 changes: 31 additions & 0 deletions src/components/Graph/BlockComponents/StageBlockComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {Text} from '@gravity-ui/uikit';

import {TooltipComponent} from '../TooltipComponent';
import type {ExtendedTBlock} from '../types';

type Props = {
block: ExtendedTBlock;
className: string;
};

export const StageBlockComponent = ({className, block}: Props) => {
const content = (
<div className={className}>
{block.operators
? block.operators.map((item) => <div key={item}>{item}</div>)
: 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)

{block.tables.join(', ')}
</div>
) : null}
</div>
);

if (!block.stats?.length) {
return content;
}

return <TooltipComponent block={block}>{content}</TooltipComponent>;
};
45 changes: 45 additions & 0 deletions src/components/Graph/GraphControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type {Graph} from '@gravity-ui/graph';
import {Button, Icon} from '@gravity-ui/uikit';

import {cn} from '../../utils/cn';

import MagnifierMinusIcon from '@gravity-ui/icons/svgs/magnifier-minus.svg';
import MagnifierPlusIcon from '@gravity-ui/icons/svgs/magnifier-plus.svg';

const b = cn('ydb-gravity-graph');

const ZOOM_STEP = 1.25;

interface Props {
graph: Graph;
}

export const GraphControls = ({graph}: Props) => {
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});
};
Comment on lines +18 to +30
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]);


return (
<div className={b('zoom-controls')}>
<Button view="raised" onClick={onZoomInClick}>
<Icon data={MagnifierPlusIcon} size={16} />
</Button>
<Button view="raised" onClick={onZoomOutClick}>
<Icon data={MagnifierMinusIcon} size={16} />
</Button>
<Button view="raised" onClick={onResetZoomClick}>
1:1
</Button>
</div>
);
};
121 changes: 121 additions & 0 deletions src/components/Graph/GravityGraph.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
.ydb-gravity-graph {
&__block {
cursor: auto;

border: none;
background: none;
}

&__block-content {
width: 100%;
padding: 8px 12px;

font-family: var(--g-font-family);
font-size: var(--g-text-body-short-font-size);
line-height: var(--g-text-body-short-line-height);

border: 1px solid var(--g-color-line-generic);
background: var(--g-color-base-float);
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.3);

&[aria-haspopup='dialog'] {
cursor: pointer;
}
}

&__block-id {
position: absolute;
top: 4px;
right: 4px;

font-size: 10px;
line-height: 1;

color: var(--g-color-text-secondary);
}

&__block-content.query {
height: 100%;

border-radius: 50%;
}

&__block-content.result {
display: flex;
justify-content: center;
align-items: center;
}

&__block-content.stage {
border-radius: 6px;
}

&__block-content.connection {
display: flex;
align-items: center;
gap: 4px;

color: var(--g-color-text-info-heavy);
border: 1px solid var(--g-color-line-info);
border-radius: 6px;
background: var(--g-color-base-info-light);
box-shadow: none;
}

&__tooltip-content {
width: 300px;
padding: 0 8px 8px;

font-family: var(--g-font-family);
font-size: var(--g-text-body-short-font-size);
line-height: var(--g-text-body-short-line-height);
}

&__tooltip-tabs {
margin-bottom: 8px;
}

&__tooltip-stat-row {
display: grid;
grid-template-columns: 100px auto;
gap: 8px;

margin: 4px 0 0;

span {
overflow: hidden;

text-overflow: ellipsis;
}
span:nth-child(1) {
color: var(--g-color-text-secondary);
}
span:nth-child(2) {
word-wrap: break-word;
}
}

&__tooltip-stat-group {
margin-top: 8px;
}

&__tooltip-stat-group + &__tooltip-stat-group {
padding-top: 8px;

border-top: 1px dotted var(--g-color-line-generic);
}

&__tooltip-stat-group-name {
font-weight: bold;
}

&__zoom-controls {
position: absolute;
z-index: 999;
top: 8px;
right: 50px;

display: flex;
gap: 2px;
}
}
Loading
Loading