ydb-embedded-ui 4.6.0 → 4.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/assets/icons/versions.svg +3 -0
  3. package/dist/components/NodeHostWrapper/NodeHostWrapper.tsx +6 -1
  4. package/dist/components/Tablet/Tablet.tsx +17 -3
  5. package/dist/components/TabletsStatistic/TabletsStatistic.tsx +23 -16
  6. package/dist/containers/App/Content.js +5 -2
  7. package/dist/containers/Cluster/Cluster.tsx +6 -13
  8. package/dist/containers/Cluster/ClusterInfo/ClusterInfo.tsx +3 -3
  9. package/dist/containers/Cluster/{utils.ts → utils.tsx} +11 -0
  10. package/dist/containers/Header/Header.scss +9 -0
  11. package/dist/containers/Header/Header.tsx +70 -14
  12. package/dist/containers/Header/breadcrumbs.ts +146 -0
  13. package/dist/containers/Node/Node.tsx +21 -27
  14. package/dist/containers/Node/NodePages.ts +10 -6
  15. package/dist/containers/Storage/StorageGroups/StorageGroups.tsx +11 -3
  16. package/dist/containers/Tablet/Tablet.tsx +35 -27
  17. package/dist/containers/Tablet/TabletInfo/TabletInfo.tsx +2 -2
  18. package/dist/containers/TabletsFilters/TabletsFilters.js +13 -15
  19. package/dist/containers/Tenant/Diagnostics/Consumers/columns/columns.tsx +1 -1
  20. package/dist/containers/Tenant/Diagnostics/DetailedOverview/DetailedOverview.scss +5 -1
  21. package/dist/containers/Tenant/Diagnostics/Diagnostics.tsx +1 -1
  22. package/dist/containers/Tenant/Diagnostics/Healthcheck/Healthcheck.scss +5 -3
  23. package/dist/containers/Tenant/Diagnostics/Healthcheck/Healthcheck.tsx +1 -1
  24. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.js +2 -1
  25. package/dist/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx +11 -13
  26. package/dist/containers/Tenant/ObjectGeneral/ObjectGeneral.tsx +2 -2
  27. package/dist/containers/Tenant/ObjectSummary/ObjectSummary.tsx +6 -2
  28. package/dist/containers/Tenant/{QueryEditor/QueryResult/QueryResult.js → Query/ExecuteResult/ExecuteResult.js} +3 -5
  29. package/dist/containers/Tenant/{QueryEditor/QueryResult/QueryResult.scss → Query/ExecuteResult/ExecuteResult.scss} +1 -1
  30. package/dist/containers/Tenant/{QueryEditor/QueryExplain/QueryExplain.js → Query/ExplainResult/ExplainResult.js} +3 -5
  31. package/dist/containers/Tenant/{QueryEditor/QueryExplain/QueryExplain.scss → Query/ExplainResult/ExplainResult.scss} +1 -1
  32. package/dist/containers/Tenant/Query/QueriesHistory/QueriesHistory.scss +20 -0
  33. package/dist/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx +60 -0
  34. package/dist/containers/Tenant/Query/Query.scss +16 -0
  35. package/dist/containers/Tenant/Query/Query.tsx +73 -0
  36. package/dist/containers/Tenant/{QueryEditor → Query/QueryEditor}/QueryEditor.js +26 -87
  37. package/dist/containers/Tenant/{QueryEditor → Query/QueryEditor}/QueryEditor.scss +7 -23
  38. package/dist/containers/Tenant/{QueryEditor → Query}/QueryEditorControls/QueryEditorControls.scss +1 -4
  39. package/dist/containers/Tenant/Query/QueryTabs/QueryTabs.tsx +59 -0
  40. package/dist/containers/Tenant/{QueryEditor → Query}/SaveQuery/SaveQuery.js +5 -5
  41. package/dist/containers/Tenant/Query/SavedQueries/SavedQueries.scss +55 -0
  42. package/dist/containers/Tenant/Query/SavedQueries/SavedQueries.tsx +150 -0
  43. package/dist/containers/Tenant/Query/i18n/en.json +12 -0
  44. package/dist/containers/Tenant/Query/i18n/ru.json +12 -0
  45. package/dist/containers/Tenant/Query/utils/getPreparedResult.ts +30 -0
  46. package/dist/containers/Tenant/Tenant.tsx +2 -18
  47. package/dist/containers/Tenant/TenantPages.tsx +8 -2
  48. package/dist/containers/Tenant/utils/constants.ts +10 -0
  49. package/dist/containers/Tenant/utils/schemaActions.ts +7 -2
  50. package/dist/containers/Tenants/Tenants.js +21 -9
  51. package/dist/routes.ts +10 -1
  52. package/dist/services/api.ts +9 -5
  53. package/dist/store/reducers/executeQuery.ts +1 -1
  54. package/dist/store/reducers/header/header.ts +31 -0
  55. package/dist/store/reducers/header/types.ts +54 -0
  56. package/dist/store/reducers/index.ts +1 -1
  57. package/dist/store/reducers/node/types.ts +2 -0
  58. package/dist/store/reducers/settings/settings.ts +1 -1
  59. package/dist/store/reducers/tablet.ts +18 -1
  60. package/dist/store/reducers/tenant/constants.ts +6 -0
  61. package/dist/store/reducers/tenant/tenant.ts +21 -2
  62. package/dist/store/reducers/tenant/types.ts +9 -2
  63. package/dist/store/reducers/topic.ts +1 -1
  64. package/dist/store/state-url-mapping.js +4 -1
  65. package/dist/types/store/query.ts +5 -0
  66. package/dist/types/store/tablet.ts +7 -4
  67. package/dist/utils/constants.ts +2 -0
  68. package/package.json +2 -1
  69. package/dist/containers/Tenant/QueryEditor/QueriesHistory/QueriesHistory.scss +0 -85
  70. package/dist/containers/Tenant/QueryEditor/QueriesHistory/QueriesHistory.tsx +0 -95
  71. package/dist/containers/Tenant/QueryEditor/SavedQueries/SavedQueries.js +0 -161
  72. package/dist/containers/Tenant/QueryEditor/SavedQueries/SavedQueries.scss +0 -93
  73. package/dist/containers/Tenant/QueryEditor/i18n/en.json +0 -3
  74. package/dist/containers/Tenant/QueryEditor/i18n/ru.json +0 -3
  75. package/dist/store/reducers/header.ts +0 -26
  76. /package/dist/containers/Tenant/{QueryEditor → Query}/Issues/Issues.scss +0 -0
  77. /package/dist/containers/Tenant/{QueryEditor → Query}/Issues/Issues.tsx +0 -0
  78. /package/dist/containers/Tenant/{QueryEditor → Query}/Issues/models.ts +0 -0
  79. /package/dist/containers/Tenant/{QueryEditor → Query}/QueryDuration/QueryDuration.scss +0 -0
  80. /package/dist/containers/Tenant/{QueryEditor → Query}/QueryDuration/QueryDuration.tsx +0 -0
  81. /package/dist/containers/Tenant/{QueryEditor → Query}/QueryEditorControls/OldQueryEditorControls.tsx +0 -0
  82. /package/dist/containers/Tenant/{QueryEditor → Query}/QueryEditorControls/QueryEditorControls.tsx +0 -0
  83. /package/dist/containers/Tenant/{QueryEditor → Query}/QueryEditorControls/shared.ts +0 -0
  84. /package/dist/containers/Tenant/{QueryEditor → Query}/SaveQuery/SaveQuery.scss +0 -0
  85. /package/dist/containers/Tenant/{QueryEditor → Query}/i18n/index.ts +0 -0
@@ -0,0 +1,73 @@
1
+ import {useDispatch} from 'react-redux';
2
+ import block from 'bem-cn-lite';
3
+
4
+ import type {EPathType} from '../../../types/api/schema';
5
+ import type {SavedQuery} from '../../../types/store/query';
6
+ import {changeUserInput} from '../../../store/reducers/executeQuery';
7
+ import {TENANT_QUERY_TABS_ID} from '../../../store/reducers/tenant/constants';
8
+ import {useSetting, useTypedSelector} from '../../../utils/hooks';
9
+ import {SAVED_QUERIES_KEY} from '../../../utils/constants';
10
+
11
+ import {QueryTabs} from './QueryTabs/QueryTabs';
12
+ import {SavedQueries} from './SavedQueries/SavedQueries';
13
+ import QueriesHistory from './QueriesHistory/QueriesHistory';
14
+ import QueryEditor from './QueryEditor/QueryEditor';
15
+
16
+ import './Query.scss';
17
+
18
+ const b = block('ydb-query');
19
+
20
+ interface QueryProps {
21
+ theme: string;
22
+ path?: string;
23
+ type?: EPathType;
24
+ }
25
+
26
+ export const Query = (props: QueryProps) => {
27
+ const dispatch = useDispatch();
28
+
29
+ const {queryTab = TENANT_QUERY_TABS_ID.newQuery} = useTypedSelector((state) => state.tenant);
30
+
31
+ const [savedQueries, setSavedQueries] = useSetting<SavedQuery[]>(SAVED_QUERIES_KEY, []);
32
+
33
+ const handleDeleteQuery = (queryName: string) => {
34
+ const newSavedQueries = savedQueries.filter(
35
+ (el) => el.name.toLowerCase() !== queryName.toLowerCase(),
36
+ );
37
+ setSavedQueries(newSavedQueries);
38
+ };
39
+
40
+ const handleUserInputChange = (value: {input: string}) => {
41
+ dispatch(changeUserInput(value));
42
+ };
43
+
44
+ const renderContent = () => {
45
+ switch (queryTab) {
46
+ case TENANT_QUERY_TABS_ID.newQuery: {
47
+ return <QueryEditor changeUserInput={handleUserInputChange} {...props} />;
48
+ }
49
+ case TENANT_QUERY_TABS_ID.history: {
50
+ return <QueriesHistory changeUserInput={handleUserInputChange} />;
51
+ }
52
+ case TENANT_QUERY_TABS_ID.saved: {
53
+ return (
54
+ <SavedQueries
55
+ changeUserInput={handleUserInputChange}
56
+ savedQueries={savedQueries}
57
+ onDeleteQuery={handleDeleteQuery}
58
+ />
59
+ );
60
+ }
61
+ default: {
62
+ return null;
63
+ }
64
+ }
65
+ };
66
+
67
+ return (
68
+ <div className={b()}>
69
+ <QueryTabs className={b('tabs')} activeTab={queryTab} />
70
+ <div className={b('content')}>{renderContent()}</div>
71
+ </div>
72
+ );
73
+ };
@@ -5,43 +5,42 @@ import cn from 'bem-cn-lite';
5
5
  import _ from 'lodash';
6
6
  import MonacoEditor from 'react-monaco-editor';
7
7
 
8
- import SplitPane from '../../../components/SplitPane';
9
- import {QueryResultTable} from '../../../components/QueryResultTable';
8
+ import SplitPane from '../../../../components/SplitPane';
9
+ import {QueryResultTable} from '../../../../components/QueryResultTable';
10
10
 
11
11
  import {
12
12
  sendExecuteQuery,
13
- changeUserInput,
14
13
  saveQueryToHistory,
15
14
  goToPreviousQuery,
16
15
  goToNextQuery,
17
16
  MONACO_HOT_KEY_ACTIONS,
18
17
  setMonacoHotKey,
19
- } from '../../../store/reducers/executeQuery';
20
- import {getExplainQuery, getExplainQueryAst} from '../../../store/reducers/explainQuery';
21
- import {getParsedSettingValue, setSettingValue} from '../../../store/reducers/settings/settings';
22
- import {setShowPreview} from '../../../store/reducers/schema/schema';
18
+ } from '../../../../store/reducers/executeQuery';
19
+ import {getExplainQuery, getExplainQueryAst} from '../../../../store/reducers/explainQuery';
20
+ import {getParsedSettingValue, setSettingValue} from '../../../../store/reducers/settings/settings';
21
+ import {setShowPreview} from '../../../../store/reducers/schema/schema';
23
22
  import {
24
23
  DEFAULT_IS_QUERY_RESULT_COLLAPSED,
25
24
  DEFAULT_SIZE_RESULT_PANE_KEY,
26
25
  SAVED_QUERIES_KEY,
27
26
  QUERY_INITIAL_MODE_KEY,
28
27
  ENABLE_ADDITIONAL_QUERY_MODES,
29
- } from '../../../utils/constants';
30
- import {useSetting} from '../../../utils/hooks';
31
- import {QueryModes} from '../../../types/store/query';
28
+ } from '../../../../utils/constants';
29
+ import {useSetting} from '../../../../utils/hooks';
30
+ import {QueryModes} from '../../../../types/store/query';
32
31
 
33
32
  import {
34
33
  PaneVisibilityActionTypes,
35
34
  paneVisibilityToggleReducerCreator,
36
- } from '../utils/paneVisibilityToggleHelpers';
37
- import Preview from '../Preview/Preview';
35
+ } from '../../utils/paneVisibilityToggleHelpers';
36
+ import Preview from '../../Preview/Preview';
38
37
 
39
- import SavedQueries from './SavedQueries/SavedQueries';
40
- import QueryResult from './QueryResult/QueryResult';
41
- import QueryExplain from './QueryExplain/QueryExplain';
42
- import {QueryEditorControls} from './QueryEditorControls/QueryEditorControls';
43
- import {OldQueryEditorControls} from './QueryEditorControls/OldQueryEditorControls';
44
- import QueriesHistory from './QueriesHistory/QueriesHistory';
38
+ import {ExecuteResult} from '../ExecuteResult/ExecuteResult';
39
+ import {ExplainResult} from '../ExplainResult/ExplainResult';
40
+ import {QueryEditorControls} from '../QueryEditorControls/QueryEditorControls';
41
+ import {OldQueryEditorControls} from '../QueryEditorControls/OldQueryEditorControls';
42
+
43
+ import {getPreparedResult} from '../utils/getPreparedResult';
45
44
 
46
45
  import './QueryEditor.scss';
47
46
 
@@ -67,6 +66,7 @@ const b = cn('query-editor');
67
66
 
68
67
  const propTypes = {
69
68
  sendExecuteQuery: PropTypes.func,
69
+ changeUserInput: PropTypes.func,
70
70
  path: PropTypes.string,
71
71
  response: PropTypes.oneOfType([PropTypes.bool, PropTypes.array]),
72
72
  executeQuery: PropTypes.object,
@@ -324,11 +324,11 @@ function QueryEditor(props) {
324
324
  />
325
325
  );
326
326
  }
327
- const textResults = getPreparedResult();
327
+ const textResults = getPreparedResult(data);
328
328
  const disabled = !textResults.length || resultType !== RESULT_TYPES.EXECUTE;
329
329
 
330
330
  return data || error ? (
331
- <QueryResult
331
+ <ExecuteResult
332
332
  result={content}
333
333
  stats={stats}
334
334
  error={error}
@@ -348,7 +348,7 @@ function QueryEditor(props) {
348
348
  } = props;
349
349
 
350
350
  return (
351
- <QueryExplain
351
+ <ExplainResult
352
352
  error={error}
353
353
  explain={data}
354
354
  astQuery={handleAstQuery}
@@ -436,46 +436,6 @@ function QueryEditor(props) {
436
436
  return queries.length - 1 === currentIndex;
437
437
  };
438
438
 
439
- const renderHistoryNavigation = () => {
440
- const {changeUserInput} = props;
441
- return (
442
- <div className={b('history-controls')}>
443
- <QueriesHistory changeUserInput={changeUserInput} />
444
- </div>
445
- );
446
- };
447
-
448
- const getPreparedResult = () => {
449
- const {
450
- executeQuery: {data},
451
- } = props;
452
- const columnDivider = '\t';
453
- const rowDivider = '\n';
454
-
455
- if (!data?.result?.length) {
456
- return '';
457
- }
458
-
459
- const columnHeaders = Object.keys(data.result[0]);
460
- const rows = [columnHeaders].concat(data.result);
461
-
462
- return rows
463
- .map((item) => {
464
- const row = [];
465
-
466
- for (const field in item) {
467
- if (typeof item[field] === 'object' || Array.isArray(item[field])) {
468
- row.push(JSON.stringify(item[field]));
469
- } else {
470
- row.push(item[field]);
471
- }
472
- }
473
-
474
- return row.join(columnDivider);
475
- })
476
- .join(rowDivider);
477
- };
478
-
479
439
  const onChangeWindow = _.throttle(() => {
480
440
  updateEditor();
481
441
  }, 100);
@@ -513,14 +473,6 @@ function QueryEditor(props) {
513
473
  setSettingValue(SAVED_QUERIES_KEY, JSON.stringify(newSavedQueries));
514
474
  };
515
475
 
516
- const onDeleteQueryHandler = (queryName) => {
517
- const {savedQueries = [], setSettingValue} = props;
518
- const newSavedQueries = savedQueries.filter(
519
- (el) => el.name.toLowerCase() !== queryName.toLowerCase(),
520
- );
521
- setSettingValue(SAVED_QUERIES_KEY, JSON.stringify(newSavedQueries));
522
- };
523
-
524
476
  const renderControls = () => {
525
477
  const {executeQuery, explainQuery, savedQueries} = props;
526
478
 
@@ -555,21 +507,6 @@ function QueryEditor(props) {
555
507
  );
556
508
  };
557
509
 
558
- const renderUpperControls = () => {
559
- const {savedQueries, changeUserInput} = props;
560
-
561
- return (
562
- <div className={b('upper-controls')}>
563
- {renderHistoryNavigation()}
564
- <SavedQueries
565
- savedQueries={savedQueries}
566
- changeUserInput={changeUserInput}
567
- onDeleteQuery={onDeleteQueryHandler}
568
- />
569
- </div>
570
- );
571
- };
572
-
573
510
  const {executeQuery, theme} = props;
574
511
  const result = renderResult();
575
512
 
@@ -584,7 +521,11 @@ function QueryEditor(props) {
584
521
  collapsedSizes={[100, 0]}
585
522
  onSplitStartDragAdditional={onSplitStartDragAdditional}
586
523
  >
587
- <div className={b('pane-wrapper')}>
524
+ <div
525
+ className={b('pane-wrapper', {
526
+ top: true,
527
+ })}
528
+ >
588
529
  <div className={b('monaco-wrapper')}>
589
530
  <div className={b('monaco')}>
590
531
  <MonacoEditor
@@ -598,7 +539,6 @@ function QueryEditor(props) {
598
539
  </div>
599
540
  </div>
600
541
  {renderControls()}
601
- {renderUpperControls()}
602
542
  </div>
603
543
  <div className={b('pane-wrapper')}>
604
544
  {props.showPreview ? renderPreview() : result}
@@ -621,7 +561,6 @@ const mapStateToProps = (state) => {
621
561
 
622
562
  const mapDispatchToProps = {
623
563
  sendExecuteQuery,
624
- changeUserInput,
625
564
  saveQueryToHistory,
626
565
  goToPreviousQuery,
627
566
  goToNextQuery,
@@ -1,4 +1,4 @@
1
- @import '../../../styles/mixins.scss';
1
+ @import '../../../../styles/mixins.scss';
2
2
 
3
3
  .query-editor {
4
4
  position: relative;
@@ -24,7 +24,8 @@
24
24
 
25
25
  width: 100%;
26
26
  height: 100%;
27
- padding-top: 9px;
27
+
28
+ border: 1px solid var(--yc-color-line-generic);
28
29
  }
29
30
 
30
31
  &__monaco-wrapper {
@@ -40,28 +41,11 @@
40
41
  flex-direction: column;
41
42
 
42
43
  background-color: var(--yc-color-base-background);
43
- }
44
-
45
- &__upper-controls {
46
- position: absolute;
47
- top: -38px;
48
- right: 20px;
49
-
50
- display: flex;
51
- gap: 12px;
52
-
53
- justify-content: flex-end;
54
- align-items: center;
55
- }
56
-
57
- &__history-controls {
58
- display: flex;
59
- align-items: center;
60
- }
61
44
 
62
- &__history-label {
63
- margin-right: 8px;
45
+ &_top {
46
+ padding: 0 16px;
64
47
 
65
- color: var(--yc-color-text-secondary);
48
+ border-bottom: 1px solid var(--yc-color-line-generic);
49
+ }
66
50
  }
67
51
  }
@@ -5,11 +5,8 @@
5
5
  align-items: flex-end;
6
6
 
7
7
  min-height: 40px;
8
- padding: 5px 20px;
8
+ padding: 5px 0px;
9
9
 
10
- border-top: 1px solid var(--yc-color-line-generic);
11
- border-bottom: 1px solid var(--yc-color-line-generic);
12
- background-color: var(--yc-color-base-background);
13
10
  gap: 24px;
14
11
 
15
12
  &__left {
@@ -0,0 +1,59 @@
1
+ import {useLocation} from 'react-router';
2
+
3
+ import {Tabs} from '@gravity-ui/uikit';
4
+
5
+ import type {TenantQueryTab} from '../../../../store/reducers/tenant/types';
6
+ import {TENANT_QUERY_TABS_ID} from '../../../../store/reducers/tenant/constants';
7
+ import {InternalLink} from '../../../../components/InternalLink/InternalLink';
8
+ import {parseQuery} from '../../../../routes';
9
+
10
+ import {TenantTabsGroups, getTenantPath} from '../../TenantPages';
11
+
12
+ import i18n from '../i18n';
13
+
14
+ const newQuery = {
15
+ id: TENANT_QUERY_TABS_ID.newQuery,
16
+ title: i18n('tabs.newQuery'),
17
+ };
18
+ const history = {
19
+ id: TENANT_QUERY_TABS_ID.history,
20
+ title: i18n('tabs.history'),
21
+ };
22
+ const saved = {
23
+ id: TENANT_QUERY_TABS_ID.saved,
24
+ title: i18n('tabs.saved'),
25
+ };
26
+
27
+ const queryEditorTabs = [newQuery, history, saved];
28
+
29
+ interface QueryEditorTabsProps {
30
+ className?: string;
31
+ activeTab?: TenantQueryTab;
32
+ }
33
+
34
+ export const QueryTabs = ({className, activeTab}: QueryEditorTabsProps) => {
35
+ const location = useLocation();
36
+ const queryParams = parseQuery(location);
37
+
38
+ return (
39
+ <div className={className}>
40
+ <Tabs
41
+ size="l"
42
+ allowNotSelected={true}
43
+ activeTab={activeTab}
44
+ items={queryEditorTabs}
45
+ wrapTo={({id}, node) => {
46
+ const path = getTenantPath({
47
+ ...queryParams,
48
+ [TenantTabsGroups.queryTab]: id,
49
+ });
50
+ return (
51
+ <InternalLink to={path} key={id}>
52
+ {node}
53
+ </InternalLink>
54
+ );
55
+ }}
56
+ />
57
+ </div>
58
+ );
59
+ };
@@ -105,21 +105,21 @@ function SaveQuery({savedQueries, onSaveQuery, saveButtonDisabled}) {
105
105
  const renderSaveButton = (onClick) => {
106
106
  return (
107
107
  <Button onClick={onClick} disabled={saveButtonDisabled}>
108
- Save query
108
+ {queryNameToEdit ? 'Edit query' : 'Save query'}
109
109
  </Button>
110
110
  );
111
111
  };
112
112
 
113
113
  const renderSaveDropdownMenu = () => {
114
114
  const items = [
115
- {
116
- action: onSaveQueryClick,
117
- text: 'Save as new',
118
- },
119
115
  {
120
116
  action: onEditQueryClick,
121
117
  text: 'Edit existing',
122
118
  },
119
+ {
120
+ action: onSaveQueryClick,
121
+ text: 'Save as new',
122
+ },
123
123
  ];
124
124
  return (
125
125
  <DropdownMenu items={items} switcher={renderSaveButton()} popupPlacement={['top']} />
@@ -0,0 +1,55 @@
1
+ @import '../../../../styles/mixins.scss';
2
+
3
+ .ydb-saved-queries {
4
+ $block: &;
5
+
6
+ overflow: auto;
7
+
8
+ height: 100%;
9
+ padding: 0 16px;
10
+
11
+ @include flex-container();
12
+ @include table-styles;
13
+
14
+ &__row {
15
+ cursor: pointer;
16
+
17
+ :hover {
18
+ #{$block}__controls {
19
+ display: flex;
20
+ }
21
+ }
22
+ }
23
+
24
+ &__query-name {
25
+ overflow: hidden;
26
+
27
+ white-space: pre-wrap;
28
+ text-overflow: ellipsis;
29
+ }
30
+
31
+ &__query {
32
+ display: flex;
33
+ flex-direction: row;
34
+ justify-content: space-between;
35
+ align-items: center;
36
+ }
37
+
38
+ &__query-body {
39
+ overflow: hidden;
40
+ flex-grow: 1;
41
+
42
+ max-width: 100%;
43
+
44
+ white-space: pre;
45
+ text-overflow: ellipsis;
46
+ }
47
+
48
+ &__controls {
49
+ display: none;
50
+ }
51
+
52
+ &__dialog-query-name {
53
+ font-weight: 500;
54
+ }
55
+ }
@@ -0,0 +1,150 @@
1
+ import {MouseEvent, useState} from 'react';
2
+ import {useDispatch} from 'react-redux';
3
+ import block from 'bem-cn-lite';
4
+
5
+ import {Dialog, Button} from '@gravity-ui/uikit';
6
+ import DataTable, {Column} from '@gravity-ui/react-data-table';
7
+
8
+ import type {SavedQuery} from '../../../../types/store/query';
9
+ import {setQueryNameToEdit} from '../../../../store/reducers/saveQuery';
10
+ import {setQueryTab} from '../../../../store/reducers/tenant/tenant';
11
+ import {TENANT_QUERY_TABS_ID} from '../../../../store/reducers/tenant/constants';
12
+
13
+ import TruncatedQuery from '../../../../components/TruncatedQuery/TruncatedQuery';
14
+ import {IconWrapper} from '../../../../components/Icon';
15
+
16
+ import {MAX_QUERY_HEIGHT, QUERY_TABLE_SETTINGS} from '../../utils/constants';
17
+
18
+ import i18n from '../i18n';
19
+
20
+ import './SavedQueries.scss';
21
+
22
+ const b = block('ydb-saved-queries');
23
+
24
+ interface DeleteDialogProps {
25
+ visible: boolean;
26
+ queryName: string;
27
+ onCancelClick: VoidFunction;
28
+ onConfirmClick: VoidFunction;
29
+ }
30
+
31
+ const DeleteDialog = ({visible, queryName, onCancelClick, onConfirmClick}: DeleteDialogProps) => {
32
+ return (
33
+ <Dialog
34
+ open={visible}
35
+ hasCloseButton={false}
36
+ size="s"
37
+ onClose={onCancelClick}
38
+ onEnterKeyDown={onConfirmClick}
39
+ >
40
+ <Dialog.Header caption={i18n('delete-dialog.header')} />
41
+ <Dialog.Body className={b('dialog-body')}>
42
+ {i18n('delete-dialog.question')}
43
+ <span className={b('dialog-query-name')}>{` ${queryName}?`}</span>
44
+ </Dialog.Body>
45
+ <Dialog.Footer
46
+ textButtonApply={i18n('delete-dialog.delete')}
47
+ textButtonCancel={i18n('delete-dialog.cancel')}
48
+ onClickButtonCancel={onCancelClick}
49
+ onClickButtonApply={onConfirmClick}
50
+ />
51
+ </Dialog>
52
+ );
53
+ };
54
+
55
+ interface SavedQueriesProps {
56
+ savedQueries: SavedQuery[];
57
+ changeUserInput: (value: {input: string}) => void;
58
+ onDeleteQuery: (queryName: string) => void;
59
+ }
60
+
61
+ export const SavedQueries = ({savedQueries, changeUserInput, onDeleteQuery}: SavedQueriesProps) => {
62
+ const dispatch = useDispatch();
63
+
64
+ const [isDeleteDialogVisible, setIsDeleteDialogVisible] = useState(false);
65
+ const [queryNameToDelete, setQueryNameToDelete] = useState<string>('');
66
+
67
+ const closeDeleteDialog = () => {
68
+ setIsDeleteDialogVisible(false);
69
+ setQueryNameToDelete('');
70
+ };
71
+
72
+ const onCancelDeleteClick = () => {
73
+ closeDeleteDialog();
74
+ };
75
+
76
+ const onConfirmDeleteClick = () => {
77
+ closeDeleteDialog();
78
+ onDeleteQuery(queryNameToDelete);
79
+ setQueryNameToDelete('');
80
+ };
81
+
82
+ const onQueryClick = (queryText: string, queryName: string) => {
83
+ changeUserInput({input: queryText});
84
+ dispatch(setQueryNameToEdit(queryName));
85
+ dispatch(setQueryTab(TENANT_QUERY_TABS_ID.newQuery));
86
+ };
87
+
88
+ const onDeleteQueryClick = (queryName: string) => {
89
+ return (event: MouseEvent) => {
90
+ event.stopPropagation();
91
+ setIsDeleteDialogVisible(true);
92
+ setQueryNameToDelete(queryName);
93
+ };
94
+ };
95
+
96
+ const columns: Column<SavedQuery>[] = [
97
+ {
98
+ name: 'name',
99
+ header: 'Name',
100
+ render: ({row: query}) => <div className={b('query-name')}>{query.name}</div>,
101
+ width: 200,
102
+ },
103
+ {
104
+ name: 'body',
105
+ header: 'Query Text',
106
+ render: ({row: query}) => (
107
+ <div className={b('query')}>
108
+ <div className={b('query-body')}>
109
+ <TruncatedQuery value={query.body} maxQueryHeight={MAX_QUERY_HEIGHT} />
110
+ </div>
111
+ <span className={b('controls')}>
112
+ <Button view="flat-secondary">
113
+ <IconWrapper name="pencil" viewBox="0 0 24 24" />
114
+ </Button>
115
+ <Button view="flat-secondary" onClick={onDeleteQueryClick(query.name)}>
116
+ <IconWrapper name="trash" viewBox="0 0 24 24" />
117
+ </Button>
118
+ </span>
119
+ </div>
120
+ ),
121
+ sortable: false,
122
+ },
123
+ ];
124
+
125
+ return (
126
+ <>
127
+ <div className={b()}>
128
+ <DataTable
129
+ theme="yandex-cloud"
130
+ columns={columns}
131
+ data={savedQueries}
132
+ settings={QUERY_TABLE_SETTINGS}
133
+ emptyDataMessage={i18n('saved.empty')}
134
+ rowClassName={() => b('row')}
135
+ onRowClick={(row) => onQueryClick(row.body, row.name)}
136
+ initialSortOrder={{
137
+ columnId: 'name',
138
+ order: DataTable.ASCENDING,
139
+ }}
140
+ />
141
+ </div>
142
+ <DeleteDialog
143
+ visible={isDeleteDialogVisible}
144
+ queryName={queryNameToDelete}
145
+ onCancelClick={onCancelDeleteClick}
146
+ onConfirmClick={onConfirmDeleteClick}
147
+ />
148
+ </>
149
+ );
150
+ };
@@ -0,0 +1,12 @@
1
+ {
2
+ "controls.query-mode-selector_type": "Type:",
3
+ "tabs.newQuery": "New query",
4
+ "tabs.history": "History",
5
+ "tabs.saved": "Saved",
6
+ "history.empty": "History is empty",
7
+ "saved.empty": "There are no saved queries",
8
+ "delete-dialog.header": "Delete query",
9
+ "delete-dialog.question": "Are you sure you want to delete query",
10
+ "delete-dialog.delete": "Delete",
11
+ "delete-dialog.cancel": "Cancel"
12
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "controls.query-mode-selector_type": "Тип:",
3
+ "tabs.newQuery": "Новый запрос",
4
+ "tabs.history": "История",
5
+ "tabs.saved": "Сохраненные",
6
+ "history.empty": "История пуста",
7
+ "saved.empty": "Нет сохраненных запросов",
8
+ "delete-dialog.header": "Удалить запрос",
9
+ "delete-dialog.question": "Вы уверены что хотите удалить запрос",
10
+ "delete-dialog.delete": "Удалить",
11
+ "delete-dialog.cancel": "Отменить"
12
+ }
@@ -0,0 +1,30 @@
1
+ import type {KeyValueRow} from '../../../../types/api/query';
2
+ import type {IQueryResult} from '../../../../types/store/query';
3
+
4
+ export const getPreparedResult = (data: IQueryResult) => {
5
+ const columnDivider = '\t';
6
+ const rowDivider = '\n';
7
+
8
+ if (!data?.result?.length) {
9
+ return '';
10
+ }
11
+
12
+ const columnHeaders = Object.keys(data.result[0]);
13
+ const rows = Array<string[] | KeyValueRow[]>(columnHeaders).concat(data.result);
14
+
15
+ return rows
16
+ .map((item) => {
17
+ const row = [];
18
+
19
+ for (const field in item) {
20
+ if (typeof item[field] === 'object' || Array.isArray(item[field])) {
21
+ row.push(JSON.stringify(item[field]));
22
+ } else {
23
+ row.push(item[field]);
24
+ }
25
+ }
26
+
27
+ return row.join(columnDivider);
28
+ })
29
+ .join(rowDivider);
30
+ };