mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-28 07:00:29 +00:00
Compare commits
2 Commits
chore/refa
...
fix/metric
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c3869ec38 | ||
|
|
5939181c8d |
@@ -18,11 +18,12 @@ export const REACT_QUERY_KEY = {
|
||||
ALERT_RULE_TIMELINE_GRAPH: 'ALERT_RULE_TIMELINE_GRAPH',
|
||||
GET_CONSUMER_LAG_DETAILS: 'GET_CONSUMER_LAG_DETAILS',
|
||||
TOGGLE_ALERT_STATE: 'TOGGLE_ALERT_STATE',
|
||||
GET_ALL_ALLERTS: 'GET_ALL_ALLERTS',
|
||||
GET_ALL_ALERTS: 'GET_ALL_ALERTS',
|
||||
REMOVE_ALERT_RULE: 'REMOVE_ALERT_RULE',
|
||||
DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE',
|
||||
GET_HOST_LIST: 'GET_HOST_LIST',
|
||||
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
|
||||
ALERT_RULES_CHART_PREVIEW: 'ALERT_RULES_CHART_PREVIEW',
|
||||
GET_ACTIVE_LICENSE_V3: 'GET_ACTIVE_LICENSE_V3',
|
||||
GET_TRACE_V2_WATERFALL: 'GET_TRACE_V2_WATERFALL',
|
||||
GET_TRACE_V2_FLAMEGRAPH: 'GET_TRACE_V2_FLAMEGRAPH',
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useIsFetching, useQueryClient } from 'react-query';
|
||||
import { Button } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { YAxisSource } from 'components/YAxisUnitSelector/types';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import QuerySectionComponent from 'container/FormAlertRules/QuerySection';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getMetricNameFromQueryData } from 'hooks/useGetYAxisUnit';
|
||||
@@ -62,6 +64,13 @@ function QuerySection(): JSX.Element {
|
||||
return currentQueryKey !== stagedQueryKey;
|
||||
}, [currentQuery, alertType, thresholdState, stagedQuery]);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const isLoadingQueries =
|
||||
useIsFetching([REACT_QUERY_KEY.ALERT_RULES_CHART_PREVIEW]) > 0;
|
||||
const handleCancelQuery = useCallback(() => {
|
||||
queryClient.cancelQueries([REACT_QUERY_KEY.ALERT_RULES_CHART_PREVIEW]);
|
||||
}, [queryClient]);
|
||||
|
||||
const runQueryHandler = useCallback(() => {
|
||||
// Reset the source param when the query is changed
|
||||
// Then manually run the query
|
||||
@@ -130,6 +139,8 @@ function QuerySection(): JSX.Element {
|
||||
setQueryCategory={onQueryCategoryChange}
|
||||
alertType={alertType}
|
||||
runQuery={runQueryHandler}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
alertDef={alertDef}
|
||||
panelType={PANEL_TYPES.TIME_SERIES}
|
||||
key={currentQuery.queryType}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
|
||||
import { INITIAL_CRITICAL_THRESHOLD } from 'container/CreateAlertV2/context/constants';
|
||||
import { Threshold } from 'container/CreateAlertV2/context/types';
|
||||
@@ -185,7 +186,7 @@ function ChartPreview({
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey: [
|
||||
'chartPreview',
|
||||
REACT_QUERY_KEY.ALERT_RULES_CHART_PREVIEW,
|
||||
userQueryKey || JSON.stringify(query),
|
||||
selectedInterval,
|
||||
minTime,
|
||||
|
||||
@@ -29,6 +29,8 @@ function QuerySection({
|
||||
setQueryCategory,
|
||||
alertType,
|
||||
runQuery,
|
||||
isLoadingQueries,
|
||||
handleCancelQuery,
|
||||
alertDef,
|
||||
panelType,
|
||||
ruleId,
|
||||
@@ -176,6 +178,8 @@ function QuerySection({
|
||||
queryType: queryCategory,
|
||||
});
|
||||
}}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
@@ -195,7 +199,11 @@ function QuerySection({
|
||||
onChange={handleQueryCategoryChange}
|
||||
tabBarExtraContent={
|
||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||
<RunQueryBtn onStageRunQuery={runQuery} />
|
||||
<RunQueryBtn
|
||||
onStageRunQuery={runQuery}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
items={items}
|
||||
@@ -237,6 +245,8 @@ interface QuerySectionProps {
|
||||
setQueryCategory: (n: EQueryType) => void;
|
||||
alertType: AlertTypes;
|
||||
runQuery: VoidFunction;
|
||||
isLoadingQueries: boolean;
|
||||
handleCancelQuery: () => void;
|
||||
alertDef: AlertDef;
|
||||
panelType: PANEL_TYPES;
|
||||
ruleId: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useIsFetching, useQueryClient } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
@@ -126,6 +126,11 @@ function FormAlertRules({
|
||||
|
||||
// use query client
|
||||
const ruleCache = useQueryClient();
|
||||
const isLoadingAlertQuery =
|
||||
useIsFetching([REACT_QUERY_KEY.ALERT_RULES_CHART_PREVIEW]) > 0;
|
||||
const handleCancelAlertQuery = useCallback(() => {
|
||||
ruleCache.cancelQueries([REACT_QUERY_KEY.ALERT_RULES_CHART_PREVIEW]);
|
||||
}, [ruleCache]);
|
||||
|
||||
const isNewRule = !ruleId || isEmpty(ruleId);
|
||||
|
||||
@@ -910,6 +915,8 @@ function FormAlertRules({
|
||||
setQueryCategory={onQueryCategoryChange}
|
||||
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
|
||||
runQuery={(): void => handleRunQuery()}
|
||||
isLoadingQueries={isLoadingAlertQuery}
|
||||
handleCancelQuery={handleCancelAlertQuery}
|
||||
alertDef={alertDef}
|
||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||
key={currentQuery.queryType}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Button } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import { ButtonWrapper } from 'container/TracesExplorer/QuerySection/styles';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { MeterExplorerEventKeys, MeterExplorerEvents } from '../events';
|
||||
|
||||
function QuerySection(): JSX.Element {
|
||||
const { handleRunQuery } = useQueryBuilder();
|
||||
|
||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.TIME_SERIES);
|
||||
|
||||
return (
|
||||
<div className="query-section">
|
||||
<QueryBuilder
|
||||
panelType={panelTypes}
|
||||
config={{ initialDataSource: DataSource.METRICS, queryVariant: 'static' }}
|
||||
version="v4"
|
||||
actions={
|
||||
<ButtonWrapper>
|
||||
<Button
|
||||
onClick={(): void => {
|
||||
handleRunQuery();
|
||||
logEvent(MeterExplorerEvents.QueryBuilderQueryChanged, {
|
||||
[MeterExplorerEventKeys.Tab]: 'explorer',
|
||||
});
|
||||
}}
|
||||
type="primary"
|
||||
>
|
||||
Run Query
|
||||
</Button>
|
||||
</ButtonWrapper>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default QuerySection;
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Button } from 'antd';
|
||||
import { useCallback } from 'react';
|
||||
import { useIsFetching, useQueryClient } from 'react-query';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import { ButtonWrapper } from 'container/TracesExplorer/QuerySection/styles';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
@@ -11,9 +14,16 @@ import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||
|
||||
function QuerySection(): JSX.Element {
|
||||
const { handleRunQuery } = useQueryBuilder();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.TIME_SERIES);
|
||||
|
||||
const isLoadingQueries = useIsFetching([REACT_QUERY_KEY.GET_QUERY_RANGE]) > 0;
|
||||
|
||||
const handleCancelQuery = useCallback(() => {
|
||||
queryClient.cancelQueries([REACT_QUERY_KEY.GET_QUERY_RANGE]);
|
||||
}, [queryClient]);
|
||||
|
||||
return (
|
||||
<div className="query-section">
|
||||
<QueryBuilder
|
||||
@@ -22,17 +32,16 @@ function QuerySection(): JSX.Element {
|
||||
version="v4"
|
||||
actions={
|
||||
<ButtonWrapper>
|
||||
<Button
|
||||
onClick={(): void => {
|
||||
<RunQueryBtn
|
||||
onStageRunQuery={(): void => {
|
||||
handleRunQuery();
|
||||
logEvent(MetricsExplorerEvents.QueryBuilderQueryChanged, {
|
||||
[MetricsExplorerEventKeys.Tab]: 'explorer',
|
||||
});
|
||||
}}
|
||||
type="primary"
|
||||
>
|
||||
Run Query
|
||||
</Button>
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
/>
|
||||
</ButtonWrapper>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Drawer, Empty, Skeleton, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGetMetricDetails } from 'hooks/metricsExplorer/useGetMetricDetails';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
@@ -108,6 +110,11 @@ function Inspect({
|
||||
reset,
|
||||
} = useInspectMetrics(appliedMetricName);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const handleCancelInspectQuery = useCallback(() => {
|
||||
queryClient.cancelQueries(REACT_QUERY_KEY.GET_INSPECT_METRICS_DETAILS);
|
||||
}, [queryClient]);
|
||||
|
||||
const handleDispatchMetricInspectionOptions = useCallback(
|
||||
(action: MetricInspectionAction): void => {
|
||||
dispatchMetricInspectionOptions(action);
|
||||
@@ -238,6 +245,8 @@ function Inspect({
|
||||
inspectMetricsTimeSeries={inspectMetricsTimeSeries}
|
||||
currentQuery={currentQueryData}
|
||||
setCurrentQuery={setCurrentQueryData}
|
||||
isLoadingQueries={isInspectMetricsLoading}
|
||||
handleCancelQuery={handleCancelInspectQuery}
|
||||
/>
|
||||
</div>
|
||||
<div className="inspect-metrics-content-second-col">
|
||||
|
||||
@@ -20,6 +20,8 @@ function QueryBuilder({
|
||||
inspectMetricsTimeSeries,
|
||||
currentQuery,
|
||||
setCurrentQuery,
|
||||
isLoadingQueries,
|
||||
handleCancelQuery,
|
||||
}: QueryBuilderProps): JSX.Element {
|
||||
const applyInspectionOptions = useCallback(() => {
|
||||
setAppliedMetricName(currentMetricName ?? '');
|
||||
@@ -39,7 +41,11 @@ function QueryBuilder({
|
||||
>
|
||||
Query Builder
|
||||
</Button>
|
||||
<RunQueryBtn onStageRunQuery={applyInspectionOptions} />
|
||||
<RunQueryBtn
|
||||
onStageRunQuery={applyInspectionOptions}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
/>
|
||||
</div>
|
||||
<Card className="inspect-metrics-query-builder-content">
|
||||
<MetricNameSearch
|
||||
|
||||
@@ -105,6 +105,8 @@ describe('QueryBuilder', () => {
|
||||
},
|
||||
} as any,
|
||||
setCurrentQuery: jest.fn(),
|
||||
isLoadingQueries: false,
|
||||
handleCancelQuery: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -58,6 +58,8 @@ export interface QueryBuilderProps {
|
||||
inspectMetricsTimeSeries: InspectMetricsSeries[];
|
||||
currentQuery: IBuilderQuery;
|
||||
setCurrentQuery: (query: IBuilderQuery) => void;
|
||||
isLoadingQueries: boolean;
|
||||
handleCancelQuery: () => void;
|
||||
}
|
||||
|
||||
export interface MetricNameSearchProps {
|
||||
|
||||
@@ -12,6 +12,8 @@ function MetricsSearch({
|
||||
currentQueryFilterExpression,
|
||||
setCurrentQueryFilterExpression,
|
||||
isLoading,
|
||||
handleCancelQuery,
|
||||
onRunQuery,
|
||||
}: MetricsSearchProps): JSX.Element {
|
||||
const handleOnChange = useCallback(
|
||||
(expression: string): void => {
|
||||
@@ -22,7 +24,8 @@ function MetricsSearch({
|
||||
|
||||
const handleStageAndRunQuery = useCallback(() => {
|
||||
onChange(currentQueryFilterExpression);
|
||||
}, [currentQueryFilterExpression, onChange]);
|
||||
onRunQuery?.();
|
||||
}, [currentQueryFilterExpression, onChange, onRunQuery]);
|
||||
|
||||
const handleRunQuery = useCallback(
|
||||
(expression: string): void => {
|
||||
@@ -53,6 +56,7 @@ function MetricsSearch({
|
||||
<RunQueryBtn
|
||||
onStageRunQuery={handleStageAndRunQuery}
|
||||
isLoadingQueries={isLoading}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
/>
|
||||
<div className="metrics-search-options">
|
||||
<DateTimeSelectionV2
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
@@ -101,6 +102,8 @@ function Summary(): JSX.Element {
|
||||
setCurrentQueryFilterExpression,
|
||||
] = useState<string>(appliedFilterExpression);
|
||||
|
||||
const [isCancelled, setIsCancelled] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentQueryFilterExpression(appliedFilterExpression);
|
||||
}, [appliedFilterExpression]);
|
||||
@@ -161,6 +164,7 @@ function Summary(): JSX.Element {
|
||||
isLoading: isGetMetricsStatsLoading,
|
||||
isError: isGetMetricsStatsError,
|
||||
error: metricsStatsError,
|
||||
reset: resetMetricsStats,
|
||||
} = useGetMetricsStats();
|
||||
|
||||
const {
|
||||
@@ -169,6 +173,7 @@ function Summary(): JSX.Element {
|
||||
isLoading: isGetMetricsTreemapLoading,
|
||||
isError: isGetMetricsTreemapError,
|
||||
error: metricsTreemapError,
|
||||
reset: resetMetricsTreemap,
|
||||
} = useGetMetricsTreemap();
|
||||
|
||||
const metricsStatsApiError = useMemo(
|
||||
@@ -193,6 +198,40 @@ function Summary(): JSX.Element {
|
||||
});
|
||||
}, [metricsTreemapQuery, getMetricsTreemap]);
|
||||
|
||||
const handleCancelQuery = useCallback(() => {
|
||||
resetMetricsStats();
|
||||
resetMetricsTreemap();
|
||||
setCurrentQueryFilterExpression(appliedFilterExpression);
|
||||
setIsCancelled(true);
|
||||
}, [
|
||||
resetMetricsStats,
|
||||
resetMetricsTreemap,
|
||||
setCurrentQueryFilterExpression,
|
||||
appliedFilterExpression,
|
||||
]);
|
||||
|
||||
const handleRunQuery = useCallback(() => {
|
||||
setIsCancelled(false);
|
||||
getMetricsStats({
|
||||
data: {
|
||||
...metricsListQuery,
|
||||
filter: { expression: currentQueryFilterExpression },
|
||||
},
|
||||
});
|
||||
getMetricsTreemap({
|
||||
data: {
|
||||
...metricsTreemapQuery,
|
||||
filter: { expression: currentQueryFilterExpression },
|
||||
},
|
||||
});
|
||||
}, [
|
||||
getMetricsStats,
|
||||
getMetricsTreemap,
|
||||
metricsListQuery,
|
||||
metricsTreemapQuery,
|
||||
currentQueryFilterExpression,
|
||||
]);
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
(expression: string) => {
|
||||
const newFilters: TagFilter = {
|
||||
@@ -319,11 +358,19 @@ function Summary(): JSX.Element {
|
||||
!isGetMetricsTreemapLoading &&
|
||||
!isGetMetricsTreemapError;
|
||||
|
||||
const isLoadingQueries =
|
||||
isGetMetricsStatsLoading || isGetMetricsTreemapLoading;
|
||||
|
||||
const showFullScreenLoading =
|
||||
(isGetMetricsStatsLoading || isGetMetricsTreemapLoading) &&
|
||||
isLoadingQueries &&
|
||||
formattedMetricsData.length === 0 &&
|
||||
!treeMapData?.data[heatmapView]?.length;
|
||||
|
||||
const showNoMetrics =
|
||||
isMetricsListDataEmpty &&
|
||||
isMetricsTreeMapDataEmpty &&
|
||||
!appliedFilterExpression;
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className="metrics-explorer-summary-tab">
|
||||
@@ -332,13 +379,30 @@ function Summary(): JSX.Element {
|
||||
onChange={handleFilterChange}
|
||||
currentQueryFilterExpression={currentQueryFilterExpression}
|
||||
setCurrentQueryFilterExpression={setCurrentQueryFilterExpression}
|
||||
isLoading={isGetMetricsStatsLoading || isGetMetricsTreemapLoading}
|
||||
isLoading={isLoadingQueries}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
onRunQuery={handleRunQuery}
|
||||
/>
|
||||
{showFullScreenLoading ? (
|
||||
<MetricsLoading />
|
||||
) : isMetricsListDataEmpty &&
|
||||
isMetricsTreeMapDataEmpty &&
|
||||
!appliedFilterExpression ? (
|
||||
) : isCancelled ? (
|
||||
<div className="no-logs-container">
|
||||
<div className="no-logs-container-content">
|
||||
<img
|
||||
className="eyes-emoji"
|
||||
src="/Images/eyesEmoji.svg"
|
||||
alt="eyes emoji"
|
||||
/>
|
||||
<Typography className="no-logs-text">
|
||||
Query cancelled.
|
||||
<span className="sub-text">
|
||||
{' '}
|
||||
Click "Run Query" to load metrics.
|
||||
</span>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
) : showNoMetrics ? (
|
||||
<NoLogs dataSource={DataSource.METRICS} />
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -29,6 +29,8 @@ export interface MetricsSearchProps {
|
||||
currentQueryFilterExpression: string;
|
||||
setCurrentQueryFilterExpression: (expression: string) => void;
|
||||
isLoading: boolean;
|
||||
handleCancelQuery: () => void;
|
||||
onRunQuery: () => void;
|
||||
}
|
||||
|
||||
export interface MetricsTreemapProps {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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';
|
||||
@@ -25,8 +24,8 @@ import PromQLQueryContainer from './QueryBuilder/promQL';
|
||||
import './QuerySection.styles.scss';
|
||||
function QuerySection({
|
||||
selectedGraph,
|
||||
queryRangeKey,
|
||||
isLoadingQueries,
|
||||
handleCancelQuery,
|
||||
selectedWidget,
|
||||
dashboardVersion,
|
||||
dashboardId,
|
||||
@@ -179,7 +178,7 @@ function QuerySection({
|
||||
label="Stage & Run Query"
|
||||
onStageRunQuery={handleRunQuery}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
queryRangeKey={queryRangeKey}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
@@ -191,8 +190,8 @@ function QuerySection({
|
||||
|
||||
interface QueryProps {
|
||||
selectedGraph: PANEL_TYPES;
|
||||
queryRangeKey?: QueryKey;
|
||||
isLoadingQueries?: boolean;
|
||||
isLoadingQueries: boolean;
|
||||
handleCancelQuery: () => void;
|
||||
selectedWidget: Widgets;
|
||||
dashboardVersion?: string;
|
||||
dashboardId?: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { memo, useEffect } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
@@ -34,6 +34,7 @@ function LeftContainer({
|
||||
isNewPanel = false,
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { selectedTime: globalSelectedInterval, minTime, maxTime } = useSelector<
|
||||
AppState,
|
||||
@@ -49,6 +50,10 @@ function LeftContainer({
|
||||
],
|
||||
[globalSelectedInterval, requestData, minTime, maxTime],
|
||||
);
|
||||
const handleCancelQuery = useCallback(() => {
|
||||
queryClient.cancelQueries(queryRangeKey);
|
||||
}, [queryClient, queryRangeKey]);
|
||||
|
||||
const queryResponse = useGetQueryRange(requestData, ENTITY_VERSION_V5, {
|
||||
enabled: !!stagedQuery,
|
||||
queryKey: queryRangeKey,
|
||||
@@ -75,8 +80,8 @@ function LeftContainer({
|
||||
<QueryContainer className="query-section-left-container">
|
||||
<QuerySection
|
||||
selectedGraph={selectedGraph}
|
||||
queryRangeKey={queryRangeKey}
|
||||
isLoadingQueries={queryResponse.isFetching}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
selectedWidget={selectedWidget}
|
||||
dashboardVersion={ENTITY_VERSION_V5}
|
||||
dashboardId={selectedDashboard?.id}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useCallback } from 'react';
|
||||
import { QueryKey, useIsFetching, useQueryClient } from 'react-query';
|
||||
import { Button } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
@@ -12,14 +10,23 @@ import {
|
||||
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||
|
||||
import './RunQueryBtn.scss';
|
||||
interface RunQueryBtnProps {
|
||||
|
||||
type RunQueryBtnProps = {
|
||||
className?: string;
|
||||
label?: string;
|
||||
isLoadingQueries?: boolean;
|
||||
handleCancelQuery?: () => void;
|
||||
onStageRunQuery?: () => void;
|
||||
queryRangeKey?: QueryKey;
|
||||
}
|
||||
disabled?: boolean;
|
||||
} & (
|
||||
| {
|
||||
onStageRunQuery: () => void;
|
||||
handleCancelQuery: () => void;
|
||||
isLoadingQueries: boolean;
|
||||
}
|
||||
| {
|
||||
onStageRunQuery?: never;
|
||||
handleCancelQuery?: never;
|
||||
isLoadingQueries?: never;
|
||||
}
|
||||
);
|
||||
|
||||
function RunQueryBtn({
|
||||
className,
|
||||
@@ -27,33 +34,17 @@ function RunQueryBtn({
|
||||
isLoadingQueries,
|
||||
handleCancelQuery,
|
||||
onStageRunQuery,
|
||||
queryRangeKey,
|
||||
disabled,
|
||||
}: RunQueryBtnProps): JSX.Element {
|
||||
const isMac = getUserOperatingSystem() === UserOperatingSystem.MACOS;
|
||||
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]);
|
||||
const isLoading = isLoadingQueries ?? false;
|
||||
|
||||
return isLoading ? (
|
||||
<Button
|
||||
type="default"
|
||||
icon={<Loader2 size={14} className="loading-icon animate-spin" />}
|
||||
className={cx('cancel-query-btn periscope-btn danger', className)}
|
||||
onClick={onCancel}
|
||||
onClick={handleCancelQuery}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -61,7 +52,7 @@ function RunQueryBtn({
|
||||
<Button
|
||||
type="primary"
|
||||
className={cx('run-query-btn periscope-btn primary', className)}
|
||||
disabled={isLoading || !onStageRunQuery}
|
||||
disabled={disabled}
|
||||
onClick={onStageRunQuery}
|
||||
icon={<Play size={14} />}
|
||||
>
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
// frontend/src/container/QueryBuilder/components/RunQueryBtn/__tests__/RunQueryBtn.test.tsx
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
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(),
|
||||
@@ -26,79 +17,76 @@ describe('RunQueryBtn', () => {
|
||||
(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);
|
||||
test('shows run button and hides cancel when isLoadingQueries is false', () => {
|
||||
render(
|
||||
<RunQueryBtn
|
||||
onStageRunQuery={jest.fn()}
|
||||
handleCancelQuery={jest.fn()}
|
||||
isLoadingQueries={false}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole('button', { name: /run query/i })).toBeEnabled();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /cancel/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows cancel button and hides run button when isLoadingQueries is true', () => {
|
||||
render(
|
||||
<RunQueryBtn
|
||||
onStageRunQuery={jest.fn()}
|
||||
handleCancelQuery={jest.fn()}
|
||||
isLoadingQueries
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /run query/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('calls onStageRunQuery when run button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
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 });
|
||||
expect(btn).toBeEnabled();
|
||||
fireEvent.click(btn);
|
||||
render(
|
||||
<RunQueryBtn
|
||||
onStageRunQuery={onRun}
|
||||
handleCancelQuery={jest.fn()}
|
||||
isLoadingQueries={false}
|
||||
/>,
|
||||
);
|
||||
await user.click(screen.getByRole('button', { name: /run query/i }));
|
||||
expect(onRun).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('disabled when onStageRunQuery is undefined', () => {
|
||||
test('calls handleCancelQuery when cancel button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCancel = jest.fn();
|
||||
render(
|
||||
<RunQueryBtn
|
||||
onStageRunQuery={jest.fn()}
|
||||
isLoadingQueries
|
||||
handleCancelQuery={onCancel}
|
||||
/>,
|
||||
);
|
||||
await user.click(screen.getByRole('button', { name: /cancel/i }));
|
||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('is disabled when no props provided', () => {
|
||||
render(<RunQueryBtn />);
|
||||
expect(screen.getByRole('button', { name: /run query/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
test('shows cancel state and calls handleCancelQuery', () => {
|
||||
const onCancel = jest.fn();
|
||||
render(<RunQueryBtn isLoadingQueries handleCancelQuery={onCancel} />);
|
||||
const cancel = screen.getByRole('button', { name: /cancel/i });
|
||||
fireEvent.click(cancel);
|
||||
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', () => {
|
||||
const { container } = render(
|
||||
<RunQueryBtn onStageRunQuery={(): void => {}} />,
|
||||
<RunQueryBtn
|
||||
onStageRunQuery={jest.fn()}
|
||||
handleCancelQuery={jest.fn()}
|
||||
isLoadingQueries={false}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.lucide-command')).toBeInTheDocument();
|
||||
expect(
|
||||
@@ -111,7 +99,11 @@ describe('RunQueryBtn', () => {
|
||||
UserOperatingSystem.WINDOWS,
|
||||
);
|
||||
const { container } = render(
|
||||
<RunQueryBtn onStageRunQuery={(): void => {}} />,
|
||||
<RunQueryBtn
|
||||
onStageRunQuery={jest.fn()}
|
||||
handleCancelQuery={jest.fn()}
|
||||
isLoadingQueries={false}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.lucide-chevron-up')).toBeInTheDocument();
|
||||
expect(container.querySelector('.lucide-command')).not.toBeInTheDocument();
|
||||
@@ -120,11 +112,20 @@ describe('RunQueryBtn', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders custom label when provided', () => {
|
||||
test('renders custom label and calls onStageRunQuery on click', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onRun = jest.fn();
|
||||
render(<RunQueryBtn onStageRunQuery={onRun} label="Stage & Run Query" />);
|
||||
expect(
|
||||
screen.getByRole('button', { name: /stage & run query/i }),
|
||||
).toBeInTheDocument();
|
||||
render(
|
||||
<RunQueryBtn
|
||||
onStageRunQuery={onRun}
|
||||
handleCancelQuery={jest.fn()}
|
||||
isLoadingQueries={false}
|
||||
label="Stage & Run Query"
|
||||
/>,
|
||||
);
|
||||
const btn = screen.getByRole('button', { name: /stage & run query/i });
|
||||
expect(btn).toBeInTheDocument();
|
||||
await user.click(btn);
|
||||
expect(onRun).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ export default function RightToolbarActions({
|
||||
if (showLiveLogs) {
|
||||
return (
|
||||
<div className="right-toolbar-actions-container">
|
||||
<RunQueryBtn />
|
||||
<RunQueryBtn disabled />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export default function RightToolbarActions({
|
||||
return (
|
||||
<div className="right-toolbar-actions-container">
|
||||
<RunQueryBtn
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
isLoadingQueries={!!isLoadingQueries}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
onStageRunQuery={onStageRunQuery}
|
||||
/>
|
||||
|
||||
@@ -431,7 +431,7 @@ export const useAlertRuleDuplicate = ({
|
||||
|
||||
const params = useUrlQuery();
|
||||
|
||||
const { refetch } = useQuery(REACT_QUERY_KEY.GET_ALL_ALLERTS, {
|
||||
const { refetch } = useQuery(REACT_QUERY_KEY.GET_ALL_ALERTS, {
|
||||
queryFn: getAll,
|
||||
cacheTime: 0,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user