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.
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
  }