Add cancel query functionality to dashboard edit panel (#10152)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled

* feat: add cancel query functionality for dashboard panels

* feat: pass loading query from edit panel

* feat: revert loading
This commit is contained in:
Aditya Singh
2026-02-02 20:32:42 +05:30
committed by GitHub
parent 518dfcbe59
commit 30a6721472
5 changed files with 124 additions and 27 deletions

View File

@@ -1,4 +1,7 @@
.dashboard-navigation {
.run-query-dashboard-btn {
min-width: 180px;
}
.ant-tabs-tab {
border: none !important;
margin-left: 0px !important;

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo } from 'react';
import { QueryKey } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import { Button, Tabs, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
@@ -35,8 +36,11 @@ import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
import PromQLQueryContainer from './QueryBuilder/promQL';
import './QuerySection.styles.scss';
function QuerySection({ selectedGraph }: QueryProps): JSX.Element {
function QuerySection({
selectedGraph,
queryRangeKey,
isLoadingQueries,
}: QueryProps): JSX.Element {
const {
currentQuery,
handleRunQuery: handleRunQueryFromQueryBuilder,
@@ -237,7 +241,13 @@ function QuerySection({ selectedGraph }: QueryProps): JSX.Element {
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
<RunQueryBtn label="Stage & Run Query" onStageRunQuery={handleRunQuery} />
<RunQueryBtn
className="run-query-dashboard-btn"
label="Stage & Run Query"
onStageRunQuery={handleRunQuery}
isLoadingQueries={isLoadingQueries}
queryRangeKey={queryRangeKey}
/>
</span>
}
items={items}
@@ -248,6 +258,8 @@ function QuerySection({ selectedGraph }: QueryProps): JSX.Element {
interface QueryProps {
selectedGraph: PANEL_TYPES;
queryRangeKey?: QueryKey;
isLoadingQueries?: boolean;
}
export default QuerySection;

View File

@@ -1,4 +1,5 @@
import { memo, useEffect } from 'react';
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -24,8 +25,8 @@ function LeftContainer({
setSelectedTracesFields,
selectedWidget,
requestData,
setRequestData,
isLoadingPanelData,
setRequestData,
setQueryResponse,
enableDrillDown = false,
}: WidgetGraphProps): JSX.Element {
@@ -35,15 +36,20 @@ function LeftContainer({
AppState,
GlobalReducer
>((state) => state.globalTime);
const queryResponse = useGetQueryRange(requestData, ENTITY_VERSION_V5, {
enabled: !!stagedQuery,
queryKey: [
const queryRangeKey = useMemo(
() => [
REACT_QUERY_KEY.GET_QUERY_RANGE,
globalSelectedInterval,
requestData,
minTime,
maxTime,
],
[globalSelectedInterval, requestData, minTime, maxTime],
);
const queryResponse = useGetQueryRange(requestData, ENTITY_VERSION_V5, {
enabled: !!stagedQuery,
queryKey: queryRangeKey,
keepPreviousData: true,
});
// Update parent component with query response for legend colors
@@ -64,7 +70,11 @@ function LeftContainer({
enableDrillDown={enableDrillDown}
/>
<QueryContainer className="query-section-left-container">
<QuerySection selectedGraph={selectedGraph} />
<QuerySection
selectedGraph={selectedGraph}
queryRangeKey={queryRangeKey}
isLoadingQueries={queryResponse.isFetching}
/>
{selectedGraph === PANEL_TYPES.LIST && (
<ExplorerColumnsRenderer
selectedLogFields={selectedLogFields}

View File

@@ -1,4 +1,7 @@
import { useCallback } from 'react';
import { QueryKey, useIsFetching, useQueryClient } from 'react-query';
import { Button } from 'antd';
import cx from 'classnames';
import {
ChevronUp,
Command,
@@ -9,35 +12,56 @@ import {
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
import './RunQueryBtn.scss';
interface RunQueryBtnProps {
className?: string;
label?: string;
isLoadingQueries?: boolean;
handleCancelQuery?: () => void;
onStageRunQuery?: () => void;
queryRangeKey?: QueryKey;
}
function RunQueryBtn({
className,
label,
isLoadingQueries,
handleCancelQuery,
onStageRunQuery,
queryRangeKey,
}: RunQueryBtnProps): JSX.Element {
const isMac = getUserOperatingSystem() === UserOperatingSystem.MACOS;
return isLoadingQueries ? (
const queryClient = useQueryClient();
const isKeyFetchingCount = useIsFetching(
queryRangeKey as QueryKey | undefined,
);
const isLoading =
typeof isLoadingQueries === 'boolean'
? isLoadingQueries
: isKeyFetchingCount > 0;
const onCancel = useCallback(() => {
if (handleCancelQuery) {
return handleCancelQuery();
}
if (queryRangeKey) {
queryClient.cancelQueries(queryRangeKey);
}
}, [handleCancelQuery, queryClient, queryRangeKey]);
return isLoading ? (
<Button
type="default"
icon={<Loader2 size={14} className="loading-icon animate-spin" />}
className="cancel-query-btn periscope-btn danger"
onClick={handleCancelQuery}
className={cx('cancel-query-btn periscope-btn danger', className)}
onClick={onCancel}
>
Cancel
</Button>
) : (
<Button
type="primary"
className="run-query-btn periscope-btn primary"
disabled={isLoadingQueries || !onStageRunQuery}
className={cx('run-query-btn periscope-btn primary', className)}
disabled={isLoading || !onStageRunQuery}
onClick={onStageRunQuery}
icon={<Play size={14} />}
>

View File

@@ -3,6 +3,16 @@ import { fireEvent, render, screen } from '@testing-library/react';
import RunQueryBtn from '../RunQueryBtn';
jest.mock('react-query', () => {
const actual = jest.requireActual('react-query');
return {
...actual,
useIsFetching: jest.fn(),
useQueryClient: jest.fn(),
};
});
import { useIsFetching, useQueryClient } from 'react-query';
// Mock OS util
jest.mock('utils/getUserOS', () => ({
getUserOperatingSystem: jest.fn(),
@@ -11,10 +21,43 @@ jest.mock('utils/getUserOS', () => ({
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
describe('RunQueryBtn', () => {
test('renders run state and triggers on click', () => {
beforeEach(() => {
jest.resetAllMocks();
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
(useIsFetching as jest.Mock).mockReturnValue(0);
(useQueryClient as jest.Mock).mockReturnValue({
cancelQueries: jest.fn(),
});
});
test('uses isLoadingQueries prop over useIsFetching', () => {
// Simulate fetching but prop forces not loading
(useIsFetching as jest.Mock).mockReturnValue(1);
const onRun = jest.fn();
render(<RunQueryBtn onStageRunQuery={onRun} isLoadingQueries={false} />);
// Should show "Run Query" (not cancel)
const runBtn = screen.getByRole('button', { name: /run query/i });
expect(runBtn).toBeInTheDocument();
expect(runBtn).toBeEnabled();
});
test('fallback cancel: uses handleCancelQuery when no key provided', () => {
(useIsFetching as jest.Mock).mockReturnValue(0);
const cancelQueries = jest.fn();
(useQueryClient as jest.Mock).mockReturnValue({ cancelQueries });
const onCancel = jest.fn();
render(<RunQueryBtn isLoadingQueries handleCancelQuery={onCancel} />);
const cancelBtn = screen.getByRole('button', { name: /cancel/i });
fireEvent.click(cancelBtn);
expect(onCancel).toHaveBeenCalledTimes(1);
expect(cancelQueries).not.toHaveBeenCalled();
});
test('renders run state and triggers on click', () => {
const onRun = jest.fn();
render(<RunQueryBtn onStageRunQuery={onRun} />);
const btn = screen.getByRole('button', { name: /run query/i });
@@ -24,17 +67,11 @@ describe('RunQueryBtn', () => {
});
test('disabled when onStageRunQuery is undefined', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
render(<RunQueryBtn />);
expect(screen.getByRole('button', { name: /run query/i })).toBeDisabled();
});
test('shows cancel state and calls handleCancelQuery', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
const onCancel = jest.fn();
render(<RunQueryBtn isLoadingQueries handleCancelQuery={onCancel} />);
const cancel = screen.getByRole('button', { name: /cancel/i });
@@ -42,10 +79,24 @@ describe('RunQueryBtn', () => {
expect(onCancel).toHaveBeenCalledTimes(1);
});
test('derives loading from queryKey via useIsFetching and cancels via queryClient', () => {
(useIsFetching as jest.Mock).mockReturnValue(1);
const cancelQueries = jest.fn();
(useQueryClient as jest.Mock).mockReturnValue({ cancelQueries });
const queryKey = ['GET_QUERY_RANGE', '1h', { some: 'req' }, 1, 2];
render(<RunQueryBtn queryRangeKey={queryKey} />);
// Button switches to cancel state
const cancelBtn = screen.getByRole('button', { name: /cancel/i });
expect(cancelBtn).toBeInTheDocument();
// Clicking cancel calls cancelQueries with the key
fireEvent.click(cancelBtn);
expect(cancelQueries).toHaveBeenCalledWith(queryKey);
});
test('shows Command + CornerDownLeft on mac', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
const { container } = render(
<RunQueryBtn onStageRunQuery={(): void => {}} />,
);
@@ -70,9 +121,6 @@ describe('RunQueryBtn', () => {
});
test('renders custom label when provided', () => {
(getUserOperatingSystem as jest.Mock).mockReturnValue(
UserOperatingSystem.MACOS,
);
const onRun = jest.fn();
render(<RunQueryBtn onStageRunQuery={onRun} label="Stage & Run Query" />);
expect(