diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QuerySection.styles.scss b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QuerySection.styles.scss
index b7cd213ae1..0b59cf4bf5 100644
--- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QuerySection.styles.scss
+++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QuerySection.styles.scss
@@ -1,4 +1,7 @@
.dashboard-navigation {
+ .run-query-dashboard-btn {
+ min-width: 180px;
+ }
.ant-tabs-tab {
border: none !important;
margin-left: 0px !important;
diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx
index 5e9bc20e98..fa07efb43e 100644
--- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx
+++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx
@@ -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={
-
+
}
items={items}
@@ -248,6 +258,8 @@ function QuerySection({ selectedGraph }: QueryProps): JSX.Element {
interface QueryProps {
selectedGraph: PANEL_TYPES;
+ queryRangeKey?: QueryKey;
+ isLoadingQueries?: boolean;
}
export default QuerySection;
diff --git a/frontend/src/container/NewWidget/LeftContainer/index.tsx b/frontend/src/container/NewWidget/LeftContainer/index.tsx
index dacc3b903c..c439e75441 100644
--- a/frontend/src/container/NewWidget/LeftContainer/index.tsx
+++ b/frontend/src/container/NewWidget/LeftContainer/index.tsx
@@ -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}
/>
-
+
{selectedGraph === PANEL_TYPES.LIST && (
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 ? (
}
- className="cancel-query-btn periscope-btn danger"
- onClick={handleCancelQuery}
+ className={cx('cancel-query-btn periscope-btn danger', className)}
+ onClick={onCancel}
>
Cancel
) : (
}
>
diff --git a/frontend/src/container/QueryBuilder/components/RunQueryBtn/__test__/RunQueryBtn.test.tsx b/frontend/src/container/QueryBuilder/components/RunQueryBtn/__test__/RunQueryBtn.test.tsx
index 1f6bf38443..532d291535 100644
--- a/frontend/src/container/QueryBuilder/components/RunQueryBtn/__test__/RunQueryBtn.test.tsx
+++ b/frontend/src/container/QueryBuilder/components/RunQueryBtn/__test__/RunQueryBtn.test.tsx
@@ -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();
+ // 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();
+
+ 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();
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();
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();
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();
+
+ // 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(
{}} />,
);
@@ -70,9 +121,6 @@ describe('RunQueryBtn', () => {
});
test('renders custom label when provided', () => {
- (getUserOperatingSystem as jest.Mock).mockReturnValue(
- UserOperatingSystem.MACOS,
- );
const onRun = jest.fn();
render();
expect(