ydb-embedded-ui 3.2.0 → 3.2.1

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 (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
+ };