ydb-embedded-ui 3.2.0 → 3.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/components/EntitiesCount/EntitiesCount.tsx +34 -0
  3. package/dist/components/EntitiesCount/i18n/en.json +3 -0
  4. package/dist/components/{AsideNavigation/Settings → EntitiesCount}/i18n/index.ts +2 -2
  5. package/dist/components/EntitiesCount/i18n/ru.json +3 -0
  6. package/dist/components/EntitiesCount/index.ts +1 -0
  7. package/dist/components/Fullscreen/Fullscreen.scss +7 -5
  8. package/dist/components/TabletsOverall/TabletsOverall.tsx +4 -4
  9. package/dist/components/TabletsStatistic/TabletsStatistic.tsx +56 -0
  10. package/dist/components/TabletsStatistic/index.ts +1 -0
  11. package/dist/containers/App/App.scss +4 -12
  12. package/dist/containers/AsideNavigation/AsideNavigation.scss +0 -18
  13. package/dist/containers/AsideNavigation/AsideNavigation.tsx +95 -33
  14. package/dist/containers/Heatmap/Heatmap.scss +0 -7
  15. package/dist/containers/Heatmap/Heatmap.tsx +203 -0
  16. package/dist/containers/Heatmap/HeatmapCanvas/HeatmapCanvas.js +2 -1
  17. package/dist/containers/Heatmap/index.ts +1 -0
  18. package/dist/containers/Node/Node.tsx +1 -1
  19. package/dist/containers/Storage/Storage.js +12 -19
  20. package/dist/containers/Tablets/Tablets.scss +0 -5
  21. package/dist/containers/Tablets/Tablets.tsx +172 -0
  22. package/dist/containers/Tablets/i18n/en.json +6 -0
  23. package/dist/{components/AsideNavigation → containers/Tablets}/i18n/index.ts +1 -1
  24. package/dist/containers/Tablets/i18n/ru.json +6 -0
  25. package/dist/containers/Tablets/index.ts +1 -0
  26. package/dist/containers/TabletsFilters/TabletsFilters.js +4 -8
  27. package/dist/containers/TabletsFilters/TabletsFilters.scss +6 -2
  28. package/dist/containers/Tenant/Diagnostics/Diagnostics.tsx +4 -8
  29. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.js +7 -7
  30. package/dist/containers/Tenants/Tenants.js +1 -1
  31. package/dist/containers/UserSettings/UserSettings.tsx +4 -3
  32. package/dist/routes.ts +1 -1
  33. package/dist/store/reducers/{heatmap.js → heatmap.ts} +33 -18
  34. package/dist/store/reducers/settings.js +12 -2
  35. package/dist/types/api/compute.ts +1 -1
  36. package/dist/types/api/schema.ts +16 -3
  37. package/dist/types/store/heatmap.ts +51 -0
  38. package/dist/utils/constants.ts +1 -37
  39. package/dist/utils/getNodesColumns.js +7 -2
  40. package/dist/utils/tablet.ts +53 -0
  41. package/package.json +2 -1
  42. package/dist/components/AsideNavigation/AsideHeader.scss +0 -147
  43. package/dist/components/AsideNavigation/AsideHeader.tsx +0 -389
  44. package/dist/components/AsideNavigation/AsideHeaderFooterItem/AsideHeaderFooterItem.scss +0 -82
  45. package/dist/components/AsideNavigation/AsideHeaderFooterItem/AsideHeaderFooterItem.tsx +0 -138
  46. package/dist/components/AsideNavigation/AsideHeaderFooterSlot/AsideHeaderFooterSlot.tsx +0 -33
  47. package/dist/components/AsideNavigation/AsideHeaderFooterSlot/SlotsContext.tsx +0 -49
  48. package/dist/components/AsideNavigation/AsideHeaderTooltip/AsideHeaderTooltip.scss +0 -16
  49. package/dist/components/AsideNavigation/AsideHeaderTooltip/AsideHeaderTooltip.tsx +0 -37
  50. package/dist/components/AsideNavigation/CompositeBar/CompositeBar.scss +0 -108
  51. package/dist/components/AsideNavigation/CompositeBar/CompositeBar.tsx +0 -282
  52. package/dist/components/AsideNavigation/Content/Content.tsx +0 -35
  53. package/dist/components/AsideNavigation/Drawer/Drawer.scss +0 -76
  54. package/dist/components/AsideNavigation/Drawer/Drawer.tsx +0 -134
  55. package/dist/components/AsideNavigation/Drawer/index.ts +0 -1
  56. package/dist/components/AsideNavigation/Logo/Logo.scss +0 -43
  57. package/dist/components/AsideNavigation/Logo/Logo.tsx +0 -82
  58. package/dist/components/AsideNavigation/Settings/README.md +0 -92
  59. package/dist/components/AsideNavigation/Settings/Settings.scss +0 -128
  60. package/dist/components/AsideNavigation/Settings/Settings.tsx +0 -270
  61. package/dist/components/AsideNavigation/Settings/SettingsMenu/SettingsMenu.scss +0 -78
  62. package/dist/components/AsideNavigation/Settings/SettingsMenu/SettingsMenu.tsx +0 -141
  63. package/dist/components/AsideNavigation/Settings/SettingsSearch/SettingsSearch.tsx +0 -57
  64. package/dist/components/AsideNavigation/Settings/collect-settings.ts +0 -156
  65. package/dist/components/AsideNavigation/Settings/filter-settings.ts +0 -38
  66. package/dist/components/AsideNavigation/Settings/helpers.ts +0 -39
  67. package/dist/components/AsideNavigation/Settings/i18n/en.json +0 -5
  68. package/dist/components/AsideNavigation/Settings/i18n/ru.json +0 -5
  69. package/dist/components/AsideNavigation/Settings/index.ts +0 -1
  70. package/dist/components/AsideNavigation/constants.ts +0 -28
  71. package/dist/components/AsideNavigation/helpers.ts +0 -34
  72. package/dist/components/AsideNavigation/i18n/en.json +0 -4
  73. package/dist/components/AsideNavigation/i18n/ru.json +0 -4
  74. package/dist/components/AsideNavigation/icons.ts +0 -32
  75. package/dist/components/AsideNavigation/types.ts +0 -23
  76. package/dist/components/TabletsStatistic/TabletsStatistic.js +0 -58
  77. package/dist/containers/Heatmap/Heatmap.js +0 -244
  78. package/dist/containers/Tablets/Tablets.js +0 -228
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.2.1](https://github.com/ydb-platform/ydb-embedded-ui/compare/v3.2.0...v3.2.1) (2023-01-12)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * align standard errors to the left ([cce100c](https://github.com/ydb-platform/ydb-embedded-ui/commit/cce100c5df83243df1fb0bc59d84d0d9b33719e6))
9
+ * **TabletsFilters:** properly display long data in select options ([ea37d9f](https://github.com/ydb-platform/ydb-embedded-ui/commit/ea37d9fc08245ccdd38a6120dd620f59a528879c))
10
+ * **TabletsFilters:** replace constants ([ea948ca](https://github.com/ydb-platform/ydb-embedded-ui/commit/ea948ca86276b5521979105b2ab99546da389e80))
11
+ * **TabletsStatistic:** process different tablets state types ([78798de](https://github.com/ydb-platform/ydb-embedded-ui/commit/78798de984bf4f6133515bb1c440e4fe0d15b07e))
12
+ * **Tenant:** always display Pools heading ([94baeff](https://github.com/ydb-platform/ydb-embedded-ui/commit/94baeff82f9c2c1aecda7c11c3b090125ba9e4b6))
13
+
3
14
  ## [3.2.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v3.1.0...v3.2.0) (2023-01-09)
4
15
 
5
16
 
@@ -0,0 +1,34 @@
1
+ import {Label} from '@gravity-ui/uikit';
2
+ import i18n from './i18n';
3
+
4
+ interface EntitiesCountProps {
5
+ current: number | string;
6
+ total?: number | string;
7
+ label?: string;
8
+ loading?: boolean;
9
+ className?: string;
10
+ }
11
+
12
+ export const EntitiesCount = ({total, current, label, loading, className}: EntitiesCountProps) => {
13
+ let content = '';
14
+
15
+ if (label) {
16
+ content += `${label}: `;
17
+ }
18
+
19
+ if (loading) {
20
+ content += '...';
21
+ } else {
22
+ content += `${current}`;
23
+
24
+ if (total && Number(total) !== Number(current)) {
25
+ content += ` ${i18n('of')} ${total}`;
26
+ }
27
+ }
28
+
29
+ return (
30
+ <Label theme="info" size="m" className={className}>
31
+ {content}
32
+ </Label>
33
+ );
34
+ };
@@ -0,0 +1,3 @@
1
+ {
2
+ "of": "of"
3
+ }
@@ -1,9 +1,9 @@
1
- import {i18n, Lang} from '../../../../utils/i18n';
1
+ import {i18n, Lang} from '../../../utils/i18n';
2
2
 
3
3
  import en from './en.json';
4
4
  import ru from './ru.json';
5
5
 
6
- const COMPONENT = 'nv-settings';
6
+ const COMPONENT = 'ydb-entities-count';
7
7
 
8
8
  i18n.registerKeyset(Lang.En, COMPONENT, en);
9
9
  i18n.registerKeyset(Lang.Ru, COMPONENT, ru);
@@ -0,0 +1,3 @@
1
+ {
2
+ "of": "из"
3
+ }
@@ -0,0 +1 @@
1
+ export * from './EntitiesCount';
@@ -1,16 +1,17 @@
1
1
  .kv-fullscreen {
2
- position: fixed;
2
+ // should expand to fill the content area, keeping aside navigation visible
3
+ // counts on .ycn-aside-header__content to have position: relative, it is set in App.scss
4
+ position: absolute;
3
5
  z-index: 10;
4
6
  top: 0;
5
- left: var(--nv-aside-header-size);
7
+ right: 0;
8
+ bottom: 0;
9
+ left: 0;
6
10
 
7
11
  display: flex;
8
12
  overflow: hidden;
9
13
  flex-grow: 1;
10
14
 
11
- width: calc(100% - var(--nv-aside-header-size));
12
- height: 100%;
13
-
14
15
  background-color: var(--yc-color-base-background);
15
16
 
16
17
  &__close-button {
@@ -18,6 +19,7 @@
18
19
  z-index: 11;
19
20
  top: 8px;
20
21
  right: 20px;
22
+
21
23
  .yc-button__text {
22
24
  display: flex;
23
25
  align-items: center;
@@ -25,7 +25,7 @@ const b = cn('kv-tablets-overall');
25
25
  type Color = keyof typeof colors;
26
26
 
27
27
  interface TabletsOverallProps {
28
- tablets: {Overall: string}[];
28
+ tablets: {Overall?: string}[];
29
29
  }
30
30
 
31
31
  function TabletsOverall({tablets}: TabletsOverallProps) {
@@ -48,10 +48,10 @@ function TabletsOverall({tablets}: TabletsOverallProps) {
48
48
 
49
49
  // determine how many tablets of what color are in "tablets"
50
50
  const statesForOverallProgress: Record<string, number> = tablets.reduce((acc, tablet) => {
51
- const color = tablet.Overall.toLowerCase();
52
- if (!acc[color]) {
51
+ const color = tablet.Overall?.toLowerCase();
52
+ if (color && !acc[color]) {
53
53
  acc[color] = 1;
54
- } else {
54
+ } else if (color) {
55
55
  acc[color]++;
56
56
  }
57
57
 
@@ -0,0 +1,56 @@
1
+ import cn from 'bem-cn-lite';
2
+ import {Link} from 'react-router-dom';
3
+
4
+ import {getTabletLabel} from '../../utils/constants';
5
+ import {mapTabletStateToColorState} from '../../utils/tablet';
6
+ import routes, {createHref} from '../../routes';
7
+
8
+ import type {TTabletStateInfo as TFullTabletStateInfo} from '../../types/api/tablet';
9
+ import type {TTabletStateInfo as TComputeTabletStateInfo} from '../../types/api/compute';
10
+
11
+ import './TabletsStatistic.scss';
12
+
13
+ const b = cn('tablets-statistic');
14
+
15
+ type ITablets = TFullTabletStateInfo[] | TComputeTabletStateInfo[];
16
+
17
+ const prepareTablets = (tablets: ITablets) => {
18
+ const res = tablets.map((tablet) => {
19
+ return {
20
+ label: getTabletLabel(tablet.Type),
21
+ type: tablet.Type,
22
+ count: tablet.Count,
23
+ state: mapTabletStateToColorState(tablet.State),
24
+ };
25
+ });
26
+
27
+ return res.sort((x, y) => String(x.label).localeCompare(String(y.label)));
28
+ };
29
+
30
+ interface TabletsStatisticProps {
31
+ tablets: ITablets;
32
+ path: string;
33
+ nodeIds: string[] | number[];
34
+ }
35
+
36
+ export const TabletsStatistic = ({tablets = [], path, nodeIds}: TabletsStatisticProps) => {
37
+ const renderTabletInfo = (item: ReturnType<typeof prepareTablets>[number], index: number) => {
38
+ return (
39
+ <Link
40
+ to={createHref(routes.tabletsFilters, undefined, {
41
+ nodeIds,
42
+ state: item.state,
43
+ type: item.type,
44
+ path,
45
+ })}
46
+ key={index}
47
+ className={b('tablet', {state: item.state?.toLowerCase()})}
48
+ >
49
+ {item.label}: {item.count}
50
+ </Link>
51
+ );
52
+ };
53
+ const preparedTablets = prepareTablets(tablets);
54
+
55
+ return <div className={b()}>{preparedTablets.map(renderTabletInfo)}</div>;
56
+ };
@@ -0,0 +1 @@
1
+ export * from './TabletsStatistic';
@@ -47,11 +47,13 @@ body,
47
47
  color: var(--yc-color-text-secondary);
48
48
  }
49
49
 
50
- .nv-aside-header__pane-container {
50
+ .ycn-aside-header__pane-container {
51
51
  height: 100%;
52
52
  }
53
53
 
54
- .nv-aside-header__content {
54
+ .ycn-aside-header__content {
55
+ position: relative;
56
+
55
57
  display: flex;
56
58
  overflow: auto;
57
59
  flex-direction: column;
@@ -116,17 +118,7 @@ body,
116
118
  }
117
119
 
118
120
  .error {
119
- display: flex;
120
- justify-content: center;
121
- align-items: center;
122
-
123
- margin: 2px;
124
- padding: 2px;
125
-
126
- text-align: center;
127
-
128
121
  color: var(--yc-color-text-danger);
129
- border-color: var(--yc-color-text-danger);
130
122
  }
131
123
 
132
124
  .data-table__row:hover .entity-status__clipboard-button {
@@ -23,21 +23,3 @@
23
23
  padding: 10px;
24
24
  }
25
25
  }
26
-
27
- .nv-aside-header-footer-item__popup {
28
- .nv-user-menu-content__organizations {
29
- margin-top: 19px;
30
- margin-left: 0;
31
- .yc-menu__item {
32
- cursor: auto;
33
- }
34
- }
35
-
36
- .yc-menu__item {
37
- //stylelint-disable-next-line declaration-no-important
38
- padding-right: 0 !important;
39
- &:hover {
40
- background-color: unset;
41
- }
42
- }
43
- }
@@ -1,16 +1,12 @@
1
- import React, {useState} from 'react';
1
+ import React, {useEffect, useState} from 'react';
2
+ import {connect} from 'react-redux';
3
+ import {useLocation} from 'react-router';
2
4
  import {useHistory} from 'react-router-dom';
3
5
  import cn from 'bem-cn-lite';
4
- import {useLocation} from 'react-router';
5
- import {connect} from 'react-redux';
6
6
 
7
- import {
8
- AsideHeader,
9
- AsideHeaderFooterItem,
10
- AsideHeaderMenuItem,
11
- SlotName,
12
- } from '../../components/AsideNavigation/AsideHeader';
13
7
  import {Icon, Button} from '@gravity-ui/uikit';
8
+ import {AsideHeader, MenuItem as AsideHeaderMenuItem, FooterItem} from '@gravity-ui/navigation';
9
+
14
10
  import signOutIcon from '../../assets/icons/signOut.svg';
15
11
  import signInIcon from '../../assets/icons/signIn.svg';
16
12
  import databaseIcon from '../../assets/icons/server.svg';
@@ -20,12 +16,17 @@ import ydbLogoIcon from '../../assets/icons/ydb.svg';
20
16
  import databasesIcon from '../../assets/icons/databases.svg';
21
17
  import userSecret from '../../assets/icons/user-secret.svg';
22
18
  import userChecked from '../../assets/icons/user-check.svg';
23
- //@ts-ignore
19
+ import settingsIcon from '../../assets/icons/settings.svg';
20
+ import supportIcon from '../../assets/icons/support.svg';
21
+
24
22
  import UserSettings from '../UserSettings/UserSettings';
23
+
25
24
  import routes, {createHref, CLUSTER_PAGES} from '../../routes';
26
25
 
27
- //@ts-ignore
28
26
  import {logout, setIsNotAuthenticated} from '../../store/reducers/authentication';
27
+ import {getSettingValue, setSettingValue} from '../../store/reducers/settings';
28
+
29
+ import {ASIDE_HEADER_COMPACT_KEY} from '../../utils/constants';
29
30
 
30
31
  import './AsideNavigation.scss';
31
32
 
@@ -77,17 +78,21 @@ interface YdbUserDropdownProps {
77
78
 
78
79
  function YdbUserDropdown({isCompact, popupAnchor, ydbUser}: YdbUserDropdownProps) {
79
80
  const [isUserDropdownVisible, setIsUserDropdownVisible] = useState(false);
80
- const iconDate = ydbUser?.login ? userChecked : userSecret;
81
+ const iconData = ydbUser?.login ? userChecked : userSecret;
81
82
  return (
82
- <AsideHeaderFooterItem
83
- isCurrent={isUserDropdownVisible}
84
- slot={SlotName.User}
85
- renderCustomIcon={() => <Icon data={iconDate} size={22} className={b('user-icon')} />}
86
- text={ydbUser?.login ?? 'Account'}
87
- isCompact={isCompact}
83
+ <FooterItem
84
+ compact={isCompact}
85
+ item={{
86
+ id: 'user-popup',
87
+ title: ydbUser?.login ?? 'Account',
88
+ current: isUserDropdownVisible,
89
+ icon: iconData,
90
+ iconSize: 22,
91
+ onItemClick: () => setIsUserDropdownVisible(true),
92
+ }}
93
+ enableTooltip={!isUserDropdownVisible}
88
94
  popupAnchor={popupAnchor}
89
95
  popupVisible={isUserDropdownVisible}
90
- onClick={() => setIsUserDropdownVisible(true)}
91
96
  onClosePopup={() => setIsUserDropdownVisible(false)}
92
97
  renderPopupContent={() => (
93
98
  <div className={b('ydb-user-wrapper')}>
@@ -105,8 +110,10 @@ function YdbUserDropdown({isCompact, popupAnchor, ydbUser}: YdbUserDropdownProps
105
110
  interface AsideNavigationProps {
106
111
  children: React.ReactNode;
107
112
  ydbUser: string;
113
+ compact: boolean;
108
114
  logout: VoidFunction;
109
115
  setIsNotAuthenticated: VoidFunction;
116
+ setSettingValue: (name: string, value: string) => void;
110
117
  }
111
118
 
112
119
  const items: MenuItem[] = [
@@ -150,10 +157,30 @@ const items: MenuItem[] = [
150
157
  },
151
158
  ];
152
159
 
160
+ enum Panel {
161
+ UserSettings = 'UserSettings',
162
+ }
163
+
153
164
  function AsideNavigation(props: AsideNavigationProps) {
154
165
  const location = useLocation();
155
166
  const history = useHistory();
156
167
 
168
+ const [visiblePanel, setVisiblePanel] = useState<Panel>();
169
+
170
+ const setIsCompact = (compact: boolean) => {
171
+ props.setSettingValue(ASIDE_HEADER_COMPACT_KEY, JSON.stringify(compact));
172
+ };
173
+
174
+ // navigation managed its compact state internally before, and its approach is not compatible with settings
175
+ // to migrate, save the incoming value again; save only `false` because `true` is the default value
176
+ // assume it is safe to remove this code block if it is at least a few months old
177
+ // there a two of these, search for a similar comment
178
+ useEffect(() => {
179
+ if (props.compact === false) {
180
+ setIsCompact(props.compact);
181
+ }
182
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
183
+
157
184
  const menuItems: AsideHeaderMenuItem[] = React.useMemo(() => {
158
185
  const {pathname} = location;
159
186
  const isClusterPage = pathname === '/cluster';
@@ -184,28 +211,51 @@ function AsideNavigation(props: AsideNavigationProps) {
184
211
  return (
185
212
  <React.Fragment>
186
213
  <AsideHeader
187
- logoText="YDB"
188
- logoIcon={ydbLogoIcon}
189
- onLogoIconClick={() => history.push('/')}
214
+ logo={{
215
+ text: 'YDB',
216
+ icon: ydbLogoIcon,
217
+ onClick: () => history.push('/'),
218
+ }}
190
219
  menuItems={menuItems}
191
- settings={<UserSettings />}
192
- initIsCompact
220
+ compact={props.compact}
221
+ onChangeCompact={setIsCompact}
193
222
  className={b()}
194
223
  renderContent={() => props.children}
195
- renderFooter={({isCompact, asideRef}) => (
224
+ renderFooter={({compact, asideRef}) => (
196
225
  <React.Fragment>
197
- <AsideHeaderFooterItem
198
- slot={SlotName.Support}
199
- iconSize={24}
200
- text="Documentation"
201
- isCompact={isCompact}
202
- onClick={() => {
203
- window.open('http://ydb.tech/docs', '_blank', 'noreferrer');
226
+ <FooterItem
227
+ compact={compact}
228
+ item={{
229
+ id: 'documentation',
230
+ title: 'Documentation',
231
+ icon: supportIcon,
232
+ iconSize: 24,
233
+ onItemClick: () => {
234
+ window.open('https://ydb.tech/docs', '_blank', 'noreferrer');
235
+ },
236
+ }}
237
+ />
238
+
239
+ <FooterItem
240
+ item={{
241
+ id: 'user-settings',
242
+ title: 'Settings',
243
+ icon: settingsIcon,
244
+ iconSize: 24,
245
+ current: visiblePanel === Panel.UserSettings,
246
+ onItemClick: () => {
247
+ setVisiblePanel(
248
+ visiblePanel === Panel.UserSettings
249
+ ? undefined
250
+ : Panel.UserSettings,
251
+ );
252
+ },
204
253
  }}
254
+ compact={compact}
205
255
  />
206
256
 
207
257
  <YdbUserDropdown
208
- isCompact={isCompact}
258
+ isCompact={compact}
209
259
  popupAnchor={asideRef}
210
260
  ydbUser={{
211
261
  login: props.ydbUser,
@@ -215,6 +265,16 @@ function AsideNavigation(props: AsideNavigationProps) {
215
265
  />
216
266
  </React.Fragment>
217
267
  )}
268
+ panelItems={[
269
+ {
270
+ id: 'user-settings',
271
+ visible: visiblePanel === Panel.UserSettings,
272
+ content: <UserSettings />,
273
+ },
274
+ ]}
275
+ onClosePanel={() => {
276
+ setVisiblePanel(undefined);
277
+ }}
218
278
  />
219
279
  </React.Fragment>
220
280
  );
@@ -225,12 +285,14 @@ const mapStateToProps = (state: any) => {
225
285
 
226
286
  return {
227
287
  ydbUser,
288
+ compact: JSON.parse(getSettingValue(state, ASIDE_HEADER_COMPACT_KEY)),
228
289
  };
229
290
  };
230
291
 
231
292
  const mapDispatchToProps = {
232
293
  logout,
233
294
  setIsNotAuthenticated,
295
+ setSettingValue,
234
296
  };
235
297
 
236
298
  export default connect(mapStateToProps, mapDispatchToProps)(AsideNavigation);
@@ -6,13 +6,6 @@
6
6
  height: 100%;
7
7
  @include flex-container();
8
8
 
9
- &__loader {
10
- display: flex;
11
- flex: 1 1 auto;
12
- justify-content: center;
13
- align-items: center;
14
- }
15
-
16
9
  &__limits {
17
10
  display: flex;
18
11
  align-items: center;
@@ -0,0 +1,203 @@
1
+ import React, {useCallback, useEffect, useState} from 'react';
2
+ import {useDispatch} from 'react-redux';
3
+ import cn from 'bem-cn-lite';
4
+
5
+ import {Checkbox, Select} from '@gravity-ui/uikit';
6
+
7
+ import {getTabletsInfo, setHeatmapOptions} from '../../store/reducers/heatmap';
8
+ import {showTooltip, hideTooltip} from '../../store/reducers/tooltip';
9
+ import {formatNumber} from '../../utils';
10
+ import {prepareQueryError} from '../../utils/query';
11
+ import {useAutofetcher, useTypedSelector} from '../../utils/hooks';
12
+ import {Loader} from '../../components/Loader';
13
+ import type {IHeatmapMetricValue} from '../../types/store/heatmap';
14
+
15
+ import {COLORS_RANGE_SIZE, getColorRange, getColorIndex, getCurrentMetricLimits} from './util';
16
+ import {HeatmapCanvas} from './HeatmapCanvas/HeatmapCanvas';
17
+ import {Histogram} from './Histogram/Histogram';
18
+
19
+ import './Heatmap.scss';
20
+
21
+ const b = cn('heatmap');
22
+ const COLORS_RANGE = getColorRange(COLORS_RANGE_SIZE);
23
+
24
+ interface HeatmapProps {
25
+ path: string;
26
+ }
27
+
28
+ export const Heatmap = ({path}: HeatmapProps) => {
29
+ const dispatch = useDispatch();
30
+
31
+ const itemsContainer = React.createRef<HTMLDivElement>();
32
+
33
+ const {autorefresh} = useTypedSelector((state) => state.schema);
34
+ const {
35
+ loading,
36
+ wasLoaded,
37
+ error,
38
+ sort,
39
+ heatmap,
40
+ metrics,
41
+ currentMetric,
42
+ data: tablets = [],
43
+ } = useTypedSelector((state) => state.heatmap);
44
+
45
+ const [selectedMetric, setSelectedMetric] = useState(['']);
46
+
47
+ useEffect(() => {
48
+ if (!currentMetric && metrics && metrics.length) {
49
+ dispatch(
50
+ setHeatmapOptions({
51
+ currentMetric: metrics[0].value,
52
+ }),
53
+ );
54
+ }
55
+ if (currentMetric) {
56
+ setSelectedMetric([currentMetric]);
57
+ }
58
+ }, [currentMetric, metrics, dispatch]);
59
+
60
+ const fetchData = useCallback(
61
+ (isBackground: boolean) => {
62
+ if (!isBackground) {
63
+ dispatch(setHeatmapOptions({wasLoaded: false}));
64
+ }
65
+ dispatch(getTabletsInfo({path}));
66
+ },
67
+ [path, dispatch],
68
+ );
69
+
70
+ useAutofetcher(fetchData, [fetchData], autorefresh);
71
+
72
+ const onShowTooltip = (...args: Parameters<typeof showTooltip>) => {
73
+ dispatch(showTooltip(...args));
74
+ };
75
+
76
+ const onHideTooltip = () => {
77
+ dispatch(hideTooltip);
78
+ };
79
+
80
+ const handleMetricChange = (value: string[]) => {
81
+ dispatch(
82
+ setHeatmapOptions({
83
+ currentMetric: value[0] as IHeatmapMetricValue,
84
+ }),
85
+ );
86
+ };
87
+ const handleCheckboxChange = () => {
88
+ dispatch(
89
+ setHeatmapOptions({
90
+ sort: !sort,
91
+ }),
92
+ );
93
+ };
94
+
95
+ const handleHeatmapChange = () => {
96
+ dispatch(
97
+ setHeatmapOptions({
98
+ heatmap: !heatmap,
99
+ }),
100
+ );
101
+ };
102
+
103
+ const renderHistogram = () => {
104
+ return (
105
+ <Histogram
106
+ tablets={tablets}
107
+ currentMetric={currentMetric}
108
+ showTooltip={onShowTooltip}
109
+ hideTooltip={onHideTooltip}
110
+ />
111
+ );
112
+ };
113
+
114
+ const renderHeatmapCanvas = () => {
115
+ const {min, max} = getCurrentMetricLimits(currentMetric, tablets);
116
+
117
+ const preparedTablets = tablets.map((tablet) => {
118
+ const value = currentMetric && Number(tablet.metrics?.[currentMetric]);
119
+ const colorIndex = getColorIndex(value, min, max);
120
+ const color = COLORS_RANGE[colorIndex];
121
+
122
+ return {
123
+ ...tablet,
124
+ color,
125
+ value,
126
+ formattedValue: formatNumber(value),
127
+ currentMetric,
128
+ };
129
+ });
130
+ const sortedTablets = sort
131
+ ? preparedTablets.sort((x, y) => Number(y.value) - Number(x.value))
132
+ : preparedTablets;
133
+
134
+ return (
135
+ <div ref={itemsContainer} className={b('items')}>
136
+ <HeatmapCanvas
137
+ tablets={sortedTablets}
138
+ parentRef={itemsContainer}
139
+ showTooltip={onShowTooltip}
140
+ hideTooltip={onHideTooltip}
141
+ currentMetric={currentMetric}
142
+ />
143
+ </div>
144
+ );
145
+ };
146
+
147
+ const renderContent = () => {
148
+ const {min, max} = getCurrentMetricLimits(currentMetric, tablets);
149
+
150
+ return (
151
+ <div className={b()}>
152
+ <div className={b('filters')}>
153
+ <Select
154
+ className={b('heatmap-select')}
155
+ value={selectedMetric}
156
+ options={metrics}
157
+ onUpdate={handleMetricChange}
158
+ width={200}
159
+ />
160
+ <div className={b('sort-checkbox')}>
161
+ <Checkbox onUpdate={handleCheckboxChange} checked={sort}>
162
+ Sort
163
+ </Checkbox>
164
+ </div>
165
+ <div className={b('histogram-checkbox')}>
166
+ <Checkbox onUpdate={handleHeatmapChange} checked={heatmap}>
167
+ Heatmap
168
+ </Checkbox>
169
+ </div>
170
+ <div className={b('limits')}>
171
+ <div className={b('limits-block')}>
172
+ <div className={b('limits-title')}>min:</div>
173
+ <div className={b('limits-value')}>
174
+ {Number.isInteger(min) ? formatNumber(min) : '—'}
175
+ </div>
176
+ </div>
177
+ <div className={b('limits-block')}>
178
+ <div className={b('limits-title')}>max:</div>
179
+ <div className={b('limits-value')}>
180
+ {Number.isInteger(max) ? formatNumber(max) : '—'}
181
+ </div>
182
+ </div>
183
+ <div className={b('limits-block')}>
184
+ <div className={b('limits-title')}>count:</div>
185
+ <div className={b('limits-value')}>{formatNumber(tablets.length)}</div>
186
+ </div>
187
+ </div>
188
+ </div>
189
+ {heatmap ? renderHeatmapCanvas() : renderHistogram()}
190
+ </div>
191
+ );
192
+ };
193
+
194
+ if (loading && !wasLoaded) {
195
+ return <Loader />;
196
+ }
197
+
198
+ if (error) {
199
+ return <div>{prepareQueryError(error)}</div>;
200
+ }
201
+
202
+ return renderContent();
203
+ };