Skip to content

Commit 99ee997

Browse files
authored
feat: support multipart responses in query (#1865)
1 parent d02a62a commit 99ee997

30 files changed

+807
-188
lines changed

config-overrides.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ module.exports = {
5858
// By default jest does not transform anything in node_modules
5959
// So this override excludes node_modules except @gravity-ui
6060
// see https://github.com/timarney/react-app-rewired/issues/241
61-
config.transformIgnorePatterns = ['node_modules/(?!(@gravity-ui)/)'];
61+
config.transformIgnorePatterns = ['node_modules/(?!(@gravity-ui|@mjackson)/)'];
6262

6363
// Add .github directory to roots
6464
config.roots = ['<rootDir>/src', '<rootDir>/.github'];

package-lock.json

+22-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@
2121
"@gravity-ui/icons": "^2.12.0",
2222
"@gravity-ui/navigation": "^2.30.0",
2323
"@gravity-ui/paranoid": "^2.0.1",
24-
"@gravity-ui/react-data-table": "^2.1.1",
24+
"@gravity-ui/react-data-table": "^2.2.1",
2525
"@gravity-ui/table": "^1.7.0",
2626
"@gravity-ui/uikit": "^6.40.0",
2727
"@gravity-ui/unipika": "^5.2.1",
2828
"@gravity-ui/websql-autocomplete": "^13.7.0",
2929
"@hookform/resolvers": "^3.10.0",
30+
"@mjackson/multipart-parser": "^0.8.2",
3031
"@reduxjs/toolkit": "^2.5.0",
3132
"@tanstack/react-table": "^8.20.6",
3233
"@ydb-platform/monaco-ghost": "^0.6.1",
@@ -163,10 +164,10 @@
163164
"typescript": "^5.7.3"
164165
},
165166
"peerDependencies": {
167+
"monaco-yql-languages": ">=1.3.0",
166168
"prop-types": "^15.8.1",
167169
"react": "^18.3.1",
168-
"react-dom": "^18.3.1",
169-
"monaco-yql-languages": ">=1.3.0"
170+
"react-dom": "^18.3.1"
170171
},
171172
"overrides": {
172173
"fork-ts-checker-webpack-plugin": "^9.0.2",

src/components/QueryResultTable/QueryResultTable.scss

+6
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,10 @@
99
&__message {
1010
padding: 15px 10px;
1111
}
12+
13+
// Must have fixed height for frequent data updates (to avoid interface shaking).
14+
// Will be fixed after migration to new @gravity-ui/table.
15+
&__table-wrapper {
16+
height: 0px;
17+
}
1218
}

src/components/QueryResultTable/QueryResultTable.tsx

+20-13
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {ColumnType, KeyValueRow} from '../../types/api/query';
77
import {cn} from '../../utils/cn';
88
import {DEFAULT_TABLE_SETTINGS} from '../../utils/constants';
99
import {getColumnWidth} from '../../utils/getColumnWidth';
10-
import {getColumnType, prepareQueryResponse} from '../../utils/query';
10+
import {getColumnType} from '../../utils/query';
1111
import {isNumeric} from '../../utils/utils';
1212
import type {ResizeableDataTableProps} from '../ResizeableDataTable/ResizeableDataTable';
1313
import {ResizeableDataTable} from '../ResizeableDataTable/ResizeableDataTable';
@@ -49,8 +49,8 @@ const prepareTypedColumns = (columns: ColumnType[], data?: KeyValueRow[]) => {
4949
});
5050
};
5151

52-
const prepareGenericColumns = (data: KeyValueRow[]) => {
53-
if (!data.length) {
52+
const prepareGenericColumns = (data?: KeyValueRow[]) => {
53+
if (!data?.length) {
5454
return [];
5555
}
5656

@@ -77,35 +77,42 @@ interface QueryResultTableProps
7777
extends Omit<ResizeableDataTableProps<KeyValueRow>, 'data' | 'columns'> {
7878
data?: KeyValueRow[];
7979
columns?: ColumnType[];
80+
settings?: Partial<Settings>;
8081
}
8182

8283
export const QueryResultTable = (props: QueryResultTableProps) => {
83-
const {columns: rawColumns, data: rawData, ...restProps} = props;
84+
const {columns, data, settings: propsSettings} = props;
8485

85-
const data = React.useMemo(() => prepareQueryResponse(rawData), [rawData]);
86-
const columns = React.useMemo(() => {
87-
return rawColumns ? prepareTypedColumns(rawColumns, data) : prepareGenericColumns(data);
88-
}, [data, rawColumns]);
86+
const preparedColumns = React.useMemo(() => {
87+
return columns ? prepareTypedColumns(columns, data) : prepareGenericColumns(data);
88+
}, [data, columns]);
89+
90+
const settings = React.useMemo(() => {
91+
return {
92+
...TABLE_SETTINGS,
93+
...propsSettings,
94+
};
95+
}, [propsSettings]);
8996

9097
// empty data is expected to be be an empty array
9198
// undefined data is not rendered at all
92-
if (!Array.isArray(rawData)) {
99+
if (!Array.isArray(data)) {
93100
return null;
94101
}
95102

96-
if (!columns.length) {
103+
if (!preparedColumns.length) {
97104
return <div className={b('message')}>{i18n('empty')}</div>;
98105
}
99106

100107
return (
101108
<ResizeableDataTable
102109
data={data}
103-
columns={columns}
104-
settings={TABLE_SETTINGS}
110+
columns={preparedColumns}
111+
settings={settings}
105112
// prevent accessing row.id in case it is present but is not the PK (i.e. may repeat)
106113
rowKey={getRowIndex}
107114
visibleRowIndex={getVisibleRowIndex}
108-
{...restProps}
115+
wrapperClassName={b('table-wrapper')}
109116
/>
110117
);
111118
};

src/containers/Tenant/Query/CancelQueryButton/CancelQueryButton.tsx

+7-15
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
import React from 'react';
2-
31
import {StopFill} from '@gravity-ui/icons';
42
import {Button, Icon} from '@gravity-ui/uikit';
53

6-
import {cancelQueryApi} from '../../../../store/reducers/cancelQuery';
74
import {cn} from '../../../../utils/cn';
85
import i18n from '../i18n';
96

@@ -12,22 +9,17 @@ import './CancelQueryButton.scss';
129
const b = cn('cancel-query-button');
1310

1411
interface CancelQueryButtonProps {
15-
queryId: string;
16-
tenantName: string;
12+
isLoading: boolean;
13+
isError: boolean;
14+
onClick?: VoidFunction;
1715
}
1816

19-
export function CancelQueryButton({queryId, tenantName}: CancelQueryButtonProps) {
20-
const [sendCancelQuery, cancelQueryResponse] = cancelQueryApi.useCancelQueryMutation();
21-
22-
const onStopButtonClick = React.useCallback(() => {
23-
sendCancelQuery({queryId, database: tenantName});
24-
}, [queryId, sendCancelQuery, tenantName]);
25-
17+
export function CancelQueryButton({isLoading, isError, onClick}: CancelQueryButtonProps) {
2618
return (
2719
<Button
28-
loading={cancelQueryResponse.isLoading}
29-
onClick={onStopButtonClick}
30-
className={b('stop-button', {error: Boolean(cancelQueryResponse.error)})}
20+
loading={isLoading}
21+
onClick={onClick}
22+
className={b('stop-button', {error: isError})}
3123
>
3224
<Icon data={StopFill} size={16} />
3325
{i18n('action.stop')}

src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx

+65-11
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import React from 'react';
22

3+
import type {Settings} from '@gravity-ui/react-data-table';
34
import {isEqual} from 'lodash';
45
import {v4 as uuidv4} from 'uuid';
56

67
import SplitPane from '../../../../components/SplitPane';
7-
import {useTracingLevelOptionAvailable} from '../../../../store/reducers/capabilities/hooks';
8+
import {cancelQueryApi} from '../../../../store/reducers/cancelQuery';
9+
import {
10+
useStreamingAvailable,
11+
useTracingLevelOptionAvailable,
12+
} from '../../../../store/reducers/capabilities/hooks';
813
import {
914
queryApi,
1015
saveQueryToHistory,
@@ -23,6 +28,7 @@ import {cn} from '../../../../utils/cn';
2328
import {
2429
DEFAULT_IS_QUERY_RESULT_COLLAPSED,
2530
DEFAULT_SIZE_RESULT_PANE_KEY,
31+
ENABLE_QUERY_STREAMING,
2632
LAST_USED_QUERY_ACTION_KEY,
2733
} from '../../../../utils/constants';
2834
import {
@@ -34,7 +40,7 @@ import {
3440
} from '../../../../utils/hooks';
3541
import {useChangedQuerySettings} from '../../../../utils/hooks/useChangedQuerySettings';
3642
import {useLastQueryExecutionSettings} from '../../../../utils/hooks/useLastQueryExecutionSettings';
37-
import {QUERY_ACTIONS} from '../../../../utils/query';
43+
import {DEFAULT_QUERY_SETTINGS, QUERY_ACTIONS} from '../../../../utils/query';
3844
import type {InitialPaneState} from '../../utils/paneVisibilityToggleHelpers';
3945
import {
4046
PaneVisibilityActionTypes,
@@ -86,8 +92,24 @@ export default function QueryEditor(props: QueryEditorProps) {
8692
LAST_USED_QUERY_ACTION_KEY,
8793
);
8894
const [lastExecutedQueryText, setLastExecutedQueryText] = React.useState<string>('');
95+
const [isQueryStreamingEnabled] = useSetting(ENABLE_QUERY_STREAMING);
96+
const isStreamingEnabled = useStreamingAvailable() && isQueryStreamingEnabled;
8997

9098
const [sendQuery] = queryApi.useUseSendQueryMutation();
99+
const [streamQuery] = queryApi.useUseStreamQueryMutation();
100+
const [sendCancelQuery, cancelQueryResponse] = cancelQueryApi.useCancelQueryMutation();
101+
102+
const runningQueryRef = React.useRef<{abort: VoidFunction} | null>(null);
103+
104+
const tableSettings = React.useMemo(() => {
105+
return isStreamingEnabled
106+
? {
107+
displayIndices: {
108+
maxIndex: (querySettings.limitRows || DEFAULT_QUERY_SETTINGS.limitRows) + 1,
109+
},
110+
}
111+
: undefined;
112+
}, [isStreamingEnabled, querySettings.limitRows]);
91113

92114
React.useEffect(() => {
93115
if (savedPath !== tenantName) {
@@ -121,14 +143,25 @@ export default function QueryEditor(props: QueryEditorProps) {
121143
}
122144
const queryId = uuidv4();
123145

124-
sendQuery({
125-
actionType: 'execute',
126-
query: text,
127-
database: tenantName,
128-
querySettings,
129-
enableTracingLevel,
130-
queryId,
131-
});
146+
if (isStreamingEnabled) {
147+
runningQueryRef.current = streamQuery({
148+
actionType: 'execute',
149+
query: text,
150+
database: tenantName,
151+
querySettings,
152+
enableTracingLevel,
153+
queryId,
154+
});
155+
} else {
156+
runningQueryRef.current = sendQuery({
157+
actionType: 'execute',
158+
query: text,
159+
database: tenantName,
160+
querySettings,
161+
enableTracingLevel,
162+
queryId,
163+
});
164+
}
132165

133166
dispatch(setShowPreview(false));
134167

@@ -155,7 +188,7 @@ export default function QueryEditor(props: QueryEditorProps) {
155188

156189
const queryId = uuidv4();
157190

158-
sendQuery({
191+
runningQueryRef.current = sendQuery({
159192
actionType: 'explain',
160193
query: text,
161194
database: tenantName,
@@ -169,6 +202,14 @@ export default function QueryEditor(props: QueryEditorProps) {
169202
dispatchResultVisibilityState(PaneVisibilityActionTypes.triggerExpand);
170203
});
171204

205+
const handleCancelRunningQuery = React.useCallback(() => {
206+
if (isStreamingEnabled && runningQueryRef.current) {
207+
runningQueryRef.current.abort();
208+
} else if (result?.queryId) {
209+
sendCancelQuery({queryId: result?.queryId, database: tenantName});
210+
}
211+
}, [isStreamingEnabled, result?.queryId, sendCancelQuery, tenantName]);
212+
172213
const onCollapseResultHandler = () => {
173214
dispatchResultVisibilityState(PaneVisibilityActionTypes.triggerCollapse);
174215
};
@@ -229,10 +270,13 @@ export default function QueryEditor(props: QueryEditorProps) {
229270
theme={theme}
230271
key={result?.queryId}
231272
result={result}
273+
cancelQueryResponse={cancelQueryResponse}
232274
tenantName={tenantName}
233275
path={path}
234276
showPreview={showPreview}
235277
queryText={lastExecutedQueryText}
278+
onCancelRunningQuery={handleCancelRunningQuery}
279+
tableSettings={tableSettings}
236280
/>
237281
</div>
238282
</SplitPane>
@@ -248,13 +292,17 @@ interface ResultProps {
248292
type?: EPathType;
249293
theme: string;
250294
result?: QueryResult;
295+
cancelQueryResponse?: Pick<QueryResult, 'isLoading' | 'error'>;
251296
tenantName: string;
252297
path: string;
253298
showPreview?: boolean;
254299
queryText: string;
300+
tableSettings?: Partial<Settings>;
301+
onCancelRunningQuery: VoidFunction;
255302
}
256303
function Result({
257304
resultVisibilityState,
305+
cancelQueryResponse,
258306
onExpandResultHandler,
259307
onCollapseResultHandler,
260308
type,
@@ -264,6 +312,8 @@ function Result({
264312
path,
265313
showPreview,
266314
queryText,
315+
tableSettings,
316+
onCancelRunningQuery,
267317
}: ResultProps) {
268318
if (showPreview) {
269319
return <Preview database={tenantName} path={path} type={type} />;
@@ -277,9 +327,13 @@ function Result({
277327
theme={theme}
278328
tenantName={tenantName}
279329
isResultsCollapsed={resultVisibilityState.collapsed}
330+
isCancelError={Boolean(cancelQueryResponse?.error)}
331+
isCancelling={Boolean(cancelQueryResponse?.isLoading)}
332+
tableSettings={tableSettings}
280333
onExpandResults={onExpandResultHandler}
281334
onCollapseResults={onCollapseResultHandler}
282335
queryText={queryText}
336+
onCancelRunningQuery={onCancelRunningQuery}
283337
/>
284338
);
285339
}

0 commit comments

Comments
 (0)