mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-03 08:33:26 +00:00
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
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:
@@ -1,4 +1,7 @@
|
|||||||
.dashboard-navigation {
|
.dashboard-navigation {
|
||||||
|
.run-query-dashboard-btn {
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
.ant-tabs-tab {
|
.ant-tabs-tab {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
margin-left: 0px !important;
|
margin-left: 0px !important;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import { QueryKey } from 'react-query';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Button, Tabs, Typography } from 'antd';
|
import { Button, Tabs, Typography } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
@@ -35,8 +36,11 @@ import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
|
|||||||
import PromQLQueryContainer from './QueryBuilder/promQL';
|
import PromQLQueryContainer from './QueryBuilder/promQL';
|
||||||
|
|
||||||
import './QuerySection.styles.scss';
|
import './QuerySection.styles.scss';
|
||||||
|
function QuerySection({
|
||||||
function QuerySection({ selectedGraph }: QueryProps): JSX.Element {
|
selectedGraph,
|
||||||
|
queryRangeKey,
|
||||||
|
isLoadingQueries,
|
||||||
|
}: QueryProps): JSX.Element {
|
||||||
const {
|
const {
|
||||||
currentQuery,
|
currentQuery,
|
||||||
handleRunQuery: handleRunQueryFromQueryBuilder,
|
handleRunQuery: handleRunQueryFromQueryBuilder,
|
||||||
@@ -237,7 +241,13 @@ function QuerySection({ selectedGraph }: QueryProps): JSX.Element {
|
|||||||
tabBarExtraContent={
|
tabBarExtraContent={
|
||||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
<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" />
|
<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>
|
</span>
|
||||||
}
|
}
|
||||||
items={items}
|
items={items}
|
||||||
@@ -248,6 +258,8 @@ function QuerySection({ selectedGraph }: QueryProps): JSX.Element {
|
|||||||
|
|
||||||
interface QueryProps {
|
interface QueryProps {
|
||||||
selectedGraph: PANEL_TYPES;
|
selectedGraph: PANEL_TYPES;
|
||||||
|
queryRangeKey?: QueryKey;
|
||||||
|
isLoadingQueries?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default QuerySection;
|
export default QuerySection;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { memo, useEffect } from 'react';
|
import { memo, useEffect } from 'react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
@@ -24,8 +25,8 @@ function LeftContainer({
|
|||||||
setSelectedTracesFields,
|
setSelectedTracesFields,
|
||||||
selectedWidget,
|
selectedWidget,
|
||||||
requestData,
|
requestData,
|
||||||
setRequestData,
|
|
||||||
isLoadingPanelData,
|
isLoadingPanelData,
|
||||||
|
setRequestData,
|
||||||
setQueryResponse,
|
setQueryResponse,
|
||||||
enableDrillDown = false,
|
enableDrillDown = false,
|
||||||
}: WidgetGraphProps): JSX.Element {
|
}: WidgetGraphProps): JSX.Element {
|
||||||
@@ -35,15 +36,20 @@ function LeftContainer({
|
|||||||
AppState,
|
AppState,
|
||||||
GlobalReducer
|
GlobalReducer
|
||||||
>((state) => state.globalTime);
|
>((state) => state.globalTime);
|
||||||
const queryResponse = useGetQueryRange(requestData, ENTITY_VERSION_V5, {
|
const queryRangeKey = useMemo(
|
||||||
enabled: !!stagedQuery,
|
() => [
|
||||||
queryKey: [
|
|
||||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||||
globalSelectedInterval,
|
globalSelectedInterval,
|
||||||
requestData,
|
requestData,
|
||||||
minTime,
|
minTime,
|
||||||
maxTime,
|
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
|
// Update parent component with query response for legend colors
|
||||||
@@ -64,7 +70,11 @@ function LeftContainer({
|
|||||||
enableDrillDown={enableDrillDown}
|
enableDrillDown={enableDrillDown}
|
||||||
/>
|
/>
|
||||||
<QueryContainer className="query-section-left-container">
|
<QueryContainer className="query-section-left-container">
|
||||||
<QuerySection selectedGraph={selectedGraph} />
|
<QuerySection
|
||||||
|
selectedGraph={selectedGraph}
|
||||||
|
queryRangeKey={queryRangeKey}
|
||||||
|
isLoadingQueries={queryResponse.isFetching}
|
||||||
|
/>
|
||||||
{selectedGraph === PANEL_TYPES.LIST && (
|
{selectedGraph === PANEL_TYPES.LIST && (
|
||||||
<ExplorerColumnsRenderer
|
<ExplorerColumnsRenderer
|
||||||
selectedLogFields={selectedLogFields}
|
selectedLogFields={selectedLogFields}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { QueryKey, useIsFetching, useQueryClient } from 'react-query';
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
import {
|
import {
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Command,
|
Command,
|
||||||
@@ -9,35 +12,56 @@ import {
|
|||||||
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||||
|
|
||||||
import './RunQueryBtn.scss';
|
import './RunQueryBtn.scss';
|
||||||
|
|
||||||
interface RunQueryBtnProps {
|
interface RunQueryBtnProps {
|
||||||
|
className?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
isLoadingQueries?: boolean;
|
isLoadingQueries?: boolean;
|
||||||
handleCancelQuery?: () => void;
|
handleCancelQuery?: () => void;
|
||||||
onStageRunQuery?: () => void;
|
onStageRunQuery?: () => void;
|
||||||
|
queryRangeKey?: QueryKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
function RunQueryBtn({
|
function RunQueryBtn({
|
||||||
|
className,
|
||||||
label,
|
label,
|
||||||
isLoadingQueries,
|
isLoadingQueries,
|
||||||
handleCancelQuery,
|
handleCancelQuery,
|
||||||
onStageRunQuery,
|
onStageRunQuery,
|
||||||
|
queryRangeKey,
|
||||||
}: RunQueryBtnProps): JSX.Element {
|
}: RunQueryBtnProps): JSX.Element {
|
||||||
const isMac = getUserOperatingSystem() === UserOperatingSystem.MACOS;
|
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
|
<Button
|
||||||
type="default"
|
type="default"
|
||||||
icon={<Loader2 size={14} className="loading-icon animate-spin" />}
|
icon={<Loader2 size={14} className="loading-icon animate-spin" />}
|
||||||
className="cancel-query-btn periscope-btn danger"
|
className={cx('cancel-query-btn periscope-btn danger', className)}
|
||||||
onClick={handleCancelQuery}
|
onClick={onCancel}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
className="run-query-btn periscope-btn primary"
|
className={cx('run-query-btn periscope-btn primary', className)}
|
||||||
disabled={isLoadingQueries || !onStageRunQuery}
|
disabled={isLoading || !onStageRunQuery}
|
||||||
onClick={onStageRunQuery}
|
onClick={onStageRunQuery}
|
||||||
icon={<Play size={14} />}
|
icon={<Play size={14} />}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,6 +3,16 @@ import { fireEvent, render, screen } from '@testing-library/react';
|
|||||||
|
|
||||||
import RunQueryBtn from '../RunQueryBtn';
|
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
|
// Mock OS util
|
||||||
jest.mock('utils/getUserOS', () => ({
|
jest.mock('utils/getUserOS', () => ({
|
||||||
getUserOperatingSystem: jest.fn(),
|
getUserOperatingSystem: jest.fn(),
|
||||||
@@ -11,10 +21,43 @@ jest.mock('utils/getUserOS', () => ({
|
|||||||
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||||
|
|
||||||
describe('RunQueryBtn', () => {
|
describe('RunQueryBtn', () => {
|
||||||
test('renders run state and triggers on click', () => {
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
(getUserOperatingSystem as jest.Mock).mockReturnValue(
|
(getUserOperatingSystem as jest.Mock).mockReturnValue(
|
||||||
UserOperatingSystem.MACOS,
|
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();
|
const onRun = jest.fn();
|
||||||
render(<RunQueryBtn onStageRunQuery={onRun} />);
|
render(<RunQueryBtn onStageRunQuery={onRun} />);
|
||||||
const btn = screen.getByRole('button', { name: /run query/i });
|
const btn = screen.getByRole('button', { name: /run query/i });
|
||||||
@@ -24,17 +67,11 @@ describe('RunQueryBtn', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('disabled when onStageRunQuery is undefined', () => {
|
test('disabled when onStageRunQuery is undefined', () => {
|
||||||
(getUserOperatingSystem as jest.Mock).mockReturnValue(
|
|
||||||
UserOperatingSystem.MACOS,
|
|
||||||
);
|
|
||||||
render(<RunQueryBtn />);
|
render(<RunQueryBtn />);
|
||||||
expect(screen.getByRole('button', { name: /run query/i })).toBeDisabled();
|
expect(screen.getByRole('button', { name: /run query/i })).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shows cancel state and calls handleCancelQuery', () => {
|
test('shows cancel state and calls handleCancelQuery', () => {
|
||||||
(getUserOperatingSystem as jest.Mock).mockReturnValue(
|
|
||||||
UserOperatingSystem.MACOS,
|
|
||||||
);
|
|
||||||
const onCancel = jest.fn();
|
const onCancel = jest.fn();
|
||||||
render(<RunQueryBtn isLoadingQueries handleCancelQuery={onCancel} />);
|
render(<RunQueryBtn isLoadingQueries handleCancelQuery={onCancel} />);
|
||||||
const cancel = screen.getByRole('button', { name: /cancel/i });
|
const cancel = screen.getByRole('button', { name: /cancel/i });
|
||||||
@@ -42,10 +79,24 @@ describe('RunQueryBtn', () => {
|
|||||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
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', () => {
|
test('shows Command + CornerDownLeft on mac', () => {
|
||||||
(getUserOperatingSystem as jest.Mock).mockReturnValue(
|
|
||||||
UserOperatingSystem.MACOS,
|
|
||||||
);
|
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<RunQueryBtn onStageRunQuery={(): void => {}} />,
|
<RunQueryBtn onStageRunQuery={(): void => {}} />,
|
||||||
);
|
);
|
||||||
@@ -70,9 +121,6 @@ describe('RunQueryBtn', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('renders custom label when provided', () => {
|
test('renders custom label when provided', () => {
|
||||||
(getUserOperatingSystem as jest.Mock).mockReturnValue(
|
|
||||||
UserOperatingSystem.MACOS,
|
|
||||||
);
|
|
||||||
const onRun = jest.fn();
|
const onRun = jest.fn();
|
||||||
render(<RunQueryBtn onStageRunQuery={onRun} label="Stage & Run Query" />);
|
render(<RunQueryBtn onStageRunQuery={onRun} label="Stage & Run Query" />);
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
Reference in New Issue
Block a user