Skip to content

Commit

Permalink
feat: stop running query
Browse files Browse the repository at this point in the history
  • Loading branch information
astandrik committed Aug 7, 2024
1 parent 68c3972 commit 3cf1d6d
Show file tree
Hide file tree
Showing 24 changed files with 600 additions and 143 deletions.
22 changes: 22 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
"@gravity-ui/navigation": "^2.16.0",
"@gravity-ui/paranoid": "^2.0.1",
"@gravity-ui/react-data-table": "^2.1.1",
"@gravity-ui/table": "^0.5.0",
"@gravity-ui/uikit": "^6.20.1",
"@gravity-ui/websql-autocomplete": "^9.1.0",
"@reduxjs/toolkit": "^2.2.3",
"@tanstack/react-table": "^8.19.3",
"axios": "^1.7.2",
"axios-retry": "^4.4.0",
"colord": "^2.9.3",
Expand All @@ -50,11 +52,10 @@
"tslib": "^2.6.3",
"url": "^0.11.3",
"use-query-params": "^2.2.1",
"uuid": "^10.0.0",
"web-vitals": "^1.1.2",
"ydb-ui-components": "^4.2.0",
"zod": "^3.23.8",
"@gravity-ui/table": "^0.5.0",
"@tanstack/react-table": "^8.19.3"
"zod": "^3.23.8"
},
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
Expand Down Expand Up @@ -144,6 +145,7 @@
"@types/react-dom": "^18.3.0",
"@types/react-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3",
"@types/uuid": "^10.0.0",
"copyfiles": "^2.4.1",
"http-proxy-middleware": "^2.0.6",
"husky": "^9.0.11",
Expand Down
3 changes: 3 additions & 0 deletions src/components/ElapsedTime/ElapsedTime.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.ydb-query-elapsed-time {
visibility: visible;
}
34 changes: 34 additions & 0 deletions src/components/ElapsedTime/ElapsedTime.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';

import {duration} from '@gravity-ui/date-utils';
import {Label} from '@gravity-ui/uikit';

import {HOUR_IN_SECONDS, SECOND_IN_MS, cn} from '../../lib';

const b = cn('ydb-query-elapsed-time');

interface ElapsedTimeProps {
className?: string;
}

export default function ElapsedTime({className}: ElapsedTimeProps) {
const [, reRender] = React.useState({});
const [startTime] = React.useState(Date.now());
const elapsedTime = Date.now() - startTime;

React.useEffect(() => {
const timerId = setInterval(() => {
reRender({});
}, SECOND_IN_MS);
return () => {
clearInterval(timerId);
};
}, []);

const elapsedTimeFormatted =
elapsedTime > HOUR_IN_SECONDS * SECOND_IN_MS
? duration(elapsedTime).format('hh:mm:ss')
: duration(elapsedTime).format('mm:ss');

return <Label className={b(null, className)}>{elapsedTimeFormatted}</Label>;
}
24 changes: 19 additions & 5 deletions src/components/QueryExecutionStatus/QueryExecutionStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import React from 'react';

import {CircleCheck, CircleInfo, CircleQuestionFill, CircleXmark} from '@gravity-ui/icons';
import {Icon, Tooltip} from '@gravity-ui/uikit';
import {
CircleCheck,
CircleInfo,
CircleQuestionFill,
CircleStop,
CircleXmark,
} from '@gravity-ui/icons';
import {Icon, Spin, Tooltip} from '@gravity-ui/uikit';
import {isAxiosError} from 'axios';

import i18n from '../../containers/Tenant/Query/i18n';
import {isQueryCancelledError} from '../../containers/Tenant/Query/utils/isQueryCancelledError';
import {cn} from '../../utils/cn';
import {useChangedQuerySettings} from '../../utils/hooks/useChangedQuerySettings';
import QuerySettingsDescription from '../QuerySettingsDescription/QuerySettingsDescription';
Expand All @@ -16,6 +23,7 @@ const b = cn('kv-query-execution-status');
interface QueryExecutionStatusProps {
className?: string;
error?: unknown;
loading?: boolean;
}

const QuerySettingsIndicator = () => {
Expand All @@ -40,13 +48,19 @@ const QuerySettingsIndicator = () => {
);
};

export const QueryExecutionStatus = ({className, error}: QueryExecutionStatusProps) => {
export const QueryExecutionStatus = ({className, error, loading}: QueryExecutionStatusProps) => {
let icon: React.ReactNode;
let label: string;

if (isAxiosError(error) && error.code === 'ECONNABORTED') {
if (loading) {
icon = <Spin size="xs" />;
label = 'Running';
} else if (isAxiosError(error) && error.code === 'ECONNABORTED') {
icon = <Icon data={CircleQuestionFill} />;
label = 'Connection aborted';
} else if (isQueryCancelledError(error)) {
icon = <Icon data={CircleStop} />;
label = 'Stopped';
} else {
const hasError = Boolean(error);
icon = (
Expand All @@ -62,7 +76,7 @@ export const QueryExecutionStatus = ({className, error}: QueryExecutionStatusPro
<div className={b(null, className)}>
{icon}
{label}
<QuerySettingsIndicator />
{isQueryCancelledError(error) ? null : <QuerySettingsIndicator />}
</div>
);
};
13 changes: 13 additions & 0 deletions src/containers/Tenant/Query/ExecuteResult/ExecuteResult.scss
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,17 @@

&__controls-right {
display: flex;
align-items: center;
gap: 12px;

height: 100%;
}

&__controls-left {
display: flex;
gap: 4px;
}

&__inspector {
overflow: auto;

Expand All @@ -71,4 +74,14 @@
width: 100%;
height: 100%;
}

&__elapsed-label {
margin-left: var(--g-spacing-3);
}

&__stop-button {
&_error {
@include query-buttons-animations();
}
}
}
68 changes: 51 additions & 17 deletions src/containers/Tenant/Query/ExecuteResult/ExecuteResult.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import React from 'react';

import {StopFill} from '@gravity-ui/icons';
import type {ControlGroupOption} from '@gravity-ui/uikit';
import {RadioButton, Tabs} from '@gravity-ui/uikit';
import {Button, Icon, RadioButton, Tabs} from '@gravity-ui/uikit';
import JSONTree from 'react-json-inspector';

import {ClipboardButton} from '../../../../components/ClipboardButton';
import Divider from '../../../../components/Divider/Divider';
import ElapsedTime from '../../../../components/ElapsedTime/ElapsedTime';
import EnableFullscreenButton from '../../../../components/EnableFullscreenButton/EnableFullscreenButton';
import Fullscreen from '../../../../components/Fullscreen/Fullscreen';
import {YDBGraph} from '../../../../components/Graph/Graph';
import {LoaderWrapper} from '../../../../components/LoaderWrapper/LoaderWrapper';
import {QueryExecutionStatus} from '../../../../components/QueryExecutionStatus';
import {QueryResultTable} from '../../../../components/QueryResultTable/QueryResultTable';
import {disableFullscreen} from '../../../../store/reducers/fullscreen';
import type {ColumnType, KeyValueRow} from '../../../../types/api/query';
import type {ColumnType, KeyValueRow, TKqpStatsQuery} from '../../../../types/api/query';
import type {ValueOf} from '../../../../types/common';
import type {IQueryResult} from '../../../../types/store/query';
import {getArray} from '../../../../utils';
Expand All @@ -26,6 +29,7 @@ import {ResultIssues} from '../Issues/Issues';
import {QueryDuration} from '../QueryDuration/QueryDuration';
import {QuerySettingsBanner} from '../QuerySettingsBanner/QuerySettingsBanner';
import {getPreparedResult} from '../utils/getPreparedResult';
import {isQueryCancelledError} from '../utils/isQueryCancelledError';

import i18n from './i18n';
import {getPlan} from './utils';
Expand All @@ -46,25 +50,33 @@ type SectionID = ValueOf<typeof resultOptionsIds>;
interface ExecuteResultProps {
data: IQueryResult | undefined;
error: unknown;
cancelError: unknown;
isResultsCollapsed?: boolean;
onCollapseResults: VoidFunction;
onExpandResults: VoidFunction;
onStopButtonClick: VoidFunction;
theme?: string;
loading?: boolean;
cancelQueryLoading?: boolean;
}

export function ExecuteResult({
data,
error,
cancelError,
isResultsCollapsed,
onCollapseResults,
onExpandResults,
onStopButtonClick,
theme,
loading,
cancelQueryLoading,
}: ExecuteResultProps) {
const [selectedResultSet, setSelectedResultSet] = React.useState(0);
const [activeSection, setActiveSection] = React.useState<SectionID>(resultOptionsIds.result);
const dispatch = useTypedDispatch();

const stats = data?.stats;
const stats: TKqpStatsQuery | undefined = data?.stats;
const resultsSetsCount = data?.resultSets?.length;
const isMulti = resultsSetsCount && resultsSetsCount > 0;
const currentResult = isMulti ? data?.resultSets?.[selectedResultSet].result : data?.result;
Expand Down Expand Up @@ -93,8 +105,8 @@ export function ExecuteResult({
};
}, [dispatch]);

const onSelectSection = (value: string) => {
setActiveSection(value as SectionID);
const onSelectSection = (value: SectionID) => {
setActiveSection(value);
};

const renderResultTable = (
Expand Down Expand Up @@ -207,7 +219,7 @@ export function ExecuteResult({
};

const renderResultSection = () => {
if (error) {
if (error && !isQueryCancelledError(error)) {
return renderIssues();
}
if (activeSection === resultOptionsIds.result) {
Expand All @@ -230,18 +242,38 @@ export function ExecuteResult({
<React.Fragment>
<div className={b('controls')}>
<div className={b('controls-right')}>
<QueryExecutionStatus error={error} />
{stats && !error && (
<QueryExecutionStatus error={error} loading={loading} />

{!error && !loading && (
<React.Fragment>
<QueryDuration duration={stats?.DurationUs} />
<Divider />
<RadioButton
options={resultOptions}
value={activeSection}
onUpdate={onSelectSection}
/>
{stats?.DurationUs !== undefined && (
<QueryDuration duration={Number(stats.DurationUs)} />
)}
{resultOptions && activeSection && (
<React.Fragment>
<Divider />
<RadioButton
options={resultOptions}
value={activeSection}
onUpdate={onSelectSection}
/>
</React.Fragment>
)}
</React.Fragment>
)}
{loading ? (
<React.Fragment>
<ElapsedTime className={b('elapsed-time')} />
<Button
loading={cancelQueryLoading}
onClick={onStopButtonClick}
className={b('stop-button', {error: Boolean(cancelError)})}
>
<Icon data={StopFill} size={16} />
{i18n('action.stop')}
</Button>
</React.Fragment>
) : null}
</div>
<div className={b('controls-left')}>
{renderClipboardButton()}
Expand All @@ -254,8 +286,10 @@ export function ExecuteResult({
/>
</div>
</div>
<QuerySettingsBanner />
<Fullscreen>{renderResultSection()}</Fullscreen>
{loading || isQueryCancelledError(error) ? null : <QuerySettingsBanner />}
<LoaderWrapper loading={loading}>
<Fullscreen>{renderResultSection()}</Fullscreen>
</LoaderWrapper>
</React.Fragment>
);
}
1 change: 1 addition & 0 deletions src/containers/Tenant/Query/ExecuteResult/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"action.result": "Result",
"action.stats": "Stats",
"action.schema": "Schema",
"action.stop": "Stop",
"action.explain-plan": "Explain Plan",
"action.copy": "Copy {{activeSection}}"
}
Loading

0 comments on commit 3cf1d6d

Please sign in to comment.