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.
- package/CHANGELOG.md +42 -0
- package/dist/assets/icons/flask.svg +3 -0
- package/dist/components/InfoViewer/formatters/common.ts +15 -0
- package/dist/components/InfoViewer/formatters/index.ts +2 -0
- package/dist/components/InfoViewer/formatters/schema.ts +43 -0
- package/dist/components/InfoViewer/schemaInfo/CDCStreamInfo.tsx +44 -0
- package/dist/components/InfoViewer/schemaInfo/PersQueueGroupInfo.tsx +34 -0
- package/dist/components/{IndexInfoViewer/IndexInfoViewer.tsx → InfoViewer/schemaInfo/TableIndexInfo.tsx} +7 -18
- package/dist/components/InfoViewer/schemaInfo/index.ts +3 -0
- package/dist/components/InfoViewer/schemaOverview/CDCStreamOverview.tsx +44 -0
- package/dist/components/InfoViewer/schemaOverview/PersQueueGroupOverview.tsx +35 -0
- package/dist/components/InfoViewer/schemaOverview/index.ts +2 -0
- package/dist/components/QueryResultTable/Cell/Cell.tsx +33 -0
- package/dist/components/QueryResultTable/Cell/index.ts +1 -0
- package/dist/components/QueryResultTable/QueryResultTable.scss +11 -0
- package/dist/components/QueryResultTable/QueryResultTable.tsx +115 -0
- package/dist/components/QueryResultTable/i18n/en.json +3 -0
- package/dist/components/QueryResultTable/i18n/index.ts +11 -0
- package/dist/components/QueryResultTable/i18n/ru.json +3 -0
- package/dist/components/QueryResultTable/index.ts +1 -0
- package/dist/containers/App/App.scss +1 -0
- package/dist/containers/Storage/DiskStateProgressBar/DiskStateProgressBar.scss +39 -14
- package/dist/containers/Storage/DiskStateProgressBar/DiskStateProgressBar.tsx +18 -7
- package/dist/containers/Storage/Pdisk/__tests__/colors.tsx +4 -3
- package/dist/containers/Storage/Vdisk/__tests__/colors.tsx +7 -7
- package/dist/containers/Tenant/Acl/Acl.js +7 -1
- package/dist/containers/Tenant/Diagnostics/DiagnosticsPages.ts +6 -2
- package/dist/containers/Tenant/Diagnostics/HotKeys/HotKeys.js +1 -1
- package/dist/containers/Tenant/Diagnostics/Overview/Overview.tsx +8 -3
- package/dist/containers/Tenant/Diagnostics/TopQueries/TopQueries.js +1 -1
- package/dist/containers/Tenant/Diagnostics/TopShards/TopShards.js +1 -1
- package/dist/containers/Tenant/ObjectSummary/ObjectSummary.tsx +36 -10
- package/dist/containers/Tenant/Preview/Preview.js +15 -57
- package/dist/containers/Tenant/Preview/Preview.scss +4 -8
- package/dist/containers/Tenant/QueryEditor/QueryEditor.js +12 -41
- package/dist/containers/Tenant/QueryEditor/QueryEditor.scss +1 -5
- package/dist/containers/Tenant/QueryEditor/QueryExplain/QueryExplain.scss +1 -2
- package/dist/containers/Tenant/QueryEditor/QueryResult/QueryResult.scss +2 -2
- package/dist/containers/Tenant/Schema/SchemaTree/SchemaTree.tsx +9 -1
- package/dist/containers/Tenant/Tenant.scss +2 -50
- package/dist/containers/Tenant/Tenant.tsx +24 -22
- package/dist/containers/Tenant/utils/schema.ts +3 -0
- package/dist/containers/Tenant/utils/schemaActions.ts +1 -2
- package/dist/containers/Tenants/Tenants.js +12 -2
- package/dist/containers/UserSettings/UserSettings.tsx +26 -3
- package/dist/services/api.d.ts +19 -2
- package/dist/services/api.js +2 -2
- package/dist/setupTests.js +4 -0
- package/dist/store/reducers/executeQuery.js +4 -9
- package/dist/store/reducers/{preview.js → preview.ts} +22 -18
- package/dist/store/reducers/settings.js +3 -1
- package/dist/store/utils.ts +88 -0
- package/dist/types/api/query.ts +147 -0
- package/dist/types/api/schema.ts +235 -2
- package/dist/types/index.ts +33 -0
- package/dist/types/store/query.ts +9 -0
- package/dist/utils/{constants.js → constants.ts} +11 -6
- package/dist/utils/index.js +0 -24
- package/dist/utils/query.test.ts +189 -0
- package/dist/utils/query.ts +156 -0
- package/dist/utils/tests/providers.tsx +29 -0
- package/package.json +2 -2
- 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.
|
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.
|
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",
|
package/dist/store/utils.js
DELETED
@@ -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
|
-
}
|