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.
Files changed (112) hide show
  1. package/README.md +41 -0
  2. package/dist/components/AutoRefreshControl/i18n/index.d.ts +1 -1
  3. package/dist/components/FullNodeViewer/i18n/index.d.ts +1 -1
  4. package/dist/components/Graph/BlockComponents/ConnectionBlockComponent.d.ts +7 -0
  5. package/dist/components/Graph/BlockComponents/ConnectionBlockComponent.js +30 -0
  6. package/dist/components/Graph/BlockComponents/ConnectionBlockComponent.js.map +1 -0
  7. package/dist/components/Graph/BlockComponents/QueryBlockComponent.d.ts +5 -0
  8. package/dist/components/Graph/BlockComponents/QueryBlockComponent.js +5 -0
  9. package/dist/components/Graph/BlockComponents/QueryBlockComponent.js.map +1 -0
  10. package/dist/components/Graph/BlockComponents/ResultBlockComponent.d.ts +7 -0
  11. package/dist/components/Graph/BlockComponents/ResultBlockComponent.js +5 -0
  12. package/dist/components/Graph/BlockComponents/ResultBlockComponent.js.map +1 -0
  13. package/dist/components/Graph/BlockComponents/StageBlockComponent.d.ts +7 -0
  14. package/dist/components/Graph/BlockComponents/StageBlockComponent.js +17 -0
  15. package/dist/components/Graph/BlockComponents/StageBlockComponent.js.map +1 -0
  16. package/dist/components/Graph/FullViewportBackground.d.ts +2 -0
  17. package/dist/components/Graph/FullViewportBackground.js +67 -0
  18. package/dist/components/Graph/FullViewportBackground.js.map +1 -0
  19. package/dist/components/Graph/GraphControls.d.ts +6 -0
  20. package/dist/components/Graph/GraphControls.js +24 -0
  21. package/dist/components/Graph/GraphControls.js.map +1 -0
  22. package/dist/components/Graph/GravityGraph.d.ts +9 -0
  23. package/dist/components/Graph/GravityGraph.js +122 -0
  24. package/dist/components/Graph/GravityGraph.js.map +1 -0
  25. package/dist/components/Graph/GravityGraph.scss +131 -0
  26. package/dist/components/Graph/NonSelectableConnection.d.ts +9 -0
  27. package/dist/components/Graph/NonSelectableConnection.js +16 -0
  28. package/dist/components/Graph/NonSelectableConnection.js.map +1 -0
  29. package/dist/components/Graph/TooltipComponent.d.ts +8 -0
  30. package/dist/components/Graph/TooltipComponent.js +63 -0
  31. package/dist/components/Graph/TooltipComponent.js.map +1 -0
  32. package/dist/components/Graph/colorsConfig.d.ts +19 -0
  33. package/dist/components/Graph/colorsConfig.js +20 -0
  34. package/dist/components/Graph/colorsConfig.js.map +1 -0
  35. package/dist/components/Graph/i18n/en.json +5 -0
  36. package/dist/components/Graph/i18n/index.d.ts +2 -0
  37. package/dist/components/Graph/i18n/index.js +5 -0
  38. package/dist/components/Graph/i18n/index.js.map +1 -0
  39. package/dist/components/Graph/runTreeLayout.d.ts +12 -0
  40. package/dist/components/Graph/runTreeLayout.js +75 -0
  41. package/dist/components/Graph/runTreeLayout.js.map +1 -0
  42. package/dist/components/Graph/treeLayout.d.ts +2 -0
  43. package/dist/components/Graph/treeLayout.js +284 -0
  44. package/dist/components/Graph/treeLayout.js.map +1 -0
  45. package/dist/components/Graph/types.d.ts +52 -0
  46. package/dist/components/Graph/types.js +2 -0
  47. package/dist/components/Graph/types.js.map +1 -0
  48. package/dist/components/Graph/utils.d.ts +6 -0
  49. package/dist/components/Graph/utils.js +88 -0
  50. package/dist/components/Graph/utils.js.map +1 -0
  51. package/dist/components/PDiskInfo/i18n/index.d.ts +1 -1
  52. package/dist/components/VDiskInfo/i18n/index.d.ts +1 -1
  53. package/dist/containers/Clusters/constants.d.ts +1 -1
  54. package/dist/containers/Nodes/i18n/index.d.ts +1 -1
  55. package/dist/containers/Operations/constants.d.ts +1 -1
  56. package/dist/containers/Tenant/Diagnostics/Overview/TransferInfo/Credentials.d.ts +1 -1
  57. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/useTenantStorageNewData.d.ts +2 -2
  58. package/dist/containers/Tenant/Query/QueryResult/components/Graph/Graph.js +9 -4
  59. package/dist/containers/Tenant/Query/QueryResult/components/Graph/Graph.js.map +1 -1
  60. package/dist/containers/Tenant/Query/QueryResult/components/Graph/Graph.scss +2 -0
  61. package/dist/containers/Tenant/TenantNavigation/useTenantNavigation.d.ts +1 -1
  62. package/dist/graphLayout.worker.d.ts +1 -0
  63. package/dist/graphLayout.worker.js +17 -0
  64. package/dist/graphLayout.worker.js.map +1 -0
  65. package/dist/lib.d.ts +1 -1
  66. package/dist/lib.js.map +1 -1
  67. package/dist/store/reducers/query/parsers/preparePlanData.js.map +1 -1
  68. package/dist/store/reducers/query/types.d.ts +38 -1
  69. package/dist/store/reducers/settings/utils.d.ts +2 -2
  70. package/dist/uiFactory/types.d.ts +3 -0
  71. package/dist/utils/disks/helpers.d.ts +2 -2
  72. package/dist/utils/hooks/useChangedQuerySettings.d.ts +2 -2
  73. package/dist/utils/hooks/useLastQueryExecutionSettings.d.ts +2 -2
  74. package/dist/utils/hooks/useQueryExecutionSettings.d.ts +2 -2
  75. package/dist/utils/prepareQueryExplain.d.ts +1 -2
  76. package/dist/utils/prepareQueryExplain.js +10 -2
  77. package/dist/utils/prepareQueryExplain.js.map +1 -1
  78. package/dist/utils/query.d.ts +1 -1
  79. package/package.json +2 -2
  80. package/src/components/Graph/BlockComponents/ConnectionBlockComponent.tsx +49 -0
  81. package/src/components/Graph/BlockComponents/QueryBlockComponent.tsx +7 -0
  82. package/src/components/Graph/BlockComponents/ResultBlockComponent.tsx +10 -0
  83. package/src/components/Graph/BlockComponents/StageBlockComponent.tsx +35 -0
  84. package/src/components/Graph/FullViewportBackground.ts +122 -0
  85. package/src/components/Graph/GraphControls.tsx +49 -0
  86. package/src/components/Graph/GravityGraph.scss +131 -0
  87. package/src/components/Graph/GravityGraph.tsx +174 -0
  88. package/src/components/Graph/NonSelectableConnection.tsx +14 -0
  89. package/src/components/Graph/TooltipComponent.tsx +114 -0
  90. package/src/components/Graph/colorsConfig.ts +21 -0
  91. package/src/components/Graph/i18n/en.json +5 -0
  92. package/src/components/Graph/i18n/index.ts +7 -0
  93. package/src/components/Graph/runTreeLayout.ts +105 -0
  94. package/src/components/Graph/treeLayout.ts +378 -0
  95. package/src/components/Graph/types.ts +56 -0
  96. package/src/components/Graph/utils.ts +112 -0
  97. package/src/containers/Tenant/Query/QueryResult/components/Graph/Graph.scss +2 -0
  98. package/src/containers/Tenant/Query/QueryResult/components/Graph/Graph.tsx +13 -7
  99. package/src/graphLayout.worker.ts +21 -0
  100. package/src/lib.ts +1 -0
  101. package/src/store/reducers/query/parsers/preparePlanData.ts +1 -3
  102. package/src/store/reducers/query/types.ts +42 -2
  103. package/src/uiFactory/types.ts +4 -0
  104. package/src/utils/prepareQueryExplain.ts +13 -5
  105. package/dist/components/Graph/Graph.d.ts +0 -12
  106. package/dist/components/Graph/Graph.js +0 -44
  107. package/dist/components/Graph/Graph.js.map +0 -1
  108. package/dist/components/Graph/colors.d.ts +0 -10
  109. package/dist/components/Graph/colors.js +0 -53
  110. package/dist/components/Graph/colors.js.map +0 -1
  111. package/src/components/Graph/Graph.tsx +0 -70
  112. 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;
@@ -0,0 +1,5 @@
1
+ {
2
+ "action_zoom-in": "Zoom in",
3
+ "action_zoom-out": "Zoom out",
4
+ "label_tables": "Tables"
5
+ }
@@ -0,0 +1,7 @@
1
+ import {registerKeysets} from '../../../utils/i18n';
2
+
3
+ import en from './en.json';
4
+
5
+ const COMPONENT = 'ydb-gravity-graph';
6
+
7
+ export default registerKeysets(COMPONENT, {en});