ydb-embedded-ui 0.2.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md 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
  ],