ydb-embedded-ui 20.5.0 → 21.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/dist/components/AutoRefreshControl/i18n/index.d.ts +1 -1
- package/dist/components/FullNodeViewer/i18n/index.d.ts +1 -1
- package/dist/components/Graph/BlockComponents/ConnectionBlockComponent.d.ts +7 -0
- package/dist/components/Graph/BlockComponents/ConnectionBlockComponent.js +30 -0
- package/dist/components/Graph/BlockComponents/ConnectionBlockComponent.js.map +1 -0
- package/dist/components/Graph/BlockComponents/QueryBlockComponent.d.ts +5 -0
- package/dist/components/Graph/BlockComponents/QueryBlockComponent.js +5 -0
- package/dist/components/Graph/BlockComponents/QueryBlockComponent.js.map +1 -0
- package/dist/components/Graph/BlockComponents/ResultBlockComponent.d.ts +7 -0
- package/dist/components/Graph/BlockComponents/ResultBlockComponent.js +5 -0
- package/dist/components/Graph/BlockComponents/ResultBlockComponent.js.map +1 -0
- package/dist/components/Graph/BlockComponents/StageBlockComponent.d.ts +7 -0
- package/dist/components/Graph/BlockComponents/StageBlockComponent.js +17 -0
- package/dist/components/Graph/BlockComponents/StageBlockComponent.js.map +1 -0
- package/dist/components/Graph/FullViewportBackground.d.ts +2 -0
- package/dist/components/Graph/FullViewportBackground.js +67 -0
- package/dist/components/Graph/FullViewportBackground.js.map +1 -0
- package/dist/components/Graph/GraphControls.d.ts +6 -0
- package/dist/components/Graph/GraphControls.js +24 -0
- package/dist/components/Graph/GraphControls.js.map +1 -0
- package/dist/components/Graph/GravityGraph.d.ts +9 -0
- package/dist/components/Graph/GravityGraph.js +122 -0
- package/dist/components/Graph/GravityGraph.js.map +1 -0
- package/dist/components/Graph/GravityGraph.scss +131 -0
- package/dist/components/Graph/NonSelectableConnection.d.ts +9 -0
- package/dist/components/Graph/NonSelectableConnection.js +16 -0
- package/dist/components/Graph/NonSelectableConnection.js.map +1 -0
- package/dist/components/Graph/TooltipComponent.d.ts +8 -0
- package/dist/components/Graph/TooltipComponent.js +63 -0
- package/dist/components/Graph/TooltipComponent.js.map +1 -0
- package/dist/components/Graph/colorsConfig.d.ts +19 -0
- package/dist/components/Graph/colorsConfig.js +20 -0
- package/dist/components/Graph/colorsConfig.js.map +1 -0
- package/dist/components/Graph/i18n/en.json +5 -0
- package/dist/components/Graph/i18n/index.d.ts +2 -0
- package/dist/components/Graph/i18n/index.js +5 -0
- package/dist/components/Graph/i18n/index.js.map +1 -0
- package/dist/components/Graph/runTreeLayout.d.ts +12 -0
- package/dist/components/Graph/runTreeLayout.js +75 -0
- package/dist/components/Graph/runTreeLayout.js.map +1 -0
- package/dist/components/Graph/treeLayout.d.ts +2 -0
- package/dist/components/Graph/treeLayout.js +284 -0
- package/dist/components/Graph/treeLayout.js.map +1 -0
- package/dist/components/Graph/types.d.ts +52 -0
- package/dist/components/Graph/types.js +2 -0
- package/dist/components/Graph/types.js.map +1 -0
- package/dist/components/Graph/utils.d.ts +6 -0
- package/dist/components/Graph/utils.js +88 -0
- package/dist/components/Graph/utils.js.map +1 -0
- package/dist/components/PDiskInfo/i18n/index.d.ts +1 -1
- package/dist/components/VDiskInfo/i18n/index.d.ts +1 -1
- package/dist/containers/Clusters/constants.d.ts +1 -1
- package/dist/containers/Nodes/i18n/index.d.ts +1 -1
- package/dist/containers/Operations/constants.d.ts +1 -1
- package/dist/containers/Tenant/Diagnostics/Overview/TransferInfo/Credentials.d.ts +1 -1
- package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/useTenantStorageNewData.d.ts +2 -2
- package/dist/containers/Tenant/Query/QueryResult/components/Graph/Graph.js +9 -4
- package/dist/containers/Tenant/Query/QueryResult/components/Graph/Graph.js.map +1 -1
- package/dist/containers/Tenant/Query/QueryResult/components/Graph/Graph.scss +2 -0
- package/dist/containers/Tenant/TenantNavigation/useTenantNavigation.d.ts +1 -1
- package/dist/graphLayout.worker.d.ts +1 -0
- package/dist/graphLayout.worker.js +17 -0
- package/dist/graphLayout.worker.js.map +1 -0
- package/dist/lib.d.ts +1 -1
- package/dist/lib.js.map +1 -1
- package/dist/store/reducers/query/parsers/preparePlanData.js.map +1 -1
- package/dist/store/reducers/query/types.d.ts +38 -1
- package/dist/store/reducers/settings/utils.d.ts +2 -2
- package/dist/uiFactory/types.d.ts +3 -0
- package/dist/utils/disks/helpers.d.ts +2 -2
- package/dist/utils/hooks/useChangedQuerySettings.d.ts +2 -2
- package/dist/utils/hooks/useLastQueryExecutionSettings.d.ts +2 -2
- package/dist/utils/hooks/useQueryExecutionSettings.d.ts +2 -2
- package/dist/utils/prepareQueryExplain.d.ts +1 -2
- package/dist/utils/prepareQueryExplain.js +10 -2
- package/dist/utils/prepareQueryExplain.js.map +1 -1
- package/dist/utils/query.d.ts +1 -1
- package/package.json +2 -2
- package/src/components/Graph/BlockComponents/ConnectionBlockComponent.tsx +49 -0
- package/src/components/Graph/BlockComponents/QueryBlockComponent.tsx +7 -0
- package/src/components/Graph/BlockComponents/ResultBlockComponent.tsx +10 -0
- package/src/components/Graph/BlockComponents/StageBlockComponent.tsx +35 -0
- package/src/components/Graph/FullViewportBackground.ts +122 -0
- package/src/components/Graph/GraphControls.tsx +49 -0
- package/src/components/Graph/GravityGraph.scss +131 -0
- package/src/components/Graph/GravityGraph.tsx +174 -0
- package/src/components/Graph/NonSelectableConnection.tsx +14 -0
- package/src/components/Graph/TooltipComponent.tsx +114 -0
- package/src/components/Graph/colorsConfig.ts +21 -0
- package/src/components/Graph/i18n/en.json +5 -0
- package/src/components/Graph/i18n/index.ts +7 -0
- package/src/components/Graph/runTreeLayout.ts +105 -0
- package/src/components/Graph/treeLayout.ts +378 -0
- package/src/components/Graph/types.ts +56 -0
- package/src/components/Graph/utils.ts +112 -0
- package/src/containers/Tenant/Query/QueryResult/components/Graph/Graph.scss +2 -0
- package/src/containers/Tenant/Query/QueryResult/components/Graph/Graph.tsx +13 -7
- package/src/graphLayout.worker.ts +21 -0
- package/src/lib.ts +1 -0
- package/src/store/reducers/query/parsers/preparePlanData.ts +1 -3
- package/src/store/reducers/query/types.ts +42 -2
- package/src/uiFactory/types.ts +4 -0
- package/src/utils/prepareQueryExplain.ts +13 -5
- package/dist/components/Graph/Graph.d.ts +0 -12
- package/dist/components/Graph/Graph.js +0 -44
- package/dist/components/Graph/Graph.js.map +0 -1
- package/dist/components/Graph/colors.d.ts +0 -10
- package/dist/components/Graph/colors.js +0 -53
- package/dist/components/Graph/colors.js.map +0 -1
- package/src/components/Graph/Graph.tsx +0 -70
- package/src/components/Graph/colors.ts +0 -75
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {Text} from '@gravity-ui/uikit';
|
|
2
|
+
|
|
3
|
+
import {cn} from '../../../utils/cn';
|
|
4
|
+
import {TooltipComponent} from '../TooltipComponent';
|
|
5
|
+
import i18n from '../i18n';
|
|
6
|
+
import type {ExtendedTBlock} from '../types';
|
|
7
|
+
|
|
8
|
+
const b = cn('ydb-gravity-graph');
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
block: ExtendedTBlock;
|
|
12
|
+
className: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const StageBlockComponent = ({className, block}: Props) => {
|
|
16
|
+
const content = (
|
|
17
|
+
<div className={className}>
|
|
18
|
+
{block.operators?.length
|
|
19
|
+
? block.operators.map((item) => <div key={item}>{item}</div>)
|
|
20
|
+
: block.name}
|
|
21
|
+
{block.tables?.map((table, index) => (
|
|
22
|
+
<div key={`${table}-${index}`} className={b('stage-table-row')} title={table}>
|
|
23
|
+
{index === 0 ? <Text color="secondary">{i18n('label_tables')}: </Text> : null}
|
|
24
|
+
{table}
|
|
25
|
+
</div>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (!block.stats?.length) {
|
|
31
|
+
return content;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return <TooltipComponent block={block}>{content}</TooltipComponent>;
|
|
35
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {Component} from '@gravity-ui/graph';
|
|
2
|
+
|
|
3
|
+
interface FullViewportBackgroundContext extends Record<string, unknown> {
|
|
4
|
+
camera: {
|
|
5
|
+
getCameraState(): {
|
|
6
|
+
relativeX: number;
|
|
7
|
+
relativeY: number;
|
|
8
|
+
relativeWidth: number;
|
|
9
|
+
relativeHeight: number;
|
|
10
|
+
scale: number;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
colors: {
|
|
14
|
+
canvas: {
|
|
15
|
+
dots: string;
|
|
16
|
+
layerBackground: string;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
constants: {
|
|
20
|
+
block: {
|
|
21
|
+
SCALES: number[];
|
|
22
|
+
};
|
|
23
|
+
system: {
|
|
24
|
+
GRID_SIZE: number;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
ctx: CanvasRenderingContext2D;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const VIEWPORT_PADDING = 10;
|
|
31
|
+
const DOT_SIZE = 2;
|
|
32
|
+
const LARGE_DOT_MULTIPLIER = 3;
|
|
33
|
+
const PATTERN_GRID_SIZE = 5;
|
|
34
|
+
|
|
35
|
+
class FullViewportBackgroundComponent extends Component {
|
|
36
|
+
private patterns?: {normal: CanvasPattern; simple: CanvasPattern};
|
|
37
|
+
private patternsColor?: string;
|
|
38
|
+
private patternsGridSize?: number;
|
|
39
|
+
|
|
40
|
+
protected render() {
|
|
41
|
+
const {camera, colors, constants, ctx} = this.context as FullViewportBackgroundContext;
|
|
42
|
+
const cameraState = camera.getCameraState();
|
|
43
|
+
const x = -cameraState.relativeX - VIEWPORT_PADDING;
|
|
44
|
+
const y = -cameraState.relativeY - VIEWPORT_PADDING;
|
|
45
|
+
const width = cameraState.relativeWidth + VIEWPORT_PADDING * 2;
|
|
46
|
+
const height = cameraState.relativeHeight + VIEWPORT_PADDING * 2;
|
|
47
|
+
|
|
48
|
+
ctx.fillStyle = colors.canvas.layerBackground;
|
|
49
|
+
ctx.fillRect(x, y, width, height);
|
|
50
|
+
|
|
51
|
+
if (cameraState.scale < constants.block.SCALES[0]) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.preparePatterns(colors.canvas.dots, constants.system.GRID_SIZE);
|
|
56
|
+
if (!this.patterns) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
ctx.fillStyle =
|
|
61
|
+
cameraState.scale > constants.block.SCALES[2]
|
|
62
|
+
? this.patterns.normal
|
|
63
|
+
: this.patterns.simple;
|
|
64
|
+
ctx.fillRect(x, y, width, height);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private preparePatterns(color: string, gridSize: number) {
|
|
68
|
+
if (this.patterns && this.patternsColor === color && this.patternsGridSize === gridSize) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const normal = this.createPattern(color, gridSize, false);
|
|
73
|
+
const simple = this.createPattern(color, gridSize, true);
|
|
74
|
+
|
|
75
|
+
if (normal && simple) {
|
|
76
|
+
this.patterns = {normal, simple};
|
|
77
|
+
this.patternsColor = color;
|
|
78
|
+
this.patternsGridSize = gridSize;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private createPattern(color: string, gridSize: number, simple: boolean) {
|
|
83
|
+
const {ctx} = this.context as FullViewportBackgroundContext;
|
|
84
|
+
const patternCanvas = document.createElement('canvas');
|
|
85
|
+
const patternSize = PATTERN_GRID_SIZE * gridSize;
|
|
86
|
+
patternCanvas.width = patternSize;
|
|
87
|
+
patternCanvas.height = patternSize;
|
|
88
|
+
|
|
89
|
+
const patternContext = patternCanvas.getContext('2d');
|
|
90
|
+
if (!patternContext) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
patternContext.fillStyle = color;
|
|
95
|
+
|
|
96
|
+
for (let row = -1; row <= PATTERN_GRID_SIZE; row += 1) {
|
|
97
|
+
for (let column = -1; column <= PATTERN_GRID_SIZE; column += 1) {
|
|
98
|
+
if (row % PATTERN_GRID_SIZE === 0 && column % PATTERN_GRID_SIZE === 0) {
|
|
99
|
+
const largeDotSize = DOT_SIZE * LARGE_DOT_MULTIPLIER;
|
|
100
|
+
patternContext.fillRect(
|
|
101
|
+
row * gridSize - largeDotSize / 2,
|
|
102
|
+
column * gridSize - largeDotSize / 2,
|
|
103
|
+
largeDotSize,
|
|
104
|
+
largeDotSize,
|
|
105
|
+
);
|
|
106
|
+
} else if (!simple) {
|
|
107
|
+
patternContext.fillRect(
|
|
108
|
+
row * gridSize - DOT_SIZE / 2,
|
|
109
|
+
column * gridSize - DOT_SIZE / 2,
|
|
110
|
+
DOT_SIZE,
|
|
111
|
+
DOT_SIZE,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return ctx.createPattern(patternCanvas, 'repeat');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const FullViewportBackground =
|
|
122
|
+
FullViewportBackgroundComponent as unknown as typeof Component;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import type {Graph} from '@gravity-ui/graph';
|
|
4
|
+
import {Button, Icon} from '@gravity-ui/uikit';
|
|
5
|
+
|
|
6
|
+
import {cn} from '../../utils/cn';
|
|
7
|
+
|
|
8
|
+
import i18n from './i18n';
|
|
9
|
+
|
|
10
|
+
import MagnifierMinusIcon from '@gravity-ui/icons/svgs/magnifier-minus.svg';
|
|
11
|
+
import MagnifierPlusIcon from '@gravity-ui/icons/svgs/magnifier-plus.svg';
|
|
12
|
+
|
|
13
|
+
const b = cn('ydb-gravity-graph');
|
|
14
|
+
|
|
15
|
+
const ZOOM_STEP = 1.25;
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
graph: Graph;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const GraphControls = ({graph}: Props) => {
|
|
22
|
+
const onZoomInClick = React.useCallback(() => {
|
|
23
|
+
const cameraScale = graph.cameraService.getCameraScale();
|
|
24
|
+
graph.zoom({scale: cameraScale * ZOOM_STEP});
|
|
25
|
+
}, [graph]);
|
|
26
|
+
|
|
27
|
+
const onZoomOutClick = React.useCallback(() => {
|
|
28
|
+
const cameraScale = graph.cameraService.getCameraScale();
|
|
29
|
+
graph.zoom({scale: cameraScale / ZOOM_STEP});
|
|
30
|
+
}, [graph]);
|
|
31
|
+
|
|
32
|
+
const onResetZoomClick = React.useCallback(() => {
|
|
33
|
+
graph.zoom({scale: 1});
|
|
34
|
+
}, [graph]);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className={b('zoom-controls')}>
|
|
38
|
+
<Button view="raised" onClick={onZoomInClick} aria-label={i18n('action_zoom-in')}>
|
|
39
|
+
<Icon data={MagnifierPlusIcon} size={16} />
|
|
40
|
+
</Button>
|
|
41
|
+
<Button view="raised" onClick={onZoomOutClick} aria-label={i18n('action_zoom-out')}>
|
|
42
|
+
<Icon data={MagnifierMinusIcon} size={16} />
|
|
43
|
+
</Button>
|
|
44
|
+
<Button view="raised" onClick={onResetZoomClick}>
|
|
45
|
+
1:1
|
|
46
|
+
</Button>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
.ydb-gravity-graph {
|
|
2
|
+
&__block {
|
|
3
|
+
cursor: auto;
|
|
4
|
+
|
|
5
|
+
border: none;
|
|
6
|
+
background: none;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
&__block-content {
|
|
10
|
+
width: 100%;
|
|
11
|
+
padding: 8px 12px;
|
|
12
|
+
|
|
13
|
+
font-family: var(--g-font-family);
|
|
14
|
+
font-size: var(--g-text-body-short-font-size);
|
|
15
|
+
line-height: var(--g-text-body-short-line-height);
|
|
16
|
+
|
|
17
|
+
border: 1px solid var(--g-color-line-generic);
|
|
18
|
+
background: var(--g-color-base-float);
|
|
19
|
+
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.3);
|
|
20
|
+
|
|
21
|
+
&[aria-haspopup='dialog'] {
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&__block-id {
|
|
27
|
+
position: absolute;
|
|
28
|
+
top: 4px;
|
|
29
|
+
right: 4px;
|
|
30
|
+
|
|
31
|
+
font-size: 10px;
|
|
32
|
+
line-height: 1;
|
|
33
|
+
|
|
34
|
+
color: var(--g-color-text-secondary);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
&__block-content.query {
|
|
38
|
+
height: 100%;
|
|
39
|
+
|
|
40
|
+
border-radius: 50%;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
&__block-content.result {
|
|
44
|
+
display: flex;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
align-items: center;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
&__block-content.stage {
|
|
50
|
+
border-radius: 6px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&__stage-table-row {
|
|
54
|
+
overflow: hidden;
|
|
55
|
+
|
|
56
|
+
white-space: nowrap;
|
|
57
|
+
text-overflow: ellipsis;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
&__block-content.connection {
|
|
61
|
+
display: flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
gap: 4px;
|
|
64
|
+
|
|
65
|
+
color: var(--g-color-text-info-heavy);
|
|
66
|
+
border: 1px solid var(--g-color-line-info);
|
|
67
|
+
border-radius: 6px;
|
|
68
|
+
background: var(--g-color-base-info-light);
|
|
69
|
+
box-shadow: none;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
&__tooltip-content {
|
|
73
|
+
overflow: auto;
|
|
74
|
+
|
|
75
|
+
width: 300px;
|
|
76
|
+
max-height: 90vh;
|
|
77
|
+
padding: 0 8px 8px;
|
|
78
|
+
|
|
79
|
+
font-family: var(--g-font-family);
|
|
80
|
+
font-size: var(--g-text-body-short-font-size);
|
|
81
|
+
line-height: var(--g-text-body-short-line-height);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
&__tooltip-tabs {
|
|
85
|
+
margin-bottom: 8px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
&__tooltip-stat-row {
|
|
89
|
+
display: grid;
|
|
90
|
+
grid-template-columns: 100px auto;
|
|
91
|
+
gap: 8px;
|
|
92
|
+
|
|
93
|
+
margin: 4px 0 0;
|
|
94
|
+
|
|
95
|
+
span {
|
|
96
|
+
overflow: hidden;
|
|
97
|
+
|
|
98
|
+
text-overflow: ellipsis;
|
|
99
|
+
}
|
|
100
|
+
span:nth-child(1) {
|
|
101
|
+
color: var(--g-color-text-secondary);
|
|
102
|
+
}
|
|
103
|
+
span:nth-child(2) {
|
|
104
|
+
word-wrap: break-word;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
&__tooltip-stat-group {
|
|
109
|
+
margin-top: 8px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
&__tooltip-stat-group + &__tooltip-stat-group {
|
|
113
|
+
padding-top: 8px;
|
|
114
|
+
|
|
115
|
+
border-top: 1px dotted var(--g-color-line-generic);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
&__tooltip-stat-group-name {
|
|
119
|
+
font-weight: bold;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
&__zoom-controls {
|
|
123
|
+
position: absolute;
|
|
124
|
+
z-index: 999;
|
|
125
|
+
top: 8px;
|
|
126
|
+
right: 50px;
|
|
127
|
+
|
|
128
|
+
display: flex;
|
|
129
|
+
gap: 2px;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import {GraphState} from '@gravity-ui/graph';
|
|
4
|
+
import type {TBlockId} from '@gravity-ui/graph';
|
|
5
|
+
import type {HookGraphParams} from '@gravity-ui/graph/react';
|
|
6
|
+
import {GraphBlock, GraphCanvas, useGraph, useGraphEvent} from '@gravity-ui/graph/react';
|
|
7
|
+
|
|
8
|
+
import {uiFactory} from '../../uiFactory/uiFactory';
|
|
9
|
+
import {cn} from '../../utils/cn';
|
|
10
|
+
|
|
11
|
+
const b = cn('ydb-gravity-graph');
|
|
12
|
+
const GRAPH_FIT_PADDING = 40;
|
|
13
|
+
|
|
14
|
+
import {ConnectionBlockComponent} from './BlockComponents/ConnectionBlockComponent';
|
|
15
|
+
import {QueryBlockComponent} from './BlockComponents/QueryBlockComponent';
|
|
16
|
+
import {ResultBlockComponent} from './BlockComponents/ResultBlockComponent';
|
|
17
|
+
import {StageBlockComponent} from './BlockComponents/StageBlockComponent';
|
|
18
|
+
import {FullViewportBackground} from './FullViewportBackground';
|
|
19
|
+
import {GraphControls} from './GraphControls';
|
|
20
|
+
import {NonSelectableConnection} from './NonSelectableConnection';
|
|
21
|
+
import {graphColorsConfig} from './colorsConfig';
|
|
22
|
+
import {runTreeLayout} from './runTreeLayout';
|
|
23
|
+
import type {Data} from './types';
|
|
24
|
+
import {parseCustomPropertyValue} from './utils';
|
|
25
|
+
|
|
26
|
+
import './GravityGraph.scss';
|
|
27
|
+
|
|
28
|
+
interface Props<T> {
|
|
29
|
+
data: Data<T>;
|
|
30
|
+
onError: (error: Error) => void;
|
|
31
|
+
theme?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const config: HookGraphParams = {
|
|
35
|
+
settings: {
|
|
36
|
+
background: FullViewportBackground,
|
|
37
|
+
connection: NonSelectableConnection,
|
|
38
|
+
showConnectionArrows: false,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const createGraphLayoutWorker = () => {
|
|
43
|
+
if (uiFactory.createGraphLayoutWorker) {
|
|
44
|
+
return uiFactory.createGraphLayoutWorker();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return new Worker(new URL('../../graphLayout.worker', import.meta.url));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const renderBlockFn = (graph: any, block: any) => {
|
|
51
|
+
const map: Record<string, React.ComponentType<any>> = {
|
|
52
|
+
query: QueryBlockComponent,
|
|
53
|
+
result: ResultBlockComponent,
|
|
54
|
+
stage: StageBlockComponent,
|
|
55
|
+
connection: ConnectionBlockComponent,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const Component = map[block.is as keyof typeof map];
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<GraphBlock graph={graph} block={block} className={b('block')}>
|
|
62
|
+
{Component ? (
|
|
63
|
+
<React.Fragment>
|
|
64
|
+
<Component
|
|
65
|
+
graph={graph}
|
|
66
|
+
block={block}
|
|
67
|
+
className={b('block-content', block.is)}
|
|
68
|
+
/>
|
|
69
|
+
{block.planNodeId !== undefined && block.is !== 'result' && (
|
|
70
|
+
<div className={b('block-id')}>#{block.planNodeId}</div>
|
|
71
|
+
)}
|
|
72
|
+
</React.Fragment>
|
|
73
|
+
) : (
|
|
74
|
+
block.id
|
|
75
|
+
)}
|
|
76
|
+
</GraphBlock>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export function GravityGraph<T>({data, onError, theme}: Props<T>) {
|
|
81
|
+
const {graph, start} = useGraph(config);
|
|
82
|
+
const cameraSizeRef = React.useRef({width: 0, height: 0});
|
|
83
|
+
const graphBlockIdsRef = React.useRef<TBlockId[]>([]);
|
|
84
|
+
const fitFrameRef = React.useRef<number>();
|
|
85
|
+
const fitGraphToViewport = React.useCallback(() => {
|
|
86
|
+
const {width, height} = graph.cameraService.getCameraState();
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
graph.state !== GraphState.READY ||
|
|
90
|
+
width <= 0 ||
|
|
91
|
+
height <= 0 ||
|
|
92
|
+
graphBlockIdsRef.current.length === 0
|
|
93
|
+
) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
graph.zoomTo(graphBlockIdsRef.current, {padding: GRAPH_FIT_PADDING});
|
|
98
|
+
}, [graph]);
|
|
99
|
+
const scheduleGraphFit = React.useCallback(() => {
|
|
100
|
+
if (fitFrameRef.current !== undefined) {
|
|
101
|
+
globalThis.cancelAnimationFrame(fitFrameRef.current);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// camera-change is emitted before its new size is committed.
|
|
105
|
+
fitFrameRef.current = globalThis.requestAnimationFrame(() => {
|
|
106
|
+
fitFrameRef.current = undefined;
|
|
107
|
+
fitGraphToViewport();
|
|
108
|
+
});
|
|
109
|
+
}, [fitGraphToViewport]);
|
|
110
|
+
|
|
111
|
+
React.useEffect(() => {
|
|
112
|
+
return () => {
|
|
113
|
+
if (fitFrameRef.current !== undefined) {
|
|
114
|
+
globalThis.cancelAnimationFrame(fitFrameRef.current);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
React.useEffect(() => {
|
|
120
|
+
return runTreeLayout({
|
|
121
|
+
request: {
|
|
122
|
+
nodes: data.nodes,
|
|
123
|
+
links: data.links,
|
|
124
|
+
},
|
|
125
|
+
createWorker: createGraphLayoutWorker,
|
|
126
|
+
onResult: ({layout, edges}) => {
|
|
127
|
+
graphBlockIdsRef.current = layout.map(({id}) => id);
|
|
128
|
+
graph.setEntities({
|
|
129
|
+
blocks: layout,
|
|
130
|
+
connections: edges,
|
|
131
|
+
});
|
|
132
|
+
scheduleGraphFit();
|
|
133
|
+
},
|
|
134
|
+
onError,
|
|
135
|
+
});
|
|
136
|
+
}, [data.nodes, data.links, graph, onError, scheduleGraphFit]);
|
|
137
|
+
|
|
138
|
+
React.useEffect(() => {
|
|
139
|
+
graph.setColors(parseCustomPropertyValue(graphColorsConfig));
|
|
140
|
+
}, [graph, theme]);
|
|
141
|
+
|
|
142
|
+
useGraphEvent(graph, 'state-change', ({state}) => {
|
|
143
|
+
if (state === GraphState.ATTACHED) {
|
|
144
|
+
graph.cameraService.set({
|
|
145
|
+
scale: 1,
|
|
146
|
+
scaleMax: 1.5,
|
|
147
|
+
});
|
|
148
|
+
graph.setConstants({
|
|
149
|
+
block: {
|
|
150
|
+
SCALES: [0.125, 0.225, 0.5], // Detailed view stays until zoom = 0.5
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
start();
|
|
154
|
+
} else if (state === GraphState.READY) {
|
|
155
|
+
scheduleGraphFit();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
useGraphEvent(graph, 'camera-change', ({width, height}) => {
|
|
160
|
+
const previousSize = cameraSizeRef.current;
|
|
161
|
+
cameraSizeRef.current = {width, height};
|
|
162
|
+
|
|
163
|
+
if (previousSize.width !== width || previousSize.height !== height) {
|
|
164
|
+
scheduleGraphFit();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<React.Fragment>
|
|
170
|
+
<GraphCanvas graph={graph} renderBlock={renderBlockFn} />
|
|
171
|
+
<GraphControls graph={graph} />
|
|
172
|
+
</React.Fragment>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {MultipointConnection} from '@gravity-ui/graph/react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Кастомный класс соединения, который отключает визуальное выделение
|
|
5
|
+
* Наследуется от MultipointConnection и переопределяет поведение
|
|
6
|
+
*/
|
|
7
|
+
export class NonSelectableConnection extends MultipointConnection {
|
|
8
|
+
override cursor: 'pointer' = 'pointer';
|
|
9
|
+
|
|
10
|
+
// Переопределяем метод для предотвращения выделения при клике
|
|
11
|
+
protected override handleEvent(event: Event) {
|
|
12
|
+
event.stopPropagation();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import {Popover, Tab, TabList, TabPanel, TabProvider} from '@gravity-ui/uikit';
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
TopologyNodeDataStatsItem,
|
|
7
|
+
TopologyNodeDataStatsSection,
|
|
8
|
+
} from '../../store/reducers/query/types';
|
|
9
|
+
import {cn} from '../../utils/cn';
|
|
10
|
+
|
|
11
|
+
import type {ExtendedTBlock} from './types';
|
|
12
|
+
|
|
13
|
+
const b = cn('ydb-gravity-graph');
|
|
14
|
+
type Props = {
|
|
15
|
+
block: ExtendedTBlock;
|
|
16
|
+
children: React.ReactElement<React.HTMLAttributes<HTMLElement>>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const getStatsContent = (stat: TopologyNodeDataStatsItem | TopologyNodeDataStatsSection) => {
|
|
20
|
+
if ('value' in stat) {
|
|
21
|
+
return (
|
|
22
|
+
<p className={b('tooltip-stat-row')} key={stat.name}>
|
|
23
|
+
<span>{stat.name}:</span>
|
|
24
|
+
<span>{stat.value}</span>
|
|
25
|
+
</p>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<section className={b('tooltip-stat-group')} key={stat.name}>
|
|
31
|
+
<div className={b('tooltip-stat-group-name')}>{stat.name}:</div>
|
|
32
|
+
{stat.items?.map(({name, value}: TopologyNodeDataStatsItem) => (
|
|
33
|
+
<p className={b('tooltip-stat-row')} key={name}>
|
|
34
|
+
<span>{name}:</span>
|
|
35
|
+
<span>{value}</span>
|
|
36
|
+
</p>
|
|
37
|
+
))}
|
|
38
|
+
</section>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const useTooltipContent = (block: ExtendedTBlock) => {
|
|
43
|
+
const firstTab = block?.stats?.[0]?.group || '';
|
|
44
|
+
const [activeTab, setActiveTab] = React.useState(firstTab);
|
|
45
|
+
const validatedActiveTab = block.stats?.some(({group}) => group === activeTab)
|
|
46
|
+
? activeTab
|
|
47
|
+
: firstTab;
|
|
48
|
+
|
|
49
|
+
React.useEffect(() => {
|
|
50
|
+
if (activeTab !== validatedActiveTab) {
|
|
51
|
+
setActiveTab(validatedActiveTab);
|
|
52
|
+
}
|
|
53
|
+
}, [activeTab, validatedActiveTab]);
|
|
54
|
+
|
|
55
|
+
return React.useMemo(
|
|
56
|
+
() => (
|
|
57
|
+
<TabProvider value={validatedActiveTab} onUpdate={setActiveTab}>
|
|
58
|
+
<TabList className={b('tooltip-tabs')}>
|
|
59
|
+
{block?.stats?.map((item) => (
|
|
60
|
+
<Tab value={item.group} key={item.group}>
|
|
61
|
+
{item.group}
|
|
62
|
+
</Tab>
|
|
63
|
+
))}
|
|
64
|
+
</TabList>
|
|
65
|
+
{block?.stats?.map((item) => (
|
|
66
|
+
<TabPanel value={item.group} key={item.group}>
|
|
67
|
+
{item.stats?.map(getStatsContent)}
|
|
68
|
+
</TabPanel>
|
|
69
|
+
))}
|
|
70
|
+
</TabProvider>
|
|
71
|
+
),
|
|
72
|
+
[block?.stats, validatedActiveTab],
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const TooltipComponent = ({block, children}: Props) => {
|
|
77
|
+
const content = useTooltipContent(block);
|
|
78
|
+
const trigger = React.cloneElement(children, {
|
|
79
|
+
role: children.props.role ?? 'button',
|
|
80
|
+
tabIndex: children.props.tabIndex ?? 0,
|
|
81
|
+
onKeyDown: (event: React.KeyboardEvent<HTMLElement>) => {
|
|
82
|
+
children.props.onKeyDown?.(event);
|
|
83
|
+
if (event.defaultPrevented) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (event.key === 'Enter') {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
event.currentTarget.click();
|
|
90
|
+
} else if (event.key === ' ') {
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
onKeyUp: (event: React.KeyboardEvent<HTMLElement>) => {
|
|
95
|
+
children.props.onKeyUp?.(event);
|
|
96
|
+
if (!event.defaultPrevented && event.key === ' ') {
|
|
97
|
+
event.currentTarget.click();
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Popover
|
|
104
|
+
content={content}
|
|
105
|
+
hasArrow
|
|
106
|
+
trigger="click"
|
|
107
|
+
placement="right-start"
|
|
108
|
+
className="ydb-gravity-graph__tooltip-content"
|
|
109
|
+
strategy="fixed"
|
|
110
|
+
>
|
|
111
|
+
{trigger}
|
|
112
|
+
</Popover>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type AbstractGraphColorsConfig = Partial<Record<string, Partial<Record<string, string>>>>;
|
|
2
|
+
|
|
3
|
+
export const graphColorsConfig = {
|
|
4
|
+
// Default @gravity-ui/graph colors
|
|
5
|
+
canvas: {
|
|
6
|
+
belowLayerBackground: '#0000',
|
|
7
|
+
border: '#0000',
|
|
8
|
+
dots: 'var(--g-color-line-generic)',
|
|
9
|
+
layerBackground: 'var(--g-color-base-background)',
|
|
10
|
+
},
|
|
11
|
+
block: {
|
|
12
|
+
text: 'var(--g-color-text-primary)',
|
|
13
|
+
background: 'var(--g-color-base-float)',
|
|
14
|
+
border: 'var(--g-color-line-generic-solid)',
|
|
15
|
+
},
|
|
16
|
+
connection: {
|
|
17
|
+
background: 'var(--g-color-line-generic-solid)',
|
|
18
|
+
hoverBackground: 'var(--g-color-line-generic-solid)',
|
|
19
|
+
selectedBackground: 'var(--g-color-line-generic-solid)',
|
|
20
|
+
},
|
|
21
|
+
} as const satisfies AbstractGraphColorsConfig;
|