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.
Files changed (24) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/assets/icons/update-arrow.svg +6 -0
  3. package/dist/components/EntityStatus/EntityStatus.js +16 -14
  4. package/dist/components/EntityStatus/EntityStatus.scss +14 -5
  5. package/dist/components/ShortyString/ShortyString.tsx +21 -8
  6. package/dist/components/ShortyString/i18n/en.json +10 -0
  7. package/dist/components/ShortyString/i18n/index.ts +11 -0
  8. package/dist/components/ShortyString/i18n/ru.json +10 -0
  9. package/dist/containers/Cluster/Cluster.tsx +3 -3
  10. package/dist/containers/Nodes/Nodes.js +5 -6
  11. package/dist/containers/Tenant/Diagnostics/DetailedOverview/DetailedOverview.scss +2 -2
  12. package/dist/containers/Tenant/Diagnostics/DetailedOverview/DetailedOverview.tsx +15 -7
  13. package/dist/containers/Tenant/Diagnostics/Healthcheck/Healthcheck.scss +11 -4
  14. package/dist/containers/Tenant/Diagnostics/Healthcheck/Healthcheck.tsx +18 -21
  15. package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuePreview/IssuePreview.tsx +8 -7
  16. package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuesList/IssuesList.tsx +14 -21
  17. package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuesViewer/IssueViewer.scss +21 -15
  18. package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuesViewer/IssuesViewer.js +52 -86
  19. package/dist/containers/Tenant/Diagnostics/Healthcheck/Preview/Preview.tsx +17 -23
  20. package/dist/containers/Tenant/Diagnostics/Healthcheck/i18n/en.json +5 -9
  21. package/dist/containers/Tenant/Diagnostics/Healthcheck/i18n/ru.json +5 -9
  22. package/dist/containers/Tenant/Tenant.tsx +2 -2
  23. package/dist/containers/Tenants/Tenants.js +6 -7
  24. 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 ExternalLink, Button, Icon} from '@gravity-ui/uikit';
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
- <ExternalLink target="_blank" href={iconPath}>
70
+ <UIKitLink target="_blank" href={iconPath}>
74
71
  {this.renderIcon()}
75
- </ExternalLink>
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 <ExternalLink href={path}>{name}</ExternalLink>;
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
- title={name}
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.renderLink()}
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
- &__name {
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 = 'Show more',
27
- collapseLabel = 'Show less',
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 text = expanded || value.length <= limit ? value : value.slice(0, limit - 4) + '\u00a0...';
35
- const label = expanded ? collapseLabel : expandLabel;
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
- {label} {length}
61
+ {toggleLabel}
49
62
  </Link>
50
63
  ) : null}
51
64
  </div>
@@ -0,0 +1,10 @@
1
+ {
2
+ "default_collapse_label": "Show less",
3
+ "default_expand_label": "Show more",
4
+ "chars_count": [
5
+ " ({{count}} symbol)",
6
+ " ({{count}} symbols)",
7
+ " ({{count}} symbols)",
8
+ " ({{count}} symbols)"
9
+ ]
10
+ }
@@ -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);
@@ -0,0 +1,10 @@
1
+ {
2
+ "default_collapse_label": "Показать меньше",
3
+ "default_expand_label": "Показать ещё",
4
+ "chars_count": [
5
+ " ({{count}} символ)",
6
+ " ({{count}} символа)",
7
+ " ({{count}} символов)",
8
+ " ({{count}} символов)"
9
+ ]
10
+ }
@@ -14,9 +14,9 @@ import ClusterInfo from '../../components/ClusterInfo/ClusterInfo';
14
14
  const b = cn('cluster');
15
15
 
16
16
  interface ClusterProps {
17
- additionalClusterInfo: any;
18
- additionalTenantsInfo: any;
19
- additionalNodesInfo: any;
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
- node.Host
119
- ? node.Host.includes(searchQuery) || String(node.NodeId).includes(searchQuery)
120
- : true,
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='No such nodes'
145
+ emptyDataMessage="No such nodes"
147
146
  />
148
147
  </div>
149
148
  </div>
@@ -19,8 +19,8 @@ $section-title-line-height: 24px;
19
19
 
20
20
  &__close-modal-button {
21
21
  position: absolute;
22
- top: 10px;
23
- right: 10px;
22
+ top: 23px;
23
+ right: 13px;
24
24
 
25
25
  & .yc-button__text {
26
26
  display: flex;
@@ -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 openModalHandler = () => {
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 tenant={props.tenantName} fetchData={false} />
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='Close'
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 tenantName={tenantName} additionalTenantInfo={additionalTenantInfo} />
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
- &__self-check-status {
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
- &__self-check-status-label {
42
+ &__issues-list-header-title {
38
43
  margin: 0 10px 0 0;
44
+
45
+ @include text-header-1();
39
46
  }
40
47
 
41
- &__self-check-update {
42
- margin-left: 20px;
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
- showMoreHandler?: VoidFunction;
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
- if (fetchData) {
40
- dispatch(getHealthcheckInfo(tenant));
41
- }
42
- }, [dispatch, fetchData, tenant]);
43
-
44
- useAutofetcher(fetchHealthcheck, [fetchHealthcheck], autorefresh);
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?: VoidFunction;
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">{data.message}</Text>
31
- <Link onClick={onShowMore}>{i18n('label.show-details')}</Link>
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 renderOverviewStatus = () => {
31
- const {self_check_result: selfCheckResult} = data;
32
- const modifier = selfCheckResult.toLowerCase();
33
-
29
+ const renderHealthcheckHeader = () => {
34
30
  return (
35
- <div className={b('self-check-status')}>
36
- <h3 className={b('self-check-status-label')}>{i18n('title.self-check-status')}</h3>
37
- <div className={b('self-check-status-indicator', {[modifier]: true})} />
38
- {selfCheckResult}
39
- <div className={b('self-check-update')}>
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
- <h3>{i18n('title.issues')}</h3>
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
- {renderOverviewStatus()}
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
- padding: 0 10px;
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
- padding-right: 20px;
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
- padding: 5px 20px 20px;
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, treeLevel, active, setInfoForActive, onClick}) => {
32
- // eslint-disable-next-line no-unused-vars
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({active})} onClick={onClick}>
22
+ <div className={issueBlock()} onClick={onClick}>
43
23
  <div className={issueBlock('field', {status: true})}>
44
- <EntityStatus status={status} name={id} />
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', {type: true})}>{type}</div>
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, treeLevel = 0) => {
44
+ (data, childrenKey) => {
81
45
  return _.map(data, (item) => {
82
46
  const {id} = item;
83
- const isActive = activeItem === item.id;
84
- const hasArrow = item[childrenKey].length;
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
- <IssueRow
91
- data={item}
92
- treeLevel={treeLevel}
93
- active={isActive}
94
- setInfoForActive={setInfoData}
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
- {renderTree(item[childrenKey], childrenKey, treeLevel + 1)}
75
+ {renderInfoPanel(rest)}
76
+ {renderTree(item[childrenKey], childrenKey)}
112
77
  </TreeView>
113
78
  );
114
79
  });
115
80
  },
116
- [data, collapsedIssues, activeItem],
81
+ [data, collapsedIssues],
117
82
  );
118
83
 
119
- const renderInfoPanel = useCallback(() => {
120
- if (!infoData) {
121
- return null;
122
- }
123
-
124
- return (
125
- <div className={issueViewerBlock('info-panel')}>
126
- <h3>Additional info for {activeItem}</h3>
127
- <JSONTree
128
- data={infoData}
129
- search={false}
130
- isExpanded={() => true}
131
- className={issueViewerBlock('inspector')}
132
- />
133
- </div>
134
- );
135
- }, [data, infoData, activeItem]);
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(() => issuesLog?.filter(({level}) => level === 1), [issuesLog]);
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
- {isStatusOK ? i18n('ok') : i18n('error')}
47
+ {selfCheckResult}
48
48
  </div>
49
- <Button size="s" onClick={onUpdate} loading={loading}>
50
- {i18n('label.update')}
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
- isStatusOK ?
61
- i18n('status_massage.ok') :
62
- firstLevelIssues?.map((issue) => (
63
- <IssuePreview
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
- "title.healthcheck": "Healthcheck",
3
- "title.issues": "Issues",
4
- "title.self-check-status": "Self check status",
5
- "label.update": "Update",
6
- "label.show-details": "Show details",
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
- "title.healthcheck": "Healthcheck",
3
- "title.issues": "Проблемы",
4
- "title.self-check-status": "Статус самопроверки",
5
- "label.update": "Обновить",
6
- "label.show-details": "Посмотреть подробности",
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
  }
@@ -43,8 +43,8 @@ const initialTenantSummaryState = {
43
43
  };
44
44
 
45
45
  interface TenantProps {
46
- additionalTenantInfo: any;
47
- additionalNodesInfo: any;
46
+ additionalTenantInfo?: any;
47
+ additionalNodesInfo?: any;
48
48
  }
49
49
 
50
50
  function Tenant(props: TenantProps) {
@@ -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.slice(-1) : '—';
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
- (item) =>
129
- item.Name.includes(searchQuery) ||
130
- this.getControlPlaneValue(item).includes(searchQuery),
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.1.0",
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": {