ydb-embedded-ui 2.0.0 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +46 -0
- package/dist/assets/icons/update-arrow.svg +6 -0
- package/dist/components/EntityStatus/EntityStatus.js +37 -10
- package/dist/components/EntityStatus/EntityStatus.scss +36 -6
- package/dist/components/NodesViewer/NodesViewer.js +1 -0
- package/dist/components/ShortyString/ShortyString.tsx +21 -8
- package/dist/components/ShortyString/i18n/en.json +10 -0
- package/dist/components/ShortyString/i18n/index.ts +11 -0
- package/dist/components/ShortyString/i18n/ru.json +10 -0
- package/dist/containers/Cluster/Cluster.tsx +3 -3
- package/dist/containers/Nodes/Nodes.js +6 -6
- package/dist/containers/Storage/StorageFilter/StorageFilter.tsx +1 -0
- package/dist/containers/Tenant/Diagnostics/DetailedOverview/DetailedOverview.scss +2 -2
- package/dist/containers/Tenant/Diagnostics/DetailedOverview/DetailedOverview.tsx +16 -9
- package/dist/containers/Tenant/Diagnostics/Healthcheck/Healthcheck.scss +18 -5
- package/dist/containers/Tenant/Diagnostics/Healthcheck/Healthcheck.tsx +83 -0
- package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuePreview/IssuePreview.tsx +35 -0
- package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuePreview/index.ts +1 -0
- package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuesList/IssuesList.tsx +62 -0
- package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuesList/index.ts +1 -0
- package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuesViewer/IssueViewer.scss +21 -15
- package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuesViewer/IssuesViewer.js +52 -86
- package/dist/containers/Tenant/Diagnostics/Healthcheck/Preview/Preview.tsx +74 -0
- package/dist/containers/Tenant/Diagnostics/Healthcheck/Preview/index.ts +1 -0
- package/dist/containers/Tenant/Diagnostics/Healthcheck/i18n/en.json +7 -0
- package/dist/containers/Tenant/Diagnostics/Healthcheck/i18n/index.ts +11 -0
- package/dist/containers/Tenant/Diagnostics/Healthcheck/i18n/ru.json +7 -0
- package/dist/containers/Tenant/Diagnostics/Healthcheck/index.ts +1 -0
- package/dist/containers/Tenant/Diagnostics/Overview/Overview.tsx +4 -21
- package/dist/containers/Tenant/QueryEditor/QueryExplain/QueryExplain.js +18 -19
- package/dist/containers/Tenant/QueryEditor/QueryResult/QueryResult.scss +1 -0
- package/dist/containers/Tenant/Tenant.tsx +2 -2
- package/dist/containers/Tenants/Tenants.js +7 -7
- package/dist/services/api.d.ts +9 -0
- package/dist/types/api/healthcheck.ts +91 -0
- package/dist/types/store/healthcheck.ts +3 -0
- package/dist/utils/hooks/index.ts +1 -0
- package/dist/utils/hooks/useAutofetcher.ts +29 -0
- package/package.json +4 -1
- package/dist/containers/Tenant/Diagnostics/Healthcheck/Healthcheck.js +0 -195
@@ -0,0 +1,62 @@
|
|
1
|
+
import cn from 'bem-cn-lite';
|
2
|
+
|
3
|
+
import {Button, Icon} from '@gravity-ui/uikit';
|
4
|
+
|
5
|
+
import updateArrow from '../../../../../assets/icons/update-arrow.svg';
|
6
|
+
|
7
|
+
import type {IHealthCheck} from '../../../../../types/store/healthcheck';
|
8
|
+
|
9
|
+
import IssuesViewer from '../IssuesViewer/IssuesViewer';
|
10
|
+
|
11
|
+
import i18n from '../i18n';
|
12
|
+
|
13
|
+
const b = cn('healthcheck');
|
14
|
+
|
15
|
+
interface IssuesListProps {
|
16
|
+
data?: IHealthCheck;
|
17
|
+
loading?: boolean;
|
18
|
+
expandedIssueId?: string;
|
19
|
+
onUpdate: VoidFunction;
|
20
|
+
}
|
21
|
+
|
22
|
+
export const IssuesList = (props: IssuesListProps) => {
|
23
|
+
const {data, loading, onUpdate, expandedIssueId} = props;
|
24
|
+
|
25
|
+
if (!data) {
|
26
|
+
return null;
|
27
|
+
}
|
28
|
+
|
29
|
+
const renderHealthcheckHeader = () => {
|
30
|
+
return (
|
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} />
|
36
|
+
</Button>
|
37
|
+
</div>
|
38
|
+
</div>
|
39
|
+
);
|
40
|
+
};
|
41
|
+
|
42
|
+
const renderHealthcheckIssues = () => {
|
43
|
+
const {issue_log: issueLog} = data;
|
44
|
+
|
45
|
+
if (!issueLog) {
|
46
|
+
return null;
|
47
|
+
}
|
48
|
+
|
49
|
+
return (
|
50
|
+
<div className={b('issues')}>
|
51
|
+
<IssuesViewer issues={issueLog} expandedIssueId={expandedIssueId} />
|
52
|
+
</div>
|
53
|
+
);
|
54
|
+
};
|
55
|
+
|
56
|
+
return (
|
57
|
+
<div className={b('issues-list')}>
|
58
|
+
{renderHealthcheckHeader()}
|
59
|
+
{renderHealthcheckIssues()}
|
60
|
+
</div>
|
61
|
+
);
|
62
|
+
};
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from './IssuesList';
|
@@ -2,30 +2,22 @@
|
|
2
2
|
|
3
3
|
.issue {
|
4
4
|
display: flex;
|
5
|
+
justify-content: space-between;
|
5
6
|
align-items: center;
|
6
7
|
|
7
8
|
height: 40px;
|
8
9
|
|
9
10
|
cursor: pointer;
|
10
11
|
|
11
|
-
&_active {
|
12
|
-
border-radius: 4px;
|
13
|
-
background: var(--yc-color-base-info);
|
14
|
-
}
|
15
|
-
|
16
12
|
&__field {
|
17
|
-
|
13
|
+
display: flex;
|
14
|
+
overflow: hidden;
|
18
15
|
|
19
16
|
&_status {
|
20
17
|
display: flex;
|
21
18
|
|
22
|
-
min-width: 470px;
|
23
|
-
|
24
19
|
white-space: nowrap;
|
25
20
|
}
|
26
|
-
&_type {
|
27
|
-
min-width: 160px;
|
28
|
-
}
|
29
21
|
&_additional {
|
30
22
|
width: max-content;
|
31
23
|
|
@@ -39,6 +31,7 @@
|
|
39
31
|
}
|
40
32
|
&_message {
|
41
33
|
overflow: hidden;
|
34
|
+
flex-shrink: 0;
|
42
35
|
|
43
36
|
width: 300px;
|
44
37
|
|
@@ -96,8 +89,10 @@
|
|
96
89
|
.issue-viewer {
|
97
90
|
display: flex;
|
98
91
|
|
92
|
+
width: 820px;
|
93
|
+
|
99
94
|
&__tree {
|
100
|
-
|
95
|
+
width: 100%;
|
101
96
|
}
|
102
97
|
|
103
98
|
&__checkbox {
|
@@ -106,11 +101,10 @@
|
|
106
101
|
|
107
102
|
&__info-panel {
|
108
103
|
position: sticky;
|
109
|
-
top: 20px;
|
110
104
|
|
111
|
-
width: 500px;
|
112
105
|
height: 100%;
|
113
|
-
|
106
|
+
margin: 11px 0;
|
107
|
+
padding: 8px 20px;
|
114
108
|
|
115
109
|
border-radius: 4px;
|
116
110
|
background: var(--yc-color-base-generic);
|
@@ -152,6 +146,8 @@
|
|
152
146
|
}
|
153
147
|
|
154
148
|
.ydb-tree-view {
|
149
|
+
$calculated-margin: calc(24px * var(--ydb-tree-view-level));
|
150
|
+
|
155
151
|
&__item {
|
156
152
|
height: 40px;
|
157
153
|
}
|
@@ -160,5 +156,15 @@
|
|
160
156
|
width: 40px;
|
161
157
|
height: 40px;
|
162
158
|
}
|
159
|
+
|
160
|
+
// Without !important this class does not have enough weight compared to styles set in TreeView
|
161
|
+
.ydb-tree-view__item {
|
162
|
+
margin-left: $calculated-margin !important;
|
163
|
+
padding-left: 0 !important;
|
164
|
+
}
|
165
|
+
|
166
|
+
.issue-viewer__info-panel {
|
167
|
+
margin-left: $calculated-margin;
|
168
|
+
}
|
163
169
|
}
|
164
170
|
}
|
@@ -13,62 +13,26 @@ import EntityStatus from '../../../../../components/EntityStatus/EntityStatus';
|
|
13
13
|
|
14
14
|
import './IssueViewer.scss';
|
15
15
|
|
16
|
-
// const indicatorBlock = cn('indicator');
|
17
|
-
|
18
|
-
// const IssueStatus = ({status, name}) => {
|
19
|
-
// const modifier = status && status.toLowerCase();
|
20
|
-
|
21
|
-
// return (
|
22
|
-
// <React.Fragment>
|
23
|
-
// <div className={indicatorBlock({[modifier]: true})} />
|
24
|
-
// {name}
|
25
|
-
// </React.Fragment>
|
26
|
-
// );
|
27
|
-
// };
|
28
|
-
|
29
16
|
const issueBlock = cn('issue');
|
30
17
|
|
31
|
-
const IssueRow = ({data,
|
32
|
-
|
33
|
-
const {id, status, message, type, reasonsItems, ...rest} = data;
|
34
|
-
|
35
|
-
useEffect(() => {
|
36
|
-
if (active) {
|
37
|
-
setInfoForActive(rest);
|
38
|
-
}
|
39
|
-
}, [active, setInfoForActive]);
|
18
|
+
const IssueRow = ({data, onClick}) => {
|
19
|
+
const {status, message, type} = data;
|
40
20
|
|
41
21
|
return (
|
42
|
-
<div className={issueBlock(
|
22
|
+
<div className={issueBlock()} onClick={onClick}>
|
43
23
|
<div className={issueBlock('field', {status: true})}>
|
44
|
-
<EntityStatus status={status} name={
|
45
|
-
{/* <IssueStatus status={status} name={id} /> */}
|
46
|
-
</div>
|
47
|
-
<div
|
48
|
-
className={issueBlock('field', {message: true})}
|
49
|
-
style={{marginLeft: -treeLevel * 24 + 'px'}}
|
50
|
-
>
|
51
|
-
{message}
|
24
|
+
<EntityStatus mode="icons" status={status} name={type} />
|
52
25
|
</div>
|
53
|
-
<div className={issueBlock('field', {
|
26
|
+
<div className={issueBlock('field', {message: true})}>{message}</div>
|
54
27
|
</div>
|
55
28
|
);
|
56
29
|
};
|
57
30
|
|
58
31
|
const issueViewerBlock = cn('issue-viewer');
|
59
32
|
|
60
|
-
const IssuesViewer = ({issues}) => {
|
33
|
+
const IssuesViewer = ({issues, expandedIssueId}) => {
|
61
34
|
const [data, setData] = useState([]);
|
62
35
|
const [collapsedIssues, setCollapsedIssues] = useState({});
|
63
|
-
const [activeItem, setActiveItem] = useState();
|
64
|
-
const [infoData, setInfoData] = useState();
|
65
|
-
|
66
|
-
useEffect(() => {
|
67
|
-
if (!activeItem && data.length) {
|
68
|
-
const {id} = data[0];
|
69
|
-
setActiveItem(id);
|
70
|
-
}
|
71
|
-
}, [data]);
|
72
36
|
|
73
37
|
useEffect(() => {
|
74
38
|
const newData = getInvertedConsequencesTree({data: issues});
|
@@ -77,67 +41,69 @@ const IssuesViewer = ({issues}) => {
|
|
77
41
|
}, [issues]);
|
78
42
|
|
79
43
|
const renderTree = useCallback(
|
80
|
-
(data, childrenKey
|
44
|
+
(data, childrenKey) => {
|
81
45
|
return _.map(data, (item) => {
|
82
46
|
const {id} = item;
|
83
|
-
|
84
|
-
|
47
|
+
|
48
|
+
// eslint-disable-next-line no-unused-vars
|
49
|
+
const {status, message, type, reasonsItems, reason, level, ...rest} = item;
|
50
|
+
|
51
|
+
if (level === 1 && expandedIssueId && id !== expandedIssueId) {
|
52
|
+
return;
|
53
|
+
}
|
54
|
+
|
55
|
+
const isCollapsed =
|
56
|
+
typeof collapsedIssues[id] === 'undefined' || collapsedIssues[id];
|
57
|
+
|
58
|
+
const toggleCollapsed = () => {
|
59
|
+
setCollapsedIssues((collapsedIssues) => ({
|
60
|
+
...collapsedIssues,
|
61
|
+
[id]: !isCollapsed,
|
62
|
+
}));
|
63
|
+
};
|
85
64
|
|
86
65
|
return (
|
87
66
|
<TreeView
|
88
67
|
key={id}
|
89
|
-
name={
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
/>
|
96
|
-
}
|
97
|
-
collapsed={
|
98
|
-
typeof collapsedIssues[id] === 'undefined' || collapsedIssues[id]
|
99
|
-
}
|
100
|
-
hasArrow={hasArrow}
|
101
|
-
onClick={() => setActiveItem(id)}
|
102
|
-
onArrowClick={() => {
|
103
|
-
const newValue =
|
104
|
-
typeof collapsedIssues[id] === 'undefined'
|
105
|
-
? false
|
106
|
-
: !collapsedIssues[id];
|
107
|
-
const newCollapsedIssues = {...collapsedIssues, [id]: newValue};
|
108
|
-
setCollapsedIssues(newCollapsedIssues);
|
109
|
-
}}
|
68
|
+
name={<IssueRow data={item} />}
|
69
|
+
collapsed={isCollapsed}
|
70
|
+
hasArrow={true}
|
71
|
+
onClick={toggleCollapsed}
|
72
|
+
onArrowClick={toggleCollapsed}
|
73
|
+
level={level - 1}
|
110
74
|
>
|
111
|
-
{
|
75
|
+
{renderInfoPanel(rest)}
|
76
|
+
{renderTree(item[childrenKey], childrenKey)}
|
112
77
|
</TreeView>
|
113
78
|
);
|
114
79
|
});
|
115
80
|
},
|
116
|
-
[data, collapsedIssues
|
81
|
+
[data, collapsedIssues],
|
117
82
|
);
|
118
83
|
|
119
|
-
const renderInfoPanel = useCallback(
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
<
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
84
|
+
const renderInfoPanel = useCallback(
|
85
|
+
(info) => {
|
86
|
+
if (!info) {
|
87
|
+
return null;
|
88
|
+
}
|
89
|
+
|
90
|
+
return (
|
91
|
+
<div className={issueViewerBlock('info-panel')}>
|
92
|
+
<JSONTree
|
93
|
+
data={info}
|
94
|
+
search={false}
|
95
|
+
isExpanded={() => true}
|
96
|
+
className={issueViewerBlock('inspector')}
|
97
|
+
/>
|
98
|
+
</div>
|
99
|
+
);
|
100
|
+
},
|
101
|
+
[data],
|
102
|
+
);
|
136
103
|
|
137
104
|
return (
|
138
105
|
<div className={issueViewerBlock()}>
|
139
106
|
<div className={issueViewerBlock('tree')}>{renderTree(data, 'reasonsItems')}</div>
|
140
|
-
{renderInfoPanel()}
|
141
107
|
</div>
|
142
108
|
);
|
143
109
|
};
|
@@ -0,0 +1,74 @@
|
|
1
|
+
import {useMemo} from 'react';
|
2
|
+
import cn from 'bem-cn-lite';
|
3
|
+
|
4
|
+
import {Button, Icon} from '@gravity-ui/uikit';
|
5
|
+
|
6
|
+
import updateArrow from '../../../../../assets/icons/update-arrow.svg';
|
7
|
+
|
8
|
+
import {SelfCheckResult} from '../../../../../types/api/healthcheck';
|
9
|
+
import type {IHealthCheck} from '../../../../../types/store/healthcheck';
|
10
|
+
|
11
|
+
import {IssuePreview} from '../IssuePreview';
|
12
|
+
|
13
|
+
import i18n from '../i18n';
|
14
|
+
|
15
|
+
const b = cn('healthcheck');
|
16
|
+
|
17
|
+
interface PreviewProps {
|
18
|
+
data?: IHealthCheck;
|
19
|
+
loading?: boolean;
|
20
|
+
onUpdate: VoidFunction;
|
21
|
+
onShowMore?: (id: string) => void;
|
22
|
+
}
|
23
|
+
|
24
|
+
export const Preview = (props: PreviewProps) => {
|
25
|
+
const {data, loading, onShowMore, onUpdate} = props;
|
26
|
+
|
27
|
+
const selfCheckResult = data?.self_check_result || SelfCheckResult.UNSPECIFIED;
|
28
|
+
const isStatusOK = selfCheckResult === SelfCheckResult.GOOD;
|
29
|
+
|
30
|
+
const issuesLog = data?.issue_log;
|
31
|
+
const firstLevelIssues = useMemo(
|
32
|
+
() => issuesLog?.filter(({level}) => level === 1),
|
33
|
+
[issuesLog],
|
34
|
+
);
|
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
|
+
{selfCheckResult}
|
48
|
+
</div>
|
49
|
+
<Button size="s" onClick={onUpdate} loading={loading} view="flat-secondary">
|
50
|
+
<Icon data={updateArrow} width={20} height={20} />
|
51
|
+
</Button>
|
52
|
+
</div>
|
53
|
+
);
|
54
|
+
};
|
55
|
+
|
56
|
+
const renderFirstLevelIssues = () => {
|
57
|
+
return (
|
58
|
+
<div className={b('preview-content')}>
|
59
|
+
{isStatusOK
|
60
|
+
? i18n('status_message.ok')
|
61
|
+
: firstLevelIssues?.map((issue) => (
|
62
|
+
<IssuePreview key={issue.id} data={issue} onShowMore={onShowMore} />
|
63
|
+
))}
|
64
|
+
</div>
|
65
|
+
);
|
66
|
+
};
|
67
|
+
|
68
|
+
return (
|
69
|
+
<div className={b('preview')}>
|
70
|
+
{renderStatus()}
|
71
|
+
{renderFirstLevelIssues()}
|
72
|
+
</div>
|
73
|
+
);
|
74
|
+
};
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from './Preview';
|
@@ -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 @@
|
|
1
|
+
export * from './Healthcheck';
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import {ReactNode,
|
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
|
-
|
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
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
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
|
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}
|
@@ -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>
|
@@ -106,7 +107,7 @@ class Tenants extends React.Component {
|
|
106
107
|
|
107
108
|
getControlPlaneValue = (item) => {
|
108
109
|
const parts = _.get(item, 'Name', []).split('/');
|
109
|
-
const defaultValue = parts.length ? parts.
|
110
|
+
const defaultValue = parts.length ? parts[parts.length - 1] : '—';
|
110
111
|
|
111
112
|
return _.get(item, 'ControlPlane.name', defaultValue);
|
112
113
|
};
|
@@ -123,12 +124,10 @@ class Tenants extends React.Component {
|
|
123
124
|
savedTenantInitialTab,
|
124
125
|
} = this.props;
|
125
126
|
|
126
|
-
const filteredTenantsBySearch = tenants.filter(
|
127
|
-
(
|
128
|
-
|
129
|
-
|
130
|
-
filter,
|
131
|
-
);
|
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
|
+
});
|
132
131
|
const filteredTenants = Tenants.filterTenants(filteredTenantsBySearch, filter);
|
133
132
|
|
134
133
|
const initialTenantGeneralTab = savedTenantInitialTab || TENANT_GENERAL_TABS[0].id;
|
@@ -150,6 +149,7 @@ class Tenants extends React.Component {
|
|
150
149
|
externalLink={isExternalLink}
|
151
150
|
className={b('name')}
|
152
151
|
name={value || 'unknown database'}
|
152
|
+
withLeftTrim={true}
|
153
153
|
status={row.Overall}
|
154
154
|
hasClipboardButton
|
155
155
|
path={createHref(routes.tenant, undefined, {
|
package/dist/services/api.d.ts
CHANGED
@@ -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
|
}
|