ydb-embedded-ui 5.1.0 → 5.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -9,9 +9,13 @@ export declare function useComponent<T extends Parameters<ComponentsRegistry['ge
9
9
  StaffCard: typeof import("../User/StaffCard").StaffCard;
10
10
  } & {
11
11
  AsideNavigation: typeof import("../../containers/AsideNavigation/AsideNavigation").AsideNavigation;
12
+ } & {
13
+ ErrorBoundary: typeof import("../ErrorBoundary/ErrorBoundary").ErrorBoundaryInner;
12
14
  })[T] extends React.ComponentType<any> ? React.ComponentType<React.PropsWithoutRef<React.ComponentProps<({
13
15
  StaffCard: typeof import("../User/StaffCard").StaffCard;
14
16
  } & {
15
17
  AsideNavigation: typeof import("../../containers/AsideNavigation/AsideNavigation").AsideNavigation;
18
+ } & {
19
+ ErrorBoundary: typeof import("../ErrorBoundary/ErrorBoundary").ErrorBoundaryInner;
16
20
  })[T]>>> : never;
17
21
  export {};
@@ -1,10 +1,13 @@
1
1
  import { StaffCard } from '../User/StaffCard';
2
2
  import { AsideNavigation } from '../../containers/AsideNavigation/AsideNavigation';
3
3
  import { ComponentsRegistryTemplate, Registry } from './registry';
4
+ import { ErrorBoundaryInner } from '../ErrorBoundary/ErrorBoundary';
4
5
  declare const componentsRegistryInner: Registry<{
5
6
  StaffCard: typeof StaffCard;
6
7
  } & {
7
8
  AsideNavigation: typeof AsideNavigation;
9
+ } & {
10
+ ErrorBoundary: typeof ErrorBoundaryInner;
8
11
  }>;
9
12
  export declare type ComponentsRegistry = ComponentsRegistryTemplate<typeof componentsRegistryInner>;
10
13
  export declare const componentsRegistry: ComponentsRegistry;
@@ -1,7 +1,9 @@
1
1
  import { StaffCard } from '../User/StaffCard';
2
2
  import { AsideNavigation } from '../../containers/AsideNavigation/AsideNavigation';
3
3
  import { Registry } from './registry';
4
+ import { ErrorBoundaryInner } from '../ErrorBoundary/ErrorBoundary';
4
5
  const componentsRegistryInner = new Registry()
5
6
  .register('StaffCard', StaffCard)
6
- .register('AsideNavigation', AsideNavigation);
7
+ .register('AsideNavigation', AsideNavigation)
8
+ .register('ErrorBoundary', ErrorBoundaryInner);
7
9
  export const componentsRegistry = componentsRegistryInner;
@@ -1,9 +1,19 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import './ErrorBoundary.scss';
3
+ export declare function ErrorBoundary({ children }: {
4
+ children?: ReactNode;
5
+ }): JSX.Element;
3
6
  interface ErrorBoundaryProps {
4
7
  children?: ReactNode;
5
8
  useRetry?: boolean;
6
9
  onReportProblem?: (error?: Error) => void;
7
10
  }
8
- export declare const ErrorBoundary: ({ children, useRetry, onReportProblem }: ErrorBoundaryProps) => JSX.Element;
11
+ export declare function ErrorBoundaryInner({ children, useRetry, onReportProblem, }: ErrorBoundaryProps): JSX.Element;
12
+ interface ErrorBoundaryFallbackProps {
13
+ error: Error;
14
+ useRetry?: boolean;
15
+ resetErrorBoundary: () => void;
16
+ onReportProblem?: (error?: Error) => void;
17
+ }
18
+ export declare function ErrorBoundaryFallback({ error, resetErrorBoundary, useRetry, onReportProblem, }: ErrorBoundaryFallbackProps): JSX.Element;
9
19
  export {};
@@ -4,13 +4,21 @@ import cn from 'bem-cn-lite';
4
4
  import { Button, Disclosure } from '@gravity-ui/uikit';
5
5
  import { registerError } from '../../utils/registerError';
6
6
  import { Illustration } from '../Illustration';
7
+ import { useComponent } from '../ComponentsProvider/ComponentsProvider';
7
8
  import i18n from './i18n';
8
9
  import './ErrorBoundary.scss';
9
10
  const b = cn('ydb-error-boundary');
10
- export const ErrorBoundary = ({ children, useRetry = true, onReportProblem }) => {
11
+ export function ErrorBoundary({ children }) {
12
+ const ErrorBoundaryComponent = useComponent('ErrorBoundary');
13
+ return _jsx(ErrorBoundaryComponent, { children: children });
14
+ }
15
+ export function ErrorBoundaryInner({ children, useRetry = true, onReportProblem, }) {
11
16
  return (_jsx(ErrorBoundaryBase, Object.assign({ onError: (error, info) => {
12
17
  registerError(error, info.componentStack, 'error-boundary');
13
18
  }, fallbackRender: ({ error, resetErrorBoundary }) => {
14
- return (_jsxs("div", Object.assign({ className: b(null) }, { children: [_jsx(Illustration, { name: "error", className: b('illustration') }), _jsxs("div", Object.assign({ className: b('content') }, { children: [_jsx("h2", Object.assign({ className: b('error-title') }, { children: i18n('error-title') })), _jsx("div", Object.assign({ className: b('error-description') }, { children: i18n('error-description') })), _jsx(Disclosure, Object.assign({ summary: i18n('show-details'), className: b('show-details'), size: "m" }, { children: _jsx("pre", Object.assign({ className: b('error-details') }, { children: error.stack })) })), _jsxs("div", Object.assign({ className: b('actions') }, { children: [useRetry && (_jsx(Button, Object.assign({ view: "outlined", onClick: resetErrorBoundary }, { children: i18n('button-reset') }))), onReportProblem && (_jsx(Button, Object.assign({ view: "outlined", onClick: () => onReportProblem(error) }, { children: i18n('report-problem') })))] }))] }))] })));
19
+ return (_jsx(ErrorBoundaryFallback, { error: error, useRetry: useRetry, resetErrorBoundary: resetErrorBoundary, onReportProblem: onReportProblem }));
15
20
  } }, { children: children })));
16
- };
21
+ }
22
+ export function ErrorBoundaryFallback({ error, resetErrorBoundary, useRetry, onReportProblem, }) {
23
+ return (_jsxs("div", Object.assign({ className: b() }, { children: [_jsx(Illustration, { name: "error", className: b('illustration') }), _jsxs("div", Object.assign({ className: b('content') }, { children: [_jsx("h2", Object.assign({ className: b('error-title') }, { children: i18n('error-title') })), _jsx("div", Object.assign({ className: b('error-description') }, { children: i18n('error-description') })), _jsx(Disclosure, Object.assign({ summary: i18n('show-details'), className: b('show-details'), size: "m" }, { children: _jsx("pre", Object.assign({ className: b('error-details') }, { children: error.stack })) })), _jsxs("div", Object.assign({ className: b('actions') }, { children: [useRetry && (_jsx(Button, Object.assign({ view: "outlined", onClick: resetErrorBoundary }, { children: i18n('button-reset') }))), onReportProblem && (_jsx(Button, Object.assign({ view: "outlined", onClick: () => onReportProblem(error) }, { children: i18n('report-problem') })))] }))] }))] })));
24
+ }
@@ -0,0 +1,6 @@
1
+ import { ReactNode } from 'react';
2
+ export interface ClusterModeGuardProps {
3
+ children: ReactNode;
4
+ mode: 'single' | 'multi';
5
+ }
6
+ export declare function ClusterModeGuard({ children, mode }: ClusterModeGuardProps): JSX.Element | null;
@@ -0,0 +1,6 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useTypedSelector } from '../../lib';
3
+ export function ClusterModeGuard({ children, mode }) {
4
+ const shouldRender = useTypedSelector((state) => mode === 'single' ? state.singleClusterMode : !state.singleClusterMode);
5
+ return shouldRender ? _jsx(_Fragment, { children: children }) : null;
6
+ }
@@ -0,0 +1 @@
1
+ export * from './ClusterModeGuard';
@@ -0,0 +1 @@
1
+ export * from './ClusterModeGuard';
@@ -3,6 +3,7 @@ export declare type SettingsElementType = 'switch' | 'radio';
3
3
  export interface SettingProps {
4
4
  type?: SettingsElementType;
5
5
  title: string;
6
+ description?: ReactNode;
6
7
  settingKey: string;
7
8
  helpPopoverContent?: ReactNode;
8
9
  options?: {
@@ -12,4 +13,4 @@ export interface SettingProps {
12
13
  defaultValue?: unknown;
13
14
  onValueUpdate?: VoidFunction;
14
15
  }
15
- export declare const Setting: ({ type, settingKey, title, helpPopoverContent, options, defaultValue, onValueUpdate, }: SettingProps) => JSX.Element;
16
+ export declare const Setting: ({ type, settingKey, title, description, helpPopoverContent, options, defaultValue, onValueUpdate, }: SettingProps) => JSX.Element;
@@ -4,7 +4,7 @@ import { Settings } from '@gravity-ui/navigation';
4
4
  import { LabelWithPopover } from '../../components/LabelWithPopover/LabelWithPopover';
5
5
  import { useSetting } from '../../utils/hooks';
6
6
  import { b } from './UserSettings';
7
- export const Setting = ({ type = 'switch', settingKey, title, helpPopoverContent, options, defaultValue, onValueUpdate, }) => {
7
+ export const Setting = ({ type = 'switch', settingKey, title, description, helpPopoverContent, options, defaultValue, onValueUpdate, }) => {
8
8
  const [settingValue, setValue] = useSetting(settingKey, defaultValue);
9
9
  const onUpdate = (value) => {
10
10
  setValue(value);
@@ -33,5 +33,5 @@ export const Setting = ({ type = 'switch', settingKey, title, helpPopoverContent
33
33
  return null;
34
34
  }
35
35
  };
36
- return (_jsx(Settings.Item, Object.assign({ title: title, highlightedTitle: title, renderTitleComponent: renderTitleComponent }, { children: getSettingsElement(type) })));
36
+ return (_jsx(Settings.Item, Object.assign({ title: title, highlightedTitle: title, description: description, renderTitleComponent: renderTitleComponent }, { children: getSettingsElement(type) })));
37
37
  };
@@ -10,6 +10,8 @@
10
10
  "settings.language.title": "Interface language",
11
11
  "settings.language.option-russian": "Russian",
12
12
  "settings.language.option-english": "English",
13
+ "settings.binaryDataInPlainTextDisplay.title": "Display binary data in plain text",
14
+ "settings.binaryDataInPlainTextDisplay.description": "Available starting from version 24.1",
13
15
  "settings.invertedDisks.title": "Inverted disks space indicators",
14
16
  "settings.useNodesEndpoint.title": "Break the Nodes tab in Diagnostics",
15
17
  "settings.useNodesEndpoint.popover": "Use /viewer/json/nodes endpoint for Nodes Tab in diagnostics. It could return incorrect data on some versions",
@@ -1,2 +1,2 @@
1
- declare const _default: (key: string, params?: import("@gravity-ui/i18n").Params | undefined) => string;
1
+ declare const _default: (key: "page.general" | "section.appearance" | "page.experiments" | "section.experiments" | "settings.theme.title" | "settings.theme.option-dark" | "settings.theme.option-light" | "settings.theme.option-system" | "settings.language.title" | "settings.language.option-russian" | "settings.language.option-english" | "settings.binaryDataInPlainTextDisplay.title" | "settings.binaryDataInPlainTextDisplay.description" | "settings.invertedDisks.title" | "settings.useNodesEndpoint.title" | "settings.useNodesEndpoint.popover" | "settings.useVirtualTables.title" | "settings.useVirtualTables.popover" | "settings.queryUseMultiSchema.title" | "settings.queryUseMultiSchema.popover", params?: import("@gravity-ui/i18n").Params | undefined) => string;
2
2
  export default _default;
@@ -1,7 +1,3 @@
1
- import { i18n, Lang } from '../../../utils/i18n';
1
+ import { registerKeysets } from '../../../utils/i18n';
2
2
  import en from './en.json';
3
- import ru from './ru.json';
4
- const COMPONENT = 'ydb-user-settings';
5
- i18n.registerKeyset(Lang.En, COMPONENT, en);
6
- i18n.registerKeyset(Lang.Ru, COMPONENT, ru);
7
- export default i18n.keyset(COMPONENT);
3
+ export default registerKeysets('ydb-user-settings', { en });
@@ -14,6 +14,7 @@ export interface SettingsPage {
14
14
  export declare type YDBEmbeddedUISettings = SettingsPage[];
15
15
  export declare const themeSetting: SettingProps;
16
16
  export declare const languageSetting: SettingProps;
17
+ export declare const binaryDataInPlainTextDisplay: SettingProps;
17
18
  export declare const invertedDisksSetting: SettingProps;
18
19
  export declare const useNodesEndpointSetting: SettingProps;
19
20
  export declare const useVirtualTables: SettingProps;
@@ -1,8 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
1
2
  import favoriteFilledIcon from '../../assets/icons/star.svg';
2
3
  import flaskIcon from '../../assets/icons/flask.svg';
3
- import { INVERTED_DISKS_KEY, LANGUAGE_KEY, THEME_KEY, USE_BACKEND_PARAMS_FOR_TABLES_KEY, USE_NODES_ENDPOINT_IN_DIAGNOSTICS_KEY, QUERY_USE_MULTI_SCHEMA_KEY, } from '../../utils/constants';
4
+ import { INVERTED_DISKS_KEY, LANGUAGE_KEY, THEME_KEY, USE_BACKEND_PARAMS_FOR_TABLES_KEY, USE_NODES_ENDPOINT_IN_DIAGNOSTICS_KEY, QUERY_USE_MULTI_SCHEMA_KEY, BINARY_DATA_IN_PLAIN_TEXT_DISPLAY, } from '../../utils/constants';
4
5
  import { Lang, defaultLang } from '../../utils/i18n';
5
6
  import i18n from './i18n';
7
+ import { ClusterModeGuard } from '../ClusterModeGuard';
6
8
  const themeOptions = [
7
9
  {
8
10
  value: 'system',
@@ -43,6 +45,11 @@ export const languageSetting = {
43
45
  window.location.reload();
44
46
  },
45
47
  };
48
+ export const binaryDataInPlainTextDisplay = {
49
+ settingKey: BINARY_DATA_IN_PLAIN_TEXT_DISPLAY,
50
+ title: i18n('settings.binaryDataInPlainTextDisplay.title'),
51
+ description: (_jsx(ClusterModeGuard, Object.assign({ mode: "multi" }, { children: i18n('settings.binaryDataInPlainTextDisplay.description') }))),
52
+ };
46
53
  export const invertedDisksSetting = {
47
54
  settingKey: INVERTED_DISKS_KEY,
48
55
  title: i18n('settings.invertedDisks.title'),
@@ -65,7 +72,7 @@ export const queryUseMultiSchemaSetting = {
65
72
  export const appearanceSection = {
66
73
  id: 'appearanceSection',
67
74
  title: i18n('section.appearance'),
68
- settings: [themeSetting, invertedDisksSetting],
75
+ settings: [themeSetting, invertedDisksSetting, binaryDataInPlainTextDisplay],
69
76
  };
70
77
  export const experimentsSection = {
71
78
  id: 'experimentsSection',
package/dist/lib.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  export { App as SingleClusterApp, AppSlots } from './containers/App';
2
2
  export { AppWithClusters as MultiClusterApp } from './containers/AppWithClusters/AppWithClusters';
3
- export { ErrorBoundary } from './components/ErrorBoundary/ErrorBoundary';
3
+ export { ErrorBoundaryInner as ErrorBoundary, ErrorBoundaryFallback, } from './components/ErrorBoundary/ErrorBoundary';
4
4
  export { configureStore, rootReducer } from './store';
5
+ export { default as appRoutes } from './routes';
5
6
  export { createApi, YdbEmbeddedAPI, YdbWebVersionAPI } from './services/api';
6
7
  export { settingsManager } from './services/settings';
7
8
  export { settings as userSettings } from './containers/UserSettings/settings';
package/dist/lib.js CHANGED
@@ -1,7 +1,8 @@
1
1
  export { App as SingleClusterApp, AppSlots } from './containers/App';
2
2
  export { AppWithClusters as MultiClusterApp } from './containers/AppWithClusters/AppWithClusters';
3
- export { ErrorBoundary } from './components/ErrorBoundary/ErrorBoundary';
3
+ export { ErrorBoundaryInner as ErrorBoundary, ErrorBoundaryFallback, } from './components/ErrorBoundary/ErrorBoundary';
4
4
  export { configureStore, rootReducer } from './store';
5
+ export { default as appRoutes } from './routes';
5
6
  export { createApi, YdbEmbeddedAPI, YdbWebVersionAPI } from './services/api';
6
7
  export { settingsManager } from './services/settings';
7
8
  export { settings as userSettings } from './containers/UserSettings/settings';
@@ -12,8 +12,10 @@ var __rest = (this && this.__rest) || function (s, e) {
12
12
  import AxiosWrapper from '@gravity-ui/axios-wrapper';
13
13
  import { backend as BACKEND, metaBackend as META_BACKEND } from '../store';
14
14
  import { prepareSortValue } from '../utils/filters';
15
+ import { BINARY_DATA_IN_PLAIN_TEXT_DISPLAY } from '../utils/constants';
15
16
  import { parseMetaCluster } from './parsers/parseMetaCluster';
16
17
  import { parseMetaTenants } from './parsers/parseMetaTenants';
18
+ import { settingsManager } from './settings';
17
19
  export class YdbEmbeddedAPI extends AxiosWrapper {
18
20
  getPath(path) {
19
21
  return `${BACKEND !== null && BACKEND !== void 0 ? BACKEND : ''}${path}`;
@@ -179,7 +181,12 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
179
181
  // Time difference to ensure that timeout from ui will be shown rather than backend error
180
182
  const uiTimeout = 9 * 60 * 1000;
181
183
  const backendTimeout = 10 * 60 * 1000;
182
- return this.post(this.getPath(`/viewer/json/query?timeout=${backendTimeout}${schema ? `&schema=${schema}` : ''}`), params, {}, {
184
+ /**
185
+ * Return strings using base64 encoding.
186
+ * @link https://github.com/ydb-platform/ydb/pull/647
187
+ */
188
+ const base64 = !settingsManager.readUserSettingsValue(BINARY_DATA_IN_PLAIN_TEXT_DISPLAY, true);
189
+ return this.post(this.getPath(`/viewer/json/query?timeout=${backendTimeout}&base64=${base64}${schema ? `&schema=${schema}` : ''}`), params, {}, {
183
190
  concurrentId,
184
191
  timeout: uiTimeout,
185
192
  });
@@ -1,5 +1,5 @@
1
1
  import { TENANT_PAGES_IDS } from '../store/reducers/tenant/constants';
2
- import { ASIDE_HEADER_COMPACT_KEY, INVERTED_DISKS_KEY, LANGUAGE_KEY, LAST_USED_QUERY_ACTION_KEY, PARTITIONS_HIDDEN_COLUMNS_KEY, QUERY_INITIAL_MODE_KEY, QUERY_USE_MULTI_SCHEMA_KEY, SAVED_QUERIES_KEY, TENANT_INITIAL_PAGE_KEY, THEME_KEY, USE_BACKEND_PARAMS_FOR_TABLES_KEY, USE_NODES_ENDPOINT_IN_DIAGNOSTICS_KEY, USE_CLUSTER_BALANCER_AS_BACKEND_KEY, } from '../utils/constants';
2
+ import { ASIDE_HEADER_COMPACT_KEY, INVERTED_DISKS_KEY, LANGUAGE_KEY, LAST_USED_QUERY_ACTION_KEY, PARTITIONS_HIDDEN_COLUMNS_KEY, QUERY_INITIAL_MODE_KEY, QUERY_USE_MULTI_SCHEMA_KEY, SAVED_QUERIES_KEY, TENANT_INITIAL_PAGE_KEY, THEME_KEY, USE_BACKEND_PARAMS_FOR_TABLES_KEY, USE_NODES_ENDPOINT_IN_DIAGNOSTICS_KEY, USE_CLUSTER_BALANCER_AS_BACKEND_KEY, BINARY_DATA_IN_PLAIN_TEXT_DISPLAY, } from '../utils/constants';
3
3
  import { QUERY_ACTIONS, QUERY_MODES } from '../utils/query';
4
4
  import { parseJson } from '../utils/utils';
5
5
  /** User settings keys and their default values */
@@ -9,6 +9,7 @@ export const DEFAULT_USER_SETTINGS = {
9
9
  [INVERTED_DISKS_KEY]: false,
10
10
  [USE_NODES_ENDPOINT_IN_DIAGNOSTICS_KEY]: false,
11
11
  [QUERY_USE_MULTI_SCHEMA_KEY]: false,
12
+ [BINARY_DATA_IN_PLAIN_TEXT_DISPLAY]: true,
12
13
  [SAVED_QUERIES_KEY]: [],
13
14
  [TENANT_INITIAL_PAGE_KEY]: TENANT_PAGES_IDS.query,
14
15
  [QUERY_INITIAL_MODE_KEY]: QUERY_MODES.script,
@@ -56,6 +56,7 @@ export declare const SAVED_QUERIES_KEY = "saved_queries";
56
56
  export declare const ASIDE_HEADER_COMPACT_KEY = "asideHeaderCompact";
57
57
  export declare const QUERIES_HISTORY_KEY = "queries_history";
58
58
  export declare const DATA_QA_TUNE_COLUMNS_POPUP = "tune-columns-popup";
59
+ export declare const BINARY_DATA_IN_PLAIN_TEXT_DISPLAY = "binaryDataInPlainTextDisplay";
59
60
  export declare const DEFAULT_SIZE_RESULT_PANE_KEY = "default-size-result-pane";
60
61
  export declare const DEFAULT_SIZE_TENANT_SUMMARY_KEY = "default-size-tenant-summary-pane";
61
62
  export declare const DEFAULT_SIZE_TENANT_KEY = "default-size-tenant-pane";
@@ -72,6 +72,7 @@ export const SAVED_QUERIES_KEY = 'saved_queries';
72
72
  export const ASIDE_HEADER_COMPACT_KEY = 'asideHeaderCompact';
73
73
  export const QUERIES_HISTORY_KEY = 'queries_history';
74
74
  export const DATA_QA_TUNE_COLUMNS_POPUP = 'tune-columns-popup';
75
+ export const BINARY_DATA_IN_PLAIN_TEXT_DISPLAY = 'binaryDataInPlainTextDisplay';
75
76
  export const DEFAULT_SIZE_RESULT_PANE_KEY = 'default-size-result-pane';
76
77
  export const DEFAULT_SIZE_TENANT_SUMMARY_KEY = 'default-size-tenant-summary-pane';
77
78
  export const DEFAULT_SIZE_TENANT_KEY = 'default-size-tenant-pane';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-embedded-ui",
3
- "version": "5.1.0",
3
+ "version": "5.2.0",
4
4
  "files": [
5
5
  "dist"
6
6
  ],
@@ -1,20 +0,0 @@
1
- {
2
- "page.general": "Общие",
3
- "section.appearance": "Внешний вид",
4
- "page.experiments": "Эксперименты",
5
- "section.experiments": "Эксперименты",
6
- "settings.theme.title": "Тема",
7
- "settings.theme.option-dark": "Тёмная",
8
- "settings.theme.option-light": "Светлая",
9
- "settings.theme.option-system": "Системная",
10
- "settings.language.title": "Язык интерфейса",
11
- "settings.language.option-russian": "Русский",
12
- "settings.language.option-english": "English",
13
- "settings.invertedDisks.title": "Инвертированные индикаторы места на дисках",
14
- "settings.useNodesEndpoint.title": "Сломать вкладку Nodes в диагностике",
15
- "settings.useNodesEndpoint.popover": "Использовать эндпоинт /viewer/json/nodes для вкладки Nodes в диагностике. Может возвращать некорректные данные на некоторых версиях",
16
- "settings.useVirtualTables.title": "Использовать таблицу с загрузкой данных по скроллу для вкладок Nodes и Storage",
17
- "settings.useVirtualTables.popover": "Это улучшит производительность, но может работать нестабильно",
18
- "settings.queryUseMultiSchema.title": "Разрешить запросы с несколькими результатами",
19
- "settings.queryUseMultiSchema.popover": "Использовать для запросов схему 'multi', которая позволяет выполнять запросы с несколькими результатами. На версиях 23-3 и старше результат не возвращается вообще"
20
- }