ydb-embedded-ui 20.3.0 → 20.4.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 (145) hide show
  1. package/dist/components/Drawer/Drawer.d.ts +2 -1
  2. package/dist/components/Drawer/Drawer.js +4 -4
  3. package/dist/components/Drawer/Drawer.js.map +1 -1
  4. package/dist/components/EntityStatus/EntityStatus.d.ts +4 -1
  5. package/dist/components/EntityStatus/EntityStatus.js +2 -2
  6. package/dist/components/EntityStatus/EntityStatus.js.map +1 -1
  7. package/dist/components/PDiskInfo/i18n/index.d.ts +1 -1
  8. package/dist/components/TenantNameWrapper/utils.d.ts +1 -1
  9. package/dist/components/TenantNameWrapper/utils.js.map +1 -1
  10. package/dist/containers/Cluster/Cluster.js +1 -1
  11. package/dist/containers/Cluster/Cluster.js.map +1 -1
  12. package/dist/containers/Cluster/ClusterDrawerHealthcheck.d.ts +8 -0
  13. package/dist/containers/Cluster/ClusterDrawerHealthcheck.js +71 -9
  14. package/dist/containers/Cluster/ClusterDrawerHealthcheck.js.map +1 -1
  15. package/dist/containers/Cluster/ClusterInfo/ClusterInfo.d.ts +2 -1
  16. package/dist/containers/Cluster/ClusterInfo/ClusterInfo.js +2 -2
  17. package/dist/containers/Cluster/ClusterInfo/ClusterInfo.js.map +1 -1
  18. package/dist/containers/Cluster/ClusterOverview/ClusterOverview.d.ts +1 -0
  19. package/dist/containers/Cluster/ClusterOverview/ClusterOverview.js.map +1 -1
  20. package/dist/containers/Cluster/ClusterOverview/components/BridgeInfoTable.d.ts +2 -0
  21. package/dist/containers/Cluster/ClusterOverview/components/BridgeInfoTable.js +54 -103
  22. package/dist/containers/Cluster/ClusterOverview/components/BridgeInfoTable.js.map +1 -1
  23. package/dist/containers/Cluster/ClusterOverview/components/BridgeInfoTable.scss +24 -89
  24. package/dist/containers/Cluster/ClusterOverview/components/bridgePileHealthcheck.d.ts +13 -0
  25. package/dist/containers/Cluster/ClusterOverview/components/bridgePileHealthcheck.js +94 -0
  26. package/dist/containers/Cluster/ClusterOverview/components/bridgePileHealthcheck.js.map +1 -0
  27. package/dist/containers/Cluster/i18n/en.json +8 -5
  28. package/dist/containers/Cluster/i18n/index.d.ts +1 -1
  29. package/dist/containers/Clusters/constants.d.ts +1 -1
  30. package/dist/containers/Header/DatabaseRightControls.d.ts +4 -2
  31. package/dist/containers/Header/DatabaseRightControls.js +2 -2
  32. package/dist/containers/Header/DatabaseRightControls.js.map +1 -1
  33. package/dist/containers/Header/Header.js +2 -2
  34. package/dist/containers/Header/Header.js.map +1 -1
  35. package/dist/containers/Header/HeaderActionsMenu.d.ts +4 -2
  36. package/dist/containers/Header/HeaderActionsMenu.js +19 -2
  37. package/dist/containers/Header/HeaderActionsMenu.js.map +1 -1
  38. package/dist/containers/Header/HeaderRightControls.d.ts +4 -2
  39. package/dist/containers/Header/HeaderRightControls.js +2 -2
  40. package/dist/containers/Header/HeaderRightControls.js.map +1 -1
  41. package/dist/containers/Header/hooks/useClusterData.d.ts +1 -0
  42. package/dist/containers/Header/hooks/useHeaderData.d.ts +1 -0
  43. package/dist/containers/Header/hooks/useHeaderData.js +1 -0
  44. package/dist/containers/Header/hooks/useHeaderData.js.map +1 -1
  45. package/dist/containers/Header/hooks/useSharedDatabasePath.d.ts +10 -0
  46. package/dist/containers/Header/hooks/useSharedDatabasePath.js +102 -0
  47. package/dist/containers/Header/hooks/useSharedDatabasePath.js.map +1 -0
  48. package/dist/containers/Header/i18n/en.json +1 -0
  49. package/dist/containers/Header/i18n/index.d.ts +1 -1
  50. package/dist/containers/Operations/constants.d.ts +1 -1
  51. package/dist/containers/Storage/PaginatedStorageGroupsTable/columns/i18n/index.d.ts +1 -1
  52. package/dist/containers/Tablets/Tablets.js +3 -3
  53. package/dist/containers/Tablets/Tablets.js.map +1 -1
  54. package/dist/containers/Tablets/TabletsTable.js +17 -27
  55. package/dist/containers/Tablets/TabletsTable.js.map +1 -1
  56. package/dist/containers/Tablets/i18n/en.json +3 -0
  57. package/dist/containers/Tablets/i18n/index.d.ts +1 -1
  58. package/dist/containers/Tablets/useTabletQueryParams.d.ts +8 -0
  59. package/dist/containers/Tablets/useTabletQueryParams.js +40 -0
  60. package/dist/containers/Tablets/useTabletQueryParams.js.map +1 -0
  61. package/dist/containers/Tablets/utils.d.ts +11 -0
  62. package/dist/containers/Tablets/utils.js +23 -0
  63. package/dist/containers/Tablets/utils.js.map +1 -0
  64. package/dist/containers/Tenant/Healthcheck/Healthcheck.d.ts +6 -1
  65. package/dist/containers/Tenant/Healthcheck/Healthcheck.js +5 -5
  66. package/dist/containers/Tenant/Healthcheck/Healthcheck.js.map +1 -1
  67. package/dist/containers/Tenant/Healthcheck/components/HealthcheckDrawer.d.ts +2 -1
  68. package/dist/containers/Tenant/Healthcheck/components/HealthcheckDrawer.js +2 -2
  69. package/dist/containers/Tenant/Healthcheck/components/HealthcheckDrawer.js.map +1 -1
  70. package/dist/containers/Tenant/Healthcheck/components/HealthcheckIssue.d.ts +5 -1
  71. package/dist/containers/Tenant/Healthcheck/components/HealthcheckIssue.js +5 -3
  72. package/dist/containers/Tenant/Healthcheck/components/HealthcheckIssue.js.map +1 -1
  73. package/dist/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/ComputeLocation.d.ts +1 -1
  74. package/dist/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/ComputeLocation.js +11 -2
  75. package/dist/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/ComputeLocation.js.map +1 -1
  76. package/dist/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/NodeInfo.js +2 -0
  77. package/dist/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/NodeInfo.js.map +1 -1
  78. package/dist/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/StorageLocation.js +4 -1
  79. package/dist/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/StorageLocation.js.map +1 -1
  80. package/dist/containers/Tenant/Healthcheck/components/HealthcheckIssues.d.ts +5 -1
  81. package/dist/containers/Tenant/Healthcheck/components/HealthcheckIssues.js +2 -2
  82. package/dist/containers/Tenant/Healthcheck/components/HealthcheckIssues.js.map +1 -1
  83. package/dist/containers/Tenant/Healthcheck/i18n/en.json +2 -1
  84. package/dist/containers/Tenant/Healthcheck/i18n/index.d.ts +1 -1
  85. package/dist/containers/Tenant/Query/SavedQueries/SavedQueries.scss +2 -0
  86. package/dist/containers/VDiskPage/i18n/index.d.ts +1 -1
  87. package/dist/routes.d.ts +4 -1
  88. package/dist/routes.js +3 -2
  89. package/dist/routes.js.map +1 -1
  90. package/dist/services/api/viewer.d.ts +3 -1
  91. package/dist/services/api/viewer.js +3 -2
  92. package/dist/services/api/viewer.js.map +1 -1
  93. package/dist/store/reducers/cluster/cluster.d.ts +2 -1
  94. package/dist/store/reducers/cluster/cluster.js +1 -0
  95. package/dist/store/reducers/cluster/cluster.js.map +1 -1
  96. package/dist/store/reducers/healthcheckInfo/types.d.ts +1 -0
  97. package/dist/store/reducers/healthcheckInfo/utils.d.ts +1 -0
  98. package/dist/store/reducers/healthcheckInfo/utils.js +43 -9
  99. package/dist/store/reducers/healthcheckInfo/utils.js.map +1 -1
  100. package/dist/store/reducers/tenants/tenants.d.ts +10 -1
  101. package/dist/store/reducers/tenants/tenants.js +33 -0
  102. package/dist/store/reducers/tenants/tenants.js.map +1 -1
  103. package/dist/store/reducers/tenants/types.d.ts +4 -0
  104. package/dist/types/api/healthcheck.d.ts +7 -0
  105. package/package.json +1 -1
  106. package/src/components/Drawer/Drawer.tsx +6 -0
  107. package/src/components/EntityStatus/EntityStatus.tsx +9 -1
  108. package/src/components/TenantNameWrapper/utils.ts +1 -1
  109. package/src/containers/Cluster/Cluster.tsx +1 -0
  110. package/src/containers/Cluster/ClusterDrawerHealthcheck.tsx +112 -13
  111. package/src/containers/Cluster/ClusterInfo/ClusterInfo.tsx +7 -1
  112. package/src/containers/Cluster/ClusterOverview/ClusterOverview.tsx +1 -0
  113. package/src/containers/Cluster/ClusterOverview/components/BridgeInfoTable.scss +24 -89
  114. package/src/containers/Cluster/ClusterOverview/components/BridgeInfoTable.tsx +156 -200
  115. package/src/containers/Cluster/ClusterOverview/components/bridgePileHealthcheck.ts +142 -0
  116. package/src/containers/Cluster/i18n/en.json +8 -5
  117. package/src/containers/Header/DatabaseRightControls.tsx +7 -1
  118. package/src/containers/Header/Header.tsx +3 -0
  119. package/src/containers/Header/HeaderActionsMenu.tsx +24 -2
  120. package/src/containers/Header/HeaderRightControls.tsx +7 -1
  121. package/src/containers/Header/hooks/useHeaderData.ts +1 -0
  122. package/src/containers/Header/hooks/useSharedDatabasePath.ts +195 -0
  123. package/src/containers/Header/i18n/en.json +1 -0
  124. package/src/containers/Tablets/Tablets.tsx +3 -3
  125. package/src/containers/Tablets/TabletsTable.tsx +37 -31
  126. package/src/containers/Tablets/i18n/en.json +3 -0
  127. package/src/containers/Tablets/useTabletQueryParams.ts +54 -0
  128. package/src/containers/Tablets/utils.ts +43 -0
  129. package/src/containers/Tenant/Healthcheck/Healthcheck.tsx +37 -4
  130. package/src/containers/Tenant/Healthcheck/components/HealthcheckDrawer.tsx +3 -0
  131. package/src/containers/Tenant/Healthcheck/components/HealthcheckIssue.tsx +17 -4
  132. package/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/ComputeLocation.tsx +13 -3
  133. package/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/NodeInfo.tsx +1 -0
  134. package/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/StorageLocation.tsx +3 -1
  135. package/src/containers/Tenant/Healthcheck/components/HealthcheckIssues.tsx +15 -2
  136. package/src/containers/Tenant/Healthcheck/i18n/en.json +2 -1
  137. package/src/containers/Tenant/Query/SavedQueries/SavedQueries.scss +2 -0
  138. package/src/routes.ts +8 -2
  139. package/src/services/api/viewer.ts +10 -2
  140. package/src/store/reducers/cluster/cluster.ts +2 -1
  141. package/src/store/reducers/healthcheckInfo/types.ts +1 -0
  142. package/src/store/reducers/healthcheckInfo/utils.ts +50 -10
  143. package/src/store/reducers/tenants/tenants.ts +55 -1
  144. package/src/store/reducers/tenants/types.ts +5 -0
  145. package/src/types/api/healthcheck.ts +8 -0
@@ -1,30 +1,39 @@
1
1
  import React from 'react';
2
2
 
3
- import {ArrowChevronRight, Ban, CircleCheck, CirclePause, LinkSlash} from '@gravity-ui/icons';
4
- import {Flex, Icon, Text} from '@gravity-ui/uikit';
3
+ import {
4
+ Archive,
5
+ ArrowChevronRight,
6
+ Ban,
7
+ ChevronRight,
8
+ CirclePause,
9
+ LinkSlash,
10
+ MapPin,
11
+ } from '@gravity-ui/icons';
12
+ import {Flex, Icon, Label, Text} from '@gravity-ui/uikit';
5
13
  import type {LabelProps} from '@gravity-ui/uikit';
6
14
 
15
+ import {EntityStatus} from '../../../../components/EntityStatus/EntityStatus';
7
16
  import {LabelWithHelpMark} from '../../../../components/LabelWithHelpMark/LabelWithHelpMark';
17
+ import {Skeleton} from '../../../../components/Skeleton/Skeleton';
18
+ import {
19
+ healthcheckApi,
20
+ selectLeavesIssues,
21
+ } from '../../../../store/reducers/healthcheckInfo/healthcheckInfo';
8
22
  import type {TBridgePile} from '../../../../types/api/cluster';
9
- import {BridgePileGroupStatus, BridgePileState} from '../../../../types/api/cluster';
23
+ import {BridgePileState} from '../../../../types/api/cluster';
10
24
  import {cn} from '../../../../utils/cn';
11
25
  import {EMPTY_DATA_PLACEHOLDER} from '../../../../utils/constants';
12
26
  import {formatNumber} from '../../../../utils/dataFormatters/dataFormatters';
27
+ import {useAutoRefreshInterval, useTypedSelector} from '../../../../utils/hooks';
28
+ import {useClusterHealthcheckQueryParams} from '../../ClusterDrawerHealthcheck';
13
29
  import i18n from '../../i18n';
14
30
 
31
+ import type {BridgePileHealthcheckTarget} from './bridgePileHealthcheck';
32
+ import {getBridgePileHealthcheck} from './bridgePileHealthcheck';
33
+
15
34
  import './BridgeInfoTable.scss';
16
35
 
17
36
  const b = cn('ydb-bridge-info-table');
18
- const progressB = cn('ydb-bridge-info-table-progress');
19
- const legendB = cn('ydb-bridge-info-table-legend');
20
-
21
- type GroupStatusTheme = 'positive' | 'warning' | 'danger' | 'dark-complementary' | 'secondary';
22
-
23
- interface PreparedGroupStatus {
24
- label: string;
25
- theme: GroupStatusTheme;
26
- value: number;
27
- }
28
37
 
29
38
  interface BridgePileStateInfo {
30
39
  label?: string;
@@ -39,25 +48,25 @@ function getBridgePileStateInfo(state?: BridgePileState): BridgePileStateInfo {
39
48
  return {
40
49
  label: i18n('value_bridge-state-primary'),
41
50
  theme: 'normal',
51
+ help: i18n('context_bridge-pile-state-primary'),
42
52
  };
43
53
  case BridgePileState.PROMOTED:
44
54
  return {
45
55
  label: i18n('value_bridge-state-promoted'),
46
- theme: 'normal',
56
+ theme: 'info',
47
57
  icon: <Icon data={ArrowChevronRight} size={12} />,
48
58
  help: i18n('context_bridge-pile-state-promoted'),
49
59
  };
50
60
  case BridgePileState.SYNCHRONIZED:
51
61
  return {
52
62
  label: i18n('value_bridge-state-synchronised'),
53
- theme: 'success',
54
- icon: <Icon data={CircleCheck} size={12} />,
63
+ theme: 'normal',
55
64
  help: i18n('context_bridge-pile-state-synchronised'),
56
65
  };
57
66
  case BridgePileState.NOT_SYNCHRONIZED:
58
67
  return {
59
68
  label: i18n('value_bridge-state-not-synchronised'),
60
- theme: 'normal',
69
+ theme: 'warning',
61
70
  icon: <Icon data={Ban} size={12} />,
62
71
  help: i18n('context_bridge-pile-state-not-synchronised'),
63
72
  };
@@ -71,7 +80,7 @@ function getBridgePileStateInfo(state?: BridgePileState): BridgePileStateInfo {
71
80
  case BridgePileState.DISCONNECTED:
72
81
  return {
73
82
  label: i18n('value_bridge-state-disconnected'),
74
- theme: 'unknown',
83
+ theme: 'danger',
75
84
  icon: <Icon data={LinkSlash} size={12} />,
76
85
  help: i18n('context_bridge-pile-state-disconnected'),
77
86
  };
@@ -88,182 +97,43 @@ function getBridgePileStateInfo(state?: BridgePileState): BridgePileStateInfo {
88
97
  }
89
98
  }
90
99
 
91
- interface BridgePileGroupStatusInfo {
92
- label: string;
93
- theme: GroupStatusTheme;
94
- }
95
-
96
- function getNodesLabel(nodes?: number) {
97
- if (nodes === undefined) {
98
- return EMPTY_DATA_PLACEHOLDER;
99
- }
100
-
101
- return i18n('value_nodes', {
102
- count: nodes,
103
- formattedCount: formatNumber(nodes),
104
- });
105
- }
106
-
107
- function getBridgePileGroupStatusInfo(status?: BridgePileGroupStatus): BridgePileGroupStatusInfo {
108
- switch (status) {
109
- case BridgePileGroupStatus.UNKNOWN:
110
- return {
111
- label: i18n('value_bridge-group-status-unknown'),
112
- theme: 'secondary',
113
- };
114
- case BridgePileGroupStatus.FULL:
115
- return {
116
- label: i18n('value_bridge-group-status-full'),
117
- theme: 'positive',
118
- };
119
- case BridgePileGroupStatus.PARTIAL:
120
- return {
121
- label: i18n('value_bridge-group-status-partial'),
122
- theme: 'warning',
123
- };
124
- case BridgePileGroupStatus.DEGRADED:
125
- return {
126
- label: i18n('value_bridge-group-status-degraded'),
127
- theme: 'danger',
128
- };
129
- case BridgePileGroupStatus.DISINTEGRATED:
130
- return {
131
- label: i18n('value_bridge-group-status-disintegrated'),
132
- theme: 'dark-complementary',
133
- };
134
- default:
135
- return {
136
- label: EMPTY_DATA_PLACEHOLDER,
137
- theme: 'secondary',
138
- };
139
- }
140
- }
141
-
142
- function getBridgePileGroupStatus(status: string): BridgePileGroupStatus | undefined {
143
- if (Object.values(BridgePileGroupStatus).includes(status as BridgePileGroupStatus)) {
144
- return status as BridgePileGroupStatus;
145
- }
146
-
147
- return undefined;
148
- }
149
-
150
- function prepareGroupStatuses(groupStatuses?: TBridgePile['GroupStatuses']): PreparedGroupStatus[] {
151
- return Object.entries(groupStatuses ?? {})
152
- .map(([status, value]) => {
153
- const groupStatus = getBridgePileGroupStatus(status);
154
- const {label, theme} = groupStatus
155
- ? getBridgePileGroupStatusInfo(groupStatus)
156
- : {label: status, theme: 'secondary' as const};
157
- const numericValue = Number(value);
158
-
159
- if (!Number.isFinite(numericValue) || numericValue <= 0) {
160
- return undefined;
161
- }
162
-
163
- return {
164
- label,
165
- theme,
166
- value: numericValue,
167
- };
168
- })
169
- .filter((status): status is PreparedGroupStatus => status !== undefined);
170
- }
171
-
172
- interface SegmentedProgressBarProps {
173
- statuses: PreparedGroupStatus[];
174
- describedBy?: string;
175
- }
176
-
177
- function SegmentedProgressBar({statuses, describedBy}: SegmentedProgressBarProps) {
178
- const total = statuses.reduce((sum, {value}) => sum + value, 0);
179
-
180
- if (!Number.isFinite(total) || total <= 0) {
181
- return (
182
- <div
183
- className={progressB({empty: true})}
184
- role="img"
185
- aria-label={i18n('context_group-statuses-empty')}
186
- />
187
- );
188
- }
189
-
190
- return (
191
- <div
192
- className={progressB()}
193
- role="img"
194
- aria-label={i18n('context_group-statuses')}
195
- aria-describedby={describedBy}
196
- >
197
- {statuses.map(({label, theme, value}) => (
198
- <div
199
- key={`${label}-${value}`}
200
- className={progressB('item', {theme})}
201
- aria-hidden="true"
202
- style={{
203
- width: `${(value / total) * 100}%`,
204
- }}
205
- />
206
- ))}
207
- </div>
208
- );
209
- }
210
-
211
- interface GroupStatusesLegendProps {
212
- statuses: PreparedGroupStatus[];
213
- id?: string;
214
- }
215
-
216
- function GroupStatusesLegend({statuses, id}: GroupStatusesLegendProps) {
217
- if (!statuses.length) {
218
- return (
219
- <Text variant="caption-2" color="secondary">
220
- {EMPTY_DATA_PLACEHOLDER}
221
- </Text>
222
- );
223
- }
224
-
225
- return (
226
- <ul id={id} className={legendB()} aria-label={i18n('context_group-statuses')}>
227
- {statuses.map(({label, theme, value}) => (
228
- <li key={`${label}-${value}`} className={legendB('item')}>
229
- <span aria-hidden="true" className={legendB('dot', {theme})} />
230
- <Text variant="caption-2" className={legendB('label')}>
231
- {label}
232
- </Text>
233
- <Text variant="caption-2" color="secondary">
234
- {formatNumber(value)}
235
- </Text>
236
- </li>
237
- ))}
238
- </ul>
239
- );
240
- }
241
-
242
100
  interface BridgeInfoTableProps {
243
101
  piles: TBridgePile[];
102
+ database?: string;
103
+ clusterName?: string;
244
104
  }
245
105
 
246
106
  interface BridgePileCardProps {
247
107
  pile: TBridgePile;
108
+ current: boolean;
109
+ healthcheckAvailable: boolean;
110
+ healthcheckSupportsPiles: boolean;
111
+ healthcheckLoading: boolean;
112
+ leavesIssues: ReturnType<typeof selectLeavesIssues>;
113
+ onHealthcheckClick: (target: BridgePileHealthcheckTarget) => void;
248
114
  }
249
115
 
250
- const BridgePileCard = React.memo(function BridgePileCard({pile}: BridgePileCardProps) {
251
- const groupStatusesLegendId = React.useId();
252
-
116
+ const BridgePileCard = React.memo(function BridgePileCard({
117
+ pile,
118
+ current,
119
+ healthcheckAvailable,
120
+ healthcheckSupportsPiles,
121
+ healthcheckLoading,
122
+ leavesIssues,
123
+ onHealthcheckClick,
124
+ }: BridgePileCardProps) {
253
125
  const stateStatus = React.useMemo(() => {
254
126
  const {label, theme, icon, help} = getBridgePileStateInfo(pile.State);
255
127
  if (!label) {
256
128
  return null;
257
129
  }
258
130
 
259
- const showStateHelp = Boolean(help);
260
-
261
131
  return (
262
132
  <LabelWithHelpMark
263
133
  theme={theme}
264
134
  size="xs"
265
135
  icon={icon}
266
- className={b('state-label', {'with-help': showStateHelp})}
136
+ className={b('state-label')}
267
137
  contentGap={2}
268
138
  help={help}
269
139
  helpMarkProps={{
@@ -279,45 +149,131 @@ const BridgePileCard = React.memo(function BridgePileCard({pile}: BridgePileCard
279
149
  );
280
150
  }, [pile.State]);
281
151
 
282
- const groupStatuses = React.useMemo(
283
- () => prepareGroupStatuses(pile.GroupStatuses),
284
- [pile.GroupStatuses],
152
+ const pileHealthcheck = React.useMemo(
153
+ () =>
154
+ getBridgePileHealthcheck(
155
+ pile.Name,
156
+ leavesIssues,
157
+ healthcheckAvailable,
158
+ healthcheckSupportsPiles,
159
+ ),
160
+ [healthcheckAvailable, healthcheckSupportsPiles, leavesIssues, pile.Name],
285
161
  );
286
- const groupStatusesDescriptionId = groupStatuses.length ? groupStatusesLegendId : undefined;
162
+
163
+ const pileName = pile.Name?.trim() || EMPTY_DATA_PLACEHOLDER;
164
+ const nodes = pile.Nodes === undefined ? EMPTY_DATA_PLACEHOLDER : formatNumber(pile.Nodes);
165
+ const handleHealthcheckClick = () => {
166
+ if (pileHealthcheck.target) {
167
+ onHealthcheckClick(pileHealthcheck.target);
168
+ }
169
+ };
287
170
 
288
171
  return (
289
172
  <Flex direction="column" gap={2} className={b('pile')}>
290
- <Flex justifyContent="space-between" alignItems="flex-start" gap={3}>
291
- <Flex direction="column" className={b('pile-title')}>
292
- <Text variant="subheader-1" ellipsis>
293
- {pile.Name?.trim() || EMPTY_DATA_PLACEHOLDER}
294
- </Text>
295
- <Text variant="caption-2" color="secondary">
296
- {getNodesLabel(pile.Nodes)}
297
- </Text>
298
- </Flex>
173
+ <Flex justifyContent="space-between" alignItems="flex-start" gap={2} width="100%">
174
+ <Text variant="subheader-1" ellipsis className={b('pile-name')}>
175
+ {pileName}
176
+ </Text>
177
+ {current ? (
178
+ <Label
179
+ theme="clear"
180
+ size="xs"
181
+ icon={<Icon data={MapPin} size={12} />}
182
+ className={b('current-pile')}
183
+ >
184
+ {i18n('label_you-are-here')}
185
+ </Label>
186
+ ) : null}
187
+ </Flex>
188
+ <Flex gap={2} alignItems="center" wrap>
299
189
  {stateStatus}
190
+ {healthcheckLoading ? (
191
+ <Skeleton className={b('healthcheck-skeleton')} />
192
+ ) : (
193
+ <EntityStatus.Label
194
+ status={pileHealthcheck.status}
195
+ size="xs"
196
+ className={b('healthcheck-status')}
197
+ onClick={pileHealthcheck.target ? handleHealthcheckClick : undefined}
198
+ endContent={
199
+ pileHealthcheck.target ? (
200
+ <Icon data={ChevronRight} size={12} />
201
+ ) : undefined
202
+ }
203
+ qa="bridge-pile-healthcheck"
204
+ >
205
+ {pileHealthcheck.target ? (
206
+ <span className={b('healthcheck-accessible-prefix')}>
207
+ {i18n('label_bridge-pile-health-status-prefix', {pileName})}
208
+ </span>
209
+ ) : null}
210
+ </EntityStatus.Label>
211
+ )}
300
212
  </Flex>
301
- <Flex direction="column" gap={1} className={b('statuses')}>
302
- <SegmentedProgressBar
303
- statuses={groupStatuses}
304
- describedBy={groupStatusesDescriptionId}
305
- />
306
- <GroupStatusesLegend statuses={groupStatuses} id={groupStatusesDescriptionId} />
213
+ <Flex gap={1} alignItems="center" className={b('nodes')}>
214
+ <Icon data={Archive} size={14} />
215
+ <Text variant="body-1">
216
+ {i18n('field_nodes')}: {nodes}
217
+ </Text>
307
218
  </Flex>
308
219
  </Flex>
309
220
  );
310
221
  });
311
222
 
312
- export const BridgeInfoTable = React.memo(function BridgeInfoTable({piles}: BridgeInfoTableProps) {
313
- const renderedPiles = React.useMemo(
314
- () => piles.map((pile, index) => <BridgePileCard key={pile.PileId ?? index} pile={pile} />),
315
- [piles],
223
+ export const BridgeInfoTable = React.memo(function BridgeInfoTable({
224
+ piles,
225
+ database,
226
+ clusterName,
227
+ }: BridgeInfoTableProps) {
228
+ const [autoRefreshInterval] = useAutoRefreshInterval();
229
+ const {handleOpenHealthcheckIssue} = useClusterHealthcheckQueryParams();
230
+ const {
231
+ currentData: healthcheckData,
232
+ isFetching: healthcheckFetching,
233
+ error: healthcheckError,
234
+ } = healthcheckApi.useGetHealthcheckInfoQuery(
235
+ {database: database ?? '', clusterName},
236
+ {
237
+ pollingInterval: autoRefreshInterval,
238
+ skip: !database,
239
+ },
316
240
  );
241
+ const leavesIssues = useTypedSelector((state) =>
242
+ selectLeavesIssues(state, database ?? '', clusterName),
243
+ );
244
+
245
+ const healthcheckAvailable = healthcheckData !== undefined && healthcheckError === undefined;
246
+ const healthcheckLoading = healthcheckData === undefined && healthcheckFetching;
247
+ const currentPileName = healthcheckData?.location?.pile?.name;
248
+ const healthcheckSupportsPiles = healthcheckData?.location?.pile !== undefined;
249
+
250
+ const sortedPiles = React.useMemo(() => {
251
+ return piles
252
+ .map((pile, index) => ({pile, index}))
253
+ .sort((left, right) => {
254
+ const leftPrimary = left.pile.State === BridgePileState.PRIMARY;
255
+ const rightPrimary = right.pile.State === BridgePileState.PRIMARY;
256
+ if (leftPrimary !== rightPrimary) {
257
+ return leftPrimary ? -1 : 1;
258
+ }
259
+ return left.index - right.index;
260
+ });
261
+ }, [piles]);
317
262
 
318
263
  return (
319
- <Flex gap={2} wrap className={b()}>
320
- {renderedPiles}
264
+ <Flex wrap className={b()}>
265
+ {sortedPiles.map(({pile, index}) => (
266
+ <BridgePileCard
267
+ key={pile.PileId ?? pile.Name ?? index}
268
+ pile={pile}
269
+ current={Boolean(currentPileName?.trim() && pile.Name === currentPileName)}
270
+ healthcheckAvailable={healthcheckAvailable}
271
+ healthcheckSupportsPiles={healthcheckSupportsPiles}
272
+ healthcheckLoading={healthcheckLoading}
273
+ leavesIssues={leavesIssues}
274
+ onHealthcheckClick={handleOpenHealthcheckIssue}
275
+ />
276
+ ))}
321
277
  </Flex>
322
278
  );
323
279
  });
@@ -0,0 +1,142 @@
1
+ import type {IssuesTree} from '../../../../store/reducers/healthcheckInfo/types';
2
+ import {
3
+ getHealthcheckIssuePileNames,
4
+ hcStatusToColorFlag,
5
+ } from '../../../../store/reducers/healthcheckInfo/utils';
6
+ import {EFlag} from '../../../../types/api/enums';
7
+ import {StatusFlag} from '../../../../types/api/healthcheck';
8
+
9
+ export interface BridgePileHealthcheckTarget {
10
+ issueId: string;
11
+ leafIssueId: string;
12
+ view: string;
13
+ }
14
+
15
+ interface BridgePileHealthcheck {
16
+ status: EFlag;
17
+ target?: BridgePileHealthcheckTarget;
18
+ }
19
+
20
+ interface Candidate {
21
+ issue: IssuesTree;
22
+ leaf: IssuesTree;
23
+ level: number;
24
+ }
25
+
26
+ const STATUS_PRIORITY: Record<StatusFlag, number> = {
27
+ [StatusFlag.UNSPECIFIED]: 0,
28
+ [StatusFlag.GREY]: 0,
29
+ [StatusFlag.GREEN]: 1,
30
+ [StatusFlag.BLUE]: 2,
31
+ [StatusFlag.YELLOW]: 3,
32
+ [StatusFlag.ORANGE]: 4,
33
+ [StatusFlag.RED]: 5,
34
+ };
35
+
36
+ function normalizeStatus(status: unknown): StatusFlag {
37
+ return typeof status === 'string' && Object.hasOwn(STATUS_PRIORITY, status)
38
+ ? (status as StatusFlag)
39
+ : StatusFlag.UNSPECIFIED;
40
+ }
41
+
42
+ function getBranch(leaf: IssuesTree): IssuesTree[] {
43
+ const branch: IssuesTree[] = [];
44
+ let current: IssuesTree | undefined = leaf;
45
+
46
+ while (current) {
47
+ branch.push(current);
48
+ current = current.parent;
49
+ }
50
+
51
+ return branch.reverse();
52
+ }
53
+
54
+ function getBranchCandidate(leaf: IssuesTree, pileName: string): Candidate | undefined {
55
+ const branch = getBranch(leaf);
56
+
57
+ for (const [branchIndex, issue] of branch.entries()) {
58
+ if (!getHealthcheckIssuePileNames(issue).includes(pileName)) {
59
+ continue;
60
+ }
61
+
62
+ // Backend guarantees increasing levels; add validation here if that can change.
63
+ const level = issue.level ?? branchIndex;
64
+ return {issue, leaf, level};
65
+ }
66
+
67
+ return undefined;
68
+ }
69
+
70
+ function compareSourceOrder(left?: number[], right?: number[]): number {
71
+ if (!left) {
72
+ return right ? 1 : 0;
73
+ }
74
+ if (!right) {
75
+ return -1;
76
+ }
77
+
78
+ const commonLength = Math.min(left.length, right.length);
79
+ for (let index = 0; index < commonLength; index += 1) {
80
+ if (left[index] !== right[index]) {
81
+ return left[index] - right[index];
82
+ }
83
+ }
84
+
85
+ return left.length - right.length;
86
+ }
87
+
88
+ function isWorseCandidate(candidate: Candidate, current: Candidate): boolean {
89
+ const candidatePriority = STATUS_PRIORITY[normalizeStatus(candidate.issue.status)];
90
+ const currentPriority = STATUS_PRIORITY[normalizeStatus(current.issue.status)];
91
+
92
+ if (candidatePriority !== currentPriority) {
93
+ return candidatePriority > currentPriority;
94
+ }
95
+
96
+ if (candidate.level !== current.level) {
97
+ return candidate.level < current.level;
98
+ }
99
+
100
+ const issueOrder = compareSourceOrder(
101
+ candidate.issue.sourceOrderPath,
102
+ current.issue.sourceOrderPath,
103
+ );
104
+ if (issueOrder !== 0) {
105
+ return issueOrder < 0;
106
+ }
107
+
108
+ return compareSourceOrder(candidate.leaf.sourceOrderPath, current.leaf.sourceOrderPath) < 0;
109
+ }
110
+
111
+ export function getBridgePileHealthcheck(
112
+ pileName: string | undefined,
113
+ leavesIssues: IssuesTree[],
114
+ healthcheckAvailable: boolean,
115
+ healthcheckSupportsPiles = healthcheckAvailable,
116
+ ): BridgePileHealthcheck {
117
+ if (!healthcheckAvailable || !pileName?.trim()) {
118
+ return {status: EFlag.Grey};
119
+ }
120
+
121
+ let selectedCandidate: Candidate | undefined;
122
+ for (const leaf of leavesIssues) {
123
+ const candidate = getBranchCandidate(leaf, pileName);
124
+ if (candidate && (!selectedCandidate || isWorseCandidate(candidate, selectedCandidate))) {
125
+ selectedCandidate = candidate;
126
+ }
127
+ }
128
+
129
+ if (!selectedCandidate) {
130
+ return {status: healthcheckSupportsPiles ? EFlag.Green : EFlag.Grey};
131
+ }
132
+
133
+ const issueStatus = normalizeStatus(selectedCandidate.issue.status);
134
+ return {
135
+ status: hcStatusToColorFlag[issueStatus],
136
+ target: {
137
+ issueId: selectedCandidate.issue.id,
138
+ leafIssueId: selectedCandidate.leaf.id,
139
+ view: selectedCandidate.leaf.categoryForUI,
140
+ },
141
+ };
142
+ }
@@ -24,16 +24,19 @@
24
24
  "value_bridge-state-suspended": "Suspended",
25
25
  "value_bridge-state-disconnected": "Disconnected",
26
26
  "value_bridge-state-unspecified": "Unspecified",
27
+ "label_you-are-here": "You are here",
28
+ "label_bridge-pile-health-status-prefix": "Health status for pile {{pileName}}:",
27
29
  "value_bridge-group-status-unknown": "Unknown",
28
30
  "value_bridge-group-status-full": "Full",
29
31
  "value_bridge-group-status-partial": "Partial",
30
32
  "value_bridge-group-status-degraded": "Degraded",
31
33
  "value_bridge-group-status-disintegrated": "Disintegrated",
32
- "context_bridge-pile-state-promoted": "Pile that is being promoted from SYNCHRONIZED\nto PRIMARY. It stays in this state during the graceful\nswitchover. After completion, YDB will automatically\nset it to PRIMARY.",
33
- "context_bridge-pile-state-synchronised": "Secondary pile where all storage groups are fully\nsynchronized with PRIMARY. In normal operation,\nthe pair works as PRIMARY + SYNCHRONISED.",
34
- "context_bridge-pile-state-not-synchronised": "Storage groups of this pile are not yet fully synchronized\nwith PRIMARY. After synchronization is complete, YDB\nwill automatically transition the pile to SYNCHRONIZED.",
35
- "context_bridge-pile-state-suspended": "Pile is being gracefully transitioned to DISCONNECTED\nstate. It remains in this state during the graceful shutdown.\nAfter completion, YDB will automatically move it\nto DISCONNECTED.",
36
- "context_bridge-pile-state-disconnected": "Pile is disconnected from PRIMARY and does\nnot participate in operation execution.",
34
+ "context_bridge-pile-state-promoted": "Pile that is being promoted from Synchronized to\u00a0Primary",
35
+ "context_bridge-pile-state-primary": "Primary pile that currently participates in operation execution.",
36
+ "context_bridge-pile-state-synchronised": "Secondary pile where all storage groups are fully synchronized with Primary",
37
+ "context_bridge-pile-state-not-synchronised": "Storage groups are not yet fully synchronized with Primary",
38
+ "context_bridge-pile-state-suspended": "Pile is being transitioned to\u00a0Disconnected state",
39
+ "context_bridge-pile-state-disconnected": "Pile is disconnected from Primary and does not participate in operation execution",
37
40
  "context_group-statuses": "Storage group statuses",
38
41
  "context_group-statuses-empty": "No storage group status data",
39
42
  "context_cpu": "CPU load",
@@ -5,7 +5,7 @@ import {ActionTooltip, Button, Icon} from '@gravity-ui/uikit';
5
5
 
6
6
  import {getConnectToDBDialog} from '../../components/ConnectToDB/ConnectToDBDialog';
7
7
  import type {PreparedTenant} from '../../store/reducers/tenants/types';
8
- import type {ClusterLinkWithTitle} from '../../types/additionalProps';
8
+ import type {AdditionalTenantsProps, ClusterLinkWithTitle} from '../../types/additionalProps';
9
9
  import {CLUSTER_LINK_CONTEXT} from '../../utils/clusterLinks/clusterLinkConstants';
10
10
 
11
11
  import {DeveloperUIControl} from './GlobalRightControls';
@@ -19,6 +19,8 @@ interface DatabaseRightControlsProps {
19
19
  databaseData?: PreparedTenant;
20
20
  isDatabaseDataLoading: boolean;
21
21
  isV2NavigationEnabled: boolean;
22
+ isViewerUser?: boolean;
23
+ prepareTenantBackend?: AdditionalTenantsProps['prepareTenantBackend'];
22
24
  showDeveloperUI: boolean;
23
25
  databaseLinks: ClusterLinkWithTitle[];
24
26
  }
@@ -29,6 +31,8 @@ export function DatabaseRightControls({
29
31
  databaseData,
30
32
  isDatabaseDataLoading,
31
33
  isV2NavigationEnabled,
34
+ isViewerUser,
35
+ prepareTenantBackend,
32
36
  showDeveloperUI,
33
37
  databaseLinks,
34
38
  }: DatabaseRightControlsProps) {
@@ -62,6 +66,8 @@ export function DatabaseRightControls({
62
66
  databaseData={databaseData}
63
67
  isDatabaseDataLoading={isDatabaseDataLoading}
64
68
  isV2NavigationEnabled={isV2NavigationEnabled}
69
+ isViewerUser={isViewerUser}
70
+ prepareTenantBackend={prepareTenantBackend}
65
71
  databaseLinks={actionDatabaseLinks}
66
72
  />
67
73
  </React.Fragment>
@@ -38,6 +38,7 @@ export function Header() {
38
38
  isDatabaseDataLoading,
39
39
  clusterLinks,
40
40
  databaseLinks,
41
+ prepareTenantBackend,
41
42
  } = useHeaderData({
42
43
  metaCapabilitiesLoaded,
43
44
  database,
@@ -89,6 +90,8 @@ export function Header() {
89
90
  isDatabaseDataLoading={isDatabaseDataLoading}
90
91
  isAddClusterAvailable={isAddClusterAvailable}
91
92
  isV2NavigationEnabled={isV2NavigationEnabled}
93
+ isViewerUser={isViewerUser}
94
+ prepareTenantBackend={prepareTenantBackend}
92
95
  clusterLinks={clusterLinks}
93
96
  databaseLinks={databaseLinks}
94
97
  handleEditCluster={handleEditCluster}