ydb-embedded-ui 0.2.0 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ### [1.0.2](https://www.github.com/ydb-platform/ydb-embedded-ui/compare/v1.0.1...v1.0.2) (2022-03-11)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **Header:** add link to internal viewer ([64af24f](https://www.github.com/ydb-platform/ydb-embedded-ui/commit/64af24f8d78cf0d34466ac129be10c0764cce3d4))
9
+
10
+ ### [1.0.1](https://www.github.com/ydb-platform/ydb-embedded-ui/compare/v1.0.0...v1.0.1) (2022-03-05)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **QueriesHistory:** should save history to local storage ([#8](https://www.github.com/ydb-platform/ydb-embedded-ui/issues/8)) ([57031ab](https://www.github.com/ydb-platform/ydb-embedded-ui/commit/57031ab16900e9d1112bbf506d5c777f94f883bb))
16
+
17
+ ## [1.0.0](https://www.github.com/ydb-platform/ydb-embedded-ui/compare/v0.2.0...v1.0.0) (2022-03-01)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * **ObjectSummary:** start time should be taken from current schema object ([e7511be](https://www.github.com/ydb-platform/ydb-embedded-ui/commit/e7511be61e5c8d2052ad3a2247a713f55049d3e6))
23
+ * **QueryEditor:** key bindings should work properly ([ebe59b3](https://www.github.com/ydb-platform/ydb-embedded-ui/commit/ebe59b3c838889ee81e308232e4c8d2ba23a1a3a))
24
+ * **QueryExplain:** should render graph in fullscreen view properly ([da511da](https://www.github.com/ydb-platform/ydb-embedded-ui/commit/da511da2fc1a36282ad99f20d5d6fd0b5b4ea05b))
25
+ * **README:** ui path should be correct ([e2668ef](https://www.github.com/ydb-platform/ydb-embedded-ui/commit/e2668ef329de4cf31fd31061dfe3b4ac091e0121))
26
+
3
27
  ## [0.2.0](https://www.github.com/ydb-platform/ydb-embedded-ui/compare/v0.1.0...v0.2.0) (2022-02-24)
4
28
 
5
29
 
package/README.md CHANGED
@@ -24,5 +24,5 @@ The build is minified and the filenames include the hashes.
24
24
 
25
25
  To test production bundle with latest YDB backend release, do the following:
26
26
  1) Build a production bundle with a few tweaks for embedded version: `npm run build:embedded`.
27
- 2) Invoke `docker run -it --hostname localhost -e YDB_LOCAL_SURVIVE_RESTART=true -dp 2135:2135 -p 8765:8765 -v ~/projects/ydb-embedded-ui-opensource/build:/ydb_data/node_1/contentmonitoring cr.yandex/yc/yandex-docker-local-ydb:latest`
27
+ 2) Invoke `docker run -it --hostname localhost -dp 2135:2135 -p 8765:8765 -v ~/projects/ydb-embedded-ui/build:/ydb_data/node_1/contentmonitoring cr.yandex/yc/yandex-docker-local-ydb:latest`
28
28
  3) Open [embedded YDB UI](http://localhost:8765/monitoring) to view it in the browser.
@@ -25,4 +25,16 @@
25
25
  font-size: var(--yc-text-body2-font-size);
26
26
  font-weight: 500;
27
27
  }
28
+
29
+ &__cluster-name-wrapper {
30
+ display: flex;
31
+ align-items: center;
32
+
33
+ height: 100%;
34
+ gap: 5px;
35
+ }
36
+
37
+ &__divider {
38
+ height: 80%;
39
+ }
28
40
  }
@@ -1,15 +1,19 @@
1
- import {useEffect} from 'react';
1
+ import React, {useEffect} from 'react';
2
2
  import {useDispatch, useSelector} from 'react-redux';
3
3
  import cn from 'bem-cn-lite';
4
4
  import {useHistory, useLocation} from 'react-router';
5
+ import {Breadcrumbs, BreadcrumbsItem, Link} from '@yandex-cloud/uikit';
5
6
 
6
- import {clusterName as clusterNameLocation} from '../../store';
7
+ import Divider from '../../components/Divider/Divider';
8
+ //@ts-ignore
9
+ import Icon from '../../components/Icon/Icon';
10
+
11
+ import {clusterName as clusterNameLocation, backend, customBackend} from '../../store';
7
12
  import {getClusterInfo} from '../../store/reducers/cluster';
8
13
  import {getHostInfo} from '../../store/reducers/host';
14
+ import {HeaderItemType} from '../../store/reducers/header';
9
15
 
10
16
  import './Header.scss';
11
- import {Breadcrumbs, BreadcrumbsItem} from '@yandex-cloud/uikit';
12
- import {HeaderItemType} from '../../store/reducers/header';
13
17
 
14
18
  const b = cn('header');
15
19
 
@@ -49,6 +53,12 @@ function Header() {
49
53
  const renderHeader = () => {
50
54
  const clusterNameFinal = singleClusterMode ? host.ClusterName : clusterName;
51
55
 
56
+ let link = backend + '/internal';
57
+
58
+ if (singleClusterMode && !customBackend) {
59
+ link = `/internal`;
60
+ }
61
+
52
62
  const breadcrumbItems = header.reduce((acc, el) => {
53
63
  acc.push({text: el.text, action: () => history.push(el.link)});
54
64
  return acc;
@@ -63,8 +73,20 @@ function Header() {
63
73
  firstDisplayedItemsCount={1}
64
74
  />
65
75
  </div>
66
- <div>
67
- {clusterNameFinal && <ClusterName name={clusterNameFinal} />}
76
+
77
+ <div className={b('cluster-name-wrapper')}>
78
+ <Link href={link} target="_blank">
79
+ Internal viewer{' '}
80
+ <Icon name="external" viewBox={'0 0 16 16'} width={16} height={16} />
81
+ </Link>
82
+ {clusterNameFinal && (
83
+ <React.Fragment>
84
+ <div className={b('divider')}>
85
+ <Divider />
86
+ </div>
87
+ <ClusterName name={clusterNameFinal} />
88
+ </React.Fragment>
89
+ )}
68
90
  </div>
69
91
  </header>
70
92
  );
@@ -1,6 +1,10 @@
1
1
  @import '../../../styles/mixins.scss';
2
2
 
3
3
  .kv-acl {
4
+ display: flex;
5
+ overflow: auto;
6
+ flex-grow: 1;
7
+
4
8
  padding: 0 12px 16px;
5
9
  @include query-data-table;
6
10
  &__message-container {
@@ -38,8 +42,4 @@
38
42
  color: var(--yc-color-text-danger);
39
43
  }
40
44
  }
41
-
42
- &__result {
43
- overflow: auto;
44
- }
45
45
  }
@@ -13,7 +13,9 @@
13
13
  max-height: 100%;
14
14
 
15
15
  &__overview-wrapper {
16
+ display: flex;
16
17
  overflow: auto;
18
+ flex-grow: 1;
17
19
 
18
20
  padding: 0 12px 16px;
19
21
  }
@@ -91,7 +93,15 @@
91
93
  }
92
94
 
93
95
  &__info {
96
+ display: flex;
94
97
  overflow: hidden;
98
+ flex-direction: column;
99
+ }
100
+
101
+ &__schema {
102
+ display: flex;
103
+ overflow: auto;
104
+ flex-grow: 1;
95
105
  }
96
106
 
97
107
  &__info-controls {
@@ -78,7 +78,7 @@ interface ObjectSummaryProps {
78
78
  onCollapseSummary: VoidFunction;
79
79
  onExpandSummary: VoidFunction;
80
80
  isCollapsed: boolean;
81
- additionalTenantInfo?: any
81
+ additionalTenantInfo?: any;
82
82
  }
83
83
 
84
84
  function ObjectSummary(props: ObjectSummaryProps) {
@@ -104,6 +104,7 @@ function ObjectSummary(props: ObjectSummaryProps) {
104
104
 
105
105
  const {name: tenantName, info: infoTab} = queryParams;
106
106
  const tenantData = _.get(data[tenantName as string], 'PathDescription.Self');
107
+ const currentSchemaData = _.get(data[currentSchemaPath], 'PathDescription.Self');
107
108
 
108
109
  const tableSchema =
109
110
  currentItem?.PathDescription?.Table || currentItem?.PathDescription?.OlapTableDescription;
@@ -152,7 +153,7 @@ function ObjectSummary(props: ObjectSummaryProps) {
152
153
  };
153
154
 
154
155
  const renderObjectOverview = () => {
155
- const startTimeInMilliseconds = tenantData?.CreateStep / 1000;
156
+ const startTimeInMilliseconds = currentSchemaData?.CreateStep / 1000;
156
157
  let createTime = 'Unknown';
157
158
  if (startTimeInMilliseconds) {
158
159
  createTime = new Date(startTimeInMilliseconds).toUTCString();
@@ -167,10 +168,16 @@ function ObjectSummary(props: ObjectSummaryProps) {
167
168
  const renderTabContent = () => {
168
169
  switch (infoTab) {
169
170
  case TenantInfoTabsIds.acl: {
170
- return <Acl additionalTenantInfo={props.additionalTenantInfo}/>;
171
+ return <Acl additionalTenantInfo={props.additionalTenantInfo} />;
171
172
  }
172
173
  case TenantInfoTabsIds.schema: {
173
- return loadingSchema ? renderLoader() : <SchemaViewer data={schema} />;
174
+ return loadingSchema ? (
175
+ renderLoader()
176
+ ) : (
177
+ <div className={b('schema')}>
178
+ <SchemaViewer data={schema} />
179
+ </div>
180
+ );
174
181
  }
175
182
  default: {
176
183
  return renderObjectOverview();
@@ -193,7 +200,9 @@ function ObjectSummary(props: ObjectSummaryProps) {
193
200
  <div className={b('tree-title')}>Navigation</div>
194
201
  </div>
195
202
  <div className={b('tree')}>
196
- {tenantData && <SchemaNode fullPath={tenantName as string} data={tenantData} isRoot />}
203
+ {tenantData && (
204
+ <SchemaNode fullPath={tenantName as string} data={tenantData} isRoot />
205
+ )}
197
206
  </div>
198
207
  </div>
199
208
  );
@@ -38,6 +38,7 @@ function QueriesHistory(props: QueriesHistoryProps) {
38
38
  };
39
39
 
40
40
  const renderSavedQueries = () => {
41
+ const reversedHistory = ([] as string[]).concat(history).reverse();
41
42
  return (
42
43
  <Popup
43
44
  className={b('popup-wrapper')}
@@ -47,7 +48,7 @@ function QueriesHistory(props: QueriesHistoryProps) {
47
48
  onClose={onCloseHistory}
48
49
  >
49
50
  <div className={b()}>
50
- {history.length === 0 ? (
51
+ {reversedHistory.length === 0 ? (
51
52
  <div className={b('empty')}>History is empty</div>
52
53
  ) : (
53
54
  <React.Fragment>
@@ -57,7 +58,7 @@ function QueriesHistory(props: QueriesHistoryProps) {
57
58
  </div>
58
59
  </div>
59
60
  <div>
60
- {history?.reverse().map((query, index) => {
61
+ {reversedHistory.map((query, index) => {
61
62
  return (
62
63
  <div
63
64
  className={b('saved-queries-row')}
@@ -1,6 +1,6 @@
1
1
  import {useEffect, useReducer, useRef, useState} from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import {connect, useDispatch} from 'react-redux';
3
+ import {connect} from 'react-redux';
4
4
  import cn from 'bem-cn-lite';
5
5
  import _ from 'lodash';
6
6
  import MonacoEditor from 'react-monaco-editor';
@@ -23,6 +23,8 @@ import {
23
23
  goToNextQuery,
24
24
  selectRunAction,
25
25
  RUN_ACTIONS_VALUES,
26
+ MONACO_HOT_KEY_ACTIONS,
27
+ setMonacoHotKey,
26
28
  } from '../../../store/reducers/executeQuery';
27
29
  import {getExplainQuery, getExplainQueryAst} from '../../../store/reducers/explainQuery';
28
30
  import {showTooltip} from '../../../store/reducers/tooltip';
@@ -45,7 +47,7 @@ import {
45
47
  paneVisibilityToggleReducerCreator,
46
48
  } from '../utils/paneVisibilityToggleHelpers';
47
49
  import Preview from '../Preview/Preview';
48
- import { setShowPreview } from '../../../store/reducers/schema';
50
+ import {setShowPreview} from '../../../store/reducers/schema';
49
51
 
50
52
  export const RUN_ACTIONS = [
51
53
  {value: RUN_ACTIONS_VALUES.script, content: 'Run Script'},
@@ -83,6 +85,7 @@ const propTypes = {
83
85
  executeQuery: PropTypes.object,
84
86
  explainQuery: PropTypes.object,
85
87
  showTooltip: PropTypes.func,
88
+ setMonacoHotKey: PropTypes.func,
86
89
  theme: PropTypes.string,
87
90
  type: PropTypes.string,
88
91
  };
@@ -95,8 +98,6 @@ const initialTenantCommonInfoState = {
95
98
  function QueryEditor(props) {
96
99
  const [resultType, setResultType] = useState(RESULT_TYPES.EXECUTE);
97
100
 
98
- const dispatch = useDispatch()
99
-
100
101
  const [resultVisibilityState, dispatchResultVisibilityState] = useReducer(
101
102
  paneVisibilityToggleReducerCreator(DEFAULT_IS_QUERY_RESULT_COLLAPSED),
102
103
  initialTenantCommonInfoState,
@@ -114,6 +115,7 @@ function QueryEditor(props) {
114
115
  window.removeEventListener('resize', onChangeWindow);
115
116
  window.removeEventListener('storage', storageEventHandler);
116
117
  window.onbeforeunload = undefined;
118
+ props.setMonacoHotKey(null);
117
119
  };
118
120
  }, []);
119
121
 
@@ -150,14 +152,29 @@ function QueryEditor(props) {
150
152
  }, [props.executeQuery, props.executeQuery]);
151
153
 
152
154
  useEffect(() => {
153
- const {
154
- path,
155
- executeQuery: {input},
156
- } = props;
157
- if (resultType === RESULT_TYPES.EXPLAIN) {
158
- props.getExplainQuery({query: input, database: path});
155
+ const {monacoHotKey, setMonacoHotKey} = props;
156
+ if (monacoHotKey === null) {
157
+ return
158
+ }
159
+ setMonacoHotKey(null);
160
+ switch (monacoHotKey) {
161
+ case MONACO_HOT_KEY_ACTIONS.sendQuery: {
162
+ return handleSendClick();
163
+ }
164
+ case MONACO_HOT_KEY_ACTIONS.goPrev: {
165
+ return handlePreviousHistoryClick();
166
+ }
167
+ case MONACO_HOT_KEY_ACTIONS.goNext: {
168
+ return handleNextHistoryClick();
169
+ }
170
+ case MONACO_HOT_KEY_ACTIONS.getExplain: {
171
+ return handleGetExplainQueryClick();
172
+ }
173
+ default: {
174
+ return;
175
+ }
159
176
  }
160
- }, [resultType]);
177
+ }, [props.monacoHotKey]);
161
178
 
162
179
  const checkIfHasUnsavedInput = (e) => {
163
180
  e.preventDefault();
@@ -165,6 +182,10 @@ function QueryEditor(props) {
165
182
  e.returnValue = '';
166
183
  };
167
184
 
185
+ const handleKeyBinding = (value) => {
186
+ return () => props.setMonacoHotKey(value);
187
+ };
188
+
168
189
  const editorDidMount = (editor, monaco) => {
169
190
  editorRef.current = editor;
170
191
  editor.focus();
@@ -183,9 +204,7 @@ function QueryEditor(props) {
183
204
  contextMenuOrder: 1,
184
205
  // Method that will be executed when the action is triggered.
185
206
  // @param editor The editor instance is passed in as a convinience
186
- run: () => {
187
- handleSendClick();
188
- },
207
+ run: handleKeyBinding(MONACO_HOT_KEY_ACTIONS.sendQuery),
189
208
  });
190
209
 
191
210
  editor.addAction({
@@ -197,9 +216,7 @@ function QueryEditor(props) {
197
216
  ],
198
217
  contextMenuGroupId: CONTEXT_MENU_GROUP_ID,
199
218
  contextMenuOrder: 2,
200
- run: () => {
201
- handlePreviousHistoryClick();
202
- },
219
+ run: handleKeyBinding(MONACO_HOT_KEY_ACTIONS.goPrev),
203
220
  });
204
221
  editor.addAction({
205
222
  id: 'next-query',
@@ -210,9 +227,7 @@ function QueryEditor(props) {
210
227
  ],
211
228
  contextMenuGroupId: CONTEXT_MENU_GROUP_ID,
212
229
  contextMenuOrder: 3,
213
- run: () => {
214
- handleNextHistoryClick();
215
- },
230
+ run: handleKeyBinding(MONACO_HOT_KEY_ACTIONS.goNext),
216
231
  });
217
232
 
218
233
  editor.addAction({
@@ -228,9 +243,7 @@ function QueryEditor(props) {
228
243
  keybindingContext: null,
229
244
  contextMenuGroupId: CONTEXT_MENU_GROUP_ID,
230
245
  contextMenuOrder: 4,
231
- run: () => {
232
- handleGetExplainQueryClick();
233
- },
246
+ run: handleKeyBinding(MONACO_HOT_KEY_ACTIONS.getExplain),
234
247
  });
235
248
  };
236
249
  const onChange = (newValue) => {
@@ -243,11 +256,12 @@ function QueryEditor(props) {
243
256
  executeQuery: {input, history, runAction},
244
257
  sendQuery,
245
258
  saveQueryToHistory,
259
+ setShowPreview,
246
260
  } = props;
247
261
 
248
262
  setResultType(RESULT_TYPES.EXECUTE);
249
263
  sendQuery({query: input, database: path, action: runAction});
250
- dispatch(setShowPreview(false))
264
+ setShowPreview(false);
251
265
 
252
266
  const {queries, currentIndex} = history;
253
267
  if (input !== queries[currentIndex]) {
@@ -257,8 +271,15 @@ function QueryEditor(props) {
257
271
  };
258
272
 
259
273
  const handleGetExplainQueryClick = () => {
274
+ const {
275
+ path,
276
+ executeQuery: {input},
277
+ getExplainQuery,
278
+ setShowPreview,
279
+ } = props;
260
280
  setResultType(RESULT_TYPES.EXPLAIN);
261
- dispatch(setShowPreview(false))
281
+ getExplainQuery({query: input, database: path});
282
+ setShowPreview(false);
262
283
  dispatchResultVisibilityState(PaneVisibilityActionTypes.triggerExpand);
263
284
  };
264
285
 
@@ -655,6 +676,7 @@ const mapStateToProps = (state) => {
655
676
  savedQueries: parseJson(getSettingValue(state, SAVED_QUERIES_KEY)),
656
677
  showPreview: state.schema.showPreview,
657
678
  currentSchema: state.schema.currentSchema,
679
+ monacoHotKey: state.executeQuery?.monacoHotKey,
658
680
  };
659
681
  };
660
682
 
@@ -669,6 +691,8 @@ const mapDispatchToProps = {
669
691
  getExplainQueryAst,
670
692
  setSettingValue,
671
693
  selectRunAction,
694
+ setShowPreview,
695
+ setMonacoHotKey,
672
696
  };
673
697
 
674
698
  QueryEditor.propTypes = propTypes;
@@ -1,14 +1,14 @@
1
- import React, {useEffect, useState, useMemo} from 'react';
1
+ import React, {useEffect, useState} from 'react';
2
2
  import cn from 'bem-cn-lite';
3
3
  import MonacoEditor from 'react-monaco-editor';
4
4
  import {Loader, RadioButton} from '@yandex-cloud/uikit';
5
5
  import JSONTree from 'react-json-inspector';
6
6
  import {LANGUAGE_S_EXPRESSION_ID} from '../../../../utils/monaco';
7
7
  import {
8
- TopologyWrapper,
9
- CompactTopologyWrapper,
10
8
  TextOverflow,
11
9
  getYdbPlanNodeShape,
10
+ getCompactTopology,
11
+ getTopology,
12
12
  } from '@yandex-cloud/paranoid';
13
13
  import {renderExplainNode} from '../../../../utils';
14
14
  import {explainVersions} from '../../../../store/reducers/explainQuery';
@@ -21,7 +21,7 @@ import Fullscreen from '../../../../components/Fullscreen/Fullscreen';
21
21
  import 'react-json-inspector/json-inspector.css';
22
22
  import './QueryExplain.scss';
23
23
  import {useDispatch, useSelector} from 'react-redux';
24
- import { disableFullscreen } from '../../../../store/reducers/fullscreen';
24
+ import {disableFullscreen} from '../../../../store/reducers/fullscreen';
25
25
 
26
26
  const b = cn('kv-query-explain');
27
27
 
@@ -61,8 +61,42 @@ const explainOptions = [
61
61
  {value: ExplainOptionIds.ast, content: 'AST'},
62
62
  ];
63
63
 
64
+ function GraphRoot(props) {
65
+ let paranoid;
66
+ useEffect(() => {
67
+ const {data, opts, shapes, version} = props;
68
+ if (version === explainVersions.v2) {
69
+ paranoid = getTopology('graphRoot', props.data, opts, shapes);
70
+ paranoid.render();
71
+ } else if (version === explainVersions.v1) {
72
+ paranoid = getCompactTopology('graphRoot', data, opts);
73
+ paranoid.renderCompactTopology();
74
+ }
75
+ }, []);
76
+
77
+ useEffect(() => {
78
+ const graphRoot = document.getElementById('graphRoot');
79
+
80
+ if (!graphRoot) {
81
+ throw new Error("Can't find element with id #graphRoot");
82
+ }
83
+
84
+ graphRoot.innerHTML = '';
85
+
86
+ const {data, opts} = props;
87
+ paranoid = getCompactTopology('graphRoot', data, opts);
88
+ paranoid.renderCompactTopology();
89
+ }, [props.opts.colors]);
90
+
91
+ useEffect(() => {
92
+ paranoid.updateData(props.data);
93
+ }, [props.data]);
94
+
95
+ return <div id="graphRoot" style={{height: '100vh'}} />;
96
+ }
97
+
64
98
  function QueryExplain(props) {
65
- const dispatch = useDispatch()
99
+ const dispatch = useDispatch();
66
100
  const [activeOption, setActiveOption] = useState(ExplainOptionIds.schema);
67
101
 
68
102
  const isFullscreen = useSelector((state) => state.fullscreen);
@@ -76,41 +110,6 @@ function QueryExplain(props) {
76
110
  const {explain = {}, theme} = props;
77
111
  const {links, nodes, version, graphDepth} = explain;
78
112
 
79
- const graph = useMemo(() => {
80
- if (links && nodes) {
81
- if (version === explainVersions.v2) {
82
- return (
83
- <TopologyWrapper
84
- data={{links, nodes}}
85
- opts={{
86
- renderNodeTitle: renderExplainNode,
87
- textOverflow: TextOverflow.Normal,
88
- colors: theme === 'dark' ? DARK_COLORS : {},
89
- initialZoomFitsCanvas: true,
90
- }}
91
- shapes={{
92
- node: getYdbPlanNodeShape,
93
- }}
94
- />
95
- );
96
- } else if (version === explainVersions.v1) {
97
- return (
98
- <CompactTopologyWrapper
99
- data={{links, nodes}}
100
- opts={{
101
- renderNodeTitle: renderExplainNode,
102
- textOverflow: TextOverflow.Normal,
103
- colors: theme === 'dark' ? DARK_COLORS : {},
104
- initialZoomFitsCanvas: true,
105
- }}
106
- />
107
- );
108
- }
109
- return 'The explanation format of the query is not supported';
110
- }
111
- return null;
112
- }, [links, nodes, theme, version]);
113
-
114
113
  useEffect(() => {
115
114
  if (!props.ast && activeOption === ExplainOptionIds.ast) {
116
115
  props.astQuery();
@@ -188,19 +187,37 @@ function QueryExplain(props) {
188
187
 
189
188
  const renderGraph = () => {
190
189
  const graphHeight = `${Math.max(graphDepth * 100, 200)}px`;
191
- const content = (
192
- <div
193
- className={b('explain-canvas-container', {
194
- hidden: activeOption !== ExplainOptionIds.schema,
195
- })}
196
- style={{height: graphHeight, minHeight: graphHeight, width: '100%'}}
197
- >
198
- {graph}
199
- </div>
200
- );
190
+
191
+ const content =
192
+ links && nodes && nodes.length ? (
193
+ <div
194
+ className={b('explain-canvas-container', {
195
+ hidden: activeOption !== ExplainOptionIds.schema,
196
+ })}
197
+ style={{
198
+ height: isFullscreen ? '100%' : graphHeight,
199
+ minHeight: graphHeight,
200
+ width: '100%',
201
+ }}
202
+ >
203
+ <GraphRoot
204
+ version={version}
205
+ data={{links, nodes}}
206
+ opts={{
207
+ renderNodeTitle: renderExplainNode,
208
+ textOverflow: TextOverflow.Normal,
209
+ colors: theme === 'dark' ? DARK_COLORS : {},
210
+ initialZoomFitsCanvas: true,
211
+ }}
212
+ shapes={{
213
+ node: getYdbPlanNodeShape,
214
+ }}
215
+ />
216
+ </div>
217
+ ) : null;
201
218
  return (
202
219
  <React.Fragment>
203
- {content}
220
+ {!isFullscreen && content}
204
221
  {isFullscreen && <Fullscreen>{content}</Fullscreen>}
205
222
  {renderStub()}
206
223
  </React.Fragment>
@@ -249,9 +266,7 @@ function QueryExplain(props) {
249
266
  )}
250
267
  </div>
251
268
  <div className={b('controls-left')}>
252
- <EnableFullscreenButton
253
- disabled={Boolean(props.error) || activeOption === ExplainOptionIds.schema}
254
- />
269
+ <EnableFullscreenButton disabled={Boolean(props.error)} />
255
270
  <PaneVisibilityToggleButtons
256
271
  onCollapse={props.onCollapseResults}
257
272
  onExpand={props.onExpandResults}
@@ -67,7 +67,7 @@ class SchemaViewer extends React.Component {
67
67
  theme="yandex-cloud"
68
68
  data={tableData}
69
69
  columns={columns}
70
- settings={{...DEFAULT_TABLE_SETTINGS, stickyTop: 107}}
70
+ settings={DEFAULT_TABLE_SETTINGS}
71
71
  dynamicRender={true}
72
72
  initialSortOrder={{columnId: SchemaViewerColumns.key, order: DataTable.DESCENDING}}
73
73
  />
@@ -1,5 +1,9 @@
1
1
  import {createRequestActionTypes, createApiRequest} from '../utils';
2
2
  import '../../services/api';
3
+ import {getValueFromLS, parseJson} from '../../utils/utils';
4
+ import {QUERIES_HISTORY_KEY} from '../../utils/constants';
5
+
6
+ const MAXIMUM_QUERIES_IN_HISTORY = 20;
3
7
 
4
8
  const SEND_QUERY = createRequestActionTypes('query', 'SEND_QUERY');
5
9
  const CHANGE_USER_INPUT = 'query/CHANGE_USER_INPUT';
@@ -7,20 +11,33 @@ const SAVE_QUERY_TO_HISTORY = 'query/SAVE_QUERY_TO_HISTORY';
7
11
  const GO_TO_PREVIOUS_QUERY = 'query/GO_TO_PREVIOUS_QUERY';
8
12
  const GO_TO_NEXT_QUERY = 'query/GO_TO_NEXT_QUERY';
9
13
  const SELECT_RUN_ACTION = 'query/SELECT_RUN_ACTION';
14
+ const MONACO_HOT_KEY = 'query/MONACO_HOT_KEY';
15
+
16
+ const queriesHistoryInitial = parseJson(getValueFromLS(QUERIES_HISTORY_KEY, []));
17
+
18
+ const sliceLimit = queriesHistoryInitial.length - MAXIMUM_QUERIES_IN_HISTORY;
10
19
 
11
20
  export const RUN_ACTIONS_VALUES = {
12
21
  script: 'execute-script',
13
22
  scan: 'execute-scan',
14
23
  };
15
24
 
25
+ export const MONACO_HOT_KEY_ACTIONS = {
26
+ sendQuery: 'sendQuery',
27
+ goPrev: 'goPrev',
28
+ goNext: 'goNext',
29
+ getExplain: 'getExplain',
30
+ };
31
+
16
32
  const initialState = {
17
33
  loading: false,
18
34
  input: '',
19
35
  history: {
20
- queries: [],
36
+ queries: queriesHistoryInitial.slice(sliceLimit < 0 ? 0 : sliceLimit),
21
37
  currentIndex: -1,
22
38
  },
23
39
  runAction: RUN_ACTIONS_VALUES.script,
40
+ monacoHotKey: null,
24
41
  };
25
42
 
26
43
  const executeQuery = (state = initialState, action) => {
@@ -67,7 +84,10 @@ const executeQuery = (state = initialState, action) => {
67
84
 
68
85
  case SAVE_QUERY_TO_HISTORY: {
69
86
  const query = action.data;
70
- const newQueries = [...state.history.queries, query];
87
+ const newQueries = [...state.history.queries, query].slice(
88
+ state.history.queries.length >= MAXIMUM_QUERIES_IN_HISTORY ? 1 : 0,
89
+ );
90
+ window.localStorage.setItem(QUERIES_HISTORY_KEY, JSON.stringify(newQueries));
71
91
  const currentIndex = newQueries.length - 1;
72
92
 
73
93
  return {
@@ -104,6 +124,13 @@ const executeQuery = (state = initialState, action) => {
104
124
  };
105
125
  }
106
126
 
127
+ case MONACO_HOT_KEY: {
128
+ return {
129
+ ...state,
130
+ monacoHotKey: action.data,
131
+ };
132
+ }
133
+
107
134
  default:
108
135
  return state;
109
136
  }
@@ -155,4 +182,11 @@ export const changeUserInput = ({input}) => {
155
182
  };
156
183
  };
157
184
 
185
+ export const setMonacoHotKey = (value) => {
186
+ return {
187
+ type: MONACO_HOT_KEY,
188
+ data: value,
189
+ };
190
+ };
191
+
158
192
  export default executeQuery;
@@ -120,6 +120,7 @@ export const PROBLEMS = 'With problems';
120
120
 
121
121
  export const THEME_KEY = 'theme';
122
122
  export const SAVED_QUERIES_KEY = 'saved_queries';
123
+ export const QUERIES_HISTORY_KEY = 'queries_history';
123
124
  export const DATA_QA_TUNE_COLUMNS_POPUP = 'tune-columns-popup';
124
125
 
125
126
  export const defaultUserSettings = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-embedded-ui",
3
- "version": "0.2.0",
3
+ "version": "1.0.2",
4
4
  "files": [
5
5
  "dist"
6
6
  ],