ydb-embedded-ui 1.13.1 → 1.14.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 (63) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/assets/icons/flask.svg +3 -0
  3. package/dist/components/InfoViewer/formatters/common.ts +15 -0
  4. package/dist/components/InfoViewer/formatters/index.ts +2 -0
  5. package/dist/components/InfoViewer/formatters/schema.ts +43 -0
  6. package/dist/components/InfoViewer/schemaInfo/CDCStreamInfo.tsx +44 -0
  7. package/dist/components/InfoViewer/schemaInfo/PersQueueGroupInfo.tsx +34 -0
  8. package/dist/components/{IndexInfoViewer/IndexInfoViewer.tsx → InfoViewer/schemaInfo/TableIndexInfo.tsx} +7 -18
  9. package/dist/components/InfoViewer/schemaInfo/index.ts +3 -0
  10. package/dist/components/InfoViewer/schemaOverview/CDCStreamOverview.tsx +44 -0
  11. package/dist/components/InfoViewer/schemaOverview/PersQueueGroupOverview.tsx +35 -0
  12. package/dist/components/InfoViewer/schemaOverview/index.ts +2 -0
  13. package/dist/components/QueryResultTable/Cell/Cell.tsx +33 -0
  14. package/dist/components/QueryResultTable/Cell/index.ts +1 -0
  15. package/dist/components/QueryResultTable/QueryResultTable.scss +11 -0
  16. package/dist/components/QueryResultTable/QueryResultTable.tsx +115 -0
  17. package/dist/components/QueryResultTable/i18n/en.json +3 -0
  18. package/dist/components/QueryResultTable/i18n/index.ts +11 -0
  19. package/dist/components/QueryResultTable/i18n/ru.json +3 -0
  20. package/dist/components/QueryResultTable/index.ts +1 -0
  21. package/dist/containers/App/App.scss +1 -0
  22. package/dist/containers/Storage/DiskStateProgressBar/DiskStateProgressBar.scss +39 -14
  23. package/dist/containers/Storage/DiskStateProgressBar/DiskStateProgressBar.tsx +18 -7
  24. package/dist/containers/Storage/Pdisk/__tests__/colors.tsx +4 -3
  25. package/dist/containers/Storage/Vdisk/__tests__/colors.tsx +7 -7
  26. package/dist/containers/Tenant/Acl/Acl.js +7 -1
  27. package/dist/containers/Tenant/Diagnostics/DiagnosticsPages.ts +6 -2
  28. package/dist/containers/Tenant/Diagnostics/HotKeys/HotKeys.js +1 -1
  29. package/dist/containers/Tenant/Diagnostics/Overview/Overview.tsx +8 -3
  30. package/dist/containers/Tenant/Diagnostics/TopQueries/TopQueries.js +1 -1
  31. package/dist/containers/Tenant/Diagnostics/TopShards/TopShards.js +1 -1
  32. package/dist/containers/Tenant/ObjectSummary/ObjectSummary.tsx +36 -10
  33. package/dist/containers/Tenant/Preview/Preview.js +15 -57
  34. package/dist/containers/Tenant/Preview/Preview.scss +4 -8
  35. package/dist/containers/Tenant/QueryEditor/QueryEditor.js +12 -41
  36. package/dist/containers/Tenant/QueryEditor/QueryEditor.scss +1 -5
  37. package/dist/containers/Tenant/QueryEditor/QueryExplain/QueryExplain.scss +1 -2
  38. package/dist/containers/Tenant/QueryEditor/QueryResult/QueryResult.scss +2 -2
  39. package/dist/containers/Tenant/Schema/SchemaTree/SchemaTree.tsx +9 -1
  40. package/dist/containers/Tenant/Tenant.scss +2 -50
  41. package/dist/containers/Tenant/Tenant.tsx +24 -22
  42. package/dist/containers/Tenant/utils/schema.ts +3 -0
  43. package/dist/containers/Tenant/utils/schemaActions.ts +1 -2
  44. package/dist/containers/Tenants/Tenants.js +12 -2
  45. package/dist/containers/UserSettings/UserSettings.tsx +26 -3
  46. package/dist/services/api.d.ts +19 -2
  47. package/dist/services/api.js +2 -2
  48. package/dist/setupTests.js +4 -0
  49. package/dist/store/reducers/executeQuery.js +4 -9
  50. package/dist/store/reducers/{preview.js → preview.ts} +22 -18
  51. package/dist/store/reducers/settings.js +3 -1
  52. package/dist/store/utils.ts +88 -0
  53. package/dist/types/api/query.ts +147 -0
  54. package/dist/types/api/schema.ts +235 -2
  55. package/dist/types/index.ts +33 -0
  56. package/dist/types/store/query.ts +9 -0
  57. package/dist/utils/{constants.js → constants.ts} +11 -6
  58. package/dist/utils/index.js +0 -24
  59. package/dist/utils/query.test.ts +189 -0
  60. package/dist/utils/query.ts +156 -0
  61. package/dist/utils/tests/providers.tsx +29 -0
  62. package/package.json +2 -2
  63. package/dist/store/utils.js +0 -51
@@ -0,0 +1,189 @@
1
+ import {parseQueryAPIExecuteResponse} from './query';
2
+
3
+ describe('API utils', () => {
4
+ describe('json/viewer/query', () => {
5
+ describe('parseQueryAPIExecuteResponse', () => {
6
+ describe('old format', () => {
7
+ describe('plain response', () => {
8
+ it('should handle empty response', () => {
9
+ expect(parseQueryAPIExecuteResponse(null).result).toBeUndefined();
10
+ });
11
+
12
+ it('should parse json string', () => {
13
+ const json = {foo: 'bar'};
14
+ const response = JSON.stringify(json);
15
+ expect(parseQueryAPIExecuteResponse(response).result).toEqual(json);
16
+ });
17
+
18
+ // it should not be in the response, but is there because of a bug
19
+ it('should ignore request plan as the response', () => {
20
+ const response = {queries: 'some queries'};
21
+ expect(parseQueryAPIExecuteResponse(response).result).toBeUndefined();
22
+ });
23
+
24
+ it('should accept key-value rows', () => {
25
+ const response = [{foo: 'bar'}];
26
+ expect(parseQueryAPIExecuteResponse(response).result).toEqual(response);
27
+ });
28
+ });
29
+
30
+ describe('deep response without stats', () => {
31
+ it('should parse json string in the result field', () => {
32
+ const json = {foo: 'bar'};
33
+ const response = {result: JSON.stringify(json)};
34
+ expect(parseQueryAPIExecuteResponse(response).result).toEqual(json);
35
+ });
36
+
37
+ // it should not be in the response, but is there because of a bug
38
+ it('should ignore request plan in the result field', () => {
39
+ const response = {result: {queries: 'some queries'}};
40
+ expect(parseQueryAPIExecuteResponse(response).result).toBeUndefined();
41
+ });
42
+
43
+ it('should accept key-value rows in the result field', () => {
44
+ const response = {result: [{foo: 'bar'}]};
45
+ expect(parseQueryAPIExecuteResponse(response).result).toEqual(response.result);
46
+ });
47
+ });
48
+
49
+ describe('deep response with stats', () => {
50
+ it('should parse json string in the result field', () => {
51
+ const json = {foo: 'bar'};
52
+ const response = {
53
+ result: JSON.stringify(json),
54
+ stats: {metric: 'good'},
55
+ };
56
+ const actual = parseQueryAPIExecuteResponse(response);
57
+ expect(actual.result).toEqual(json);
58
+ expect(actual.stats).toEqual(response.stats);
59
+ });
60
+
61
+ // it should not be in the response, but is there because of a bug
62
+ it('should ignore request plan in the result field', () => {
63
+ const response = {
64
+ result: {queries: 'some queries'},
65
+ stats: {metric: 'good'},
66
+ };
67
+ const actual = parseQueryAPIExecuteResponse(response);
68
+ expect(actual.result).toBeUndefined();
69
+ expect(actual.stats).toEqual(response.stats);
70
+ });
71
+
72
+ it('should accept key-value rows in the result field', () => {
73
+ const response = {
74
+ result: [{foo: 'bar'}],
75
+ stats: {metric: 'good'},
76
+ };
77
+ const actual = parseQueryAPIExecuteResponse(response);
78
+ expect(actual.result).toEqual(response.result);
79
+ expect(actual.stats).toEqual(response.stats);
80
+ });
81
+
82
+ it('should accept stats without a result', () => {
83
+ const response = {
84
+ stats: {metric: 'good'},
85
+ };
86
+ const actual = parseQueryAPIExecuteResponse(response);
87
+ expect(actual.result).toBeUndefined();
88
+ expect(actual.stats).toEqual(response.stats);
89
+ });
90
+ });
91
+ });
92
+
93
+ describe('new format', () => {
94
+ describe('response without stats', () => {
95
+ it('should parse modern schema', () => {
96
+ const response = {
97
+ result: [['42', 'hello world']],
98
+ columns: [{
99
+ name: 'id',
100
+ type: 'Uint64?'
101
+ }, {
102
+ name: 'value',
103
+ type: 'Utf8?'
104
+ }],
105
+ };
106
+ const actual = parseQueryAPIExecuteResponse(response);
107
+ expect(actual.result).toEqual([{
108
+ id: '42',
109
+ value: 'hello world'
110
+ }]);
111
+ expect(actual.columns).toEqual(response.columns);
112
+ });
113
+
114
+ it('should handle empty response for classic schema', () => {
115
+ expect(parseQueryAPIExecuteResponse(null).result).toBeUndefined();
116
+ });
117
+
118
+ it('should parse plain classic schema', () => {
119
+ const response = [{foo: 'bar'}];
120
+ expect(parseQueryAPIExecuteResponse(response).result).toEqual(response);
121
+ });
122
+
123
+ it('should parse deep classic schema', () => {
124
+ const response = {result: [{foo: 'bar'}]};
125
+ expect(parseQueryAPIExecuteResponse(response).result).toEqual(response.result);
126
+ });
127
+
128
+ it('should parse ydb schema', () => {
129
+ const response = {result: [{foo: 'bar'}]};
130
+ expect(parseQueryAPIExecuteResponse(response).result).toEqual(response.result);
131
+ });
132
+ });
133
+
134
+ describe('response with stats', () => {
135
+ it('should parse modern schema', () => {
136
+ const response = {
137
+ result: [['42', 'hello world']],
138
+ columns: [{
139
+ name: 'id',
140
+ type: 'Uint64?'
141
+ }, {
142
+ name: 'value',
143
+ type: 'Utf8?'
144
+ }],
145
+ stats: {metric: 'good'},
146
+ };
147
+ const actual = parseQueryAPIExecuteResponse(response);
148
+ expect(actual.result).toEqual([{
149
+ id: '42',
150
+ value: 'hello world'
151
+ }]);
152
+ expect(actual.columns).toEqual(response.columns);
153
+ expect(actual.stats).toEqual(response.stats);
154
+ });
155
+
156
+ it('should parse classic schema', () => {
157
+ const response = {
158
+ result: [{foo: 'bar'}],
159
+ stats: {metric: 'good'},
160
+ };
161
+ const actual = parseQueryAPIExecuteResponse(response);
162
+ expect(actual.result).toEqual(response.result);
163
+ expect(actual.stats).toEqual(response.stats);
164
+ });
165
+
166
+ it('should parse ydb schema', () => {
167
+ const response = {
168
+ result: [{foo: 'bar'}],
169
+ stats: {metric: 'good'},
170
+ };
171
+ const actual = parseQueryAPIExecuteResponse(response);
172
+ expect(actual.result).toEqual(response.result);
173
+ expect(actual.stats).toEqual(response.stats);
174
+ });
175
+
176
+ it('should accept stats without a result', () => {
177
+ const response = {
178
+ stats: {metric: 'good'},
179
+ };
180
+ const actual = parseQueryAPIExecuteResponse(response);
181
+ expect(actual.result).toBeUndefined();
182
+ expect(actual.columns).toBeUndefined();
183
+ expect(actual.stats).toEqual(response.stats);
184
+ });
185
+ });
186
+ });
187
+ });
188
+ });
189
+ });
@@ -0,0 +1,156 @@
1
+ import {YQLType} from '../types';
2
+ import type {
3
+ AnyExecuteResponse,
4
+ CommonFields,
5
+ DeepExecuteResponse,
6
+ DeprecatedExecuteResponsePlain,
7
+ ExecuteClassicResponsePlain,
8
+ ExecuteModernResponse,
9
+ KeyValueRow,
10
+ QueryAPIExecuteResponse,
11
+ Schemas,
12
+ } from '../types/api/query';
13
+ import type {IQueryResult} from '../types/store/query';
14
+
15
+ // eslint-disable-next-line complexity
16
+ export const getColumnType = (type: string) => {
17
+ switch (type.replace(/\?$/, '')) {
18
+ case YQLType.Bool:
19
+ return 'boolean';
20
+ case YQLType.Int8:
21
+ case YQLType.Int16:
22
+ case YQLType.Int32:
23
+ case YQLType.Int64:
24
+ case YQLType.Uint8:
25
+ case YQLType.Uint16:
26
+ case YQLType.Uint32:
27
+ case YQLType.Uint64:
28
+ case YQLType.Float:
29
+ case YQLType.Double:
30
+ case YQLType.Decimal:
31
+ return 'number';
32
+ case YQLType.String:
33
+ case YQLType.Utf8:
34
+ case YQLType.Json:
35
+ case YQLType.JsonDocument:
36
+ case YQLType.Yson:
37
+ case YQLType.Uuid:
38
+ return 'string';
39
+ case YQLType.Date:
40
+ case YQLType.Datetime:
41
+ case YQLType.Timestamp:
42
+ case YQLType.Interval:
43
+ case YQLType.TzDate:
44
+ case YQLType.TzDateTime:
45
+ case YQLType.TzTimestamp:
46
+ return 'date';
47
+ default:
48
+ return undefined;
49
+ }
50
+ }
51
+
52
+ const parseExecuteModernResponse = (data: ExecuteModernResponse): IQueryResult => {
53
+ const {result, columns, ...restData} = data;
54
+
55
+ return {
56
+ result: result && columns && result.map((row) => {
57
+ return row.reduce((newRow: KeyValueRow, cellData, columnIndex) => {
58
+ const {name} = columns[columnIndex];
59
+ newRow[name] = cellData;
60
+ return newRow;
61
+ }, {});
62
+ }),
63
+ columns,
64
+ ...restData,
65
+ };
66
+ };
67
+
68
+ const parseDeprecatedExecuteResponseValue = (data?: DeprecatedExecuteResponsePlain | ExecuteClassicResponsePlain): KeyValueRow[] | undefined => {
69
+ if (!data) {
70
+ return undefined;
71
+ }
72
+
73
+ if (typeof data === 'string') {
74
+ try {
75
+ return JSON.parse(data);
76
+ } catch (e) {
77
+ return undefined;
78
+ }
79
+ }
80
+
81
+ if (Array.isArray(data)) {
82
+ return data;
83
+ }
84
+
85
+ // Plan is not a valid response in this case
86
+ return undefined;
87
+ };
88
+
89
+ const hasResult = (data: AnyExecuteResponse): data is DeepExecuteResponse => Boolean(
90
+ data && typeof data === 'object' && 'result' in data
91
+ );
92
+
93
+ const isModern = (response: AnyExecuteResponse): response is ExecuteModernResponse => Boolean(
94
+ response &&
95
+ !Array.isArray(response) &&
96
+ Array.isArray((response as ExecuteModernResponse).result) &&
97
+ Array.isArray((response as ExecuteModernResponse).columns)
98
+ );
99
+
100
+ const hasCommonFields = (data: AnyExecuteResponse): data is CommonFields => Boolean(
101
+ data && typeof data === 'object' && ('ast' in data || 'plan' in data || 'stats' in data)
102
+ );
103
+
104
+ // complex logic because of the variety of possible responses
105
+ // after all backends are updated to the latest version, it can be simplified
106
+ export const parseQueryAPIExecuteResponse = <T extends Schemas>(data: QueryAPIExecuteResponse<T>): IQueryResult => {
107
+ if (!data) {
108
+ return {};
109
+ }
110
+
111
+ if (hasResult(data)) {
112
+ if (isModern(data)) {
113
+ return parseExecuteModernResponse(data);
114
+ }
115
+
116
+ return {
117
+ ...data,
118
+ result: parseDeprecatedExecuteResponseValue(data.result),
119
+ };
120
+ }
121
+
122
+ if (hasCommonFields(data)) {
123
+ return data;
124
+ }
125
+
126
+ return {
127
+ result: parseDeprecatedExecuteResponseValue(data),
128
+ };
129
+ };
130
+
131
+ export const prepareQueryResponse = (data?: KeyValueRow[]) => {
132
+ if (!Array.isArray(data)) {
133
+ return [];
134
+ }
135
+
136
+ return data.map((row) => {
137
+ const formattedData: KeyValueRow = {};
138
+
139
+ for (const field in row) {
140
+ if (Object.prototype.hasOwnProperty.call(row, field)) {
141
+ const type = typeof row[field];
142
+ if (type === 'object' || type === 'boolean' || Array.isArray(row[field])) {
143
+ formattedData[field] = JSON.stringify(row[field]);
144
+ } else {
145
+ formattedData[field] = row[field];
146
+ }
147
+ }
148
+ }
149
+
150
+ return formattedData;
151
+ });
152
+ };
153
+
154
+ export function prepareQueryError(error: any) {
155
+ return error.data?.error?.message || error.data || error.statusText || JSON.stringify(error);
156
+ }
@@ -0,0 +1,29 @@
1
+ import React, {PropsWithChildren} from 'react';
2
+ import {Provider} from 'react-redux'
3
+ import {render} from '@testing-library/react'
4
+ import type {RenderOptions} from '@testing-library/react'
5
+
6
+ import configureStore from '../../store';
7
+
8
+ interface ExtendedRenderOptions extends Omit<RenderOptions, 'queries'> {
9
+ storeConfiguration?: {
10
+ store?: any;
11
+ history?: any;
12
+ };
13
+ }
14
+
15
+ export const renderWithStore = (
16
+ ui: React.ReactElement,
17
+ {
18
+ storeConfiguration = configureStore(),
19
+ ...renderOptions
20
+ }: ExtendedRenderOptions = {}
21
+ ) => {
22
+ const {store} = storeConfiguration;
23
+
24
+ function Wrapper({children}: PropsWithChildren<{}>) {
25
+ return <Provider store={store}>{children}</Provider>
26
+ }
27
+
28
+ return {store, ...render(ui, {wrapper: Wrapper, ...renderOptions})}
29
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-embedded-ui",
3
- "version": "1.13.1",
3
+ "version": "1.14.0",
4
4
  "files": [
5
5
  "dist"
6
6
  ],
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@yandex-cloud/i18n": "0.6.0",
13
- "@yandex-cloud/paranoid": "1.0.0",
13
+ "@yandex-cloud/paranoid": "^1.2.1",
14
14
  "@yandex-cloud/react-data-table": "0.2.1",
15
15
  "axios": "0.19.2",
16
16
  "bem-cn-lite": "4.0.0",
@@ -1,51 +0,0 @@
1
- import createToast from '../utils/createToast';
2
- import {SET_UNAUTHENTICATED} from './reducers/authentication';
3
-
4
- export const nop = (result) => result;
5
-
6
- export function createRequestActionTypes(prefix, type) {
7
- return {
8
- REQUEST: `${prefix}/${type}_REQUEST`,
9
- SUCCESS: `${prefix}/${type}_SUCCESS`,
10
- FAILURE: `${prefix}/${type}_FAILURE`,
11
- };
12
- }
13
-
14
- export function createApiRequest({actions, request, dataHandler = nop}) {
15
- const doRequest = async function (dispatch, getState) {
16
- dispatch({
17
- type: actions.REQUEST,
18
- });
19
-
20
- try {
21
- const result = await request;
22
- const data = dataHandler(result, getState);
23
-
24
- dispatch({
25
- type: actions.SUCCESS,
26
- data,
27
- });
28
-
29
- return data;
30
- } catch (error) {
31
- if (error && error.status === 401) {
32
- dispatch({
33
- type: SET_UNAUTHENTICATED.SUCCESS,
34
- });
35
- } else if (error && Number(error.status) >= 500 && error.statusText) {
36
- createToast({
37
- name: 'Request failure',
38
- title: 'Request failure',
39
- type: 'error',
40
- content: `${error.status} ${error.statusText}`,
41
- });
42
- }
43
- dispatch({
44
- type: actions.FAILURE,
45
- error,
46
- });
47
- }
48
- };
49
-
50
- return doRequest;
51
- }