ydb-embedded-ui 1.13.1 → 1.14.0

Sign up to get free protection for your applications and to get access to all the features.
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
- }