Skip to content

Commit 3cf1d6d

Browse files
committed
feat: stop running query
1 parent 68c3972 commit 3cf1d6d

File tree

24 files changed

+600
-143
lines changed

24 files changed

+600
-143
lines changed

package-lock.json

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

package.json

+5-3
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
"@gravity-ui/navigation": "^2.16.0",
2222
"@gravity-ui/paranoid": "^2.0.1",
2323
"@gravity-ui/react-data-table": "^2.1.1",
24+
"@gravity-ui/table": "^0.5.0",
2425
"@gravity-ui/uikit": "^6.20.1",
2526
"@gravity-ui/websql-autocomplete": "^9.1.0",
2627
"@reduxjs/toolkit": "^2.2.3",
28+
"@tanstack/react-table": "^8.19.3",
2729
"axios": "^1.7.2",
2830
"axios-retry": "^4.4.0",
2931
"colord": "^2.9.3",
@@ -50,11 +52,10 @@
5052
"tslib": "^2.6.3",
5153
"url": "^0.11.3",
5254
"use-query-params": "^2.2.1",
55+
"uuid": "^10.0.0",
5356
"web-vitals": "^1.1.2",
5457
"ydb-ui-components": "^4.2.0",
55-
"zod": "^3.23.8",
56-
"@gravity-ui/table": "^0.5.0",
57-
"@tanstack/react-table": "^8.19.3"
58+
"zod": "^3.23.8"
5859
},
5960
"scripts": {
6061
"analyze": "source-map-explorer 'build/static/js/*.js'",
@@ -144,6 +145,7 @@
144145
"@types/react-dom": "^18.3.0",
145146
"@types/react-router": "^5.1.20",
146147
"@types/react-router-dom": "^5.3.3",
148+
"@types/uuid": "^10.0.0",
147149
"copyfiles": "^2.4.1",
148150
"http-proxy-middleware": "^2.0.6",
149151
"husky": "^9.0.11",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.ydb-query-elapsed-time {
2+
visibility: visible;
3+
}
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
3+
import {duration} from '@gravity-ui/date-utils';
4+
import {Label} from '@gravity-ui/uikit';
5+
6+
import {HOUR_IN_SECONDS, SECOND_IN_MS, cn} from '../../lib';
7+
8+
const b = cn('ydb-query-elapsed-time');
9+
10+
interface ElapsedTimeProps {
11+
className?: string;
12+
}
13+
14+
export default function ElapsedTime({className}: ElapsedTimeProps) {
15+
const [, reRender] = React.useState({});
16+
const [startTime] = React.useState(Date.now());
17+
const elapsedTime = Date.now() - startTime;
18+
19+
React.useEffect(() => {
20+
const timerId = setInterval(() => {
21+
reRender({});
22+
}, SECOND_IN_MS);
23+
return () => {
24+
clearInterval(timerId);
25+
};
26+
}, []);
27+
28+
const elapsedTimeFormatted =
29+
elapsedTime > HOUR_IN_SECONDS * SECOND_IN_MS
30+
? duration(elapsedTime).format('hh:mm:ss')
31+
: duration(elapsedTime).format('mm:ss');
32+
33+
return <Label className={b(null, className)}>{elapsedTimeFormatted}</Label>;
34+
}

src/components/QueryExecutionStatus/QueryExecutionStatus.tsx

+19-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import React from 'react';
22

3-
import {CircleCheck, CircleInfo, CircleQuestionFill, CircleXmark} from '@gravity-ui/icons';
4-
import {Icon, Tooltip} from '@gravity-ui/uikit';
3+
import {
4+
CircleCheck,
5+
CircleInfo,
6+
CircleQuestionFill,
7+
CircleStop,
8+
CircleXmark,
9+
} from '@gravity-ui/icons';
10+
import {Icon, Spin, Tooltip} from '@gravity-ui/uikit';
511
import {isAxiosError} from 'axios';
612

713
import i18n from '../../containers/Tenant/Query/i18n';
14+
import {isQueryCancelledError} from '../../containers/Tenant/Query/utils/isQueryCancelledError';
815
import {cn} from '../../utils/cn';
916
import {useChangedQuerySettings} from '../../utils/hooks/useChangedQuerySettings';
1017
import QuerySettingsDescription from '../QuerySettingsDescription/QuerySettingsDescription';
@@ -16,6 +23,7 @@ const b = cn('kv-query-execution-status');
1623
interface QueryExecutionStatusProps {
1724
className?: string;
1825
error?: unknown;
26+
loading?: boolean;
1927
}
2028

2129
const QuerySettingsIndicator = () => {
@@ -40,13 +48,19 @@ const QuerySettingsIndicator = () => {
4048
);
4149
};
4250

43-
export const QueryExecutionStatus = ({className, error}: QueryExecutionStatusProps) => {
51+
export const QueryExecutionStatus = ({className, error, loading}: QueryExecutionStatusProps) => {
4452
let icon: React.ReactNode;
4553
let label: string;
4654

47-
if (isAxiosError(error) && error.code === 'ECONNABORTED') {
55+
if (loading) {
56+
icon = <Spin size="xs" />;
57+
label = 'Running';
58+
} else if (isAxiosError(error) && error.code === 'ECONNABORTED') {
4859
icon = <Icon data={CircleQuestionFill} />;
4960
label = 'Connection aborted';
61+
} else if (isQueryCancelledError(error)) {
62+
icon = <Icon data={CircleStop} />;
63+
label = 'Stopped';
5064
} else {
5165
const hasError = Boolean(error);
5266
icon = (
@@ -62,7 +76,7 @@ export const QueryExecutionStatus = ({className, error}: QueryExecutionStatusPro
6276
<div className={b(null, className)}>
6377
{icon}
6478
{label}
65-
<QuerySettingsIndicator />
79+
{isQueryCancelledError(error) ? null : <QuerySettingsIndicator />}
6680
</div>
6781
);
6882
};

src/containers/Tenant/Query/ExecuteResult/ExecuteResult.scss

+13
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,17 @@
4848

4949
&__controls-right {
5050
display: flex;
51+
align-items: center;
5152
gap: 12px;
5253

5354
height: 100%;
5455
}
56+
5557
&__controls-left {
5658
display: flex;
5759
gap: 4px;
5860
}
61+
5962
&__inspector {
6063
overflow: auto;
6164

@@ -71,4 +74,14 @@
7174
width: 100%;
7275
height: 100%;
7376
}
77+
78+
&__elapsed-label {
79+
margin-left: var(--g-spacing-3);
80+
}
81+
82+
&__stop-button {
83+
&_error {
84+
@include query-buttons-animations();
85+
}
86+
}
7487
}

src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx

+51-17
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import React from 'react';
22

3+
import {StopFill} from '@gravity-ui/icons';
34
import type {ControlGroupOption} from '@gravity-ui/uikit';
4-
import {RadioButton, Tabs} from '@gravity-ui/uikit';
5+
import {Button, Icon, RadioButton, Tabs} from '@gravity-ui/uikit';
56
import JSONTree from 'react-json-inspector';
67

78
import {ClipboardButton} from '../../../../components/ClipboardButton';
89
import Divider from '../../../../components/Divider/Divider';
10+
import ElapsedTime from '../../../../components/ElapsedTime/ElapsedTime';
911
import EnableFullscreenButton from '../../../../components/EnableFullscreenButton/EnableFullscreenButton';
1012
import Fullscreen from '../../../../components/Fullscreen/Fullscreen';
1113
import {YDBGraph} from '../../../../components/Graph/Graph';
14+
import {LoaderWrapper} from '../../../../components/LoaderWrapper/LoaderWrapper';
1215
import {QueryExecutionStatus} from '../../../../components/QueryExecutionStatus';
1316
import {QueryResultTable} from '../../../../components/QueryResultTable/QueryResultTable';
1417
import {disableFullscreen} from '../../../../store/reducers/fullscreen';
15-
import type {ColumnType, KeyValueRow} from '../../../../types/api/query';
18+
import type {ColumnType, KeyValueRow, TKqpStatsQuery} from '../../../../types/api/query';
1619
import type {ValueOf} from '../../../../types/common';
1720
import type {IQueryResult} from '../../../../types/store/query';
1821
import {getArray} from '../../../../utils';
@@ -26,6 +29,7 @@ import {ResultIssues} from '../Issues/Issues';
2629
import {QueryDuration} from '../QueryDuration/QueryDuration';
2730
import {QuerySettingsBanner} from '../QuerySettingsBanner/QuerySettingsBanner';
2831
import {getPreparedResult} from '../utils/getPreparedResult';
32+
import {isQueryCancelledError} from '../utils/isQueryCancelledError';
2933

3034
import i18n from './i18n';
3135
import {getPlan} from './utils';
@@ -46,25 +50,33 @@ type SectionID = ValueOf<typeof resultOptionsIds>;
4650
interface ExecuteResultProps {
4751
data: IQueryResult | undefined;
4852
error: unknown;
53+
cancelError: unknown;
4954
isResultsCollapsed?: boolean;
5055
onCollapseResults: VoidFunction;
5156
onExpandResults: VoidFunction;
57+
onStopButtonClick: VoidFunction;
5258
theme?: string;
59+
loading?: boolean;
60+
cancelQueryLoading?: boolean;
5361
}
5462

5563
export function ExecuteResult({
5664
data,
5765
error,
66+
cancelError,
5867
isResultsCollapsed,
5968
onCollapseResults,
6069
onExpandResults,
70+
onStopButtonClick,
6171
theme,
72+
loading,
73+
cancelQueryLoading,
6274
}: ExecuteResultProps) {
6375
const [selectedResultSet, setSelectedResultSet] = React.useState(0);
6476
const [activeSection, setActiveSection] = React.useState<SectionID>(resultOptionsIds.result);
6577
const dispatch = useTypedDispatch();
6678

67-
const stats = data?.stats;
79+
const stats: TKqpStatsQuery | undefined = data?.stats;
6880
const resultsSetsCount = data?.resultSets?.length;
6981
const isMulti = resultsSetsCount && resultsSetsCount > 0;
7082
const currentResult = isMulti ? data?.resultSets?.[selectedResultSet].result : data?.result;
@@ -93,8 +105,8 @@ export function ExecuteResult({
93105
};
94106
}, [dispatch]);
95107

96-
const onSelectSection = (value: string) => {
97-
setActiveSection(value as SectionID);
108+
const onSelectSection = (value: SectionID) => {
109+
setActiveSection(value);
98110
};
99111

100112
const renderResultTable = (
@@ -207,7 +219,7 @@ export function ExecuteResult({
207219
};
208220

209221
const renderResultSection = () => {
210-
if (error) {
222+
if (error && !isQueryCancelledError(error)) {
211223
return renderIssues();
212224
}
213225
if (activeSection === resultOptionsIds.result) {
@@ -230,18 +242,38 @@ export function ExecuteResult({
230242
<React.Fragment>
231243
<div className={b('controls')}>
232244
<div className={b('controls-right')}>
233-
<QueryExecutionStatus error={error} />
234-
{stats && !error && (
245+
<QueryExecutionStatus error={error} loading={loading} />
246+
247+
{!error && !loading && (
235248
<React.Fragment>
236-
<QueryDuration duration={stats?.DurationUs} />
237-
<Divider />
238-
<RadioButton
239-
options={resultOptions}
240-
value={activeSection}
241-
onUpdate={onSelectSection}
242-
/>
249+
{stats?.DurationUs !== undefined && (
250+
<QueryDuration duration={Number(stats.DurationUs)} />
251+
)}
252+
{resultOptions && activeSection && (
253+
<React.Fragment>
254+
<Divider />
255+
<RadioButton
256+
options={resultOptions}
257+
value={activeSection}
258+
onUpdate={onSelectSection}
259+
/>
260+
</React.Fragment>
261+
)}
243262
</React.Fragment>
244263
)}
264+
{loading ? (
265+
<React.Fragment>
266+
<ElapsedTime className={b('elapsed-time')} />
267+
<Button
268+
loading={cancelQueryLoading}
269+
onClick={onStopButtonClick}
270+
className={b('stop-button', {error: Boolean(cancelError)})}
271+
>
272+
<Icon data={StopFill} size={16} />
273+
{i18n('action.stop')}
274+
</Button>
275+
</React.Fragment>
276+
) : null}
245277
</div>
246278
<div className={b('controls-left')}>
247279
{renderClipboardButton()}
@@ -254,8 +286,10 @@ export function ExecuteResult({
254286
/>
255287
</div>
256288
</div>
257-
<QuerySettingsBanner />
258-
<Fullscreen>{renderResultSection()}</Fullscreen>
289+
{loading || isQueryCancelledError(error) ? null : <QuerySettingsBanner />}
290+
<LoaderWrapper loading={loading}>
291+
<Fullscreen>{renderResultSection()}</Fullscreen>
292+
</LoaderWrapper>
259293
</React.Fragment>
260294
);
261295
}

src/containers/Tenant/Query/ExecuteResult/i18n/en.json

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"action.result": "Result",
44
"action.stats": "Stats",
55
"action.schema": "Schema",
6+
"action.stop": "Stop",
67
"action.explain-plan": "Explain Plan",
78
"action.copy": "Copy {{activeSection}}"
89
}

0 commit comments

Comments
 (0)