ydb-embedded-ui 1.8.8 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +22 -0
- package/dist/components/BasicNodeViewer/BasicNodeViewer.scss +43 -0
- package/dist/components/BasicNodeViewer/BasicNodeViewer.tsx +53 -0
- package/dist/components/BasicNodeViewer/index.ts +1 -0
- package/dist/components/EntityStatus/EntityStatus.js +15 -3
- package/dist/components/FullNodeViewer/FullNodeViewer.js +29 -48
- package/dist/components/FullNodeViewer/FullNodeViewer.scss +0 -45
- package/dist/components/IndexInfoViewer/IndexInfoViewer.tsx +52 -0
- package/dist/components/InfoViewer/index.ts +4 -0
- package/dist/components/InfoViewer/utils.ts +32 -0
- package/dist/components/ProgressViewer/ProgressViewer.js +1 -1
- package/dist/containers/Node/Node.scss +5 -1
- package/dist/containers/Node/Node.tsx +7 -1
- package/dist/containers/Node/NodeOverview/NodeOverview.tsx +1 -3
- package/dist/containers/Node/NodeStructure/NodeStructure.scss +30 -1
- package/dist/containers/Node/NodeStructure/PDiskTitleBadge.tsx +25 -0
- package/dist/containers/Node/NodeStructure/Pdisk.tsx +24 -2
- package/dist/containers/Nodes/Nodes.js +1 -0
- package/dist/containers/Tenant/Diagnostics/Overview/Overview.tsx +16 -2
- package/dist/containers/Tenant/Schema/SchemaInfoViewer/SchemaInfoViewer.js +20 -16
- package/dist/containers/Tenant/Schema/SchemaTree/SchemaTree.tsx +1 -0
- package/dist/types/api/schema.ts +36 -0
- package/dist/types/api/storage.ts +54 -0
- package/dist/utils/getNodesColumns.js +2 -0
- package/dist/utils/pdisk.ts +74 -0
- package/dist/utils/tooltip.js +27 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
@@ -1,5 +1,27 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [1.9.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.8.8...v1.9.0) (2022-07-29)
|
4
|
+
|
5
|
+
|
6
|
+
### Features
|
7
|
+
|
8
|
+
* **Node:** display endpoints in overview ([89e9e47](https://github.com/ydb-platform/ydb-embedded-ui/commit/89e9e470499b6f458e8949211d97293c0b7d9b97))
|
9
|
+
* **Node:** display node basic info above tabs ([aafb15b](https://github.com/ydb-platform/ydb-embedded-ui/commit/aafb15b399bf116026eff36f3c4ac817e2c40e18))
|
10
|
+
* **Node:** more informative pdisks panels ([342712b](https://github.com/ydb-platform/ydb-embedded-ui/commit/342712bcaa793971e1ca354da57fb962639ef90c))
|
11
|
+
* **Nodes:** show node endpoints in tooltip ([34be559](https://github.com/ydb-platform/ydb-embedded-ui/commit/34be55957e02f947ede30b43f22fde82d21df308))
|
12
|
+
* **Tenant:** table index overview ([2aed714](https://github.com/ydb-platform/ydb-embedded-ui/commit/2aed71488cde1175e6569c236ab609bb126f9cf3))
|
13
|
+
* **Tenant:** virtualized tree in schema ([815f558](https://github.com/ydb-platform/ydb-embedded-ui/commit/815f5588e5fed6fb86f69653c4937e975465372f))
|
14
|
+
* utils for parsing bitfields in pdisk data ([da22b4a](https://github.com/ydb-platform/ydb-embedded-ui/commit/da22b4afde9efe4d9605cefb69ddd51aed989722))
|
15
|
+
|
16
|
+
|
17
|
+
### Bug Fixes
|
18
|
+
|
19
|
+
* **Node:** fix pdisk title items width ([ca5fec6](https://github.com/ydb-platform/ydb-embedded-ui/commit/ca5fec6388364b7d1d6362f1bda36431d9c29749))
|
20
|
+
* **Nodes:** hide tooltip on unmount ([54e4fdc](https://github.com/ydb-platform/ydb-embedded-ui/commit/54e4fdc8045c555338e79d89a93faf58e888fa0e))
|
21
|
+
* **ProgressViewer:** apply provided custom class name ([aa60e9d](https://github.com/ydb-platform/ydb-embedded-ui/commit/aa60e9d1b9c0752853f4323d3bcfd220bedd272d))
|
22
|
+
* **Tenant:** display all table props in overview ([d70e311](https://github.com/ydb-platform/ydb-embedded-ui/commit/d70e311296f6a4d1781f6e72929c70e0db7c3226))
|
23
|
+
* **Tenant:** display PartCount first in table overview ([8c09746](https://github.com/ydb-platform/ydb-embedded-ui/commit/8c09746b026a23a36fe31be94057cc92535aceaa))
|
24
|
+
|
3
25
|
## [1.8.8](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.8.7...v1.8.8) (2022-07-21)
|
4
26
|
|
5
27
|
|
@@ -0,0 +1,43 @@
|
|
1
|
+
@import '../../styles/mixins.scss';
|
2
|
+
|
3
|
+
.basic-node-viewer {
|
4
|
+
font-size: var(--yc-text-body-2-font-size);
|
5
|
+
line-height: var(--yc-text-body-2-line-height);
|
6
|
+
|
7
|
+
display: flex;
|
8
|
+
align-items: center;
|
9
|
+
|
10
|
+
margin: 15px 0;
|
11
|
+
|
12
|
+
&__title {
|
13
|
+
margin: 0 20px 0 0;
|
14
|
+
|
15
|
+
font-size: var(--yc-text-body-2-font-size);
|
16
|
+
font-weight: 600;
|
17
|
+
line-height: var(--yc-text-body-2-line-height);
|
18
|
+
text-transform: uppercase;
|
19
|
+
}
|
20
|
+
|
21
|
+
&__id {
|
22
|
+
margin: 0 15px 0 24px;
|
23
|
+
}
|
24
|
+
|
25
|
+
&__label {
|
26
|
+
margin-right: 10px;
|
27
|
+
|
28
|
+
font-size: var(--yc-text-body-2-font-size);
|
29
|
+
line-height: 18px;
|
30
|
+
white-space: nowrap;
|
31
|
+
|
32
|
+
color: var(--yc-color-text-hint);
|
33
|
+
|
34
|
+
.yc-root_theme_dark & {
|
35
|
+
color: var(--yc-color-text-hint);
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
&__link {
|
40
|
+
margin-left: 5px;
|
41
|
+
@extend .link;
|
42
|
+
}
|
43
|
+
}
|
@@ -0,0 +1,53 @@
|
|
1
|
+
import cn from 'bem-cn-lite';
|
2
|
+
|
3
|
+
import EntityStatus from '../EntityStatus/EntityStatus';
|
4
|
+
import Tags from '../Tags/Tags';
|
5
|
+
import Icon from '../Icon/Icon';
|
6
|
+
|
7
|
+
import './BasicNodeViewer.scss';
|
8
|
+
|
9
|
+
const b = cn('basic-node-viewer');
|
10
|
+
|
11
|
+
interface BasicNodeViewerProps {
|
12
|
+
node: any;
|
13
|
+
additionalNodesInfo?: any;
|
14
|
+
className?: string;
|
15
|
+
}
|
16
|
+
|
17
|
+
export const BasicNodeViewer = ({node, additionalNodesInfo, className}: BasicNodeViewerProps) => {
|
18
|
+
const nodeHref = additionalNodesInfo?.getNodeRef
|
19
|
+
? additionalNodesInfo.getNodeRef(node) + 'internal'
|
20
|
+
: undefined;
|
21
|
+
|
22
|
+
return (
|
23
|
+
<div className={b(null, className)}>
|
24
|
+
{node ? (
|
25
|
+
<>
|
26
|
+
<div className={b('title')}>Node</div>
|
27
|
+
<EntityStatus status={node.SystemState} name={node.Host} />
|
28
|
+
{nodeHref && (
|
29
|
+
<a
|
30
|
+
rel="noopener noreferrer"
|
31
|
+
className={b('link', {external: true})}
|
32
|
+
href={nodeHref}
|
33
|
+
target="_blank"
|
34
|
+
>
|
35
|
+
<Icon name="external" />
|
36
|
+
</a>
|
37
|
+
)}
|
38
|
+
|
39
|
+
<div className={b('id')}>
|
40
|
+
<label className={b('label')}>NodeID</label>
|
41
|
+
<label>{node.NodeId}</label>
|
42
|
+
</div>
|
43
|
+
|
44
|
+
<Tags tags={[node.DataCenter]} />
|
45
|
+
<Tags tags={node.Roles} tagsType="blue" />
|
46
|
+
</>
|
47
|
+
) : (
|
48
|
+
<div className="error">no data</div>
|
49
|
+
)}
|
50
|
+
|
51
|
+
</div>
|
52
|
+
);
|
53
|
+
};
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from './BasicNodeViewer';
|
@@ -12,6 +12,8 @@ class EntityStatus extends React.Component {
|
|
12
12
|
static propTypes = {
|
13
13
|
status: PropTypes.string,
|
14
14
|
name: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
15
|
+
onNameMouseEnter: PropTypes.func,
|
16
|
+
onNameMouseLeave: PropTypes.func,
|
15
17
|
path: PropTypes.string,
|
16
18
|
size: PropTypes.string,
|
17
19
|
label: PropTypes.string,
|
@@ -49,19 +51,29 @@ class EntityStatus extends React.Component {
|
|
49
51
|
);
|
50
52
|
}
|
51
53
|
renderLink() {
|
52
|
-
const {externalLink, name, path} = this.props;
|
54
|
+
const {externalLink, name, path, onNameMouseEnter, onNameMouseLeave} = this.props;
|
53
55
|
|
54
56
|
if (externalLink) {
|
55
57
|
return <ExternalLink href={path}>{name}</ExternalLink>;
|
56
58
|
}
|
57
59
|
|
58
60
|
return path ? (
|
59
|
-
<Link
|
61
|
+
<Link
|
62
|
+
title={name}
|
63
|
+
to={path}
|
64
|
+
onMouseEnter={onNameMouseEnter}
|
65
|
+
onMouseLeave={onNameMouseLeave}
|
66
|
+
>
|
60
67
|
{name}
|
61
68
|
</Link>
|
62
69
|
) : (
|
63
70
|
name && (
|
64
|
-
<span
|
71
|
+
<span
|
72
|
+
className={b('name')}
|
73
|
+
title={name}
|
74
|
+
onMouseEnter={onNameMouseEnter}
|
75
|
+
onMouseLeave={onNameMouseLeave}
|
76
|
+
>
|
65
77
|
{name}
|
66
78
|
</span>
|
67
79
|
)
|
@@ -3,11 +3,8 @@ import cn from 'bem-cn-lite';
|
|
3
3
|
import PropTypes from 'prop-types';
|
4
4
|
|
5
5
|
import InfoViewer from '../InfoViewer/InfoViewer';
|
6
|
-
import EntityStatus from '../EntityStatus/EntityStatus';
|
7
6
|
import ProgressViewer from '../ProgressViewer/ProgressViewer';
|
8
7
|
import PoolUsage from '../PoolUsage/PoolUsage';
|
9
|
-
import Tags from '../Tags/Tags';
|
10
|
-
import Icon from '../Icon/Icon';
|
11
8
|
|
12
9
|
import {LOAD_AVERAGE_TIME_INTERVALS} from '../../utils/constants';
|
13
10
|
import {calcUptime} from '../../utils';
|
@@ -22,7 +19,6 @@ class FullNodeViewer extends React.Component {
|
|
22
19
|
node: PropTypes.object.isRequired,
|
23
20
|
backend: PropTypes.string,
|
24
21
|
singleClusterMode: PropTypes.bool,
|
25
|
-
additionalNodesInfo: PropTypes.object,
|
26
22
|
};
|
27
23
|
|
28
24
|
static defaultProps = {
|
@@ -30,10 +26,12 @@ class FullNodeViewer extends React.Component {
|
|
30
26
|
};
|
31
27
|
|
32
28
|
render() {
|
33
|
-
const {node, className
|
34
|
-
|
35
|
-
|
36
|
-
:
|
29
|
+
const {node, className} = this.props;
|
30
|
+
|
31
|
+
const endpointsInfo = node.Endpoints?.map(({Name, Address}) => ({
|
32
|
+
label: Name,
|
33
|
+
value: Address,
|
34
|
+
}));
|
37
35
|
|
38
36
|
const commonInfo = [
|
39
37
|
{label: 'Version', value: node.Version},
|
@@ -50,52 +48,35 @@ class FullNodeViewer extends React.Component {
|
|
50
48
|
return (
|
51
49
|
<div className={`${b()} ${className}`}>
|
52
50
|
{node ? (
|
53
|
-
<div>
|
54
|
-
<div
|
55
|
-
<div className={b('title')}>
|
56
|
-
<
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
className={b('link', {external: true})}
|
61
|
-
href={nodeHref}
|
62
|
-
target="_blank"
|
63
|
-
>
|
64
|
-
<Icon name="external" />
|
65
|
-
</a>
|
66
|
-
)}
|
67
|
-
|
68
|
-
<div className={b('row', {id: true})}>
|
69
|
-
<label className={b('label', {id: true})}>NodeID</label>
|
70
|
-
<label>{node.NodeId}</label>
|
51
|
+
<div className={b('common-info')}>
|
52
|
+
<div>
|
53
|
+
<div className={b('section-title')}>Pools</div>
|
54
|
+
<div className={b('section', {pools: true})}>
|
55
|
+
{node.PoolStats.map((pool, poolIndex) => (
|
56
|
+
<PoolUsage key={poolIndex} data={pool} />
|
57
|
+
))}
|
71
58
|
</div>
|
72
|
-
|
73
|
-
<Tags tags={[node.DataCenter]} />
|
74
|
-
<Tags tags={node.Roles} tagsType="blue" />
|
75
59
|
</div>
|
76
60
|
|
77
|
-
|
78
|
-
<div>
|
79
|
-
<div className={b('section-title')}>Pools</div>
|
80
|
-
<div className={b('section', {pools: true})}>
|
81
|
-
{node.PoolStats.map((pool, poolIndex) => (
|
82
|
-
<PoolUsage key={poolIndex} data={pool} />
|
83
|
-
))}
|
84
|
-
</div>
|
85
|
-
</div>
|
86
|
-
|
61
|
+
{endpointsInfo && endpointsInfo.length && (
|
87
62
|
<InfoViewer
|
88
|
-
title="
|
63
|
+
title="Endpoints"
|
89
64
|
className={b('section')}
|
90
|
-
info={
|
65
|
+
info={endpointsInfo}
|
91
66
|
/>
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
67
|
+
)}
|
68
|
+
|
69
|
+
<InfoViewer
|
70
|
+
title="Common info"
|
71
|
+
className={b('section')}
|
72
|
+
info={commonInfo}
|
73
|
+
/>
|
74
|
+
|
75
|
+
<InfoViewer
|
76
|
+
title="Load average"
|
77
|
+
className={b('section', {average: true})}
|
78
|
+
info={averageInfo}
|
79
|
+
/>
|
99
80
|
</div>
|
100
81
|
) : (
|
101
82
|
<div className="error">no data</div>
|
@@ -4,26 +4,6 @@
|
|
4
4
|
font-size: var(--yc-text-body-2-font-size);
|
5
5
|
line-height: var(--yc-text-body-2-line-height);
|
6
6
|
|
7
|
-
&__title {
|
8
|
-
margin: 0 20px 0 0;
|
9
|
-
|
10
|
-
font-size: var(--yc-text-body-2-font-size);
|
11
|
-
font-weight: 600;
|
12
|
-
line-height: var(--yc-text-body-2-line-height);
|
13
|
-
text-transform: uppercase;
|
14
|
-
}
|
15
|
-
|
16
|
-
&__row {
|
17
|
-
display: flex;
|
18
|
-
align-items: center;
|
19
|
-
|
20
|
-
margin: 15px 0;
|
21
|
-
|
22
|
-
&_id {
|
23
|
-
margin: 0 15px 0 24px;
|
24
|
-
}
|
25
|
-
}
|
26
|
-
|
27
7
|
&__common-info {
|
28
8
|
display: flex;
|
29
9
|
flex-direction: column;
|
@@ -46,26 +26,6 @@
|
|
46
26
|
min-width: 60px;
|
47
27
|
}
|
48
28
|
|
49
|
-
&__label {
|
50
|
-
min-width: 100px;
|
51
|
-
margin-right: 25px;
|
52
|
-
|
53
|
-
font-size: var(--yc-text-body-2-font-size);
|
54
|
-
line-height: 18px;
|
55
|
-
white-space: nowrap;
|
56
|
-
|
57
|
-
color: var(--yc-color-text-hint);
|
58
|
-
|
59
|
-
.yc-root_theme_dark & {
|
60
|
-
color: var(--yc-color-text-hint);
|
61
|
-
}
|
62
|
-
|
63
|
-
&_id {
|
64
|
-
min-width: auto;
|
65
|
-
margin-right: 10px;
|
66
|
-
}
|
67
|
-
}
|
68
|
-
|
69
29
|
&__section-title {
|
70
30
|
margin: 15px 0 10px;
|
71
31
|
|
@@ -73,9 +33,4 @@
|
|
73
33
|
font-weight: 600;
|
74
34
|
line-height: var(--yc-text-body-2-line-height);
|
75
35
|
}
|
76
|
-
|
77
|
-
&__link {
|
78
|
-
margin-left: 5px;
|
79
|
-
@extend .link;
|
80
|
-
}
|
81
36
|
}
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import type {TEvDescribeSchemeResult, TIndexDescription} from '../../types/api/schema';
|
2
|
+
import {InfoViewer, createInfoFormatter} from '../InfoViewer';
|
3
|
+
|
4
|
+
const DISPLAYED_FIELDS: Set<keyof TIndexDescription> = new Set([
|
5
|
+
'Type',
|
6
|
+
'State',
|
7
|
+
'DataSize',
|
8
|
+
'KeyColumnNames',
|
9
|
+
'DataColumnNames',
|
10
|
+
]);
|
11
|
+
|
12
|
+
const formatItem = createInfoFormatter<TIndexDescription>({
|
13
|
+
Type: (value) => value?.substring(10), // trims EIndexType prefix
|
14
|
+
State: (value) => value?.substring(11), // trims EIndexState prefix
|
15
|
+
KeyColumnNames: (value) => value?.join(', '),
|
16
|
+
DataColumnNames: (value) => value?.join(', '),
|
17
|
+
}, {
|
18
|
+
KeyColumnNames: 'Columns',
|
19
|
+
DataColumnNames: 'Includes',
|
20
|
+
});
|
21
|
+
|
22
|
+
interface IndexInfoViewerProps {
|
23
|
+
data?: TEvDescribeSchemeResult;
|
24
|
+
}
|
25
|
+
|
26
|
+
export const IndexInfoViewer = ({data}: IndexInfoViewerProps) => {
|
27
|
+
if (!data) {
|
28
|
+
return (
|
29
|
+
<div className="error">no index data</div>
|
30
|
+
);
|
31
|
+
}
|
32
|
+
|
33
|
+
const TableIndex = data.PathDescription?.TableIndex;
|
34
|
+
const info: Array<{label?: string, value?: unknown}> = [];
|
35
|
+
|
36
|
+
let key: keyof TIndexDescription;
|
37
|
+
for (key in TableIndex) {
|
38
|
+
if (DISPLAYED_FIELDS.has(key)) {
|
39
|
+
info.push(formatItem(key, TableIndex?.[key]));
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
return (
|
44
|
+
<>
|
45
|
+
{info.length ? (
|
46
|
+
<InfoViewer info={info}></InfoViewer>
|
47
|
+
) : (
|
48
|
+
<>Empty</>
|
49
|
+
)}
|
50
|
+
</>
|
51
|
+
);
|
52
|
+
};
|
@@ -0,0 +1,32 @@
|
|
1
|
+
type LabelMap<T> = {
|
2
|
+
[label in keyof T]?: string;
|
3
|
+
}
|
4
|
+
|
5
|
+
type FieldMappers<T> = {
|
6
|
+
[label in keyof T]?: (value: T[label]) => string | undefined;
|
7
|
+
}
|
8
|
+
|
9
|
+
function formatLabel<Shape>(label: keyof Shape, map: LabelMap<Shape>) {
|
10
|
+
return map[label] ?? label;
|
11
|
+
}
|
12
|
+
|
13
|
+
function formatValue<Shape, Key extends keyof Shape>(
|
14
|
+
label: Key,
|
15
|
+
value: Shape[Key],
|
16
|
+
mappers: FieldMappers<Shape>,
|
17
|
+
) {
|
18
|
+
const mapper = mappers[label];
|
19
|
+
const mappedValue = mapper ? mapper(value) : value;
|
20
|
+
|
21
|
+
return String(mappedValue ?? '');
|
22
|
+
}
|
23
|
+
|
24
|
+
export function createInfoFormatter<Shape extends Record<string, any>>(
|
25
|
+
fieldMappers?: FieldMappers<Shape>,
|
26
|
+
labelMap?: LabelMap<Shape>,
|
27
|
+
) {
|
28
|
+
return <Key extends keyof Shape>(label: Key, value: Shape[Key]) => ({
|
29
|
+
label: formatLabel(label, labelMap || {}),
|
30
|
+
value: formatValue(label, value, fieldMappers || {}),
|
31
|
+
});
|
32
|
+
}
|
@@ -76,7 +76,7 @@ export class ProgressViewer extends React.Component {
|
|
76
76
|
|
77
77
|
if (!isNaN(fillWidth)) {
|
78
78
|
return (
|
79
|
-
<div className={b({size})}>
|
79
|
+
<div className={b({size}, className)}>
|
80
80
|
<div className={b('line', {bg})} style={lineStyle}></div>
|
81
81
|
<span className={b('text', {text})}>
|
82
82
|
{`${valueText} ${divider} ${capacityText}`}
|
@@ -13,6 +13,7 @@ import Storage from '../Storage/Storage';
|
|
13
13
|
import NodeOverview from './NodeOverview/NodeOverview';
|
14
14
|
import NodeStructure from './NodeStructure/NodeStructure';
|
15
15
|
import Loader from '../../components/Loader/Loader';
|
16
|
+
import {BasicNodeViewer} from '../../components/BasicNodeViewer';
|
16
17
|
|
17
18
|
import {getNodeInfo, resetNode} from '../../store/reducers/node';
|
18
19
|
import routes, {CLUSTER_PAGES, createHref} from '../../routes';
|
@@ -133,7 +134,6 @@ function Node(props: NodeProps) {
|
|
133
134
|
case OVERVIEW: {
|
134
135
|
return (
|
135
136
|
<NodeOverview
|
136
|
-
additionalNodesInfo={additionalNodesInfo}
|
137
137
|
node={node}
|
138
138
|
className={b('overview-wrapper')}
|
139
139
|
/>
|
@@ -162,6 +162,12 @@ function Node(props: NodeProps) {
|
|
162
162
|
if (node) {
|
163
163
|
return (
|
164
164
|
<div className={b(null, props.className)}>
|
165
|
+
<BasicNodeViewer
|
166
|
+
node={node}
|
167
|
+
additionalNodesInfo={props.additionalNodesInfo}
|
168
|
+
className={b('header')}
|
169
|
+
/>
|
170
|
+
|
165
171
|
{renderTabs()}
|
166
172
|
|
167
173
|
<div className={b('content')}>{renderTabContent()}</div>
|
@@ -5,16 +5,14 @@ import {backend} from '../../../store';
|
|
5
5
|
|
6
6
|
interface NodeOverviewProps {
|
7
7
|
node: any;
|
8
|
-
additionalNodesInfo: any;
|
9
8
|
className?: string;
|
10
9
|
}
|
11
10
|
|
12
|
-
function NodeOverview({node,
|
11
|
+
function NodeOverview({node, className}: NodeOverviewProps) {
|
13
12
|
return (
|
14
13
|
<FullNodeViewer
|
15
14
|
node={node}
|
16
15
|
backend={backend}
|
17
|
-
additionalNodesInfo={additionalNodesInfo}
|
18
16
|
className={className}
|
19
17
|
/>
|
20
18
|
);
|
@@ -44,9 +44,38 @@
|
|
44
44
|
&__pdisk-title-wrapper {
|
45
45
|
display: flex;
|
46
46
|
align-items: center;
|
47
|
-
gap:
|
47
|
+
gap: 16px;
|
48
48
|
|
49
49
|
font-weight: 600;
|
50
|
+
|
51
|
+
.entity-status__status-icon {
|
52
|
+
margin-right: 0;
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
&__pdisk-title-item {
|
57
|
+
display: flex;
|
58
|
+
gap: 4px;
|
59
|
+
|
60
|
+
&-label {
|
61
|
+
font-weight: 400;
|
62
|
+
|
63
|
+
color: var(--yc-color-text-secondary);
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
&__pdisk-title-id {
|
68
|
+
min-width: 110px;
|
69
|
+
}
|
70
|
+
|
71
|
+
&__pdisk-title-type {
|
72
|
+
justify-content: flex-end;
|
73
|
+
|
74
|
+
min-width: 50px;
|
75
|
+
}
|
76
|
+
|
77
|
+
&__pdisk-title-size {
|
78
|
+
min-width: 150px;
|
50
79
|
}
|
51
80
|
|
52
81
|
&__pdisk-details {
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import {ReactNode} from 'react';
|
2
|
+
import cn from 'bem-cn-lite';
|
3
|
+
|
4
|
+
const b = cn('kv-node-structure');
|
5
|
+
|
6
|
+
interface PDiskTitleBadgeProps {
|
7
|
+
label?: string;
|
8
|
+
value: ReactNode;
|
9
|
+
className?: string;
|
10
|
+
}
|
11
|
+
|
12
|
+
export function PDiskTitleBadge({label, value, className}: PDiskTitleBadgeProps) {
|
13
|
+
return (
|
14
|
+
<span className={b('pdisk-title-item', className)}>
|
15
|
+
{label && (
|
16
|
+
<span className={b('pdisk-title-item-label')}>
|
17
|
+
{label}:
|
18
|
+
</span>
|
19
|
+
)}
|
20
|
+
<span className={b('pdisk-title-item-value')}>
|
21
|
+
{value}
|
22
|
+
</span>
|
23
|
+
</span>
|
24
|
+
);
|
25
|
+
}
|
@@ -14,9 +14,11 @@ import {Vdisk} from './Vdisk';
|
|
14
14
|
|
15
15
|
import {bytesToGB, pad9} from '../../../utils/utils';
|
16
16
|
import {formatStorageValuesToGb} from '../../../utils';
|
17
|
+
import {getPDiskType} from '../../../utils/pdisk';
|
17
18
|
|
18
19
|
import {DEFAULT_TABLE_SETTINGS} from '../../../utils/constants';
|
19
20
|
import {valueIsDefined} from './NodeStructure';
|
21
|
+
import {PDiskTitleBadge} from './PDiskTitleBadge';
|
20
22
|
|
21
23
|
const b = cn('kv-node-structure');
|
22
24
|
|
@@ -230,6 +232,7 @@ export function PDisk(props: PDiskProps) {
|
|
230
232
|
}
|
231
233
|
if (valueIsDefined(Category)) {
|
232
234
|
pdiskInfo.push({label: 'Category', value: Category});
|
235
|
+
pdiskInfo.push({label: 'Type', value: getPDiskType(data)});
|
233
236
|
}
|
234
237
|
pdiskInfo.push({
|
235
238
|
label: 'Allocated Size',
|
@@ -286,8 +289,27 @@ export function PDisk(props: PDiskProps) {
|
|
286
289
|
<div className={b('pdisk')} id={props.id}>
|
287
290
|
<div className={b('pdisk-header')}>
|
288
291
|
<div className={b('pdisk-title-wrapper')}>
|
289
|
-
<
|
290
|
-
<
|
292
|
+
<EntityStatus status={data.Device} />
|
293
|
+
<PDiskTitleBadge
|
294
|
+
label="PDiskID"
|
295
|
+
value={data.PDiskId}
|
296
|
+
className={b('pdisk-title-id')}
|
297
|
+
/>
|
298
|
+
<PDiskTitleBadge
|
299
|
+
value={getPDiskType(data)}
|
300
|
+
className={b('pdisk-title-type')}
|
301
|
+
/>
|
302
|
+
<ProgressViewer
|
303
|
+
value={data.TotalSize - data.AvailableSize}
|
304
|
+
capacity={data.TotalSize}
|
305
|
+
formatValues={formatStorageValuesToGb}
|
306
|
+
colorizeProgress={true}
|
307
|
+
className={b('pdisk-title-size')}
|
308
|
+
/>
|
309
|
+
<PDiskTitleBadge
|
310
|
+
label="VDisks"
|
311
|
+
value={data.vDisks.length}
|
312
|
+
/>
|
291
313
|
</div>
|
292
314
|
<Button onClick={unfolded ? onClosePDiskDetails : onOpenPDiskDetails} view="flat-secondary">
|
293
315
|
<ArrowToggle direction={unfolded ? 'top' : 'bottom'} />
|
@@ -6,9 +6,10 @@ import {Loader} from '@yandex-cloud/uikit';
|
|
6
6
|
|
7
7
|
//@ts-ignore
|
8
8
|
import SchemaInfoViewer from '../../Schema/SchemaInfoViewer/SchemaInfoViewer';
|
9
|
+
import {IndexInfoViewer} from '../../../../components/IndexInfoViewer/IndexInfoViewer';
|
9
10
|
|
10
11
|
import type {EPathType} from '../../../../types/api/schema';
|
11
|
-
import {isColumnEntityType, isTableType} from '../../utils/schema';
|
12
|
+
import {isColumnEntityType, isTableType, mapPathTypeToNavigationTreeType} from '../../utils/schema';
|
12
13
|
import {AutoFetcher} from '../../../../utils/autofetcher';
|
13
14
|
//@ts-ignore
|
14
15
|
import {getSchema} from '../../../../store/reducers/schema';
|
@@ -112,11 +113,24 @@ function Overview(props: OverviewProps) {
|
|
112
113
|
);
|
113
114
|
};
|
114
115
|
|
116
|
+
const renderContent = () => {
|
117
|
+
switch (mapPathTypeToNavigationTreeType(props.type)) {
|
118
|
+
case 'index':
|
119
|
+
return (
|
120
|
+
<IndexInfoViewer data={schemaData} />
|
121
|
+
);
|
122
|
+
default:
|
123
|
+
return (
|
124
|
+
<SchemaInfoViewer fullPath={currentItem.Path} data={schemaData} />
|
125
|
+
);
|
126
|
+
}
|
127
|
+
}
|
128
|
+
|
115
129
|
return loading && !wasLoaded ? (
|
116
130
|
renderLoader()
|
117
131
|
) : (
|
118
132
|
<div className={props.className}>
|
119
|
-
|
133
|
+
{renderContent()}
|
120
134
|
</div>
|
121
135
|
);
|
122
136
|
}
|
@@ -28,29 +28,33 @@ class SchemaInfoViewer extends React.Component {
|
|
28
28
|
if (data) {
|
29
29
|
const {PathDescription = {}} = data;
|
30
30
|
const {TableStats = {}, TabletMetrics = {}} = PathDescription;
|
31
|
-
const
|
32
|
-
TableStats &&
|
33
|
-
Object.keys(TableStats).map((key) => ({
|
34
|
-
label: key,
|
35
|
-
value: TableStats[key].toString(),
|
36
|
-
}));
|
31
|
+
const {PartCount, ...restTableStats} = TableStats;
|
37
32
|
|
38
|
-
const
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
value: this.formatTabletMetricsValue(key, TabletMetrics[key].toString()),
|
43
|
-
}));
|
33
|
+
const priorityInfo = [{
|
34
|
+
label: 'PartCount',
|
35
|
+
value: PartCount,
|
36
|
+
}].filter(({value}) => value !== undefined);
|
44
37
|
|
45
|
-
|
46
|
-
|
38
|
+
const tableStatsInfo = Object.keys(restTableStats).map((key) => ({
|
39
|
+
label: key,
|
40
|
+
value: TableStats[key].toString(),
|
41
|
+
}));
|
47
42
|
|
48
|
-
const
|
43
|
+
const tabletMetricsInfo = Object.keys(TabletMetrics).map((key) => ({
|
44
|
+
label: key,
|
45
|
+
value: this.formatTabletMetricsValue(key, TabletMetrics[key].toString()),
|
46
|
+
}));
|
49
47
|
|
48
|
+
const generalInfo = [
|
49
|
+
...priorityInfo,
|
50
|
+
...tabletMetricsInfo,
|
51
|
+
...tableStatsInfo,
|
52
|
+
];
|
53
|
+
|
50
54
|
return (
|
51
55
|
<div className={b()}>
|
52
56
|
<div className={b('item')}>
|
53
|
-
{
|
57
|
+
{generalInfo.length ? (
|
54
58
|
<InfoViewer info={generalInfo}></InfoViewer>
|
55
59
|
) : (
|
56
60
|
<div>Empty</div>
|
package/dist/types/api/schema.ts
CHANGED
@@ -54,6 +54,8 @@ interface TPathDescription {
|
|
54
54
|
|
55
55
|
ColumnStoreDescription?: unknown;
|
56
56
|
ColumnTableDescription?: unknown;
|
57
|
+
|
58
|
+
TableIndex?: TIndexDescription;
|
57
59
|
}
|
58
60
|
|
59
61
|
interface TDirEntry {
|
@@ -80,6 +82,27 @@ interface TDirEntry {
|
|
80
82
|
Version?: TPathVersion;
|
81
83
|
}
|
82
84
|
|
85
|
+
export interface TIndexDescription {
|
86
|
+
Name?: string;
|
87
|
+
/** uint64 */
|
88
|
+
LocalPathId?: string;
|
89
|
+
|
90
|
+
Type?: EIndexType;
|
91
|
+
State?: EIndexState;
|
92
|
+
|
93
|
+
KeyColumnNames?: string[];
|
94
|
+
|
95
|
+
/** uint64 */
|
96
|
+
SchemaVersion?: string;
|
97
|
+
|
98
|
+
/** uint64 */
|
99
|
+
PathOwnerId?: string;
|
100
|
+
|
101
|
+
DataColumnNames?: string[];
|
102
|
+
/** uint64 */
|
103
|
+
DataSize?: string;
|
104
|
+
}
|
105
|
+
|
83
106
|
// incomplete
|
84
107
|
export enum EPathType {
|
85
108
|
EPathTypeInvalid = 'EPathTypeInvalid',
|
@@ -112,6 +135,19 @@ enum EPathState {
|
|
112
135
|
EPathStateMoving = 'EPathStateMoving',
|
113
136
|
}
|
114
137
|
|
138
|
+
enum EIndexType {
|
139
|
+
EIndexTypeInvalid = 'EIndexTypeInvalid',
|
140
|
+
EIndexTypeGlobal = 'EIndexTypeGlobal',
|
141
|
+
EIndexTypeGlobalAsync = 'EIndexTypeGlobalAsync',
|
142
|
+
}
|
143
|
+
|
144
|
+
enum EIndexState {
|
145
|
+
EIndexStateInvalid = 'EIndexStateInvalid',
|
146
|
+
EIndexStateReady = 'EIndexStateReady',
|
147
|
+
EIndexStateNotReady = 'EIndexStateNotReady',
|
148
|
+
EIndexStateWriteOnly = 'EIndexStateWriteOnly',
|
149
|
+
}
|
150
|
+
|
115
151
|
// incomplete
|
116
152
|
interface TPathVersion {
|
117
153
|
/** uint64 */
|
@@ -0,0 +1,54 @@
|
|
1
|
+
enum EFlag {
|
2
|
+
Grey = 'Grey',
|
3
|
+
Green = 'Green',
|
4
|
+
Yellow = 'Yellow',
|
5
|
+
Orange = 'Orange',
|
6
|
+
Red = 'Red',
|
7
|
+
}
|
8
|
+
|
9
|
+
enum TPDiskState {
|
10
|
+
Initial = 'Initial',
|
11
|
+
InitialFormatRead = 'InitialFormatRead',
|
12
|
+
InitialFormatReadError = 'InitialFormatReadError',
|
13
|
+
InitialSysLogRead = 'InitialSysLogRead',
|
14
|
+
InitialSysLogReadError = 'InitialSysLogReadError',
|
15
|
+
InitialSysLogParseError = 'InitialSysLogParseError',
|
16
|
+
InitialCommonLogRead = 'InitialCommonLogRead',
|
17
|
+
InitialCommonLogReadError = 'InitialCommonLogReadError',
|
18
|
+
InitialCommonLogParseError = 'InitialCommonLogParseError',
|
19
|
+
CommonLoggerInitError = 'CommonLoggerInitError',
|
20
|
+
Normal = 'Normal',
|
21
|
+
OpenFileError = 'OpenFileError',
|
22
|
+
ChunkQuotaError = 'ChunkQuotaError',
|
23
|
+
DeviceIoError = 'DeviceIoError',
|
24
|
+
|
25
|
+
Missing = 'Missing',
|
26
|
+
Timeout = 'Timeout',
|
27
|
+
NodeDisconnected = 'NodeDisconnected',
|
28
|
+
Unknown = 'Unknown',
|
29
|
+
}
|
30
|
+
|
31
|
+
export interface TPDiskStateInfo {
|
32
|
+
PDiskId?: number;
|
33
|
+
/** uint64 */
|
34
|
+
CreateTime?: string;
|
35
|
+
/** uint64 */
|
36
|
+
ChangeTime?: string;
|
37
|
+
Path?: string;
|
38
|
+
/** uint64 */
|
39
|
+
Guid?: string;
|
40
|
+
/** uint64 */
|
41
|
+
Category?: string;
|
42
|
+
/** uint64 */
|
43
|
+
AvailableSize?: string;
|
44
|
+
/** uint64 */
|
45
|
+
TotalSize?: string;
|
46
|
+
State?: TPDiskState;
|
47
|
+
NodeId?: number;
|
48
|
+
Count?: number;
|
49
|
+
Device?: EFlag;
|
50
|
+
Realtime?: EFlag;
|
51
|
+
StateFlag?: EFlag;
|
52
|
+
Overall?: EFlag;
|
53
|
+
SerialNumber?: string;
|
54
|
+
}
|
@@ -33,6 +33,8 @@ export function getNodesColumns({showTooltip, hideTooltip, tabletsPath, getNodeR
|
|
33
33
|
<div className={b('host-name-wrapper')}>
|
34
34
|
<EntityStatus
|
35
35
|
name={row.Host}
|
36
|
+
onNameMouseEnter={(e) => showTooltip(e.target, row, 'nodeEndpoints')}
|
37
|
+
onNameMouseLeave={hideTooltip}
|
36
38
|
status={row.Overall}
|
37
39
|
path={getDefaultNodePath(row.NodeId)}
|
38
40
|
hasClipboardButton
|
@@ -0,0 +1,74 @@
|
|
1
|
+
import type {TPDiskStateInfo} from "../types/api/storage";
|
2
|
+
|
3
|
+
// TODO: move to utils or index after converting them to TS
|
4
|
+
/**
|
5
|
+
* Parses a binary string containing a bit field into an object with binary values.
|
6
|
+
* This is an implementation based on string manipulation, since JS can only perform
|
7
|
+
* bitwise operations with 32-bits integers, and YDB sends uint64.
|
8
|
+
* @see https://en.cppreference.com/w/c/language/bit_field
|
9
|
+
* @param binaryString - binary string representing a bit field
|
10
|
+
* @param bitFieldStruct - bit field description, <field => size in bits>, in order starting from the rightmost bit
|
11
|
+
* @returns object with binary values
|
12
|
+
*/
|
13
|
+
export const parseBitField = <T extends Record<string, number>>(
|
14
|
+
binaryString: string,
|
15
|
+
bitFieldStruct: T,
|
16
|
+
): Record<keyof T, string> => {
|
17
|
+
const fields: Partial<Record<keyof T, string>> = {};
|
18
|
+
|
19
|
+
Object.entries(bitFieldStruct).reduce((prefixSize, [field, size]: [keyof T, number]) => {
|
20
|
+
const end = binaryString.length - prefixSize;
|
21
|
+
const start = end - size;
|
22
|
+
fields[field] = binaryString.substring(start, end) || '0';
|
23
|
+
|
24
|
+
return prefixSize + size;
|
25
|
+
}, 0);
|
26
|
+
|
27
|
+
return fields as Record<keyof T, string>;
|
28
|
+
};
|
29
|
+
|
30
|
+
export enum IPDiskType {
|
31
|
+
ROT = 'ROT',
|
32
|
+
SSD = 'SSD',
|
33
|
+
MVME = 'NVME',
|
34
|
+
}
|
35
|
+
|
36
|
+
// Bear with me.
|
37
|
+
// Disk type is determined by the field Category.
|
38
|
+
// Category is a bit field defined as follows:
|
39
|
+
// struct {
|
40
|
+
// ui64 IsSolidState : 1;
|
41
|
+
// ui64 Kind : 55;
|
42
|
+
// ui64 TypeExt : 8;
|
43
|
+
// }
|
44
|
+
// For compatibility TypeExt is not used for old types (ROT, SSD), so the following scheme is used:
|
45
|
+
// ROT -> IsSolidState# 0, TypeExt# 0
|
46
|
+
// SSD -> IsSolidState# 1, TypeExt# 0
|
47
|
+
// NVME -> IsSolidState# 1, TypeExt# 2
|
48
|
+
// Reference on bit fields: https://en.cppreference.com/w/c/language/bit_field
|
49
|
+
export const getPDiskType = (data: TPDiskStateInfo): IPDiskType | undefined => {
|
50
|
+
if (!data.Category) {
|
51
|
+
return undefined;
|
52
|
+
}
|
53
|
+
|
54
|
+
// Category is uint64, too big for Number or bitwise operators, thus BigInt and a custom parser
|
55
|
+
const categotyBin = BigInt(data.Category).toString(2);
|
56
|
+
const categoryBitField = parseBitField(categotyBin, {
|
57
|
+
isSolidState: 1,
|
58
|
+
kind: 55,
|
59
|
+
typeExt: 8,
|
60
|
+
});
|
61
|
+
|
62
|
+
if (categoryBitField.isSolidState === '1') {
|
63
|
+
switch (parseInt(categoryBitField.typeExt, 2)) {
|
64
|
+
case 0:
|
65
|
+
return IPDiskType.SSD;
|
66
|
+
case 2:
|
67
|
+
return IPDiskType.MVME;
|
68
|
+
}
|
69
|
+
} else if (categoryBitField.typeExt === '0') {
|
70
|
+
return IPDiskType.ROT;
|
71
|
+
}
|
72
|
+
|
73
|
+
return undefined;
|
74
|
+
};
|
package/dist/utils/tooltip.js
CHANGED
@@ -109,6 +109,32 @@ const NodeTooltip = (props) => {
|
|
109
109
|
);
|
110
110
|
};
|
111
111
|
|
112
|
+
const NodeEndpointsTooltip = (props) => {
|
113
|
+
const {data} = props;
|
114
|
+
return (
|
115
|
+
data && (
|
116
|
+
<div className={nodeB()}>
|
117
|
+
<table>
|
118
|
+
<tbody>
|
119
|
+
{data.Rack && (
|
120
|
+
<tr>
|
121
|
+
<td className={nodeB('label')}>Rack</td>
|
122
|
+
<td className={nodeB('value')}>{data.Rack}</td>
|
123
|
+
</tr>
|
124
|
+
)}
|
125
|
+
{data.Endpoints && data.Endpoints.length && data.Endpoints.map(({Name, Address}) => (
|
126
|
+
<tr key={Name}>
|
127
|
+
<td className={nodeB('label')}>{Name}</td>
|
128
|
+
<td className={nodeB('value')}>{Address}</td>
|
129
|
+
</tr>
|
130
|
+
))}
|
131
|
+
</tbody>
|
132
|
+
</table>
|
133
|
+
</div>
|
134
|
+
)
|
135
|
+
);
|
136
|
+
};
|
137
|
+
|
112
138
|
const tabletsOverallB = cn('tabletsOverall-tooltip');
|
113
139
|
|
114
140
|
const TabletsOverallTooltip = (props) => {
|
@@ -175,6 +201,7 @@ export const tooltipTemplates = {
|
|
175
201
|
tablet: (data, additionalData) => <TabletTooltip data={data} additionalData={additionalData} />,
|
176
202
|
// eslint-disable-next-line react/display-name
|
177
203
|
node: (data) => <NodeTooltip data={data} />,
|
204
|
+
nodeEndpoints: (data) => <NodeEndpointsTooltip data={data} />,
|
178
205
|
// eslint-disable-next-line react/display-name
|
179
206
|
tabletsOverall: (data) => <TabletsOverallTooltip data={data} />,
|
180
207
|
// eslint-disable-next-line react/display-name
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "ydb-embedded-ui",
|
3
|
-
"version": "1.
|
3
|
+
"version": "1.9.0",
|
4
4
|
"files": [
|
5
5
|
"dist"
|
6
6
|
],
|
@@ -40,7 +40,7 @@
|
|
40
40
|
"reselect": "4.0.0",
|
41
41
|
"sass": "1.32.8",
|
42
42
|
"web-vitals": "1.1.2",
|
43
|
-
"ydb-ui-components": "2.
|
43
|
+
"ydb-ui-components": "2.4.0"
|
44
44
|
},
|
45
45
|
"scripts": {
|
46
46
|
"start": "react-app-rewired start",
|