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 +15 -0
- package/dist/containers/Storage/Storage.js +28 -5
- package/dist/containers/Storage/StorageFilter/StorageFilter.tsx +52 -0
- package/dist/containers/Storage/StorageFilter/index.ts +1 -0
- package/dist/containers/Storage/StorageGroups/StorageGroups.tsx +2 -1
- package/dist/containers/Storage/UsageFilter/UsageFilter.scss +31 -0
- package/dist/containers/Storage/UsageFilter/UsageFilter.tsx +100 -0
- package/dist/containers/Storage/UsageFilter/i18n/en.json +10 -0
- package/dist/containers/Storage/UsageFilter/i18n/index.ts +11 -0
- package/dist/containers/Storage/UsageFilter/i18n/ru.json +10 -0
- package/dist/containers/Storage/UsageFilter/index.ts +1 -0
- package/dist/containers/Storage/utils/index.ts +13 -19
- package/dist/containers/Tenant/ObjectGeneral/ObjectGeneral.scss +0 -22
- package/dist/containers/Tenant/ObjectGeneral/ObjectGeneral.tsx +8 -45
- package/dist/containers/Tenant/ObjectGeneralTabs/ObjectGeneralTabs.scss +9 -0
- package/dist/containers/Tenant/ObjectGeneralTabs/ObjectGeneralTabs.tsx +68 -0
- package/dist/containers/Tenant/QueryEditor/QueryEditor.scss +1 -1
- package/dist/containers/Tenant/Tenant.tsx +27 -23
- package/dist/store/reducers/storage.js +80 -19
- package/dist/utils/storage.ts +12 -0
- package/package.json +1 -1
- package/dist/containers/Storage/StorageFilter/StorageFilter.js +0 -31
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
|
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 ===
|
230
|
+
if (count.total === flatListStorageEntities.length) {
|
227
231
|
label += count.total;
|
228
232
|
} else {
|
229
|
-
label += `${
|
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
|
-
|
247
|
-
|
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 {
|
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,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 @@
|
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
12
|
+
return levels[0];
|
13
13
|
}
|
14
14
|
|
15
15
|
if (warn <= value && value < crit) {
|
16
|
-
return
|
16
|
+
return levels[1];
|
17
17
|
}
|
18
18
|
|
19
19
|
if (crit <= value) {
|
20
|
-
return
|
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
|
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
|
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
|
-
|
101
|
-
setSettingValue,
|
102
|
-
};
|
103
|
-
|
104
|
-
export default connect(null, mapDispatchToProps)(ObjectGeneral);
|
67
|
+
export default ObjectGeneral;
|
@@ -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);
|
@@ -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
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
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
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
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,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;
|