ydb-embedded-ui 1.12.1 → 1.13.1

Sign up to get free protection for your applications and to get access to all the features.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.13.1](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.13.0...v1.13.1) (2022-09-02)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **Storage:** fix groups/nodes counter ([9b59ae0](https://github.com/ydb-platform/ydb-embedded-ui/commit/9b59ae0d045beff7aa45560e028618a88bd8483f))
9
+
10
+ ## [1.13.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.12.2...v1.13.0) (2022-09-01)
11
+
12
+
13
+ ### Features
14
+
15
+ * **Storage:** add usage filter component ([a35067f](https://github.com/ydb-platform/ydb-embedded-ui/commit/a35067f8c34ad5d3faf4fb9381c0d6023df9afbd))
16
+ * **Storage:** usage filter ([276f027](https://github.com/ydb-platform/ydb-embedded-ui/commit/276f0270a458601929624a4872ec81e001931853))
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * **Storage:** properly debounce text input filter ([bc5e8fd](https://github.com/ydb-platform/ydb-embedded-ui/commit/bc5e8fd7b067b850f0376b55d995213292b8a31e))
22
+ * **Storage:** use current list size for counter ([e6fea58](https://github.com/ydb-platform/ydb-embedded-ui/commit/e6fea58b075de4c35ad8a60d339417c1e7204d83))
23
+ * **Tenant:** move general tabs outside navigation ([5bf21ea](https://github.com/ydb-platform/ydb-embedded-ui/commit/5bf21eac6f38c0392c8dc6e04be1b6fd0e147064))
24
+
25
+ ## [1.12.2](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.12.1...v1.12.2) (2022-08-29)
26
+
27
+
28
+ ### Bug Fixes
29
+
30
+ * **Storage:** bright red usage starting from 90% ([69b7ed2](https://github.com/ydb-platform/ydb-embedded-ui/commit/69b7ed248151f518ffc5fabbdccf5ea9bbcd9405))
31
+ * **Storage:** display usage without gte sign ([39630a2](https://github.com/ydb-platform/ydb-embedded-ui/commit/39630a2a06b574d53d0ef74c1b3e0dc96b9666a8))
32
+
3
33
  ## [1.12.1](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.12.0...v1.12.1) (2022-08-26)
4
34
 
5
35
 
@@ -5,7 +5,8 @@ import cn from 'bem-cn-lite';
5
5
  import DataTable from '@yandex-cloud/react-data-table';
6
6
  import {RadioButton, Label} from '@yandex-cloud/uikit';
7
7
 
8
- import StorageFilter from './StorageFilter/StorageFilter';
8
+ import {StorageFilter} from './StorageFilter';
9
+ import {UsageFilter} from './UsageFilter';
9
10
  import {AutoFetcher} from '../../utils/autofetcher';
10
11
  import {TableSkeleton} from '../../components/TableSkeleton/TableSkeleton';
11
12
 
@@ -16,12 +17,14 @@ import {
16
17
  VisibleEntities,
17
18
  setVisibleEntities,
18
19
  setStorageFilter,
20
+ setUsageFilter,
19
21
  getNodesObject,
20
22
  StorageTypes,
21
23
  setStorageType,
22
24
  VisibleEntitiesTitles,
23
25
  getStoragePoolsGroupsCount,
24
26
  getStorageNodesCount,
27
+ getUsageFilterOptions,
25
28
  } from '../../store/reducers/storage';
26
29
  import {getNodesList} from '../../store/reducers/clusterNodes';
27
30
  import StorageGroups from './StorageGroups/StorageGroups';
@@ -211,6 +214,7 @@ class Storage extends React.Component {
211
214
  storageType,
212
215
  groupsCount,
213
216
  nodesCount,
217
+ flatListStorageEntities,
214
218
  loading,
215
219
  wasLoaded,
216
220
  } = this.props;
@@ -223,10 +227,11 @@ class Storage extends React.Component {
223
227
  return label;
224
228
  }
225
229
 
226
- if (count.total === count.found) {
227
- label += count.total;
230
+ // count.total can be missing in old versions
231
+ if (flatListStorageEntities.length === Number(count.total) || !count.total) {
232
+ label += flatListStorageEntities.length;
228
233
  } else {
229
- label += `${count.found} of ${count.total}`;
234
+ label += `${flatListStorageEntities.length} of ${count.total}`;
230
235
  }
231
236
 
232
237
  return label;
@@ -234,17 +239,22 @@ class Storage extends React.Component {
234
239
 
235
240
  renderControls() {
236
241
  const {
242
+ filter,
237
243
  setStorageFilter,
238
244
  visibleEntities,
239
245
  storageType,
246
+ usageFilter,
247
+ setUsageFilter,
248
+ usageFilterOptions,
240
249
  } = this.props;
241
250
 
242
251
  return (
243
252
  <div className={b('controls')}>
244
253
  <div className={b('search')}>
245
254
  <StorageFilter
246
- changeReduxStorageFilter={setStorageFilter}
247
- storageType={storageType}
255
+ placeholder={storageType === StorageTypes.groups ? 'Group ID, Pool name' : 'Node ID, FQDN'}
256
+ onChange={setStorageFilter}
257
+ value={filter}
248
258
  />
249
259
  </div>
250
260
  <RadioButton value={visibleEntities} onUpdate={this.onGroupVisibilityChange}>
@@ -267,6 +277,16 @@ class Storage extends React.Component {
267
277
  {StorageTypes.nodes}
268
278
  </RadioButton.Option>
269
279
  </RadioButton>
280
+
281
+ {storageType === StorageTypes.groups && (
282
+ <UsageFilter
283
+ value={usageFilter}
284
+ onChange={setUsageFilter}
285
+ groups={usageFilterOptions}
286
+ disabled={usageFilterOptions.length === 0}
287
+ />
288
+ )}
289
+
270
290
  <Label theme="info" size="m">
271
291
  {this.renderEntitiesCount()}
272
292
  </Label>
@@ -302,6 +322,7 @@ function mapStateToProps(state) {
302
322
  visible: visibleEntities,
303
323
  type: storageType,
304
324
  filter,
325
+ usageFilter,
305
326
  } = state.storage;
306
327
 
307
328
  return {
@@ -316,6 +337,8 @@ function mapStateToProps(state) {
316
337
  visibleEntities,
317
338
  storageType,
318
339
  filter,
340
+ usageFilter,
341
+ usageFilterOptions: getUsageFilterOptions(state),
319
342
  };
320
343
  }
321
344
 
@@ -323,6 +346,7 @@ const mapDispatchToProps = {
323
346
  getStorageInfo,
324
347
  setInitialState,
325
348
  setStorageFilter,
349
+ setUsageFilter,
326
350
  setVisibleEntities: setVisibleEntities,
327
351
  getNodesList,
328
352
  setStorageType,
@@ -0,0 +1,52 @@
1
+ import {useEffect, useRef, useState} from 'react';
2
+
3
+ import {TextInput} from '@yandex-cloud/uikit';
4
+
5
+ interface StorageFilterProps {
6
+ className?: string;
7
+ value?: string;
8
+ placeholder?: string;
9
+ onChange?: (value: string) => void;
10
+ debounce?: number;
11
+ }
12
+
13
+ export const StorageFilter = (props: StorageFilterProps) => {
14
+ const {
15
+ className,
16
+ value = '',
17
+ placeholder,
18
+ onChange,
19
+ debounce = 200,
20
+ } = props;
21
+ const [filterValue, setFilterValue] = useState(value);
22
+ const timer = useRef<number>();
23
+
24
+ useEffect(() => {
25
+ setFilterValue((prevValue) => {
26
+ if (prevValue !== value) {
27
+ return value;
28
+ }
29
+
30
+ return prevValue;
31
+ });
32
+ }, [value]);
33
+
34
+ const changeFilter = (newValue: string) => {
35
+ setFilterValue(newValue);
36
+
37
+ window.clearTimeout(timer.current);
38
+ timer.current = window.setTimeout(() => {
39
+ onChange?.(newValue);
40
+ }, debounce);
41
+ };
42
+
43
+ return (
44
+ <TextInput
45
+ className={className}
46
+ placeholder={placeholder}
47
+ value={filterValue}
48
+ onUpdate={changeFilter}
49
+ hasClear
50
+ />
51
+ );
52
+ }
@@ -0,0 +1 @@
1
+ export * from './StorageFilter';
@@ -14,9 +14,10 @@ import {VisibleEntities} from '../../../store/reducers/storage';
14
14
  import {bytesToGB, bytesToSpeed} from '../../../utils/utils';
15
15
  //@ts-ignore
16
16
  import {stringifyVdiskId} from '../../../utils';
17
+ import {getUsage, isFullDonorData} from '../../../utils/storage';
17
18
 
18
19
  import Vdisk from '../Vdisk/Vdisk';
19
- import {isFullDonorData, getDegradedSeverity, getUsageSeverity, getUsage} from '../utils';
20
+ import {getDegradedSeverity, getUsageSeverity} from '../utils';
20
21
 
21
22
  import './StorageGroups.scss';
22
23
 
@@ -131,9 +132,9 @@ function StorageGroups({data, tableSettings, visibleEntities, nodes}: StorageGro
131
132
  return row.Limit ? (
132
133
  <Label
133
134
  theme={getUsageSeverity(usage)}
134
- className={b('usage-label', {overload: usage >= 100})}
135
+ className={b('usage-label', {overload: usage >= 90})}
135
136
  >
136
- {usage}%
137
+ {usage}%
137
138
  </Label>
138
139
  ) : '-';
139
140
  },
@@ -0,0 +1,31 @@
1
+ .usage-filter {
2
+ &__option {
3
+ flex-grow: 1;
4
+
5
+ &-title {
6
+ height: var(--yc-text-body-1-line-height);
7
+
8
+ font-size: var(--yc-text-body-1-font-size);
9
+ line-height: var(--yc-text-body-1-line-height);
10
+ }
11
+
12
+ &-meta {
13
+ padding: 0 5px;
14
+ position: relative;
15
+ border-radius: 3px;
16
+ font-size: var(--yc-text-caption-2-font-size);
17
+ line-height: var(--yc-text-caption-2-line-height);
18
+ }
19
+
20
+ &-bar {
21
+ position: absolute;
22
+ left: 0;
23
+ top: 0;
24
+ bottom: 0;
25
+ z-index: -1;
26
+
27
+ background-color: var(--yc-color-infographics-info-medium);
28
+ border-radius: 3px;
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,100 @@
1
+ import {useEffect, useMemo, useRef, useState} from 'react';
2
+ import cn from 'bem-cn-lite';
3
+
4
+ import {Select, SelectOption} from '@yandex-cloud/uikit';
5
+
6
+ import EntityStatus from "../../../components/EntityStatus/EntityStatus";
7
+
8
+ import {getUsageSeverityForEntityStatus} from '../utils';
9
+
10
+ import i18n from './i18n';
11
+ import './UsageFilter.scss';
12
+
13
+ interface UsageFilterItem {
14
+ threshold: number;
15
+ count: number;
16
+ }
17
+
18
+ interface UsageFilterProps {
19
+ className?: string;
20
+ value?: string[];
21
+ groups?: UsageFilterItem[];
22
+ onChange?: (value: string[]) => void;
23
+ debounce?: number;
24
+ disabled?: boolean;
25
+ }
26
+
27
+ const b = cn('usage-filter');
28
+
29
+ export const UsageFilter = (props: UsageFilterProps) => {
30
+ const {
31
+ className,
32
+ value = [],
33
+ groups = [],
34
+ onChange,
35
+ debounce = 200,
36
+ disabled,
37
+ } = props;
38
+
39
+ const [filterValue, setFilterValue] = useState(value);
40
+ const timer = useRef<number>();
41
+
42
+ useEffect(() => {
43
+ // sync inner state with external value
44
+ setFilterValue((prevValue) => {
45
+ if (prevValue.join(',') !== value.join(',')) {
46
+ return value;
47
+ }
48
+
49
+ return prevValue;
50
+ });
51
+ }, [value]);
52
+
53
+ const options = useMemo(() => groups.map(({threshold, count}) => ({
54
+ value: String(threshold),
55
+ text: `${threshold}%`,
56
+ data: {count}
57
+ })), [groups]);
58
+
59
+ const handleUpdate = (newValue: string[]) => {
60
+ setFilterValue(newValue);
61
+
62
+ window.clearTimeout(timer.current);
63
+ timer.current = window.setTimeout(() => {
64
+ onChange?.(newValue);
65
+ }, debounce);
66
+ };
67
+
68
+ const maxWidth = Math.max(...groups.map(({count}) => count));
69
+
70
+ const renderOption = ({value, data, text}: SelectOption) => (
71
+ <div className={b('option')}>
72
+ <EntityStatus
73
+ className={b('option-title')}
74
+ status={getUsageSeverityForEntityStatus(Number(value))}
75
+ name={text}
76
+ size="xs"
77
+ />
78
+ <div className={b('option-meta')}>
79
+ {i18n('groups_count', {count: data.count})}
80
+ <div className={b('option-bar')} style={{width: `${data.count / maxWidth * 100}%`}} />
81
+ </div>
82
+ </div>
83
+ );
84
+
85
+ return (
86
+ <Select
87
+ className={b(null, className)}
88
+ label={i18n('label')}
89
+ value={filterValue}
90
+ placeholder={i18n('default_value')}
91
+ options={options}
92
+ multiple
93
+ onUpdate={handleUpdate}
94
+ renderOption={renderOption}
95
+ getOptionHeight={() => 50}
96
+ popupWidth={280}
97
+ disabled={disabled}
98
+ />
99
+ );
100
+ };
@@ -0,0 +1,10 @@
1
+ {
2
+ "label": "Usage:",
3
+ "default_value": "Any",
4
+ "groups_count": [
5
+ "{{count}} group",
6
+ "{{count}} groups",
7
+ "{{count}} groups",
8
+ "No groups"
9
+ ]
10
+ }
@@ -0,0 +1,11 @@
1
+ import {i18n, Lang} from '../../../../utils/i18n';
2
+
3
+ import en from './en.json';
4
+ import ru from './ru.json';
5
+
6
+ const COMPONENT = 'ydb-usage-filter';
7
+
8
+ i18n.registerKeyset(Lang.En, COMPONENT, en);
9
+ i18n.registerKeyset(Lang.Ru, COMPONENT, ru);
10
+
11
+ export default i18n.keyset(COMPONENT);
@@ -0,0 +1,10 @@
1
+ {
2
+ "label": "Использование:",
3
+ "default_value": "Любое",
4
+ "groups_count": [
5
+ "{{count}} группа",
6
+ "{{count}} группы",
7
+ "{{count}} групп",
8
+ "Нет групп"
9
+ ]
10
+ }
@@ -0,0 +1 @@
1
+ export * from './UsageFilter';
@@ -1,33 +1,33 @@
1
- import type {TVDiskStateInfo, TVSlotId} from '../../../types/api/storage';
2
1
  import type {IStoragePoolGroup} from '../../../types/store/storage';
3
2
 
4
3
  export * from './constants';
5
4
 
6
- export const isFullDonorData = (donor: TVDiskStateInfo | TVSlotId): donor is TVDiskStateInfo =>
7
- 'VDiskId' in donor;
8
-
9
- const generateEvaluator = (warn: number, crit: number) =>
5
+ const generateEvaluator = <
6
+ OkLevel extends string,
7
+ WarnLevel extends string,
8
+ CritLevel extends string
9
+ >(warn: number, crit: number, levels: [OkLevel, WarnLevel, CritLevel]) =>
10
10
  (value: number) => {
11
11
  if (0 <= value && value < warn) {
12
- return 'success';
12
+ return levels[0];
13
13
  }
14
14
 
15
15
  if (warn <= value && value < crit) {
16
- return 'warning';
16
+ return levels[1];
17
17
  }
18
18
 
19
19
  if (crit <= value) {
20
- return 'danger';
20
+ return levels[2];
21
21
  }
22
22
 
23
23
  return undefined;
24
24
  };
25
25
 
26
- const defaultDegradationEvaluator = generateEvaluator(1, 2);
26
+ const defaultDegradationEvaluator = generateEvaluator(1, 2, ['success', 'warning', 'danger']);
27
27
 
28
28
  const degradationEvaluators = {
29
- 'block-4-2': generateEvaluator(1, 2),
30
- 'mirror-3-dc': generateEvaluator(1, 3),
29
+ 'block-4-2': generateEvaluator(1, 2, ['success', 'warning', 'danger']),
30
+ 'mirror-3-dc': generateEvaluator(1, 3, ['success', 'warning', 'danger']),
31
31
  };
32
32
 
33
33
  const canEvaluateErasureSpecies = (value?: string): value is keyof typeof degradationEvaluators =>
@@ -41,11 +41,5 @@ export const getDegradedSeverity = (group: IStoragePoolGroup) => {
41
41
  return evaluate(group.Missing);
42
42
  };
43
43
 
44
- export const getUsageSeverity = generateEvaluator(80, 85);
45
-
46
- export const getUsage = (data: IStoragePoolGroup, step = 1) => {
47
- // if limit is 0, display 0
48
- const usage = Math.round((data.Used * 100) / data.Limit) || 0;
49
-
50
- return Math.floor(usage / step) * step;
51
- };
44
+ export const getUsageSeverity = generateEvaluator(80, 85, ['success', 'warning', 'danger']);
45
+ export const getUsageSeverityForEntityStatus = generateEvaluator(80, 85, ['Green', 'Yellow', 'Red']);
@@ -9,28 +9,6 @@
9
9
  height: 100%;
10
10
  max-height: 100%;
11
11
 
12
- &__tabs {
13
- display: flex;
14
- gap: 20px;
15
-
16
- padding: 13px 20px 0;
17
-
18
- box-shadow: inset 0 -1px 0 0 var(--yc-color-line-generic);
19
- .yc-tabs {
20
- box-shadow: unset;
21
- }
22
- }
23
- &__tab {
24
- text-decoration: none;
25
-
26
- // fix for bug in uikit, gap is set for yc-tabs__item:not(:last-child),
27
- // it doesn't work for wrapped items
28
- // feel free to remove if the bug is fixed
29
- &:not(:last-child) {
30
- margin-right: var(--yc-tabs-gap);
31
- }
32
- }
33
-
34
12
  &__loader {
35
13
  display: flex;
36
14
  }
@@ -1,20 +1,15 @@
1
- import {connect} from 'react-redux';
2
- import {Link} from 'react-router-dom';
3
- import cn from 'bem-cn-lite';
4
- import {useLocation} from 'react-router';
5
1
  import qs from 'qs';
6
- import _ from 'lodash';
2
+ import {useLocation} from 'react-router';
3
+ import cn from 'bem-cn-lite';
4
+
5
+ import {useThemeValue} from '@yandex-cloud/uikit';
6
+
7
+ import type {EPathType} from '../../../types/api/schema';
7
8
 
8
- import {Tabs, useThemeValue} from '@yandex-cloud/uikit';
9
- //@ts-ignore
10
9
  import QueryEditor from '../QueryEditor/QueryEditor';
11
10
  import Diagnostics from '../Diagnostics/Diagnostics';
12
11
 
13
- import {TenantGeneralTabsIds, TenantTabsGroups, TENANT_GENERAL_TABS} from '../TenantPages';
14
- import routes, {createHref} from '../../../routes';
15
- import {setSettingValue} from '../../../store/reducers/settings';
16
- import {TENANT_INITIAL_TAB_KEY} from '../../../utils/constants';
17
- import type {EPathType} from '../../../types/api/schema';
12
+ import {TenantGeneralTabsIds} from '../TenantPages';
18
13
 
19
14
  import './ObjectGeneral.scss';
20
15
 
@@ -24,7 +19,6 @@ interface ObjectGeneralProps {
24
19
  type?: EPathType;
25
20
  additionalTenantInfo?: any;
26
21
  additionalNodesInfo?: any;
27
- setSettingValue: (name: string, value: string) => void;
28
22
  }
29
23
 
30
24
  function ObjectGeneral(props: ObjectGeneralProps) {
@@ -38,32 +32,6 @@ function ObjectGeneral(props: ObjectGeneralProps) {
38
32
 
39
33
  const {name: tenantName, general: generalTab} = queryParams;
40
34
 
41
- const renderTabs = () => {
42
- return (
43
- <div className={b('tabs')}>
44
- <Tabs
45
- size="xl"
46
- items={TENANT_GENERAL_TABS}
47
- activeTab={generalTab as string}
48
- wrapTo={({id}, node) => {
49
- const path = createHref(routes.tenant, undefined, {
50
- ...queryParams,
51
- name: tenantName as string,
52
- [TenantTabsGroups.general]: id,
53
- });
54
- return (
55
- <Link to={path} key={id} className={b('tab')}>
56
- {node}
57
- </Link>
58
- );
59
- }}
60
- allowNotSelected
61
- onSelectTab={(id) => props.setSettingValue(TENANT_INITIAL_TAB_KEY, id)}
62
- />
63
- </div>
64
- );
65
- };
66
-
67
35
  const renderTabContent = () => {
68
36
  const {type, additionalTenantInfo, additionalNodesInfo} = props;
69
37
  switch (generalTab) {
@@ -88,7 +56,6 @@ function ObjectGeneral(props: ObjectGeneralProps) {
88
56
  }
89
57
  return (
90
58
  <div className={b()}>
91
- {renderTabs()}
92
59
  {renderTabContent()}
93
60
  </div>
94
61
  );
@@ -97,8 +64,4 @@ function ObjectGeneral(props: ObjectGeneralProps) {
97
64
  return renderContent();
98
65
  }
99
66
 
100
- const mapDispatchToProps = {
101
- setSettingValue,
102
- };
103
-
104
- export default connect(null, mapDispatchToProps)(ObjectGeneral);
67
+ export default ObjectGeneral;
@@ -0,0 +1,9 @@
1
+ @import '../../../styles/mixins.scss';
2
+
3
+ .object-general-tabs {
4
+ padding: 12px 20px 0 12px;
5
+
6
+ &__tab {
7
+ text-decoration: none;
8
+ }
9
+ }
@@ -0,0 +1,68 @@
1
+ import qs from 'qs';
2
+ import {connect} from 'react-redux';
3
+ import {useLocation} from 'react-router';
4
+ import {Link} from 'react-router-dom';
5
+ import cn from 'bem-cn-lite';
6
+
7
+ import {Tabs} from '@yandex-cloud/uikit';
8
+
9
+ import routes, {createHref} from '../../../routes';
10
+ import {TENANT_INITIAL_TAB_KEY} from '../../../utils/constants';
11
+ import {setSettingValue} from '../../../store/reducers/settings';
12
+
13
+ import {TenantTabsGroups, TENANT_GENERAL_TABS} from '../TenantPages';
14
+
15
+ import './ObjectGeneralTabs.scss';
16
+
17
+ const b = cn('object-general-tabs');
18
+
19
+ interface ObjectGeneralTabsProps {
20
+ setSettingValue: (name: string, value: string) => void;
21
+ }
22
+
23
+ function ObjectGeneralTabs(props: ObjectGeneralTabsProps) {
24
+ const location = useLocation();
25
+
26
+ const queryParams = qs.parse(location.search, {
27
+ ignoreQueryPrefix: true,
28
+ });
29
+
30
+ const {name: tenantName, general: generalTab} = queryParams;
31
+
32
+ const renderContent = () => {
33
+ if (!tenantName) {
34
+ return null;
35
+ }
36
+ return (
37
+ <div className={b()}>
38
+ <Tabs
39
+ size="xl"
40
+ items={TENANT_GENERAL_TABS}
41
+ activeTab={generalTab as string}
42
+ wrapTo={({id}, node) => {
43
+ const path = createHref(routes.tenant, undefined, {
44
+ ...queryParams,
45
+ name: tenantName as string,
46
+ [TenantTabsGroups.general]: id,
47
+ });
48
+ return (
49
+ <Link to={path} key={id} className={b('tab')}>
50
+ {node}
51
+ </Link>
52
+ );
53
+ }}
54
+ allowNotSelected
55
+ onSelectTab={(id) => props.setSettingValue(TENANT_INITIAL_TAB_KEY, id)}
56
+ />
57
+ </div>
58
+ );
59
+ };
60
+
61
+ return renderContent();
62
+ }
63
+
64
+ const mapDispatchToProps = {
65
+ setSettingValue,
66
+ };
67
+
68
+ export default connect(null, mapDispatchToProps)(ObjectGeneralTabs);
@@ -45,7 +45,7 @@
45
45
  &__upper-controls {
46
46
  position: absolute;
47
47
  top: -38px;
48
- right: 16px;
48
+ right: 20px;
49
49
 
50
50
  display: flex;
51
51
  gap: 12px;
@@ -7,8 +7,9 @@ import qs from 'qs';
7
7
  import EmptyState from '../../components/EmptyState/EmptyState';
8
8
  import {Illustration} from '../../components/Illustration';
9
9
 
10
- import ObjectSummary from './ObjectSummary/ObjectSummary';
11
10
  import {setHeader} from '../../store/reducers/header';
11
+ import ObjectGeneralTabs from './ObjectGeneralTabs/ObjectGeneralTabs';
12
+ import ObjectSummary from './ObjectSummary/ObjectSummary';
12
13
  import ObjectGeneral from './ObjectGeneral/ObjectGeneral';
13
14
  //@ts-ignore
14
15
  import SplitPane from '../../components/SplitPane';
@@ -131,28 +132,31 @@ function Tenant(props: TenantProps) {
131
132
  description="You don’t have the necessary roles to view this page."
132
133
  />
133
134
  ) : (
134
- <SplitPane
135
- defaultSizePaneKey={DEFAULT_SIZE_TENANT_KEY}
136
- defaultSizes={[25, 75]}
137
- triggerCollapse={summaryVisibilityState.triggerCollapse}
138
- triggerExpand={summaryVisibilityState.triggerExpand}
139
- minSize={[36, 200]}
140
- onSplitStartDragAdditional={onSplitStartDragAdditional}
141
- >
142
- <ObjectSummary
143
- type={currentPathType}
144
- subType={currentPathSubType}
145
- onCollapseSummary={onCollapseSummaryHandler}
146
- onExpandSummary={onExpandSummaryHandler}
147
- isCollapsed={summaryVisibilityState.collapsed}
148
- additionalTenantInfo={props.additionalTenantInfo}
149
- />
150
- <ObjectGeneral
151
- type={currentPathType}
152
- additionalTenantInfo={props.additionalTenantInfo}
153
- additionalNodesInfo={props.additionalNodesInfo}
154
- />
155
- </SplitPane>
135
+ <>
136
+ <ObjectGeneralTabs />
137
+ <SplitPane
138
+ defaultSizePaneKey={DEFAULT_SIZE_TENANT_KEY}
139
+ defaultSizes={[25, 75]}
140
+ triggerCollapse={summaryVisibilityState.triggerCollapse}
141
+ triggerExpand={summaryVisibilityState.triggerExpand}
142
+ minSize={[36, 200]}
143
+ onSplitStartDragAdditional={onSplitStartDragAdditional}
144
+ >
145
+ <ObjectSummary
146
+ type={currentPathType}
147
+ subType={currentPathSubType}
148
+ onCollapseSummary={onCollapseSummaryHandler}
149
+ onExpandSummary={onExpandSummaryHandler}
150
+ isCollapsed={summaryVisibilityState.collapsed}
151
+ additionalTenantInfo={props.additionalTenantInfo}
152
+ />
153
+ <ObjectGeneral
154
+ type={currentPathType}
155
+ additionalTenantInfo={props.additionalTenantInfo}
156
+ additionalNodesInfo={props.additionalNodesInfo}
157
+ />
158
+ </SplitPane>
159
+ </>
156
160
  )}
157
161
  </div>
158
162
  );
@@ -3,6 +3,7 @@ import '../../services/api';
3
3
  import _ from 'lodash';
4
4
  import {createSelector} from 'reselect';
5
5
  import {calcUptime} from '../../utils';
6
+ import {getUsage} from '../../utils/storage';
6
7
 
7
8
  export const VisibleEntities = {
8
9
  All: 'All',
@@ -24,6 +25,7 @@ export const StorageTypes = {
24
25
  const FETCH_STORAGE = createRequestActionTypes('storage', 'FETCH_STORAGE');
25
26
  const SET_INITIAL = 'storage/SET_INITIAL';
26
27
  const SET_FILTER = 'storage/SET_FILTER';
28
+ const SET_USAGE_FILTER = 'storage/SET_USAGE_FILTER';
27
29
  const SET_VISIBLE_GROUPS = 'storage/SET_VISIBLE_GROUPS';
28
30
  const SET_STORAGE_TYPE = 'storage/SET_STORAGE_TYPE';
29
31
 
@@ -31,6 +33,7 @@ const initialState = {
31
33
  loading: true,
32
34
  wasLoaded: false,
33
35
  filter: '',
36
+ usageFilter: [],
34
37
  visible: VisibleEntities.Missing,
35
38
  type: StorageTypes.groups,
36
39
  };
@@ -70,6 +73,12 @@ const storage = function z(state = initialState, action) {
70
73
  filter: action.data,
71
74
  };
72
75
  }
76
+ case SET_USAGE_FILTER: {
77
+ return {
78
+ ...state,
79
+ usageFilter: action.data,
80
+ };
81
+ }
73
82
  case SET_VISIBLE_GROUPS: {
74
83
  return {
75
84
  ...state,
@@ -82,6 +91,8 @@ const storage = function z(state = initialState, action) {
82
91
  return {
83
92
  ...state,
84
93
  type: action.data,
94
+ filter: '',
95
+ usageFilter: [],
85
96
  wasLoaded: false,
86
97
  error: undefined,
87
98
  };
@@ -117,6 +128,14 @@ export function setStorageFilter(value) {
117
128
  data: value,
118
129
  };
119
130
  }
131
+
132
+ export function setUsageFilter(value) {
133
+ return {
134
+ type: SET_USAGE_FILTER,
135
+ data: value,
136
+ };
137
+ }
138
+
120
139
  export function setVisibleEntities(value) {
121
140
  return {
122
141
  type: SET_VISIBLE_GROUPS,
@@ -135,6 +154,7 @@ export const getStorageNodesCount = (state) => ({
135
154
  found: state.storage.data?.FoundNodes || 0,
136
155
  });
137
156
  export const getStorageFilter = (state) => state.storage.filter;
157
+ export const getUsageFilter = (state) => state.storage.usageFilter;
138
158
  export const getVisibleEntities = (state) => state.storage.visible;
139
159
  export const getStorageType = (state) => state.storage.type;
140
160
  export const getNodesObject = (state) =>
@@ -271,26 +291,67 @@ export const getVisibleEntitiesList = createSelector(
271
291
  },
272
292
  );
273
293
 
274
- export const getFilteredEntities = createSelector(
275
- [getStorageFilter, getStorageType, getVisibleEntitiesList],
276
- (filter, type, entities) => {
277
- const cleanedFilter = filter.trim().toLowerCase();
278
- if (!cleanedFilter) {
279
- return entities;
280
- } else {
281
- return _.filter(entities, (e) => {
282
- if (type === StorageTypes.groups) {
283
- return (
284
- e.PoolName.toLowerCase().includes(cleanedFilter) ||
285
- e.GroupID?.toString().includes(cleanedFilter)
286
- );
287
- }
288
- return (
289
- e.NodeId.toString().includes(cleanedFilter) ||
290
- e.FQDN.toLowerCase().includes(cleanedFilter)
291
- );
292
- });
294
+ const filterByText = (entities, type, text) => {
295
+ const cleanedFilter = text.trim().toLowerCase();
296
+
297
+ if (!cleanedFilter) {
298
+ return entities;
299
+ }
300
+
301
+ return entities.filter((entity) => {
302
+ if (type === StorageTypes.groups) {
303
+ return (
304
+ entity.PoolName.toLowerCase().includes(cleanedFilter) ||
305
+ entity.GroupID?.toString().includes(cleanedFilter)
306
+ );
293
307
  }
308
+
309
+ return (
310
+ entity.NodeId.toString().includes(cleanedFilter) ||
311
+ entity.FQDN.toLowerCase().includes(cleanedFilter)
312
+ );
313
+ });
314
+ };
315
+
316
+ const filterByUsage = (entities, usage) => {
317
+ if (!Array.isArray(usage) || usage.length === 0) {
318
+ return entities;
319
+ }
320
+
321
+ return entities.filter((entity) => {
322
+ const entityUsage = getUsage(entity, 5);
323
+ return usage.some((val) => Number(val) <= entityUsage && entityUsage < Number(val) + 5);
324
+ });
325
+ };
326
+
327
+ export const getFilteredEntities = createSelector(
328
+ [getStorageFilter, getUsageFilter, getStorageType, getVisibleEntitiesList],
329
+ (textFilter, usageFilter, type, entities) => {
330
+ let result = entities;
331
+ result = filterByText(result, type, textFilter);
332
+ result = filterByUsage(result, usageFilter);
333
+ return result;
334
+ },
335
+ );
336
+
337
+ export const getUsageFilterOptions = createSelector(
338
+ getVisibleEntitiesList,
339
+ (entities) => {
340
+ const items = {};
341
+
342
+ entities.forEach((entity) => {
343
+ const usage = getUsage(entity, 5);
344
+
345
+ if (!Object.hasOwn(items, usage)) {
346
+ items[usage] = 0;
347
+ }
348
+
349
+ items[usage] += 1;
350
+ });
351
+
352
+ return Object.entries(items)
353
+ .map(([threshold, count]) => ({threshold, count}))
354
+ .sort((a, b) => b.threshold - a.threshold);
294
355
  },
295
356
  );
296
357
 
@@ -0,0 +1,12 @@
1
+ import type {TVDiskStateInfo, TVSlotId} from '../types/api/storage';
2
+ import type {IStoragePoolGroup} from '../types/store/storage';
3
+
4
+ export const isFullDonorData = (donor: TVDiskStateInfo | TVSlotId): donor is TVDiskStateInfo =>
5
+ 'VDiskId' in donor;
6
+
7
+ export const getUsage = (data: IStoragePoolGroup, step = 1) => {
8
+ // if limit is 0, display 0
9
+ const usage = Math.round((data.Used * 100) / data.Limit) || 0;
10
+
11
+ return Math.floor(usage / step) * step;
12
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-embedded-ui",
3
- "version": "1.12.1",
3
+ "version": "1.13.1",
4
4
  "files": [
5
5
  "dist"
6
6
  ],
@@ -1,31 +0,0 @@
1
- import {useEffect, useState} from 'react';
2
- import {TextInput} from '@yandex-cloud/uikit';
3
- import {StorageTypes} from '../../../store/reducers/storage';
4
-
5
- function StorageFilter(props) {
6
- const [filter, setFilter] = useState('');
7
- let timer;
8
-
9
- useEffect(() => {
10
- return () => clearTimeout(timer);
11
- }, []);
12
-
13
- useEffect(() => {
14
- setFilter('');
15
- props.changeReduxStorageFilter('');
16
- }, [props.storageType]);
17
-
18
- const changeFilter = (value) => {
19
- clearTimeout(timer);
20
- setFilter(value);
21
- timer = setTimeout(() => {
22
- props.changeReduxStorageFilter(value);
23
- }, 200);
24
- };
25
- const placeholder =
26
- props.storageType === StorageTypes.groups ? 'Group ID, Pool name' : 'Node ID, FQDN';
27
-
28
- return <TextInput placeholder={placeholder} value={filter} onUpdate={changeFilter} hasClear />;
29
- }
30
-
31
- export default StorageFilter;