ydb-embedded-ui 2.1.0 → 2.2.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.
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": {