ydb-embedded-ui 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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);