ydb-embedded-ui 2.0.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.
- 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
|
}
|