ydb-embedded-ui 1.12.2 → 1.13.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.13.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.12.2...v1.13.0) (2022-09-01)
4
+
5
+
6
+ ### Features
7
+
8
+ * **Storage:** add usage filter component ([a35067f](https://github.com/ydb-platform/ydb-embedded-ui/commit/a35067f8c34ad5d3faf4fb9381c0d6023df9afbd))
9
+ * **Storage:** usage filter ([276f027](https://github.com/ydb-platform/ydb-embedded-ui/commit/276f0270a458601929624a4872ec81e001931853))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * **Storage:** properly debounce text input filter ([bc5e8fd](https://github.com/ydb-platform/ydb-embedded-ui/commit/bc5e8fd7b067b850f0376b55d995213292b8a31e))
15
+ * **Storage:** use current list size for counter ([e6fea58](https://github.com/ydb-platform/ydb-embedded-ui/commit/e6fea58b075de4c35ad8a60d339417c1e7204d83))
16
+ * **Tenant:** move general tabs outside navigation ([5bf21ea](https://github.com/ydb-platform/ydb-embedded-ui/commit/5bf21eac6f38c0392c8dc6e04be1b6fd0e147064))
17
+
3
18
  ## [1.12.2](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.12.1...v1.12.2) (2022-08-29)
4
19
 
5
20
 
@@ -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,10 @@ class Storage extends React.Component {
223
227
  return label;
224
228
  }
225
229
 
226
- if (count.total === count.found) {
230
+ if (count.total === flatListStorageEntities.length) {
227
231
  label += count.total;
228
232
  } else {
229
- label += `${count.found} of ${count.total}`;
233
+ label += `${flatListStorageEntities.length} of ${count.total}`;
230
234
  }
231
235
 
232
236
  return label;
@@ -234,17 +238,22 @@ class Storage extends React.Component {
234
238
 
235
239
  renderControls() {
236
240
  const {
241
+ filter,
237
242
  setStorageFilter,
238
243
  visibleEntities,
239
244
  storageType,
245
+ usageFilter,
246
+ setUsageFilter,
247
+ usageFilterOptions,
240
248
  } = this.props;
241
249
 
242
250
  return (
243
251
  <div className={b('controls')}>
244
252
  <div className={b('search')}>
245
253
  <StorageFilter
246
- changeReduxStorageFilter={setStorageFilter}
247
- storageType={storageType}
254
+ placeholder={storageType === StorageTypes.groups ? 'Group ID, Pool name' : 'Node ID, FQDN'}
255
+ onChange={setStorageFilter}
256
+ value={filter}
248
257
  />
249
258
  </div>
250
259
  <RadioButton value={visibleEntities} onUpdate={this.onGroupVisibilityChange}>
@@ -267,6 +276,16 @@ class Storage extends React.Component {
267
276
  {StorageTypes.nodes}
268
277
  </RadioButton.Option>
269
278
  </RadioButton>
279
+
280
+ {storageType === StorageTypes.groups && (
281
+ <UsageFilter
282
+ value={usageFilter}
283
+ onChange={setUsageFilter}
284
+ groups={usageFilterOptions}
285
+ disabled={usageFilterOptions.length === 0}
286
+ />
287
+ )}
288
+
270
289
  <Label theme="info" size="m">
271
290
  {this.renderEntitiesCount()}
272
291
  </Label>
@@ -302,6 +321,7 @@ function mapStateToProps(state) {
302
321
  visible: visibleEntities,
303
322
  type: storageType,
304
323
  filter,
324
+ usageFilter,
305
325
  } = state.storage;
306
326
 
307
327
  return {
@@ -316,6 +336,8 @@ function mapStateToProps(state) {
316
336
  visibleEntities,
317
337
  storageType,
318
338
  filter,
339
+ usageFilter,
340
+ usageFilterOptions: getUsageFilterOptions(state),
319
341
  };
320
342
  }
321
343
 
@@ -323,6 +345,7 @@ const mapDispatchToProps = {
323
345
  getStorageInfo,
324
346
  setInitialState,
325
347
  setStorageFilter,
348
+ setUsageFilter,
326
349
  setVisibleEntities: setVisibleEntities,
327
350
  getNodesList,
328
351
  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
 
@@ -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.2",
3
+ "version": "1.13.0",
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;