ydb-embedded-ui 1.8.8 → 1.9.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 +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",
|