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 +30 -0
- package/dist/containers/Storage/Storage.js +30 -6
- 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 +4 -3
- 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,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
|
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
|
-
|
227
|
-
|
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 += `${
|
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
|
-
|
247
|
-
|
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 {
|
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 >=
|
135
|
+
className={b('usage-label', {overload: usage >= 90})}
|
135
136
|
>
|
136
|
-
|
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,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;
|