ydb-embedded-ui 1.12.1 → 1.13.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.
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;