ydb-embedded-ui 2.1.0 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +28 -0
- package/dist/assets/icons/update-arrow.svg +6 -0
- package/dist/components/EntityStatus/EntityStatus.js +16 -14
- package/dist/components/EntityStatus/EntityStatus.scss +14 -5
- package/dist/components/ShortyString/ShortyString.tsx +21 -8
- package/dist/components/ShortyString/i18n/en.json +10 -0
- package/dist/components/ShortyString/i18n/index.ts +11 -0
- package/dist/components/ShortyString/i18n/ru.json +10 -0
- package/dist/containers/Cluster/Cluster.tsx +3 -3
- package/dist/containers/Nodes/Nodes.js +5 -6
- package/dist/containers/Tenant/Diagnostics/DetailedOverview/DetailedOverview.scss +2 -2
- package/dist/containers/Tenant/Diagnostics/DetailedOverview/DetailedOverview.tsx +15 -7
- package/dist/containers/Tenant/Diagnostics/Healthcheck/Healthcheck.scss +11 -4
- package/dist/containers/Tenant/Diagnostics/Healthcheck/Healthcheck.tsx +18 -21
- package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuePreview/IssuePreview.tsx +8 -7
- package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuesList/IssuesList.tsx +14 -21
- package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuesViewer/IssueViewer.scss +21 -15
- package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuesViewer/IssuesViewer.js +52 -86
- package/dist/containers/Tenant/Diagnostics/Healthcheck/Preview/Preview.tsx +17 -23
- package/dist/containers/Tenant/Diagnostics/Healthcheck/i18n/en.json +5 -9
- package/dist/containers/Tenant/Diagnostics/Healthcheck/i18n/ru.json +5 -9
- package/dist/containers/Tenant/Tenant.tsx +2 -2
- package/dist/containers/Tenants/Tenants.js +6 -7
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
@@ -1,5 +1,33 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [2.2.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v2.1.0...v2.2.0) (2022-10-14)
|
4
|
+
|
5
|
+
|
6
|
+
### Features
|
7
|
+
|
8
|
+
* **Healthcheck:** rework issues list in modal ([e7cb0df](https://github.com/ydb-platform/ydb-embedded-ui/commit/e7cb0df58e22c8c9cd25aae83b78be4808e9ba81))
|
9
|
+
|
10
|
+
|
11
|
+
### Bug Fixes
|
12
|
+
|
13
|
+
* **EntityStatus:** enable component to left trim links ([fbc6c51](https://github.com/ydb-platform/ydb-embedded-ui/commit/fbc6c51f9fbea3c1a7f5f70cb542971a41f4d8b3))
|
14
|
+
* fix pre-commit prettier linting and add json linting ([#189](https://github.com/ydb-platform/ydb-embedded-ui/issues/189)) ([047415d](https://github.com/ydb-platform/ydb-embedded-ui/commit/047415d2d69ecf4a2d99f0092b9e6735bd8efbc0))
|
15
|
+
* **Healthcheck:** delete unneeded i18n translations ([0c6de90](https://github.com/ydb-platform/ydb-embedded-ui/commit/0c6de9031607e4cde1387387393a9cfc9e1e2b8f))
|
16
|
+
* **Healthcheck:** enable update button in modal to fetch data ([de0b06e](https://github.com/ydb-platform/ydb-embedded-ui/commit/de0b06e7f2d3536df1b3896cbf86a947b2e7a291))
|
17
|
+
* **Healthcheck:** fix layout shift on scrollbar appearance ([ccdde6e](https://github.com/ydb-platform/ydb-embedded-ui/commit/ccdde6e065abbdb1c22a2c3bdd17e63f706d0f77))
|
18
|
+
* **Healthcheck:** fix styles for long issues trees ([32f1a8d](https://github.com/ydb-platform/ydb-embedded-ui/commit/32f1a8db58d9f84073327b92dcd80a5b4626a526))
|
19
|
+
* **Healthcheck:** fix variable typo ([0f0e056](https://github.com/ydb-platform/ydb-embedded-ui/commit/0f0e056576b9ec18fc3ce574d3742d55e5da6e35))
|
20
|
+
* **Healthcheck:** full check status in a preview ([bc0b51e](https://github.com/ydb-platform/ydb-embedded-ui/commit/bc0b51eedd4ff3b4ae1650946832f463a6703c12))
|
21
|
+
* **Healthcheck:** make modal show only one first level issue ([cdc95a7](https://github.com/ydb-platform/ydb-embedded-ui/commit/cdc95a7412c1266d990df7e2807630a8f4c88780))
|
22
|
+
* **Healthcheck:** redesign healthcheck header ([867f57a](https://github.com/ydb-platform/ydb-embedded-ui/commit/867f57aed84b7b72c22a816c6ac02387490ff495))
|
23
|
+
* **Healthcheck:** replace update button with icon ([709a994](https://github.com/ydb-platform/ydb-embedded-ui/commit/709a994544f068db1b0fe09009ecb4d8db46fc38))
|
24
|
+
* **Healthcheck:** update styles to be closer to the design ([aa1083d](https://github.com/ydb-platform/ydb-embedded-ui/commit/aa1083d299e24590336eeb3d913a9c53fd77bad6))
|
25
|
+
* **Nodes:** case insensitive search ([11d2c98](https://github.com/ydb-platform/ydb-embedded-ui/commit/11d2c985e0c30bb74ed07e22273d8b3459b54c89))
|
26
|
+
* **QueryEditor:** smarter error message trim ([8632948](https://github.com/ydb-platform/ydb-embedded-ui/commit/863294828090dc8eb2595884283d0996156c3785))
|
27
|
+
* **Tenants:** case insensitive search ([0ad93f5](https://github.com/ydb-platform/ydb-embedded-ui/commit/0ad93f57dcbba7d9746be54a4ba7b76ab4d45108))
|
28
|
+
* **Tenants:** fix filtering by ControlPlane name ([4941c82](https://github.com/ydb-platform/ydb-embedded-ui/commit/4941c821cdbb7c5d0da26a3b0d5c00d8979401c0))
|
29
|
+
* **Tenants:** left trim db names in db list ([81bf0fa](https://github.com/ydb-platform/ydb-embedded-ui/commit/81bf0fafe901d3601dc04fdf71939e914493ff1c))
|
30
|
+
|
3
31
|
## [2.1.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v2.0.0...v2.1.0) (2022-10-04)
|
4
32
|
|
5
33
|
|
@@ -0,0 +1,6 @@
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
2
|
+
<path d="M21 12C21 16.9706 16.9706 21 12 21C9.5 21 6.5 19 5 17" stroke='currentColor' stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
3
|
+
<path d="M5 21L5 17L9 17" stroke='currentColor' stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
4
|
+
<path d="M3 12C3 7.02944 7.02944 3 12 3C14.5 3 17.5 5 19 7" stroke='currentColor' stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
5
|
+
<path d="M19 3L19 7L15 7" stroke='currentColor' stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
6
|
+
</svg>
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
3
3
|
import cn from 'bem-cn-lite';
|
4
4
|
import {Link} from 'react-router-dom';
|
5
|
-
import {ClipboardButton, Link as
|
5
|
+
import {ClipboardButton, Link as UIKitLink, Button, Icon} from '@gravity-ui/uikit';
|
6
6
|
|
7
7
|
import circleInfoIcon from '../../assets/icons/circle-info.svg';
|
8
8
|
import circleExclamationIcon from '../../assets/icons/circle-exclamation.svg';
|
@@ -35,6 +35,7 @@ class EntityStatus extends React.Component {
|
|
35
35
|
externalLink: PropTypes.bool,
|
36
36
|
className: PropTypes.string,
|
37
37
|
mode: PropTypes.oneOf(['color', 'icons']),
|
38
|
+
withLeftTrim: PropTypes.bool,
|
38
39
|
};
|
39
40
|
|
40
41
|
static defaultProps = {
|
@@ -45,6 +46,7 @@ class EntityStatus extends React.Component {
|
|
45
46
|
showStatus: true,
|
46
47
|
externalLink: false,
|
47
48
|
mode: 'color',
|
49
|
+
withLeftTrim: false,
|
48
50
|
};
|
49
51
|
renderIcon() {
|
50
52
|
const {status, size, showStatus, mode} = this.props;
|
@@ -56,12 +58,7 @@ class EntityStatus extends React.Component {
|
|
56
58
|
const modifiers = {state: status.toLowerCase(), size};
|
57
59
|
|
58
60
|
if (mode === 'icons' && icons[status]) {
|
59
|
-
return (
|
60
|
-
<Icon
|
61
|
-
className={b('status-icon', modifiers)}
|
62
|
-
data={icons[status]}
|
63
|
-
/>
|
64
|
-
);
|
61
|
+
return <Icon className={b('status-icon', modifiers)} data={icons[status]} />;
|
65
62
|
}
|
66
63
|
|
67
64
|
return <div className={b('status-color', modifiers)} />;
|
@@ -70,21 +67,25 @@ class EntityStatus extends React.Component {
|
|
70
67
|
const {iconPath} = this.props;
|
71
68
|
|
72
69
|
return (
|
73
|
-
<
|
70
|
+
<UIKitLink target="_blank" href={iconPath}>
|
74
71
|
{this.renderIcon()}
|
75
|
-
</
|
72
|
+
</UIKitLink>
|
76
73
|
);
|
77
74
|
}
|
78
75
|
renderLink() {
|
79
76
|
const {externalLink, name, path, onNameMouseEnter, onNameMouseLeave} = this.props;
|
80
77
|
|
81
78
|
if (externalLink) {
|
82
|
-
return
|
79
|
+
return (
|
80
|
+
<UIKitLink className={b('name')} href={path}>
|
81
|
+
{name}
|
82
|
+
</UIKitLink>
|
83
|
+
);
|
83
84
|
}
|
84
85
|
|
85
86
|
return path ? (
|
86
87
|
<Link
|
87
|
-
|
88
|
+
className={b('name')}
|
88
89
|
to={path}
|
89
90
|
onMouseEnter={onNameMouseEnter}
|
90
91
|
onMouseLeave={onNameMouseLeave}
|
@@ -95,7 +96,6 @@ class EntityStatus extends React.Component {
|
|
95
96
|
name && (
|
96
97
|
<span
|
97
98
|
className={b('name')}
|
98
|
-
title={name}
|
99
99
|
onMouseEnter={onNameMouseEnter}
|
100
100
|
onMouseLeave={onNameMouseLeave}
|
101
101
|
>
|
@@ -108,14 +108,16 @@ class EntityStatus extends React.Component {
|
|
108
108
|
const {name, label, iconPath, hasClipboardButton, className} = this.props;
|
109
109
|
|
110
110
|
return (
|
111
|
-
<div className={b(null, className)}>
|
111
|
+
<div className={b(null, className)} title={name}>
|
112
112
|
{iconPath ? this.renderStatusLink() : this.renderIcon()}
|
113
113
|
{label && (
|
114
114
|
<span title={label} className={b('label')}>
|
115
115
|
{label}
|
116
116
|
</span>
|
117
117
|
)}
|
118
|
-
{this.
|
118
|
+
<span className={b('link', {'with-left-trim': this.props.withLeftTrim})}>
|
119
|
+
{this.renderLink()}
|
120
|
+
</span>
|
119
121
|
{hasClipboardButton && (
|
120
122
|
<Button
|
121
123
|
component="span"
|
@@ -31,11 +31,7 @@
|
|
31
31
|
}
|
32
32
|
|
33
33
|
a {
|
34
|
-
overflow: hidden;
|
35
|
-
|
36
|
-
white-space: nowrap;
|
37
34
|
text-decoration: none;
|
38
|
-
text-overflow: ellipsis;
|
39
35
|
|
40
36
|
color: var(--yc-color-text-link);
|
41
37
|
}
|
@@ -53,12 +49,25 @@
|
|
53
49
|
color: var(--yc-color-text-complementary);
|
54
50
|
}
|
55
51
|
|
56
|
-
&
|
52
|
+
&__link {
|
53
|
+
overflow: hidden;
|
54
|
+
|
57
55
|
white-space: nowrap;
|
56
|
+
text-overflow: ellipsis;
|
57
|
+
}
|
58
|
+
|
59
|
+
&__link_with-left-trim {
|
60
|
+
direction: rtl;
|
61
|
+
|
62
|
+
.entity-status__name {
|
63
|
+
unicode-bidi: plaintext;
|
64
|
+
}
|
58
65
|
}
|
59
66
|
|
60
67
|
&__status-color,
|
61
68
|
&__status-icon {
|
69
|
+
flex-shrink: 0;
|
70
|
+
|
62
71
|
margin-right: 8px;
|
63
72
|
|
64
73
|
border-radius: 3px;
|
@@ -3,6 +3,7 @@ import cn from 'bem-cn-lite';
|
|
3
3
|
|
4
4
|
import {Link} from '@gravity-ui/uikit';
|
5
5
|
|
6
|
+
import i18n from './i18n';
|
6
7
|
import './ShortyString.scss';
|
7
8
|
|
8
9
|
const block = cn('kv-shorty-string');
|
@@ -10,6 +11,8 @@ const block = cn('kv-shorty-string');
|
|
10
11
|
type Props = {
|
11
12
|
value?: string;
|
12
13
|
limit?: number;
|
14
|
+
/** in strict mode the text always trims at the limit, otherwise it is allowed to overflow a little */
|
15
|
+
strict?: boolean;
|
13
16
|
displayLength?: boolean;
|
14
17
|
render?: (value: string) => React.ReactNode;
|
15
18
|
onToggle?: () => void;
|
@@ -20,19 +23,29 @@ type Props = {
|
|
20
23
|
export default function ShortyString({
|
21
24
|
value = '',
|
22
25
|
limit = 200,
|
26
|
+
strict = false,
|
23
27
|
displayLength = true,
|
24
28
|
render = (v: string) => v,
|
25
29
|
onToggle,
|
26
|
-
expandLabel = '
|
27
|
-
collapseLabel = '
|
30
|
+
expandLabel = i18n('default_expand_label'),
|
31
|
+
collapseLabel = i18n('default_collapse_label'),
|
28
32
|
}: Props) {
|
29
33
|
const [expanded, setExpanded] = React.useState(false);
|
30
|
-
const hasToggle = value.length > limit;
|
31
|
-
const length =
|
32
|
-
displayLength && !expanded ? `(${value.length} symbols)` : undefined;
|
33
34
|
|
34
|
-
const
|
35
|
-
const
|
35
|
+
const toggleLabelAction = expanded ? collapseLabel : expandLabel;
|
36
|
+
const toggleLabelSymbolsCount = displayLength && !expanded
|
37
|
+
? i18n('chars_count', {count: value.length})
|
38
|
+
: '';
|
39
|
+
const toggleLabel = toggleLabelAction + toggleLabelSymbolsCount;
|
40
|
+
|
41
|
+
// showing toogle button with a label that is longer than the hidden part is pointless,
|
42
|
+
// hence compare to limit + length in the not-strict mode
|
43
|
+
const hasToggle = value.length > limit + (strict ? 0 : toggleLabel.length);
|
44
|
+
|
45
|
+
const text = expanded || !hasToggle
|
46
|
+
? value
|
47
|
+
: value.slice(0, limit - 4) + '\u00a0...';
|
48
|
+
|
36
49
|
return (
|
37
50
|
<div className={block()}>
|
38
51
|
{render(text)}
|
@@ -45,7 +58,7 @@ export default function ShortyString({
|
|
45
58
|
onToggle?.();
|
46
59
|
}}
|
47
60
|
>
|
48
|
-
{
|
61
|
+
{toggleLabel}
|
49
62
|
</Link>
|
50
63
|
) : null}
|
51
64
|
</div>
|
@@ -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-shorty-string';
|
7
|
+
|
8
|
+
i18n.registerKeyset(Lang.En, COMPONENT, en);
|
9
|
+
i18n.registerKeyset(Lang.Ru, COMPONENT, ru);
|
10
|
+
|
11
|
+
export default i18n.keyset(COMPONENT);
|
@@ -14,9 +14,9 @@ import ClusterInfo from '../../components/ClusterInfo/ClusterInfo';
|
|
14
14
|
const b = cn('cluster');
|
15
15
|
|
16
16
|
interface ClusterProps {
|
17
|
-
additionalClusterInfo
|
18
|
-
additionalTenantsInfo
|
19
|
-
additionalNodesInfo
|
17
|
+
additionalClusterInfo?: any;
|
18
|
+
additionalTenantsInfo?: any;
|
19
|
+
additionalNodesInfo?: any;
|
20
20
|
}
|
21
21
|
|
22
22
|
function Cluster(props: ClusterProps) {
|
@@ -114,11 +114,10 @@ class Nodes extends React.Component {
|
|
114
114
|
});
|
115
115
|
|
116
116
|
let preparedNodes = searchQuery
|
117
|
-
? nodes.filter((node) =>
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
)
|
117
|
+
? nodes.filter((node) => {
|
118
|
+
const re = new RegExp(searchQuery, 'i');
|
119
|
+
return node.Host ? re.test(node.Host) || re.test(String(node.NodeId)) : true;
|
120
|
+
})
|
122
121
|
: nodes;
|
123
122
|
preparedNodes = preparedNodes.map((node) => ({
|
124
123
|
...node,
|
@@ -143,7 +142,7 @@ class Nodes extends React.Component {
|
|
143
142
|
columnId: 'NodeId',
|
144
143
|
order: DataTable.ASCENDING,
|
145
144
|
}}
|
146
|
-
emptyDataMessage=
|
145
|
+
emptyDataMessage="No such nodes"
|
147
146
|
/>
|
148
147
|
</div>
|
149
148
|
</div>
|
@@ -26,11 +26,12 @@ const b = cn('kv-detailed-overview');
|
|
26
26
|
function DetailedOverview(props: DetailedOverviewProps) {
|
27
27
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
28
28
|
|
29
|
-
const
|
30
|
-
currentSchemaPath,
|
31
|
-
} = useSelector((state: any) => state.schema);
|
29
|
+
const [expandedIssueId, setExpandedIssueId] = useState<string>();
|
32
30
|
|
33
|
-
const
|
31
|
+
const {currentSchemaPath} = useSelector((state: any) => state.schema);
|
32
|
+
|
33
|
+
const openModalHandler = (id: string) => {
|
34
|
+
setExpandedIssueId(id);
|
34
35
|
setIsModalVisible(true);
|
35
36
|
};
|
36
37
|
|
@@ -41,12 +42,16 @@ function DetailedOverview(props: DetailedOverviewProps) {
|
|
41
42
|
const renderModal = () => {
|
42
43
|
return (
|
43
44
|
<Modal open={isModalVisible} onClose={closeModalHandler} className={b('modal')}>
|
44
|
-
<Healthcheck
|
45
|
+
<Healthcheck
|
46
|
+
tenant={props.tenantName}
|
47
|
+
fetchData={false}
|
48
|
+
expandedIssueId={expandedIssueId}
|
49
|
+
/>
|
45
50
|
<Button
|
46
51
|
className={b('close-modal-button')}
|
47
52
|
onClick={closeModalHandler}
|
48
53
|
view="flat-secondary"
|
49
|
-
title=
|
54
|
+
title="Close"
|
50
55
|
>
|
51
56
|
<Icon name="close" viewBox={'0 0 16 16 '} height={20} width={20} />
|
52
57
|
</Button>
|
@@ -62,7 +67,10 @@ function DetailedOverview(props: DetailedOverviewProps) {
|
|
62
67
|
{isTenant ? (
|
63
68
|
<>
|
64
69
|
<div className={b('section')}>
|
65
|
-
<TenantOverview
|
70
|
+
<TenantOverview
|
71
|
+
tenantName={tenantName}
|
72
|
+
additionalTenantInfo={additionalTenantInfo}
|
73
|
+
/>
|
66
74
|
</div>
|
67
75
|
<div className={b('section')}>
|
68
76
|
<Healthcheck
|
@@ -1,7 +1,12 @@
|
|
1
1
|
@use '../DetailedOverview/DetailedOverview.scss' as detailedOverview;
|
2
2
|
@import '../../../../styles/mixins.scss';
|
3
|
+
@import '@gravity-ui/uikit/styles/mixins.scss';
|
3
4
|
|
4
5
|
.healthcheck {
|
6
|
+
// Since most of the inner containers have fixed width, we can set fixed width here as well
|
7
|
+
// Thus we will get rid of unneeded layout shift when scrollbar appear
|
8
|
+
min-width: 885px;
|
9
|
+
|
5
10
|
&__issues-list {
|
6
11
|
padding: 25px 20px 20px;
|
7
12
|
}
|
@@ -27,19 +32,21 @@
|
|
27
32
|
padding: 15px 0;
|
28
33
|
}
|
29
34
|
|
30
|
-
&
|
35
|
+
&__issues-list-header {
|
31
36
|
display: flex;
|
32
37
|
align-items: center;
|
33
38
|
|
34
39
|
margin-bottom: 20px;
|
35
40
|
}
|
36
41
|
|
37
|
-
&
|
42
|
+
&__issues-list-header-title {
|
38
43
|
margin: 0 10px 0 0;
|
44
|
+
|
45
|
+
@include text-header-1();
|
39
46
|
}
|
40
47
|
|
41
|
-
&
|
42
|
-
margin-left:
|
48
|
+
&__issues-list-header-update {
|
49
|
+
margin-left: 10px;
|
43
50
|
}
|
44
51
|
|
45
52
|
&__status-wrapper {
|
@@ -17,18 +17,14 @@ interface HealthcheckProps {
|
|
17
17
|
tenant: string;
|
18
18
|
preview?: boolean;
|
19
19
|
fetchData?: boolean;
|
20
|
-
|
20
|
+
expandedIssueId?: string;
|
21
|
+
showMoreHandler?: (id: string) => void;
|
21
22
|
}
|
22
23
|
|
23
24
|
const b = cn('healthcheck');
|
24
25
|
|
25
26
|
export const Healthcheck = (props: HealthcheckProps) => {
|
26
|
-
const {
|
27
|
-
tenant,
|
28
|
-
preview,
|
29
|
-
fetchData = true,
|
30
|
-
showMoreHandler,
|
31
|
-
} = props;
|
27
|
+
const {tenant, preview, fetchData = true, showMoreHandler, expandedIssueId} = props;
|
32
28
|
|
33
29
|
const dispatch = useDispatch();
|
34
30
|
|
@@ -36,12 +32,18 @@ export const Healthcheck = (props: HealthcheckProps) => {
|
|
36
32
|
const {autorefresh} = useSelector((state: any) => state.schema);
|
37
33
|
|
38
34
|
const fetchHealthcheck = useCallback(() => {
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
35
|
+
dispatch(getHealthcheckInfo(tenant));
|
36
|
+
}, [dispatch, tenant]);
|
37
|
+
|
38
|
+
useAutofetcher(
|
39
|
+
() => {
|
40
|
+
if (fetchData) {
|
41
|
+
fetchHealthcheck();
|
42
|
+
}
|
43
|
+
},
|
44
|
+
[fetchData, fetchHealthcheck],
|
45
|
+
autorefresh,
|
46
|
+
);
|
45
47
|
|
46
48
|
const renderContent = () => {
|
47
49
|
if (error) {
|
@@ -67,20 +69,15 @@ export const Healthcheck = (props: HealthcheckProps) => {
|
|
67
69
|
) : (
|
68
70
|
<IssuesList
|
69
71
|
data={data}
|
72
|
+
expandedIssueId={expandedIssueId}
|
70
73
|
loading={loading}
|
71
74
|
onUpdate={fetchHealthcheck}
|
72
75
|
/>
|
73
76
|
);
|
74
77
|
}
|
75
78
|
|
76
|
-
return (
|
77
|
-
<div className="error">{i18n('no-data')}</div>
|
78
|
-
);
|
79
|
+
return <div className="error">{i18n('no-data')}</div>;
|
79
80
|
};
|
80
81
|
|
81
|
-
return (
|
82
|
-
<div className={b()}>
|
83
|
-
{renderContent()}
|
84
|
-
</div>
|
85
|
-
);
|
82
|
+
return <div className={b()}>{renderContent()}</div>;
|
86
83
|
};
|
@@ -11,14 +11,11 @@ const b = cn('healthcheck');
|
|
11
11
|
|
12
12
|
interface IssuePreviewProps {
|
13
13
|
data?: IssueLog;
|
14
|
-
onShowMore?:
|
14
|
+
onShowMore?: (id: string) => void;
|
15
15
|
}
|
16
16
|
|
17
17
|
export const IssuePreview = (props: IssuePreviewProps) => {
|
18
|
-
const {
|
19
|
-
data,
|
20
|
-
onShowMore,
|
21
|
-
} = props;
|
18
|
+
const {data, onShowMore} = props;
|
22
19
|
|
23
20
|
if (!data) {
|
24
21
|
return null;
|
@@ -27,8 +24,12 @@ export const IssuePreview = (props: IssuePreviewProps) => {
|
|
27
24
|
return (
|
28
25
|
<div className={b('issue-preview')}>
|
29
26
|
<EntityStatus mode="icons" status={data.status} name={data.type} />
|
30
|
-
<Text as="div" color="secondary" variant="body-2">
|
31
|
-
|
27
|
+
<Text as="div" color="secondary" variant="body-2">
|
28
|
+
{data.message}
|
29
|
+
</Text>
|
30
|
+
<Link onClick={() => onShowMore && onShowMore(data.id)}>
|
31
|
+
{i18n('label.show-details')}
|
32
|
+
</Link>
|
32
33
|
</div>
|
33
34
|
);
|
34
35
|
};
|
@@ -1,6 +1,8 @@
|
|
1
1
|
import cn from 'bem-cn-lite';
|
2
2
|
|
3
|
-
import {Button} from '@gravity-ui/uikit';
|
3
|
+
import {Button, Icon} from '@gravity-ui/uikit';
|
4
|
+
|
5
|
+
import updateArrow from '../../../../../assets/icons/update-arrow.svg';
|
4
6
|
|
5
7
|
import type {IHealthCheck} from '../../../../../types/store/healthcheck';
|
6
8
|
|
@@ -13,32 +15,24 @@ const b = cn('healthcheck');
|
|
13
15
|
interface IssuesListProps {
|
14
16
|
data?: IHealthCheck;
|
15
17
|
loading?: boolean;
|
18
|
+
expandedIssueId?: string;
|
16
19
|
onUpdate: VoidFunction;
|
17
20
|
}
|
18
21
|
|
19
22
|
export const IssuesList = (props: IssuesListProps) => {
|
20
|
-
const {
|
21
|
-
data,
|
22
|
-
loading,
|
23
|
-
onUpdate,
|
24
|
-
} = props;
|
23
|
+
const {data, loading, onUpdate, expandedIssueId} = props;
|
25
24
|
|
26
25
|
if (!data) {
|
27
26
|
return null;
|
28
27
|
}
|
29
28
|
|
30
|
-
const
|
31
|
-
const {self_check_result: selfCheckResult} = data;
|
32
|
-
const modifier = selfCheckResult.toLowerCase();
|
33
|
-
|
29
|
+
const renderHealthcheckHeader = () => {
|
34
30
|
return (
|
35
|
-
<div className={b('
|
36
|
-
<h3 className={b('
|
37
|
-
<div className={b('
|
38
|
-
|
39
|
-
|
40
|
-
<Button size="s" onClick={onUpdate} loading={loading}>
|
41
|
-
{i18n('label.update')}
|
31
|
+
<div className={b('issues-list-header')}>
|
32
|
+
<h3 className={b('issues-list-header-title')}>{i18n('title.healthcheck')}</h3>
|
33
|
+
<div className={b('issues-list-header-update')}>
|
34
|
+
<Button size="s" onClick={onUpdate} loading={loading} view="flat-secondary">
|
35
|
+
<Icon data={updateArrow} height={20} width={20} />
|
42
36
|
</Button>
|
43
37
|
</div>
|
44
38
|
</div>
|
@@ -54,15 +48,14 @@ export const IssuesList = (props: IssuesListProps) => {
|
|
54
48
|
|
55
49
|
return (
|
56
50
|
<div className={b('issues')}>
|
57
|
-
<
|
58
|
-
<IssuesViewer issues={issueLog} />
|
51
|
+
<IssuesViewer issues={issueLog} expandedIssueId={expandedIssueId} />
|
59
52
|
</div>
|
60
53
|
);
|
61
|
-
}
|
54
|
+
};
|
62
55
|
|
63
56
|
return (
|
64
57
|
<div className={b('issues-list')}>
|
65
|
-
{
|
58
|
+
{renderHealthcheckHeader()}
|
66
59
|
{renderHealthcheckIssues()}
|
67
60
|
</div>
|
68
61
|
);
|
@@ -2,30 +2,22 @@
|
|
2
2
|
|
3
3
|
.issue {
|
4
4
|
display: flex;
|
5
|
+
justify-content: space-between;
|
5
6
|
align-items: center;
|
6
7
|
|
7
8
|
height: 40px;
|
8
9
|
|
9
10
|
cursor: pointer;
|
10
11
|
|
11
|
-
&_active {
|
12
|
-
border-radius: 4px;
|
13
|
-
background: var(--yc-color-base-info);
|
14
|
-
}
|
15
|
-
|
16
12
|
&__field {
|
17
|
-
|
13
|
+
display: flex;
|
14
|
+
overflow: hidden;
|
18
15
|
|
19
16
|
&_status {
|
20
17
|
display: flex;
|
21
18
|
|
22
|
-
min-width: 470px;
|
23
|
-
|
24
19
|
white-space: nowrap;
|
25
20
|
}
|
26
|
-
&_type {
|
27
|
-
min-width: 160px;
|
28
|
-
}
|
29
21
|
&_additional {
|
30
22
|
width: max-content;
|
31
23
|
|
@@ -39,6 +31,7 @@
|
|
39
31
|
}
|
40
32
|
&_message {
|
41
33
|
overflow: hidden;
|
34
|
+
flex-shrink: 0;
|
42
35
|
|
43
36
|
width: 300px;
|
44
37
|
|
@@ -96,8 +89,10 @@
|
|
96
89
|
.issue-viewer {
|
97
90
|
display: flex;
|
98
91
|
|
92
|
+
width: 820px;
|
93
|
+
|
99
94
|
&__tree {
|
100
|
-
|
95
|
+
width: 100%;
|
101
96
|
}
|
102
97
|
|
103
98
|
&__checkbox {
|
@@ -106,11 +101,10 @@
|
|
106
101
|
|
107
102
|
&__info-panel {
|
108
103
|
position: sticky;
|
109
|
-
top: 20px;
|
110
104
|
|
111
|
-
width: 500px;
|
112
105
|
height: 100%;
|
113
|
-
|
106
|
+
margin: 11px 0;
|
107
|
+
padding: 8px 20px;
|
114
108
|
|
115
109
|
border-radius: 4px;
|
116
110
|
background: var(--yc-color-base-generic);
|
@@ -152,6 +146,8 @@
|
|
152
146
|
}
|
153
147
|
|
154
148
|
.ydb-tree-view {
|
149
|
+
$calculated-margin: calc(24px * var(--ydb-tree-view-level));
|
150
|
+
|
155
151
|
&__item {
|
156
152
|
height: 40px;
|
157
153
|
}
|
@@ -160,5 +156,15 @@
|
|
160
156
|
width: 40px;
|
161
157
|
height: 40px;
|
162
158
|
}
|
159
|
+
|
160
|
+
// Without !important this class does not have enough weight compared to styles set in TreeView
|
161
|
+
.ydb-tree-view__item {
|
162
|
+
margin-left: $calculated-margin !important;
|
163
|
+
padding-left: 0 !important;
|
164
|
+
}
|
165
|
+
|
166
|
+
.issue-viewer__info-panel {
|
167
|
+
margin-left: $calculated-margin;
|
168
|
+
}
|
163
169
|
}
|
164
170
|
}
|
@@ -13,62 +13,26 @@ import EntityStatus from '../../../../../components/EntityStatus/EntityStatus';
|
|
13
13
|
|
14
14
|
import './IssueViewer.scss';
|
15
15
|
|
16
|
-
// const indicatorBlock = cn('indicator');
|
17
|
-
|
18
|
-
// const IssueStatus = ({status, name}) => {
|
19
|
-
// const modifier = status && status.toLowerCase();
|
20
|
-
|
21
|
-
// return (
|
22
|
-
// <React.Fragment>
|
23
|
-
// <div className={indicatorBlock({[modifier]: true})} />
|
24
|
-
// {name}
|
25
|
-
// </React.Fragment>
|
26
|
-
// );
|
27
|
-
// };
|
28
|
-
|
29
16
|
const issueBlock = cn('issue');
|
30
17
|
|
31
|
-
const IssueRow = ({data,
|
32
|
-
|
33
|
-
const {id, status, message, type, reasonsItems, ...rest} = data;
|
34
|
-
|
35
|
-
useEffect(() => {
|
36
|
-
if (active) {
|
37
|
-
setInfoForActive(rest);
|
38
|
-
}
|
39
|
-
}, [active, setInfoForActive]);
|
18
|
+
const IssueRow = ({data, onClick}) => {
|
19
|
+
const {status, message, type} = data;
|
40
20
|
|
41
21
|
return (
|
42
|
-
<div className={issueBlock(
|
22
|
+
<div className={issueBlock()} onClick={onClick}>
|
43
23
|
<div className={issueBlock('field', {status: true})}>
|
44
|
-
<EntityStatus status={status} name={
|
45
|
-
{/* <IssueStatus status={status} name={id} /> */}
|
46
|
-
</div>
|
47
|
-
<div
|
48
|
-
className={issueBlock('field', {message: true})}
|
49
|
-
style={{marginLeft: -treeLevel * 24 + 'px'}}
|
50
|
-
>
|
51
|
-
{message}
|
24
|
+
<EntityStatus mode="icons" status={status} name={type} />
|
52
25
|
</div>
|
53
|
-
<div className={issueBlock('field', {
|
26
|
+
<div className={issueBlock('field', {message: true})}>{message}</div>
|
54
27
|
</div>
|
55
28
|
);
|
56
29
|
};
|
57
30
|
|
58
31
|
const issueViewerBlock = cn('issue-viewer');
|
59
32
|
|
60
|
-
const IssuesViewer = ({issues}) => {
|
33
|
+
const IssuesViewer = ({issues, expandedIssueId}) => {
|
61
34
|
const [data, setData] = useState([]);
|
62
35
|
const [collapsedIssues, setCollapsedIssues] = useState({});
|
63
|
-
const [activeItem, setActiveItem] = useState();
|
64
|
-
const [infoData, setInfoData] = useState();
|
65
|
-
|
66
|
-
useEffect(() => {
|
67
|
-
if (!activeItem && data.length) {
|
68
|
-
const {id} = data[0];
|
69
|
-
setActiveItem(id);
|
70
|
-
}
|
71
|
-
}, [data]);
|
72
36
|
|
73
37
|
useEffect(() => {
|
74
38
|
const newData = getInvertedConsequencesTree({data: issues});
|
@@ -77,67 +41,69 @@ const IssuesViewer = ({issues}) => {
|
|
77
41
|
}, [issues]);
|
78
42
|
|
79
43
|
const renderTree = useCallback(
|
80
|
-
(data, childrenKey
|
44
|
+
(data, childrenKey) => {
|
81
45
|
return _.map(data, (item) => {
|
82
46
|
const {id} = item;
|
83
|
-
|
84
|
-
|
47
|
+
|
48
|
+
// eslint-disable-next-line no-unused-vars
|
49
|
+
const {status, message, type, reasonsItems, reason, level, ...rest} = item;
|
50
|
+
|
51
|
+
if (level === 1 && expandedIssueId && id !== expandedIssueId) {
|
52
|
+
return;
|
53
|
+
}
|
54
|
+
|
55
|
+
const isCollapsed =
|
56
|
+
typeof collapsedIssues[id] === 'undefined' || collapsedIssues[id];
|
57
|
+
|
58
|
+
const toggleCollapsed = () => {
|
59
|
+
setCollapsedIssues((collapsedIssues) => ({
|
60
|
+
...collapsedIssues,
|
61
|
+
[id]: !isCollapsed,
|
62
|
+
}));
|
63
|
+
};
|
85
64
|
|
86
65
|
return (
|
87
66
|
<TreeView
|
88
67
|
key={id}
|
89
|
-
name={
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
/>
|
96
|
-
}
|
97
|
-
collapsed={
|
98
|
-
typeof collapsedIssues[id] === 'undefined' || collapsedIssues[id]
|
99
|
-
}
|
100
|
-
hasArrow={hasArrow}
|
101
|
-
onClick={() => setActiveItem(id)}
|
102
|
-
onArrowClick={() => {
|
103
|
-
const newValue =
|
104
|
-
typeof collapsedIssues[id] === 'undefined'
|
105
|
-
? false
|
106
|
-
: !collapsedIssues[id];
|
107
|
-
const newCollapsedIssues = {...collapsedIssues, [id]: newValue};
|
108
|
-
setCollapsedIssues(newCollapsedIssues);
|
109
|
-
}}
|
68
|
+
name={<IssueRow data={item} />}
|
69
|
+
collapsed={isCollapsed}
|
70
|
+
hasArrow={true}
|
71
|
+
onClick={toggleCollapsed}
|
72
|
+
onArrowClick={toggleCollapsed}
|
73
|
+
level={level - 1}
|
110
74
|
>
|
111
|
-
{
|
75
|
+
{renderInfoPanel(rest)}
|
76
|
+
{renderTree(item[childrenKey], childrenKey)}
|
112
77
|
</TreeView>
|
113
78
|
);
|
114
79
|
});
|
115
80
|
},
|
116
|
-
[data, collapsedIssues
|
81
|
+
[data, collapsedIssues],
|
117
82
|
);
|
118
83
|
|
119
|
-
const renderInfoPanel = useCallback(
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
<
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
84
|
+
const renderInfoPanel = useCallback(
|
85
|
+
(info) => {
|
86
|
+
if (!info) {
|
87
|
+
return null;
|
88
|
+
}
|
89
|
+
|
90
|
+
return (
|
91
|
+
<div className={issueViewerBlock('info-panel')}>
|
92
|
+
<JSONTree
|
93
|
+
data={info}
|
94
|
+
search={false}
|
95
|
+
isExpanded={() => true}
|
96
|
+
className={issueViewerBlock('inspector')}
|
97
|
+
/>
|
98
|
+
</div>
|
99
|
+
);
|
100
|
+
},
|
101
|
+
[data],
|
102
|
+
);
|
136
103
|
|
137
104
|
return (
|
138
105
|
<div className={issueViewerBlock()}>
|
139
106
|
<div className={issueViewerBlock('tree')}>{renderTree(data, 'reasonsItems')}</div>
|
140
|
-
{renderInfoPanel()}
|
141
107
|
</div>
|
142
108
|
);
|
143
109
|
};
|
@@ -1,7 +1,9 @@
|
|
1
1
|
import {useMemo} from 'react';
|
2
2
|
import cn from 'bem-cn-lite';
|
3
3
|
|
4
|
-
import {Button} from '@gravity-ui/uikit';
|
4
|
+
import {Button, Icon} from '@gravity-ui/uikit';
|
5
|
+
|
6
|
+
import updateArrow from '../../../../../assets/icons/update-arrow.svg';
|
5
7
|
|
6
8
|
import {SelfCheckResult} from '../../../../../types/api/healthcheck';
|
7
9
|
import type {IHealthCheck} from '../../../../../types/store/healthcheck';
|
@@ -15,23 +17,21 @@ const b = cn('healthcheck');
|
|
15
17
|
interface PreviewProps {
|
16
18
|
data?: IHealthCheck;
|
17
19
|
loading?: boolean;
|
18
|
-
onShowMore?: VoidFunction;
|
19
20
|
onUpdate: VoidFunction;
|
21
|
+
onShowMore?: (id: string) => void;
|
20
22
|
}
|
21
23
|
|
22
24
|
export const Preview = (props: PreviewProps) => {
|
23
|
-
const {
|
24
|
-
data,
|
25
|
-
loading,
|
26
|
-
onShowMore,
|
27
|
-
onUpdate,
|
28
|
-
} = props;
|
25
|
+
const {data, loading, onShowMore, onUpdate} = props;
|
29
26
|
|
30
27
|
const selfCheckResult = data?.self_check_result || SelfCheckResult.UNSPECIFIED;
|
31
28
|
const isStatusOK = selfCheckResult === SelfCheckResult.GOOD;
|
32
29
|
|
33
30
|
const issuesLog = data?.issue_log;
|
34
|
-
const firstLevelIssues = useMemo(
|
31
|
+
const firstLevelIssues = useMemo(
|
32
|
+
() => issuesLog?.filter(({level}) => level === 1),
|
33
|
+
[issuesLog],
|
34
|
+
);
|
35
35
|
|
36
36
|
if (!data) {
|
37
37
|
return null;
|
@@ -44,10 +44,10 @@ export const Preview = (props: PreviewProps) => {
|
|
44
44
|
<div className={b('status-wrapper')}>
|
45
45
|
<div className={b('preview-title')}>{i18n('title.healthcheck')}</div>
|
46
46
|
<div className={b('self-check-status-indicator', {[modifier]: true})}>
|
47
|
-
{
|
47
|
+
{selfCheckResult}
|
48
48
|
</div>
|
49
|
-
<Button size="s" onClick={onUpdate} loading={loading}>
|
50
|
-
{
|
49
|
+
<Button size="s" onClick={onUpdate} loading={loading} view="flat-secondary">
|
50
|
+
<Icon data={updateArrow} width={20} height={20} />
|
51
51
|
</Button>
|
52
52
|
</div>
|
53
53
|
);
|
@@ -56,17 +56,11 @@ export const Preview = (props: PreviewProps) => {
|
|
56
56
|
const renderFirstLevelIssues = () => {
|
57
57
|
return (
|
58
58
|
<div className={b('preview-content')}>
|
59
|
-
{
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
key={issue.id}
|
65
|
-
data={issue}
|
66
|
-
onShowMore={onShowMore}
|
67
|
-
/>
|
68
|
-
))
|
69
|
-
}
|
59
|
+
{isStatusOK
|
60
|
+
? i18n('status_message.ok')
|
61
|
+
: firstLevelIssues?.map((issue) => (
|
62
|
+
<IssuePreview key={issue.id} data={issue} onShowMore={onShowMore} />
|
63
|
+
))}
|
70
64
|
</div>
|
71
65
|
);
|
72
66
|
};
|
@@ -1,11 +1,7 @@
|
|
1
1
|
{
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
"status_massage.ok": "No issues have been found on this database",
|
8
|
-
"ok": "Ok",
|
9
|
-
"error": "Error",
|
10
|
-
"no-data": "no healthcheck data"
|
2
|
+
"title.healthcheck": "Healthcheck",
|
3
|
+
"label.update": "Update",
|
4
|
+
"label.show-details": "Show details",
|
5
|
+
"status_message.ok": "No issues have been found on this database",
|
6
|
+
"no-data": "no healthcheck data"
|
11
7
|
}
|
@@ -1,11 +1,7 @@
|
|
1
1
|
{
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
"status_massage.ok": "В базе данных нет проблем",
|
8
|
-
"ok": "Ok",
|
9
|
-
"error": "Ошибка",
|
10
|
-
"no-data": "нет данных healthcheck"
|
2
|
+
"title.healthcheck": "Healthcheck",
|
3
|
+
"label.update": "Обновить",
|
4
|
+
"label.show-details": "Посмотреть подробности",
|
5
|
+
"status_message.ok": "В базе данных нет проблем",
|
6
|
+
"no-data": "нет данных healthcheck"
|
11
7
|
}
|
@@ -107,7 +107,7 @@ class Tenants extends React.Component {
|
|
107
107
|
|
108
108
|
getControlPlaneValue = (item) => {
|
109
109
|
const parts = _.get(item, 'Name', []).split('/');
|
110
|
-
const defaultValue = parts.length ? parts.
|
110
|
+
const defaultValue = parts.length ? parts[parts.length - 1] : '—';
|
111
111
|
|
112
112
|
return _.get(item, 'ControlPlane.name', defaultValue);
|
113
113
|
};
|
@@ -124,12 +124,10 @@ class Tenants extends React.Component {
|
|
124
124
|
savedTenantInitialTab,
|
125
125
|
} = this.props;
|
126
126
|
|
127
|
-
const filteredTenantsBySearch = tenants.filter(
|
128
|
-
(
|
129
|
-
|
130
|
-
|
131
|
-
filter,
|
132
|
-
);
|
127
|
+
const filteredTenantsBySearch = tenants.filter((item) => {
|
128
|
+
const re = new RegExp(searchQuery, 'i');
|
129
|
+
return re.test(item.Name) || re.test(this.getControlPlaneValue(item));
|
130
|
+
});
|
133
131
|
const filteredTenants = Tenants.filterTenants(filteredTenantsBySearch, filter);
|
134
132
|
|
135
133
|
const initialTenantGeneralTab = savedTenantInitialTab || TENANT_GENERAL_TABS[0].id;
|
@@ -151,6 +149,7 @@ class Tenants extends React.Component {
|
|
151
149
|
externalLink={isExternalLink}
|
152
150
|
className={b('name')}
|
153
151
|
name={value || 'unknown database'}
|
152
|
+
withLeftTrim={true}
|
154
153
|
status={row.Overall}
|
155
154
|
hasClipboardButton
|
156
155
|
path={createHref(routes.tenant, undefined, {
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "ydb-embedded-ui",
|
3
|
-
"version": "2.
|
3
|
+
"version": "2.2.0",
|
4
4
|
"files": [
|
5
5
|
"dist"
|
6
6
|
],
|
@@ -57,6 +57,9 @@
|
|
57
57
|
],
|
58
58
|
"*.{js,jsx,ts,tsx}": [
|
59
59
|
"eslint --fix --quiet"
|
60
|
+
],
|
61
|
+
"*.{json}": [
|
62
|
+
"prettier --write"
|
60
63
|
]
|
61
64
|
},
|
62
65
|
"jest": {
|