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.
Files changed (40) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/assets/icons/update-arrow.svg +6 -0
  3. package/dist/components/EntityStatus/EntityStatus.js +37 -10
  4. package/dist/components/EntityStatus/EntityStatus.scss +36 -6
  5. package/dist/components/NodesViewer/NodesViewer.js +1 -0
  6. package/dist/components/ShortyString/ShortyString.tsx +21 -8
  7. package/dist/components/ShortyString/i18n/en.json +10 -0
  8. package/dist/components/ShortyString/i18n/index.ts +11 -0
  9. package/dist/components/ShortyString/i18n/ru.json +10 -0
  10. package/dist/containers/Cluster/Cluster.tsx +3 -3
  11. package/dist/containers/Nodes/Nodes.js +6 -6
  12. package/dist/containers/Storage/StorageFilter/StorageFilter.tsx +1 -0
  13. package/dist/containers/Tenant/Diagnostics/DetailedOverview/DetailedOverview.scss +2 -2
  14. package/dist/containers/Tenant/Diagnostics/DetailedOverview/DetailedOverview.tsx +16 -9
  15. package/dist/containers/Tenant/Diagnostics/Healthcheck/Healthcheck.scss +18 -5
  16. package/dist/containers/Tenant/Diagnostics/Healthcheck/Healthcheck.tsx +83 -0
  17. package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuePreview/IssuePreview.tsx +35 -0
  18. package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuePreview/index.ts +1 -0
  19. package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuesList/IssuesList.tsx +62 -0
  20. package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuesList/index.ts +1 -0
  21. package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuesViewer/IssueViewer.scss +21 -15
  22. package/dist/containers/Tenant/Diagnostics/Healthcheck/IssuesViewer/IssuesViewer.js +52 -86
  23. package/dist/containers/Tenant/Diagnostics/Healthcheck/Preview/Preview.tsx +74 -0
  24. package/dist/containers/Tenant/Diagnostics/Healthcheck/Preview/index.ts +1 -0
  25. package/dist/containers/Tenant/Diagnostics/Healthcheck/i18n/en.json +7 -0
  26. package/dist/containers/Tenant/Diagnostics/Healthcheck/i18n/index.ts +11 -0
  27. package/dist/containers/Tenant/Diagnostics/Healthcheck/i18n/ru.json +7 -0
  28. package/dist/containers/Tenant/Diagnostics/Healthcheck/index.ts +1 -0
  29. package/dist/containers/Tenant/Diagnostics/Overview/Overview.tsx +4 -21
  30. package/dist/containers/Tenant/QueryEditor/QueryExplain/QueryExplain.js +18 -19
  31. package/dist/containers/Tenant/QueryEditor/QueryResult/QueryResult.scss +1 -0
  32. package/dist/containers/Tenant/Tenant.tsx +2 -2
  33. package/dist/containers/Tenants/Tenants.js +7 -7
  34. package/dist/services/api.d.ts +9 -0
  35. package/dist/types/api/healthcheck.ts +91 -0
  36. package/dist/types/store/healthcheck.ts +3 -0
  37. package/dist/utils/hooks/index.ts +1 -0
  38. package/dist/utils/hooks/useAutofetcher.ts +29 -0
  39. package/package.json +4 -1
  40. 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
- padding: 0 10px;
13
+ display: flex;
14
+ overflow: hidden;
18
15
 
19
16
  &_status {
20
17
  display: flex;
21
18
 
22
- min-width: 470px;
23
-
24
19
  white-space: nowrap;
25
20
  }
26
- &_type {
27
- min-width: 160px;
28
- }
29
21
  &_additional {
30
22
  width: max-content;
31
23
 
@@ -39,6 +31,7 @@
39
31
  }
40
32
  &_message {
41
33
  overflow: hidden;
34
+ flex-shrink: 0;
42
35
 
43
36
  width: 300px;
44
37
 
@@ -96,8 +89,10 @@
96
89
  .issue-viewer {
97
90
  display: flex;
98
91
 
92
+ width: 820px;
93
+
99
94
  &__tree {
100
- padding-right: 20px;
95
+ width: 100%;
101
96
  }
102
97
 
103
98
  &__checkbox {
@@ -106,11 +101,10 @@
106
101
 
107
102
  &__info-panel {
108
103
  position: sticky;
109
- top: 20px;
110
104
 
111
- width: 500px;
112
105
  height: 100%;
113
- padding: 5px 20px 20px;
106
+ margin: 11px 0;
107
+ padding: 8px 20px;
114
108
 
115
109
  border-radius: 4px;
116
110
  background: var(--yc-color-base-generic);
@@ -152,6 +146,8 @@
152
146
  }
153
147
 
154
148
  .ydb-tree-view {
149
+ $calculated-margin: calc(24px * var(--ydb-tree-view-level));
150
+
155
151
  &__item {
156
152
  height: 40px;
157
153
  }
@@ -160,5 +156,15 @@
160
156
  width: 40px;
161
157
  height: 40px;
162
158
  }
159
+
160
+ // Without !important this class does not have enough weight compared to styles set in TreeView
161
+ .ydb-tree-view__item {
162
+ margin-left: $calculated-margin !important;
163
+ padding-left: 0 !important;
164
+ }
165
+
166
+ .issue-viewer__info-panel {
167
+ margin-left: $calculated-margin;
168
+ }
163
169
  }
164
170
  }
@@ -13,62 +13,26 @@ import EntityStatus from '../../../../../components/EntityStatus/EntityStatus';
13
13
 
14
14
  import './IssueViewer.scss';
15
15
 
16
- // const indicatorBlock = cn('indicator');
17
-
18
- // const IssueStatus = ({status, name}) => {
19
- // const modifier = status && status.toLowerCase();
20
-
21
- // return (
22
- // <React.Fragment>
23
- // <div className={indicatorBlock({[modifier]: true})} />
24
- // {name}
25
- // </React.Fragment>
26
- // );
27
- // };
28
-
29
16
  const issueBlock = cn('issue');
30
17
 
31
- const IssueRow = ({data, treeLevel, active, setInfoForActive, onClick}) => {
32
- // eslint-disable-next-line no-unused-vars
33
- const {id, status, message, type, reasonsItems, ...rest} = data;
34
-
35
- useEffect(() => {
36
- if (active) {
37
- setInfoForActive(rest);
38
- }
39
- }, [active, setInfoForActive]);
18
+ const IssueRow = ({data, onClick}) => {
19
+ const {status, message, type} = data;
40
20
 
41
21
  return (
42
- <div className={issueBlock({active})} onClick={onClick}>
22
+ <div className={issueBlock()} onClick={onClick}>
43
23
  <div className={issueBlock('field', {status: true})}>
44
- <EntityStatus status={status} name={id} />
45
- {/* <IssueStatus status={status} name={id} /> */}
46
- </div>
47
- <div
48
- className={issueBlock('field', {message: true})}
49
- style={{marginLeft: -treeLevel * 24 + 'px'}}
50
- >
51
- {message}
24
+ <EntityStatus mode="icons" status={status} name={type} />
52
25
  </div>
53
- <div className={issueBlock('field', {type: true})}>{type}</div>
26
+ <div className={issueBlock('field', {message: true})}>{message}</div>
54
27
  </div>
55
28
  );
56
29
  };
57
30
 
58
31
  const issueViewerBlock = cn('issue-viewer');
59
32
 
60
- const IssuesViewer = ({issues}) => {
33
+ const IssuesViewer = ({issues, expandedIssueId}) => {
61
34
  const [data, setData] = useState([]);
62
35
  const [collapsedIssues, setCollapsedIssues] = useState({});
63
- const [activeItem, setActiveItem] = useState();
64
- const [infoData, setInfoData] = useState();
65
-
66
- useEffect(() => {
67
- if (!activeItem && data.length) {
68
- const {id} = data[0];
69
- setActiveItem(id);
70
- }
71
- }, [data]);
72
36
 
73
37
  useEffect(() => {
74
38
  const newData = getInvertedConsequencesTree({data: issues});
@@ -77,67 +41,69 @@ const IssuesViewer = ({issues}) => {
77
41
  }, [issues]);
78
42
 
79
43
  const renderTree = useCallback(
80
- (data, childrenKey, treeLevel = 0) => {
44
+ (data, childrenKey) => {
81
45
  return _.map(data, (item) => {
82
46
  const {id} = item;
83
- const isActive = activeItem === item.id;
84
- const hasArrow = item[childrenKey].length;
47
+
48
+ // eslint-disable-next-line no-unused-vars
49
+ const {status, message, type, reasonsItems, reason, level, ...rest} = item;
50
+
51
+ if (level === 1 && expandedIssueId && id !== expandedIssueId) {
52
+ return;
53
+ }
54
+
55
+ const isCollapsed =
56
+ typeof collapsedIssues[id] === 'undefined' || collapsedIssues[id];
57
+
58
+ const toggleCollapsed = () => {
59
+ setCollapsedIssues((collapsedIssues) => ({
60
+ ...collapsedIssues,
61
+ [id]: !isCollapsed,
62
+ }));
63
+ };
85
64
 
86
65
  return (
87
66
  <TreeView
88
67
  key={id}
89
- name={
90
- <IssueRow
91
- data={item}
92
- treeLevel={treeLevel}
93
- active={isActive}
94
- setInfoForActive={setInfoData}
95
- />
96
- }
97
- collapsed={
98
- typeof collapsedIssues[id] === 'undefined' || collapsedIssues[id]
99
- }
100
- hasArrow={hasArrow}
101
- onClick={() => setActiveItem(id)}
102
- onArrowClick={() => {
103
- const newValue =
104
- typeof collapsedIssues[id] === 'undefined'
105
- ? false
106
- : !collapsedIssues[id];
107
- const newCollapsedIssues = {...collapsedIssues, [id]: newValue};
108
- setCollapsedIssues(newCollapsedIssues);
109
- }}
68
+ name={<IssueRow data={item} />}
69
+ collapsed={isCollapsed}
70
+ hasArrow={true}
71
+ onClick={toggleCollapsed}
72
+ onArrowClick={toggleCollapsed}
73
+ level={level - 1}
110
74
  >
111
- {renderTree(item[childrenKey], childrenKey, treeLevel + 1)}
75
+ {renderInfoPanel(rest)}
76
+ {renderTree(item[childrenKey], childrenKey)}
112
77
  </TreeView>
113
78
  );
114
79
  });
115
80
  },
116
- [data, collapsedIssues, activeItem],
81
+ [data, collapsedIssues],
117
82
  );
118
83
 
119
- const renderInfoPanel = useCallback(() => {
120
- if (!infoData) {
121
- return null;
122
- }
123
-
124
- return (
125
- <div className={issueViewerBlock('info-panel')}>
126
- <h3>Additional info for {activeItem}</h3>
127
- <JSONTree
128
- data={infoData}
129
- search={false}
130
- isExpanded={() => true}
131
- className={issueViewerBlock('inspector')}
132
- />
133
- </div>
134
- );
135
- }, [data, infoData, activeItem]);
84
+ const renderInfoPanel = useCallback(
85
+ (info) => {
86
+ if (!info) {
87
+ return null;
88
+ }
89
+
90
+ return (
91
+ <div className={issueViewerBlock('info-panel')}>
92
+ <JSONTree
93
+ data={info}
94
+ search={false}
95
+ isExpanded={() => true}
96
+ className={issueViewerBlock('inspector')}
97
+ />
98
+ </div>
99
+ );
100
+ },
101
+ [data],
102
+ );
136
103
 
137
104
  return (
138
105
  <div className={issueViewerBlock()}>
139
106
  <div className={issueViewerBlock('tree')}>{renderTree(data, 'reasonsItems')}</div>
140
- {renderInfoPanel()}
141
107
  </div>
142
108
  );
143
109
  };
@@ -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,7 @@
1
+ {
2
+ "title.healthcheck": "Healthcheck",
3
+ "label.update": "Update",
4
+ "label.show-details": "Show details",
5
+ "status_message.ok": "No issues have been found on this database",
6
+ "no-data": "no healthcheck data"
7
+ }
@@ -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,7 @@
1
+ {
2
+ "title.healthcheck": "Healthcheck",
3
+ "label.update": "Обновить",
4
+ "label.show-details": "Посмотреть подробности",
5
+ "status_message.ok": "В базе данных нет проблем",
6
+ "no-data": "нет данных healthcheck"
7
+ }
@@ -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;
@@ -43,8 +43,8 @@ const initialTenantSummaryState = {
43
43
  };
44
44
 
45
45
  interface TenantProps {
46
- additionalTenantInfo: any;
47
- additionalNodesInfo: any;
46
+ additionalTenantInfo?: any;
47
+ additionalNodesInfo?: any;
48
48
  }
49
49
 
50
50
  function Tenant(props: TenantProps) {
@@ -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.slice(-1) : '—';
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
- (item) =>
128
- item.Name.includes(searchQuery) ||
129
- this.getControlPlaneValue(item).includes(searchQuery),
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, {
@@ -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
  }