ydb-embedded-ui 2.0.0 → 2.1.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 (30) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/components/EntityStatus/EntityStatus.js +28 -3
  3. package/dist/components/EntityStatus/EntityStatus.scss +22 -1
  4. package/dist/components/NodesViewer/NodesViewer.js +1 -0
  5. package/dist/containers/Nodes/Nodes.js +1 -0
  6. package/dist/containers/Storage/StorageFilter/StorageFilter.tsx +1 -0
  7. package/dist/containers/Tenant/Diagnostics/DetailedOverview/DetailedOverview.tsx +2 -3
  8. package/dist/containers/Tenant/Diagnostics/Healthcheck/Healthcheck.scss +7 -1
  9. package/dist/containers/Tenant/Diagnostics/Healthcheck/Healthcheck.tsx +86 -0
  10. package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuePreview/IssuePreview.tsx +34 -0
  11. package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuePreview/index.ts +1 -0
  12. package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuesList/IssuesList.tsx +69 -0
  13. package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuesList/index.ts +1 -0
  14. package/dist/containers/Tenant/Diagnostics/Healthcheck/Preview/Preview.tsx +80 -0
  15. package/dist/containers/Tenant/Diagnostics/Healthcheck/Preview/index.ts +1 -0
  16. package/dist/containers/Tenant/Diagnostics/Healthcheck/i18n/en.json +11 -0
  17. package/dist/containers/Tenant/Diagnostics/Healthcheck/i18n/index.ts +11 -0
  18. package/dist/containers/Tenant/Diagnostics/Healthcheck/i18n/ru.json +11 -0
  19. package/dist/containers/Tenant/Diagnostics/Healthcheck/index.ts +1 -0
  20. package/dist/containers/Tenant/Diagnostics/Overview/Overview.tsx +4 -21
  21. package/dist/containers/Tenant/QueryEditor/QueryExplain/QueryExplain.js +18 -19
  22. package/dist/containers/Tenant/QueryEditor/QueryResult/QueryResult.scss +1 -0
  23. package/dist/containers/Tenants/Tenants.js +1 -0
  24. package/dist/services/api.d.ts +9 -0
  25. package/dist/types/api/healthcheck.ts +91 -0
  26. package/dist/types/store/healthcheck.ts +3 -0
  27. package/dist/utils/hooks/index.ts +1 -0
  28. package/dist/utils/hooks/useAutofetcher.ts +29 -0
  29. package/package.json +1 -1
  30. package/dist/containers/Tenant/Diagnostics/Healthcheck/Healthcheck.js +0 -195
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.1.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v2.0.0...v2.1.0) (2022-10-04)
4
+
5
+
6
+ ### Features
7
+
8
+ * autofocus all text search fields ([a38ee84](https://github.com/ydb-platform/ydb-embedded-ui/commit/a38ee84abad4202f5e9b8af897eb68d2c006233a))
9
+ * **Healthcheck:** display first level issues in overview ([10b4bf5](https://github.com/ydb-platform/ydb-embedded-ui/commit/10b4bf5d15d32f028702ff8cfecca0e06bc5616f))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * fix production assets paths ([8eaad0f](https://github.com/ydb-platform/ydb-embedded-ui/commit/8eaad0f1db109c4cf3cbf7d11ad32ea335a6b0c1))
15
+ * **Healthcheck:** add translations ([75f9851](https://github.com/ydb-platform/ydb-embedded-ui/commit/75f9851a35766ef692805a6f154d40340b003487))
16
+ * move eslint hooks rule extension to src config ([179b81d](https://github.com/ydb-platform/ydb-embedded-ui/commit/179b81d60adf422addc8d72f947800c72bd3e4c5))
17
+ * **QueryEditor:** disable fullscreen button for empty result ([4825b5b](https://github.com/ydb-platform/ydb-embedded-ui/commit/4825b5b8dcb89fcafd828dabbace521ddc429922))
18
+ * **QueryEditor:** fix query stats spacings ([b836d72](https://github.com/ydb-platform/ydb-embedded-ui/commit/b836d72824a791b3fde2b9e4585c6c9b42385265))
19
+ * **useAutofetcher:** private autofetcher instance for each usage ([3f34b7a](https://github.com/ydb-platform/ydb-embedded-ui/commit/3f34b7aee2042562a42e6d1a7daf03ffddd888c0))
20
+
3
21
  ## [2.0.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.14.2...v2.0.0) (2022-09-26)
4
22
 
5
23
 
@@ -2,10 +2,22 @@ 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} from '@gravity-ui/uikit';
5
+ import {ClipboardButton, Link as ExternalLink, Button, Icon} from '@gravity-ui/uikit';
6
+
7
+ import circleInfoIcon from '../../assets/icons/circle-info.svg';
8
+ import circleExclamationIcon from '../../assets/icons/circle-exclamation.svg';
9
+ import triangleExclamationIcon from '../../assets/icons/triangle-exclamation.svg';
10
+ import circleTimesIcon from '../../assets/icons/circle-xmark.svg';
6
11
 
7
12
  import './EntityStatus.scss';
8
13
 
14
+ const icons = {
15
+ BLUE: circleInfoIcon,
16
+ YELLOW: circleExclamationIcon,
17
+ ORANGE: triangleExclamationIcon,
18
+ RED: circleTimesIcon,
19
+ };
20
+
9
21
  const b = cn('entity-status');
10
22
 
11
23
  class EntityStatus extends React.Component {
@@ -22,6 +34,7 @@ class EntityStatus extends React.Component {
22
34
  showStatus: PropTypes.bool,
23
35
  externalLink: PropTypes.bool,
24
36
  className: PropTypes.string,
37
+ mode: PropTypes.oneOf(['color', 'icons']),
25
38
  };
26
39
 
27
40
  static defaultProps = {
@@ -31,15 +44,27 @@ class EntityStatus extends React.Component {
31
44
  label: '',
32
45
  showStatus: true,
33
46
  externalLink: false,
47
+ mode: 'color',
34
48
  };
35
49
  renderIcon() {
36
- const {status, size, showStatus} = this.props;
50
+ const {status, size, showStatus, mode} = this.props;
37
51
 
38
52
  if (!showStatus) {
39
53
  return null;
40
54
  }
41
55
 
42
- return <div className={b('status-icon', {state: status.toLowerCase(), size})} />;
56
+ const modifiers = {state: status.toLowerCase(), size};
57
+
58
+ if (mode === 'icons' && icons[status]) {
59
+ return (
60
+ <Icon
61
+ className={b('status-icon', modifiers)}
62
+ data={icons[status]}
63
+ />
64
+ );
65
+ }
66
+
67
+ return <div className={b('status-color', modifiers)} />;
43
68
  }
44
69
  renderStatusLink() {
45
70
  const {iconPath} = this.props;
@@ -57,6 +57,7 @@
57
57
  white-space: nowrap;
58
58
  }
59
59
 
60
+ &__status-color,
60
61
  &__status-icon {
61
62
  margin-right: 8px;
62
63
 
@@ -64,19 +65,24 @@
64
65
  &_size_xs {
65
66
  aspect-ratio: 1;
66
67
 
68
+ width: 12px;
67
69
  height: 12px;
68
70
  }
69
71
  &_size_s {
70
72
  aspect-ratio: 1;
71
73
 
74
+ width: 16px;
72
75
  height: 16px;
73
76
  }
74
77
  &_size_m {
75
78
  aspect-ratio: 1;
76
79
 
80
+ width: 18px;
77
81
  height: 18px;
78
82
  }
83
+ }
79
84
 
85
+ &__status-color {
80
86
  &_state_running,
81
87
  &_state_green {
82
88
  background-color: var(--yc-color-infographics-positive-heavy);
@@ -96,7 +102,22 @@
96
102
  background: var(--yc-color-text-complementary);
97
103
  }
98
104
  &_state_orange {
99
- background: var(--yc-color-text-warning-heavy);
105
+ background: var(--yc-color-base-warning-orange);
106
+ }
107
+ }
108
+
109
+ &__status-icon {
110
+ &_state_blue {
111
+ color: var(--yc-color-infographics-info-heavy);
112
+ }
113
+ &_state_yellow {
114
+ color: var(--yc-color-infographics-warning-heavy);
115
+ }
116
+ &_state_orange {
117
+ color: var(--yc-color-base-warning-orange);
118
+ }
119
+ &_state_red {
120
+ color: var(--yc-color-infographics-danger-heavy);
100
121
  }
101
122
  }
102
123
 
@@ -106,6 +106,7 @@ class NodesViewer extends React.PureComponent {
106
106
  text={searchQuery}
107
107
  onUpdate={handleSearchQuery}
108
108
  hasClear
109
+ autoFocus
109
110
  />
110
111
  <ProblemFilter value={filter} onChange={this.onChangeProblemFilter} />
111
112
  <Label theme="info" size="m">{`Nodes: ${nodesToShow.length}`}</Label>
@@ -87,6 +87,7 @@ class Nodes extends React.Component {
87
87
  text={searchQuery}
88
88
  onUpdate={this.handleSearchQueryChange}
89
89
  hasClear
90
+ autoFocus
90
91
  />
91
92
  <ProblemFilter value={filter} onChange={this.handleFilterChange} />
92
93
  <Label theme="info" size="m">{`Nodes: ${nodes?.length}`}</Label>
@@ -47,6 +47,7 @@ export const StorageFilter = (props: StorageFilterProps) => {
47
47
  value={filterValue}
48
48
  onUpdate={changeFilter}
49
49
  hasClear
50
+ autoFocus
50
51
  />
51
52
  );
52
53
  }
@@ -8,8 +8,7 @@ import type {EPathType} from '../../../../types/api/schema';
8
8
  //@ts-ignore
9
9
  import Icon from '../../../../components/Icon/Icon';
10
10
  import Overview from '../Overview/Overview';
11
- //@ts-ignore
12
- import Healthcheck from '../Healthcheck/Healthcheck';
11
+ import {Healthcheck} from '../Healthcheck';
13
12
  //@ts-ignore
14
13
  import TenantOverview from '../TenantOverview/TenantOverview';
15
14
 
@@ -42,7 +41,7 @@ function DetailedOverview(props: DetailedOverviewProps) {
42
41
  const renderModal = () => {
43
42
  return (
44
43
  <Modal open={isModalVisible} onClose={closeModalHandler} className={b('modal')}>
45
- <Healthcheck tenant={props.tenantName} />
44
+ <Healthcheck tenant={props.tenantName} fetchData={false} />
46
45
  <Button
47
46
  className={b('close-modal-button')}
48
47
  onClick={closeModalHandler}
@@ -2,7 +2,13 @@
2
2
  @import '../../../../styles/mixins.scss';
3
3
 
4
4
  .healthcheck {
5
- padding: 25px 20px 20px;
5
+ &__issues-list {
6
+ padding: 25px 20px 20px;
7
+ }
8
+
9
+ &__issue-preview {
10
+ margin-bottom: 15px;
11
+ }
6
12
 
7
13
  &__issues {
8
14
  overflow-x: hidden;
@@ -0,0 +1,86 @@
1
+ import {useCallback} from 'react';
2
+ import {useDispatch, useSelector} from 'react-redux';
3
+ import cn from 'bem-cn-lite';
4
+
5
+ import {Loader} from '@gravity-ui/uikit';
6
+
7
+ import {getHealthcheckInfo} from '../../../../store/reducers/healthcheckInfo';
8
+ import {useAutofetcher} from '../../../../utils/hooks';
9
+
10
+ import {IssuesList} from './IssuesList';
11
+ import {Preview} from './Preview';
12
+
13
+ import i18n from './i18n';
14
+ import './Healthcheck.scss';
15
+
16
+ interface HealthcheckProps {
17
+ tenant: string;
18
+ preview?: boolean;
19
+ fetchData?: boolean;
20
+ showMoreHandler?: VoidFunction;
21
+ }
22
+
23
+ const b = cn('healthcheck');
24
+
25
+ export const Healthcheck = (props: HealthcheckProps) => {
26
+ const {
27
+ tenant,
28
+ preview,
29
+ fetchData = true,
30
+ showMoreHandler,
31
+ } = props;
32
+
33
+ const dispatch = useDispatch();
34
+
35
+ const {data, loading, wasLoaded, error} = useSelector((state: any) => state.healthcheckInfo);
36
+ const {autorefresh} = useSelector((state: any) => state.schema);
37
+
38
+ const fetchHealthcheck = useCallback(() => {
39
+ if (fetchData) {
40
+ dispatch(getHealthcheckInfo(tenant));
41
+ }
42
+ }, [dispatch, fetchData, tenant]);
43
+
44
+ useAutofetcher(fetchHealthcheck, [fetchHealthcheck], autorefresh);
45
+
46
+ const renderContent = () => {
47
+ if (error) {
48
+ return error.statusText;
49
+ }
50
+
51
+ if (loading && !wasLoaded) {
52
+ return (
53
+ <div className={b('loader')}>
54
+ <Loader size="m" />
55
+ </div>
56
+ );
57
+ }
58
+
59
+ if (data && data['self_check_result']) {
60
+ return preview ? (
61
+ <Preview
62
+ data={data}
63
+ loading={loading}
64
+ onShowMore={showMoreHandler}
65
+ onUpdate={fetchHealthcheck}
66
+ />
67
+ ) : (
68
+ <IssuesList
69
+ data={data}
70
+ loading={loading}
71
+ onUpdate={fetchHealthcheck}
72
+ />
73
+ );
74
+ }
75
+
76
+ return (
77
+ <div className="error">{i18n('no-data')}</div>
78
+ );
79
+ };
80
+
81
+ return (
82
+ <div className={b()}>
83
+ {renderContent()}
84
+ </div>
85
+ );
86
+ };
@@ -0,0 +1,34 @@
1
+ import cn from 'bem-cn-lite';
2
+
3
+ import {Link, Text} from '@gravity-ui/uikit';
4
+
5
+ import EntityStatus from '../../../../../components/EntityStatus/EntityStatus';
6
+ import {IssueLog} from '../../../../../types/api/healthcheck';
7
+
8
+ import i18n from '../i18n';
9
+
10
+ const b = cn('healthcheck');
11
+
12
+ interface IssuePreviewProps {
13
+ data?: IssueLog;
14
+ onShowMore?: VoidFunction;
15
+ }
16
+
17
+ export const IssuePreview = (props: IssuePreviewProps) => {
18
+ const {
19
+ data,
20
+ onShowMore,
21
+ } = props;
22
+
23
+ if (!data) {
24
+ return null;
25
+ }
26
+
27
+ return (
28
+ <div className={b('issue-preview')}>
29
+ <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>
32
+ </div>
33
+ );
34
+ };
@@ -0,0 +1 @@
1
+ export * from './IssuePreview';
@@ -0,0 +1,69 @@
1
+ import cn from 'bem-cn-lite';
2
+
3
+ import {Button} from '@gravity-ui/uikit';
4
+
5
+ import type {IHealthCheck} from '../../../../../types/store/healthcheck';
6
+
7
+ import IssuesViewer from '../IssuesViewer/IssuesViewer';
8
+
9
+ import i18n from '../i18n';
10
+
11
+ const b = cn('healthcheck');
12
+
13
+ interface IssuesListProps {
14
+ data?: IHealthCheck;
15
+ loading?: boolean;
16
+ onUpdate: VoidFunction;
17
+ }
18
+
19
+ export const IssuesList = (props: IssuesListProps) => {
20
+ const {
21
+ data,
22
+ loading,
23
+ onUpdate,
24
+ } = props;
25
+
26
+ if (!data) {
27
+ return null;
28
+ }
29
+
30
+ const renderOverviewStatus = () => {
31
+ const {self_check_result: selfCheckResult} = data;
32
+ const modifier = selfCheckResult.toLowerCase();
33
+
34
+ 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')}
42
+ </Button>
43
+ </div>
44
+ </div>
45
+ );
46
+ };
47
+
48
+ const renderHealthcheckIssues = () => {
49
+ const {issue_log: issueLog} = data;
50
+
51
+ if (!issueLog) {
52
+ return null;
53
+ }
54
+
55
+ return (
56
+ <div className={b('issues')}>
57
+ <h3>{i18n('title.issues')}</h3>
58
+ <IssuesViewer issues={issueLog} />
59
+ </div>
60
+ );
61
+ }
62
+
63
+ return (
64
+ <div className={b('issues-list')}>
65
+ {renderOverviewStatus()}
66
+ {renderHealthcheckIssues()}
67
+ </div>
68
+ );
69
+ };
@@ -0,0 +1 @@
1
+ export * from './IssuesList';
@@ -0,0 +1,80 @@
1
+ import {useMemo} from 'react';
2
+ import cn from 'bem-cn-lite';
3
+
4
+ import {Button} from '@gravity-ui/uikit';
5
+
6
+ import {SelfCheckResult} from '../../../../../types/api/healthcheck';
7
+ import type {IHealthCheck} from '../../../../../types/store/healthcheck';
8
+
9
+ import {IssuePreview} from '../IssuePreview';
10
+
11
+ import i18n from '../i18n';
12
+
13
+ const b = cn('healthcheck');
14
+
15
+ interface PreviewProps {
16
+ data?: IHealthCheck;
17
+ loading?: boolean;
18
+ onShowMore?: VoidFunction;
19
+ onUpdate: VoidFunction;
20
+ }
21
+
22
+ export const Preview = (props: PreviewProps) => {
23
+ const {
24
+ data,
25
+ loading,
26
+ onShowMore,
27
+ onUpdate,
28
+ } = props;
29
+
30
+ const selfCheckResult = data?.self_check_result || SelfCheckResult.UNSPECIFIED;
31
+ const isStatusOK = selfCheckResult === SelfCheckResult.GOOD;
32
+
33
+ const issuesLog = data?.issue_log;
34
+ const firstLevelIssues = useMemo(() => issuesLog?.filter(({level}) => level === 1), [issuesLog]);
35
+
36
+ if (!data) {
37
+ return null;
38
+ }
39
+
40
+ const renderStatus = () => {
41
+ const modifier = selfCheckResult.toLowerCase();
42
+
43
+ return (
44
+ <div className={b('status-wrapper')}>
45
+ <div className={b('preview-title')}>{i18n('title.healthcheck')}</div>
46
+ <div className={b('self-check-status-indicator', {[modifier]: true})}>
47
+ {isStatusOK ? i18n('ok') : i18n('error')}
48
+ </div>
49
+ <Button size="s" onClick={onUpdate} loading={loading}>
50
+ {i18n('label.update')}
51
+ </Button>
52
+ </div>
53
+ );
54
+ };
55
+
56
+ const renderFirstLevelIssues = () => {
57
+ return (
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
+ }
70
+ </div>
71
+ );
72
+ };
73
+
74
+ return (
75
+ <div className={b('preview')}>
76
+ {renderStatus()}
77
+ {renderFirstLevelIssues()}
78
+ </div>
79
+ );
80
+ };
@@ -0,0 +1 @@
1
+ export * from './Preview';
@@ -0,0 +1,11 @@
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"
11
+ }
@@ -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-diagnostics-healthcheck';
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,11 @@
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"
11
+ }
@@ -0,0 +1 @@
1
+ export * from './Healthcheck';
@@ -1,4 +1,4 @@
1
- import {ReactNode, useCallback, useEffect, useMemo} from 'react';
1
+ import {ReactNode, useMemo} from 'react';
2
2
  import {useDispatch, useSelector} from 'react-redux';
3
3
  import cn from 'bem-cn-lite';
4
4
 
@@ -14,11 +14,11 @@ import {
14
14
 
15
15
  import {EPathType} from '../../../../types/api/schema';
16
16
  import {isColumnEntityType, isTableType} from '../../utils/schema';
17
- import {AutoFetcher} from '../../../../utils/autofetcher';
18
17
  //@ts-ignore
19
18
  import {getSchema} from '../../../../store/reducers/schema';
20
19
  //@ts-ignore
21
20
  import {getOlapStats} from '../../../../store/reducers/olapStats';
21
+ import {useAutofetcher} from '../../../../utils/hooks';
22
22
 
23
23
  import './Overview.scss';
24
24
 
@@ -57,8 +57,6 @@ interface OverviewProps {
57
57
 
58
58
  const b = cn('kv-tenant-overview');
59
59
 
60
- const autofetcher = new AutoFetcher();
61
-
62
60
  function Overview(props: OverviewProps) {
63
61
  const {tenantName, type} = props;
64
62
 
@@ -76,29 +74,14 @@ function Overview(props: OverviewProps) {
76
74
  data: { result: olapStats } = { result: undefined },
77
75
  } = useSelector((state: any) => state.olapStats);
78
76
 
79
- const fetchOverviewData = useCallback(() => {
77
+ useAutofetcher(() => {
80
78
  const schemaPath = currentSchemaPath || tenantName;
81
79
  dispatch(getSchema({path: schemaPath}));
82
80
 
83
81
  if (isTableType(type) && isColumnEntityType(type)) {
84
82
  dispatch(getOlapStats({path: schemaPath}));
85
83
  }
86
- }, [currentSchemaPath, dispatch, tenantName, type]);
87
-
88
- useEffect(fetchOverviewData, [fetchOverviewData]);
89
-
90
- useEffect(() => {
91
- autofetcher.stop();
92
-
93
- if (autorefresh) {
94
- autofetcher.start();
95
- autofetcher.fetch(() => fetchOverviewData());
96
- }
97
-
98
- return () => {
99
- autofetcher.stop();
100
- };
101
- }, [autorefresh, fetchOverviewData]);
84
+ }, [currentSchemaPath, dispatch, tenantName, type], autorefresh);
102
85
 
103
86
  const tableSchema =
104
87
  currentItem?.PathDescription?.Table || currentItem?.PathDescription?.ColumnTableDescription;
@@ -141,16 +141,23 @@ function QueryExplain(props) {
141
141
  );
142
142
  };
143
143
 
144
- const renderTextExplain = () => {
145
- const {explain} = props;
146
-
147
- if (!explain) {
148
- return renderStub();
144
+ const hasContent = () => {
145
+ switch (activeOption) {
146
+ case ExplainOptionIds.schema:
147
+ return Boolean(props.explain?.nodes?.length);
148
+ case ExplainOptionIds.json:
149
+ return Boolean(props.explain);
150
+ case ExplainOptionIds.ast:
151
+ return Boolean(props.ast);
152
+ default:
153
+ return false;
149
154
  }
155
+ };
150
156
 
157
+ const renderTextExplain = () => {
151
158
  const content = (
152
159
  <JSONTree
153
- data={explain.pristine}
160
+ data={props.explain?.pristine}
154
161
  isExpanded={() => true}
155
162
  className={b('inspector')}
156
163
  searchOptions={{
@@ -168,13 +175,6 @@ function QueryExplain(props) {
168
175
  };
169
176
 
170
177
  const renderAstExplain = () => {
171
- if (!props.ast) {
172
- return (
173
- <div className={b('text-message')}>
174
- There is no AST explanation for the request
175
- </div>
176
- );
177
- }
178
178
  const content = (
179
179
  <div className={b('ast')}>
180
180
  <MonacoEditor
@@ -196,11 +196,6 @@ function QueryExplain(props) {
196
196
  const renderGraph = () => {
197
197
  const {explain = {}, theme} = props;
198
198
  const {links, nodes, version} = explain;
199
-
200
- if (!explain.nodes?.length) {
201
- return renderStub();
202
- }
203
-
204
199
  const content =
205
200
  links && nodes && nodes.length ? (
206
201
  <div
@@ -259,6 +254,10 @@ function QueryExplain(props) {
259
254
  return renderError();
260
255
  }
261
256
 
257
+ if (!hasContent()) {
258
+ return renderStub();
259
+ }
260
+
262
261
  switch (activeOption) {
263
262
  case ExplainOptionIds.json: {
264
263
  return renderTextExplain();
@@ -293,7 +292,7 @@ function QueryExplain(props) {
293
292
  )}
294
293
  </div>
295
294
  <div className={b('controls-left')}>
296
- <EnableFullscreenButton disabled={Boolean(props.error)} />
295
+ <EnableFullscreenButton disabled={Boolean(props.error) || !hasContent()} />
297
296
  <PaneVisibilityToggleButtons
298
297
  onCollapse={props.onCollapseResults}
299
298
  onExpand={props.onExpandResults}
@@ -51,6 +51,7 @@
51
51
  }
52
52
  &__inspector {
53
53
  max-width: calc(100% - 50px);
54
+ padding: 15px 10px;
54
55
  @include json-tree-styles();
55
56
  &_fullscreen {
56
57
  overflow: auto;
@@ -98,6 +98,7 @@ class Tenants extends React.Component {
98
98
  text={searchQuery}
99
99
  onUpdate={handleSearchQuery}
100
100
  hasClear
101
+ autoFocus
101
102
  />
102
103
  <ProblemFilter value={filter} onChange={changeFilter} />
103
104
  </div>
@@ -30,6 +30,15 @@ interface Window {
30
30
  },
31
31
  axiosOptions?: AxiosOptions,
32
32
  ) => Promise<import('../types/api/query').QueryAPIResponse<Action, Schema>>;
33
+ getExplainQuery: (
34
+ query: string,
35
+ database: string,
36
+ ) => Promise<import('../types/api/query').QueryAPIExplainResponse<'explain'>>;
37
+ getExplainQueryAst: (
38
+ query: string,
39
+ database: string,
40
+ ) => Promise<import('../types/api/query').QueryAPIExplainResponse<'explain-ast'>>;
41
+ getHealthcheckInfo: (database: string) => Promise<import('../types/api/healthcheck').HealthCheckAPIResponse>,
33
42
  [method: string]: Function;
34
43
  };
35
44
  }
@@ -0,0 +1,91 @@
1
+ export enum SelfCheckResult {
2
+ UNSPECIFIED = 'UNSPECIFIED',
3
+ GOOD = 'GOOD',
4
+ DEGRADED = 'DEGRADED',
5
+ MAINTENANCE_REQUIRED = 'MAINTENANCE_REQUIRED',
6
+ EMERGENCY = 'EMERGENCY',
7
+ }
8
+
9
+ enum StatusFlag {
10
+ UNSPECIFIED = 'UNSPECIFIED',
11
+ GREY = 'GREY',
12
+ GREEN = 'GREEN',
13
+ BLUE = 'BLUE',
14
+ YELLOW = 'YELLOW',
15
+ ORANGE = 'ORANGE',
16
+ RED = 'RED',
17
+ }
18
+
19
+ interface LocationNode {
20
+ id: number;
21
+ host: string;
22
+ port: number;
23
+ }
24
+
25
+ interface LocationStoragePDisk {
26
+ id: string;
27
+ path: string;
28
+ }
29
+
30
+ interface LocationStorageVDisk {
31
+ id: string;
32
+ pdisk: LocationStoragePDisk;
33
+ }
34
+
35
+ interface LocationStorageGroup {
36
+ id: string;
37
+ vdisk: LocationStorageVDisk;
38
+ }
39
+
40
+ interface LocationStoragePool {
41
+ name: string;
42
+ group: LocationStorageGroup;
43
+ }
44
+
45
+ interface LocationStorage {
46
+ node: LocationNode;
47
+ pool: LocationStoragePool;
48
+ }
49
+
50
+ interface LocationComputePool {
51
+ name: string;
52
+ }
53
+
54
+ interface LocationComputeTablet {
55
+ type: string;
56
+ id?: string[];
57
+ count: number;
58
+ }
59
+
60
+ interface LocationCompute {
61
+ node: LocationNode;
62
+ pool: LocationComputePool;
63
+ tablet: LocationComputeTablet;
64
+ }
65
+
66
+ interface LocationDatabase {
67
+ name: string;
68
+ }
69
+
70
+ interface Location {
71
+ storage: LocationStorage;
72
+ compute: LocationCompute;
73
+ database: LocationDatabase;
74
+ }
75
+
76
+ export interface IssueLog {
77
+ id: string;
78
+ status: StatusFlag;
79
+ message: string;
80
+ location: Location;
81
+ reason?: string[];
82
+ type: string;
83
+ level: number;
84
+ }
85
+
86
+ export interface HealthCheckAPIResponse {
87
+ // eslint-disable-next-line camelcase
88
+ self_check_result: SelfCheckResult;
89
+ // eslint-disable-next-line camelcase
90
+ issue_log?: IssueLog[];
91
+ }
@@ -0,0 +1,3 @@
1
+ import type {HealthCheckAPIResponse} from "../api/healthcheck";
2
+
3
+ export type IHealthCheck = HealthCheckAPIResponse;
@@ -0,0 +1 @@
1
+ export * from './useAutofetcher';
@@ -0,0 +1,29 @@
1
+ import {DependencyList, useEffect, useRef} from 'react';
2
+
3
+ import {AutoFetcher} from '../autofetcher';
4
+
5
+ export const useAutofetcher = (fetchData: VoidFunction, deps: DependencyList, enabled = true) => {
6
+ const ref = useRef<AutoFetcher | null>(null);
7
+
8
+ if (ref.current === null) {
9
+ ref.current = new AutoFetcher();
10
+ }
11
+
12
+ const autofetcher = ref.current;
13
+
14
+ // initial fetch
15
+ useEffect(fetchData, deps); // eslint-disable-line react-hooks/exhaustive-deps
16
+
17
+ useEffect(() => {
18
+ autofetcher.stop();
19
+
20
+ if (enabled) {
21
+ autofetcher.start();
22
+ autofetcher.fetch(fetchData);
23
+ }
24
+
25
+ return () => {
26
+ autofetcher.stop();
27
+ };
28
+ }, [enabled, ...deps]); // eslint-disable-line react-hooks/exhaustive-deps
29
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-embedded-ui",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "files": [
5
5
  "dist"
6
6
  ],
@@ -1,195 +0,0 @@
1
- import React from 'react';
2
- import PropTypes from 'prop-types';
3
- import {connect} from 'react-redux';
4
- import cn from 'bem-cn-lite';
5
-
6
- import {Loader, Button} from '@gravity-ui/uikit';
7
-
8
- import IssuesViewer from './IssuesViewer/IssuesViewer';
9
-
10
- import {getHealthcheckInfo} from '../../../../store/reducers/healthcheckInfo';
11
- import {hideTooltip, showTooltip} from '../../../../store/reducers/tooltip';
12
- import {AutoFetcher} from '../../../../utils/autofetcher';
13
-
14
- import './Healthcheck.scss';
15
-
16
- const b = cn('healthcheck');
17
-
18
- class Healthcheck extends React.Component {
19
- static propTypes = {
20
- data: PropTypes.object,
21
- loading: PropTypes.bool,
22
- wasLoaded: PropTypes.bool,
23
- error: PropTypes.object,
24
- getHealthcheckInfo: PropTypes.func,
25
- tenant: PropTypes.string,
26
- preview: PropTypes.bool,
27
- showMoreHandler: PropTypes.func,
28
- };
29
-
30
- autofetcher;
31
-
32
- componentDidMount() {
33
- this.autofetcher = new AutoFetcher();
34
- this.fetchHealthcheck();
35
- if (this.props.autorefresh) {
36
- this.autofetcher.start();
37
- this.autofetcher.fetch(() => this.fetchHealthcheck());
38
- }
39
- }
40
-
41
- componentDidUpdate(prevProps) {
42
- const {autorefresh} = this.props;
43
-
44
- if (autorefresh && !prevProps.autorefresh) {
45
- this.fetchHealthcheck();
46
- this.autofetcher.stop();
47
- this.autofetcher.start();
48
- this.autofetcher.fetch(() => this.fetchHealthcheck());
49
- }
50
- if (!autorefresh && prevProps.autorefresh) {
51
- this.autofetcher.stop();
52
- }
53
- }
54
-
55
- componentWillUnmount() {
56
- this.autofetcher.stop();
57
- }
58
-
59
- fetchHealthcheck = () => {
60
- const {tenant, getHealthcheckInfo} = this.props;
61
- getHealthcheckInfo(tenant);
62
- };
63
-
64
- renderLoader() {
65
- return (
66
- <div className={b('loader')}>
67
- <Loader size="m" />
68
- </div>
69
- );
70
- }
71
-
72
- renderUpdateButton() {
73
- const {loading} = this.props;
74
- return (
75
- <Button size="s" onClick={this.fetchHealthcheck} loading={loading}>
76
- Update
77
- </Button>
78
- );
79
- }
80
-
81
- renderPreview = () => {
82
- const {data, showMoreHandler} = this.props;
83
- const {self_check_result: selfCheckResult} = data;
84
- const modifier = selfCheckResult.toLowerCase();
85
-
86
- const statusOk = selfCheckResult === 'GOOD';
87
- const text = statusOk
88
- ? 'No issues have been found on this database.'
89
- : 'Several issues have been found on this database.';
90
-
91
- return (
92
- <div>
93
- <div className={b('status-wrapper')}>
94
- <div className={b('preview-title')}>Healthcheck</div>
95
- <div className={b('self-check-status-indicator', {[modifier]: true})}>
96
- {statusOk ? 'Ok' : 'Error'}
97
- </div>
98
- {this.renderUpdateButton()}
99
- </div>
100
- <div className={b('preview-content')}>
101
- {text}
102
- {!statusOk && (
103
- <Button
104
- view="flat-info"
105
- onClick={showMoreHandler}
106
- size="s"
107
- >
108
- Show details
109
- </Button>
110
- )}
111
- </div>
112
- </div>
113
- );
114
- };
115
-
116
- renderOverviewStatus = () => {
117
- const {data} = this.props;
118
- const {self_check_result: selfCheckResult} = data;
119
- const modifier = selfCheckResult.toLowerCase();
120
-
121
- return (
122
- <div className={b('self-check-status')}>
123
- <h3 className={b('self-check-status-label')}>Self check status</h3>
124
- <div className={b('self-check-status-indicator', {[modifier]: true})} />
125
- {selfCheckResult}
126
- <div className={b('self-check-update')}>{this.renderUpdateButton()}</div>
127
- </div>
128
- );
129
- };
130
-
131
- renderHealthcheckIssues() {
132
- const {data, showTooltip, hideTooltip} = this.props;
133
- const {issue_log: issueLog} = data;
134
-
135
- if (!issueLog) {
136
- return null;
137
- }
138
-
139
- return (
140
- <div className={b('issues')}>
141
- <h3>Issues</h3>
142
- <IssuesViewer
143
- issues={issueLog}
144
- showTooltip={showTooltip}
145
- hideTooltip={hideTooltip}
146
- />
147
- </div>
148
- );
149
- }
150
-
151
- renderContent = () => {
152
- const {preview} = this.props;
153
- return preview ? (
154
- this.renderPreview()
155
- ) : (
156
- <div className={b()}>
157
- {this.renderOverviewStatus()}
158
- {this.renderHealthcheckIssues()}
159
- </div>
160
- );
161
- };
162
-
163
- render() {
164
- const {error, data, loading, wasLoaded} = this.props;
165
-
166
- if (error) {
167
- return <div>{error.statusText}</div>;
168
- } else if (data && data['self_check_result']) {
169
- return this.renderContent();
170
- } else if (loading && !wasLoaded) {
171
- return this.renderLoader();
172
- } else return <div className="error">no healthcheck data</div>;
173
- }
174
- }
175
-
176
- function mapStateToProps(state) {
177
- const {data, loading, wasLoaded, error} = state.healthcheckInfo;
178
- const {autorefresh} = state.schema;
179
-
180
- return {
181
- data,
182
- loading,
183
- wasLoaded,
184
- error,
185
- autorefresh,
186
- };
187
- }
188
-
189
- const mapDispatchToProps = {
190
- getHealthcheckInfo,
191
- hideTooltip,
192
- showTooltip,
193
- };
194
-
195
- export default connect(mapStateToProps, mapDispatchToProps)(Healthcheck);