mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 20:38:59 +08:00
feat: added context redirection from panels to explorer pages (#7141)
* feat: added context redirection from panels to explorer pages * feat: added graph coordinate - context redirection * feat: fixed tooltip overlapping the button * feat: code fix * feat: removed unneccesary comment * feat: added logic to resolve variables * feat: added better logic to handle specific and panel redirection using query * feat: added multi query support by datasource to panels redirction * feat: fixing createbutton display logic * feat: added logic and ui for specific line redirection * feat: added logic to compute query with groupby * feat: code fix and added aysnc await * feat: added context redirection to fullview and edit view panels (#7252) * feat: added context redirection to fullview and edit view panels * feat: restricted redirection query to have only one query * feat: added is buttonEnabled logic of graphs * feat: code cleanup * feat: for one query removed the queryname from onclick button * feat: removed redirection option from action menu * feat: redesign the format api flow to avoid delay in clickbutton appearance * feat: updated the create filter logic for groupBys * feat: handled the error on format api
This commit is contained in:
parent
e04e58d8b3
commit
0320285a25
@ -10,10 +10,11 @@ import { X } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import CeleryTaskGraph from '../CeleryTaskGraph/CeleryTaskGraph';
|
||||
import { createFiltersFromData } from '../CeleryUtils';
|
||||
import { useNavigateToTraces } from '../useNavigateToTraces';
|
||||
import { useNavigateToExplorer } from '../useNavigateToExplorer';
|
||||
|
||||
export type CeleryTaskData = {
|
||||
entity: string;
|
||||
@ -55,7 +56,7 @@ export default function CeleryTaskDetail({
|
||||
const startTime = taskData.timeRange[0];
|
||||
const endTime = taskData.timeRange[1];
|
||||
|
||||
const navigateToTrace = useNavigateToTraces();
|
||||
const navigateToExplorer = useNavigateToExplorer();
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
@ -105,7 +106,12 @@ export default function CeleryTaskDetail({
|
||||
endTime,
|
||||
source: widgetData.title,
|
||||
});
|
||||
navigateToTrace(filters, startTime, endTime);
|
||||
navigateToExplorer({
|
||||
filters,
|
||||
dataSource: DataSource.TRACES,
|
||||
startTime,
|
||||
endTime,
|
||||
});
|
||||
}}
|
||||
start={startTime}
|
||||
end={endTime}
|
||||
|
@ -185,8 +185,15 @@ function CeleryTaskBar({
|
||||
headerMenuList={[...ViewMenuAction]}
|
||||
onDragSelect={onDragSelect}
|
||||
isQueryEnabled={queryEnabled}
|
||||
onClickHandler={(...args): void =>
|
||||
onGraphClick(celerySlowestTasksTableWidgetData, ...args)
|
||||
onClickHandler={(xValue, yValue, mouseX, mouseY, data): void =>
|
||||
onGraphClick(
|
||||
celerySlowestTasksTableWidgetData,
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
data,
|
||||
)
|
||||
}
|
||||
customSeries={getCustomSeries}
|
||||
dataAvailable={checkIfDataExists}
|
||||
@ -198,8 +205,15 @@ function CeleryTaskBar({
|
||||
headerMenuList={[...ViewMenuAction]}
|
||||
onDragSelect={onDragSelect}
|
||||
isQueryEnabled={queryEnabled}
|
||||
onClickHandler={(...args): void =>
|
||||
onGraphClick(celeryFailedTasksTableWidgetData, ...args)
|
||||
onClickHandler={(xValue, yValue, mouseX, mouseY, data): void =>
|
||||
onGraphClick(
|
||||
celeryFailedTasksTableWidgetData,
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
data,
|
||||
)
|
||||
}
|
||||
customSeries={getCustomSeries}
|
||||
/>
|
||||
@ -210,8 +224,15 @@ function CeleryTaskBar({
|
||||
headerMenuList={[...ViewMenuAction]}
|
||||
onDragSelect={onDragSelect}
|
||||
isQueryEnabled={queryEnabled}
|
||||
onClickHandler={(...args): void =>
|
||||
onGraphClick(celeryRetryTasksTableWidgetData, ...args)
|
||||
onClickHandler={(xValue, yValue, mouseX, mouseY, data): void =>
|
||||
onGraphClick(
|
||||
celeryRetryTasksTableWidgetData,
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
data,
|
||||
)
|
||||
}
|
||||
customSeries={getCustomSeries}
|
||||
/>
|
||||
@ -222,8 +243,15 @@ function CeleryTaskBar({
|
||||
headerMenuList={[...ViewMenuAction]}
|
||||
onDragSelect={onDragSelect}
|
||||
isQueryEnabled={queryEnabled}
|
||||
onClickHandler={(...args): void =>
|
||||
onGraphClick(celerySuccessTasksTableWidgetData, ...args)
|
||||
onClickHandler={(xValue, yValue, mouseX, mouseY, data): void =>
|
||||
onGraphClick(
|
||||
celerySuccessTasksTableWidgetData,
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
data,
|
||||
)
|
||||
}
|
||||
customSeries={getCustomSeries}
|
||||
/>
|
||||
|
@ -18,6 +18,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import {
|
||||
@ -25,7 +26,7 @@ import {
|
||||
createFiltersFromData,
|
||||
getFiltersFromQueryParams,
|
||||
} from '../CeleryUtils';
|
||||
import { useNavigateToTraces } from '../useNavigateToTraces';
|
||||
import { useNavigateToExplorer } from '../useNavigateToExplorer';
|
||||
import { celeryTaskLatencyWidgetData } from './CeleryTaskGraphUtils';
|
||||
|
||||
interface TabData {
|
||||
@ -145,15 +146,21 @@ function CeleryTaskLatencyGraph({
|
||||
[handleSetTimeStamp],
|
||||
);
|
||||
|
||||
const navigateToTraces = useNavigateToTraces();
|
||||
const navigateToExplorer = useNavigateToExplorer();
|
||||
|
||||
const goToTraces = useCallback(() => {
|
||||
const { start, end } = getStartAndEndTimesInMilliseconds(selectedTimeStamp);
|
||||
const filters = createFiltersFromData({
|
||||
[entityData?.entity as string]: entityData?.value,
|
||||
});
|
||||
navigateToTraces(filters, start, end, true);
|
||||
}, [entityData, navigateToTraces, selectedTimeStamp]);
|
||||
navigateToExplorer({
|
||||
filters,
|
||||
dataSource: DataSource.TRACES,
|
||||
startTime: start,
|
||||
endTime: end,
|
||||
sameTab: true,
|
||||
});
|
||||
}, [entityData, navigateToExplorer, selectedTimeStamp]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
|
121
frontend/src/components/CeleryTask/useNavigateToExplorer.ts
Normal file
121
frontend/src/components/CeleryTask/useNavigateToExplorer.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import useUpdatedQuery from 'container/GridCardLayout/useResolveQuery';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
export interface NavigateToExplorerProps {
|
||||
filters: TagFilterItem[];
|
||||
dataSource: DataSource;
|
||||
startTime?: number;
|
||||
endTime?: number;
|
||||
sameTab?: boolean;
|
||||
shouldResolveQuery?: boolean;
|
||||
}
|
||||
|
||||
export function useNavigateToExplorer(): (
|
||||
props: NavigateToExplorerProps,
|
||||
) => void {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const prepareQuery = useCallback(
|
||||
(selectedFilters: TagFilterItem[], dataSource: DataSource): Query => ({
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData
|
||||
.map((item) => ({
|
||||
...item,
|
||||
dataSource,
|
||||
aggregateOperator: MetricAggregateOperator.NOOP,
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: selectedFilters,
|
||||
},
|
||||
groupBy: [],
|
||||
disabled: false,
|
||||
}))
|
||||
.slice(0, 1),
|
||||
queryFormulas: [],
|
||||
},
|
||||
}),
|
||||
[currentQuery],
|
||||
);
|
||||
|
||||
const { getUpdatedQuery } = useUpdatedQuery();
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
return useCallback(
|
||||
async (props: NavigateToExplorerProps): Promise<void> => {
|
||||
const {
|
||||
filters,
|
||||
dataSource,
|
||||
startTime,
|
||||
endTime,
|
||||
sameTab,
|
||||
shouldResolveQuery,
|
||||
} = props;
|
||||
const urlParams = new URLSearchParams();
|
||||
if (startTime && endTime) {
|
||||
urlParams.set(QueryParams.startTime, startTime.toString());
|
||||
urlParams.set(QueryParams.endTime, endTime.toString());
|
||||
} else {
|
||||
urlParams.set(QueryParams.startTime, (minTime / 1000000).toString());
|
||||
urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString());
|
||||
}
|
||||
|
||||
let preparedQuery = prepareQuery(filters, dataSource);
|
||||
|
||||
if (shouldResolveQuery) {
|
||||
await getUpdatedQuery({
|
||||
widgetConfig: {
|
||||
query: preparedQuery,
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
},
|
||||
selectedDashboard,
|
||||
})
|
||||
.then((query) => {
|
||||
preparedQuery = query;
|
||||
})
|
||||
.catch(() => {
|
||||
notifications.error({
|
||||
message: 'Unable to resolve variables',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const JSONCompositeQuery = encodeURIComponent(JSON.stringify(preparedQuery));
|
||||
|
||||
const basePath =
|
||||
dataSource === DataSource.TRACES
|
||||
? ROUTES.TRACES_EXPLORER
|
||||
: ROUTES.LOGS_EXPLORER;
|
||||
const newExplorerPath = `${basePath}?${urlParams.toString()}&${
|
||||
QueryParams.compositeQuery
|
||||
}=${JSONCompositeQuery}`;
|
||||
|
||||
window.open(newExplorerPath, sameTab ? '_self' : '_blank');
|
||||
},
|
||||
[
|
||||
prepareQuery,
|
||||
minTime,
|
||||
maxTime,
|
||||
getUpdatedQuery,
|
||||
selectedDashboard,
|
||||
notifications,
|
||||
],
|
||||
);
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
export function useNavigateToTraces(): (
|
||||
filters: TagFilterItem[],
|
||||
startTime?: number,
|
||||
endTime?: number,
|
||||
sameTab?: boolean,
|
||||
) => void {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const prepareQuery = useCallback(
|
||||
(selectedFilters: TagFilterItem[]): Query => ({
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: MetricAggregateOperator.NOOP,
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: selectedFilters,
|
||||
},
|
||||
})),
|
||||
},
|
||||
}),
|
||||
[currentQuery],
|
||||
);
|
||||
|
||||
return useCallback(
|
||||
(
|
||||
filters: TagFilterItem[],
|
||||
startTime?: number,
|
||||
endTime?: number,
|
||||
sameTab?: boolean,
|
||||
): void => {
|
||||
const urlParams = new URLSearchParams();
|
||||
if (startTime && endTime) {
|
||||
urlParams.set(QueryParams.startTime, startTime.toString());
|
||||
urlParams.set(QueryParams.endTime, endTime.toString());
|
||||
} else {
|
||||
urlParams.set(QueryParams.startTime, (minTime / 1000000).toString());
|
||||
urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString());
|
||||
}
|
||||
|
||||
const JSONCompositeQuery = encodeURIComponent(
|
||||
JSON.stringify(prepareQuery(filters)),
|
||||
);
|
||||
|
||||
const newTraceExplorerPath = `${
|
||||
ROUTES.TRACES_EXPLORER
|
||||
}?${urlParams.toString()}&${
|
||||
QueryParams.compositeQuery
|
||||
}=${JSONCompositeQuery}`;
|
||||
|
||||
window.open(newTraceExplorerPath, sameTab ? '_self' : '_blank');
|
||||
},
|
||||
[minTime, maxTime, prepareQuery],
|
||||
);
|
||||
}
|
@ -48,6 +48,8 @@ function FullView({
|
||||
tableProcessedDataRef,
|
||||
isDependedDataLoaded = false,
|
||||
onToggleModelHandler,
|
||||
onClickHandler,
|
||||
setCurrentGraphRef,
|
||||
}: FullViewProps): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { selectedTime: globalSelectedTime } = useSelector<
|
||||
@ -60,6 +62,10 @@ function FullView({
|
||||
|
||||
const fullViewRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentGraphRef(fullViewRef);
|
||||
}, [setCurrentGraphRef]);
|
||||
|
||||
const { selectedDashboard, isDashboardLocked } = useDashboard();
|
||||
|
||||
const getSelectedTime = useCallback(
|
||||
@ -249,6 +255,7 @@ function FullView({
|
||||
onDragSelect={onDragSelect}
|
||||
tableProcessedDataRef={tableProcessedDataRef}
|
||||
searchTerm={searchTerm}
|
||||
onClickHandler={onClickHandler}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@ import { UplotProps } from 'components/Uplot/Uplot';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
||||
import { Dispatch, MutableRefObject, RefObject, SetStateAction } from 'react';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
@ -57,6 +57,7 @@ export interface FullViewProps {
|
||||
yAxisUnit?: string;
|
||||
isDependedDataLoaded?: boolean;
|
||||
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
|
||||
setCurrentGraphRef: Dispatch<SetStateAction<RefObject<HTMLDivElement> | null>>;
|
||||
}
|
||||
|
||||
export interface GraphManagerProps extends UplotProps {
|
||||
|
@ -2,6 +2,7 @@ import '../GridCardLayout.styles.scss';
|
||||
|
||||
import { Skeleton, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@ -17,6 +18,7 @@ import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
Dispatch,
|
||||
RefObject,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
@ -25,13 +27,16 @@ import {
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useGraphClickToShowButton } from '../useGraphClickToShowButton';
|
||||
import useNavigateToExplorerPages from '../useNavigateToExplorerPages';
|
||||
import WidgetHeader from '../WidgetHeader';
|
||||
import FullView from './FullView';
|
||||
import { Modal } from './styles';
|
||||
import { WidgetGraphComponentProps } from './types';
|
||||
import { getLocalStorageGraphVisibilityState } from './utils';
|
||||
import { getLocalStorageGraphVisibilityState, handleGraphClick } from './utils';
|
||||
|
||||
function WidgetGraphComponent({
|
||||
widget,
|
||||
@ -67,6 +72,11 @@ function WidgetGraphComponent({
|
||||
);
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [
|
||||
currentGraphRef,
|
||||
setCurrentGraphRef,
|
||||
] = useState<RefObject<HTMLDivElement> | null>(graphRef);
|
||||
|
||||
useEffect(() => {
|
||||
if (!lineChartRef.current) return;
|
||||
|
||||
@ -78,6 +88,8 @@ function WidgetGraphComponent({
|
||||
|
||||
const tableProcessedDataRef = useRef<RowData[]>([]);
|
||||
|
||||
const navigateToExplorerPages = useNavigateToExplorerPages();
|
||||
|
||||
const { setLayouts, selectedDashboard, setSelectedDashboard } = useDashboard();
|
||||
|
||||
const onToggleModal = useCallback(
|
||||
@ -230,6 +242,40 @@ function WidgetGraphComponent({
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
|
||||
const graphClick = useGraphClickToShowButton({
|
||||
graphRef: currentGraphRef?.current ? currentGraphRef : graphRef,
|
||||
isButtonEnabled: (widget?.query?.builder?.queryData ?? []).some(
|
||||
(q) =>
|
||||
q.dataSource === DataSource.TRACES || q.dataSource === DataSource.LOGS,
|
||||
),
|
||||
buttonClassName: 'view-onclick-show-button',
|
||||
});
|
||||
|
||||
const navigateToExplorer = useNavigateToExplorer();
|
||||
|
||||
const graphClickHandler = (
|
||||
xValue: number,
|
||||
yValue: number,
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
metric?: { [key: string]: string },
|
||||
queryData?: { queryName: string; inFocusOrNot: boolean },
|
||||
): void => {
|
||||
handleGraphClick({
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
metric,
|
||||
queryData,
|
||||
widget,
|
||||
navigateToExplorerPages,
|
||||
navigateToExplorer,
|
||||
notifications,
|
||||
graphClick,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@ -280,6 +326,8 @@ function WidgetGraphComponent({
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
onToggleModelHandler={onToggleModelHandler}
|
||||
tableProcessedDataRef={tableProcessedDataRef}
|
||||
onClickHandler={onClickHandler ?? graphClickHandler}
|
||||
setCurrentGraphRef={setCurrentGraphRef}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
@ -322,7 +370,7 @@ function WidgetGraphComponent({
|
||||
setRequestData={setRequestData}
|
||||
setGraphVisibility={setGraphVisibility}
|
||||
graphVisibility={graphVisibility}
|
||||
onClickHandler={onClickHandler}
|
||||
onClickHandler={onClickHandler ?? graphClickHandler}
|
||||
onDragSelect={onDragSelect}
|
||||
tableProcessedDataRef={tableProcessedDataRef}
|
||||
customTooltipElement={customTooltipElement}
|
||||
|
@ -1,10 +1,17 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import { NavigateToExplorerProps } from 'components/CeleryTask/useNavigateToExplorer';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { GraphClickProps } from '../useGraphClickToShowButton';
|
||||
import { NavigateToExplorerPagesProps } from '../useNavigateToExplorerPages';
|
||||
import { LegendEntryProps } from './FullView/types';
|
||||
import {
|
||||
showAllDataSet,
|
||||
@ -151,3 +158,83 @@ export const isDataAvailableByPanelType = (
|
||||
|
||||
return Boolean(getPanelData()?.length);
|
||||
};
|
||||
|
||||
interface HandleGraphClickParams {
|
||||
xValue: number;
|
||||
yValue: number;
|
||||
mouseX: number;
|
||||
mouseY: number;
|
||||
metric?: { [key: string]: string };
|
||||
queryData?: { queryName: string; inFocusOrNot: boolean };
|
||||
widget: Widgets;
|
||||
navigateToExplorerPages: (
|
||||
props: NavigateToExplorerPagesProps,
|
||||
) => Promise<{
|
||||
[queryName: string]: {
|
||||
filters: TagFilterItem[];
|
||||
dataSource?: string;
|
||||
};
|
||||
}>;
|
||||
navigateToExplorer: (props: NavigateToExplorerProps) => void;
|
||||
notifications: NotificationInstance;
|
||||
graphClick: (props: GraphClickProps) => void;
|
||||
}
|
||||
|
||||
export const handleGraphClick = async ({
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
metric,
|
||||
queryData,
|
||||
widget,
|
||||
navigateToExplorerPages,
|
||||
navigateToExplorer,
|
||||
notifications,
|
||||
graphClick,
|
||||
}: HandleGraphClickParams): Promise<void> => {
|
||||
const { stepInterval } = widget?.query?.builder?.queryData?.[0] ?? {};
|
||||
|
||||
try {
|
||||
const result = await navigateToExplorerPages({
|
||||
widget,
|
||||
requestData: {
|
||||
...metric,
|
||||
queryName: queryData?.queryName || '',
|
||||
inFocusOrNot: queryData?.inFocusOrNot || false,
|
||||
},
|
||||
});
|
||||
|
||||
const keys = Object.keys(result);
|
||||
const menuItems = keys.map((key) => ({
|
||||
text:
|
||||
keys.length === 1
|
||||
? `View ${
|
||||
(result[key].dataSource as DataSource) === DataSource.TRACES
|
||||
? 'Traces'
|
||||
: 'Logs'
|
||||
}`
|
||||
: `View ${
|
||||
(result[key].dataSource as DataSource) === DataSource.TRACES
|
||||
? 'Traces'
|
||||
: 'Logs'
|
||||
}: ${key}`,
|
||||
onClick: (): void =>
|
||||
navigateToExplorer({
|
||||
filters: result[key].filters,
|
||||
dataSource: result[key].dataSource as DataSource,
|
||||
startTime: xValue,
|
||||
endTime: xValue + (stepInterval ?? 60),
|
||||
shouldResolveQuery: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
graphClick({ xValue, yValue, mouseX, mouseY, metric, queryData, menuItems });
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: 'Failed to process graph click',
|
||||
description:
|
||||
error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -291,6 +291,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
.view-onclick-show-button {
|
||||
position: absolute;
|
||||
background: var(--bg-vanilla-100);
|
||||
border: 2px solid var(--bg-vanilla-300);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
box-shadow: none;
|
||||
list-style-type: none;
|
||||
padding: 4px;
|
||||
color: var(--bg-ink-100);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
overflow: auto;
|
||||
max-height: 150px;
|
||||
width: max-content;
|
||||
max-width: 200px;
|
||||
|
||||
.menu-item {
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
padding-right: 12px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-vanilla-200);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.fullscreen-grid-container {
|
||||
.react-grid-layout {
|
||||
@ -374,4 +406,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.view-onclick-show-button {
|
||||
background: var(--bg-ink-400);
|
||||
border-color: var(--bg-ink-300);
|
||||
color: var(--bg-vanilla-100);
|
||||
|
||||
.menu-item {
|
||||
&:hover {
|
||||
background-color: var(--bg-ink-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,191 @@
|
||||
import './GridCardLayout.styles.scss';
|
||||
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
interface ClickToShowButtonProps {
|
||||
graphRef: React.RefObject<HTMLDivElement>;
|
||||
buttonClassName?: string;
|
||||
isButtonEnabled?: boolean;
|
||||
}
|
||||
|
||||
export interface GraphClickProps {
|
||||
xValue: number;
|
||||
yValue: number;
|
||||
mouseX: number;
|
||||
mouseY: number;
|
||||
metric?: { [key: string]: string };
|
||||
queryData?: { queryName: string; inFocusOrNot: boolean };
|
||||
menuItems?: Array<{
|
||||
text: string;
|
||||
onClick: (
|
||||
xValue: number,
|
||||
yValue: number,
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
metric?: { [key: string]: string },
|
||||
queryData?: { queryName: string; inFocusOrNot: boolean },
|
||||
) => void;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const useGraphClickToShowButton = ({
|
||||
graphRef,
|
||||
buttonClassName = 'view-onclick-show-button',
|
||||
isButtonEnabled = true,
|
||||
}: ClickToShowButtonProps): ((props: GraphClickProps) => void) => {
|
||||
const activeButtonRef = useRef<HTMLButtonElement | HTMLUListElement | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const hideTooltips = (): void => {
|
||||
const elements = [
|
||||
{ id: 'overlay', selector: '#overlay' },
|
||||
{ className: 'uplot-tooltip', selector: '.uplot-tooltip' },
|
||||
];
|
||||
|
||||
elements.forEach(({ selector }) => {
|
||||
const element = document.querySelector(selector) as HTMLElement;
|
||||
if (element) {
|
||||
element.style.display = 'none';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const cleanup = useCallback((): void => {
|
||||
if (activeButtonRef.current) {
|
||||
activeButtonRef.current.remove();
|
||||
activeButtonRef.current = null;
|
||||
}
|
||||
|
||||
// Restore tooltips
|
||||
['#overlay', '.uplot-tooltip'].forEach((selector) => {
|
||||
const element = document.querySelector(selector) as HTMLElement;
|
||||
if (element) {
|
||||
element.style.display = 'block';
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const createMenu = (
|
||||
xValue: number,
|
||||
yValue: number,
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
menuItems: Array<{
|
||||
text: string;
|
||||
onClick: (
|
||||
xValue: number,
|
||||
yValue: number,
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
metric?: { [key: string]: string },
|
||||
queryData?: { queryName: string; inFocusOrNot: boolean },
|
||||
) => void;
|
||||
}>,
|
||||
metric?: { [key: string]: string },
|
||||
queryData?: { queryName: string; inFocusOrNot: boolean },
|
||||
): void => {
|
||||
const menuList = document.createElement('ul');
|
||||
menuList.className = buttonClassName;
|
||||
menuList.style.position = 'absolute';
|
||||
menuList.style.zIndex = '9999';
|
||||
|
||||
const graphBounds = graphRef.current?.getBoundingClientRect();
|
||||
if (!graphBounds) return;
|
||||
|
||||
graphRef.current?.appendChild(menuList);
|
||||
|
||||
// After appending, get menu dimensions and adjust if needed so it stays within the graph boundaries
|
||||
const menuBounds = menuList.getBoundingClientRect();
|
||||
|
||||
// Calculate position considering menu dimensions
|
||||
let finalLeft = mouseX;
|
||||
let finalTop = mouseY;
|
||||
|
||||
// Adjust horizontal position if menu would overflow
|
||||
if (mouseX + menuBounds.width > graphBounds.width) {
|
||||
finalLeft = mouseX - menuBounds.width;
|
||||
}
|
||||
// Ensure menu doesn't go off the left edge
|
||||
finalLeft = Math.max(0, finalLeft);
|
||||
|
||||
// Adjust vertical position if menu would overflow
|
||||
if (mouseY + menuBounds.height > graphBounds.height) {
|
||||
finalTop = mouseY - menuBounds.height;
|
||||
}
|
||||
// Ensure menu doesn't go off the top edge
|
||||
finalTop = Math.max(0, finalTop);
|
||||
|
||||
menuList.style.left = `${finalLeft}px`;
|
||||
menuList.style.top = `${finalTop}px`;
|
||||
|
||||
// Create a list item for each menu option provided in props
|
||||
menuItems.forEach((item) => {
|
||||
const listItem = document.createElement('li');
|
||||
listItem.textContent = item.text;
|
||||
listItem.className = 'menu-item';
|
||||
// Style the list item as needed (padding, cursor, etc.)
|
||||
listItem.onclick = (e: MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
// Execute the provided onClick handler for this menu item
|
||||
item.onClick(xValue, yValue, mouseX, mouseY, metric, queryData);
|
||||
cleanup();
|
||||
};
|
||||
menuList.appendChild(listItem);
|
||||
});
|
||||
|
||||
activeButtonRef.current = menuList;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleOutsideClick = (e: MouseEvent): void => {
|
||||
if (!graphRef.current?.contains(e.target as Node)) {
|
||||
cleanup();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleOutsideClick);
|
||||
return (): void => {
|
||||
document.removeEventListener('click', handleOutsideClick);
|
||||
cleanup();
|
||||
};
|
||||
}, [cleanup, graphRef]);
|
||||
|
||||
return useCallback(
|
||||
(props: GraphClickProps) => {
|
||||
cleanup();
|
||||
const {
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
metric,
|
||||
queryData,
|
||||
menuItems,
|
||||
} = props;
|
||||
|
||||
if (
|
||||
isButtonEnabled &&
|
||||
!isUndefined(props.xValue) &&
|
||||
props.queryData &&
|
||||
queryData?.inFocusOrNot &&
|
||||
Object.keys(queryData).length > 0
|
||||
) {
|
||||
hideTooltips();
|
||||
// createButton(xValue, yValue, mouseX, mouseY, metric, queryData);
|
||||
createMenu(
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
menuItems || [],
|
||||
metric,
|
||||
queryData,
|
||||
);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[buttonClassName, graphRef, isButtonEnabled, cleanup],
|
||||
);
|
||||
};
|
@ -0,0 +1,148 @@
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useCallback } from 'react';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
Query,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { extractQueryNamesFromExpression } from './utils';
|
||||
|
||||
type GraphClickMetaData = {
|
||||
[key: string]: string | boolean;
|
||||
queryName: string;
|
||||
inFocusOrNot: boolean;
|
||||
};
|
||||
export interface NavigateToExplorerPagesProps {
|
||||
widget: Widgets;
|
||||
requestData?: GraphClickMetaData;
|
||||
}
|
||||
|
||||
// Helper to create group by filters from request data
|
||||
const createGroupByFilters = (
|
||||
groupBy: BaseAutocompleteData[],
|
||||
requestData: GraphClickMetaData,
|
||||
): TagFilterItem[] =>
|
||||
groupBy
|
||||
.map((gb) => {
|
||||
const value = requestData[gb.key];
|
||||
return value
|
||||
? [
|
||||
{
|
||||
id: v4(),
|
||||
key: gb,
|
||||
op: '=',
|
||||
value,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
})
|
||||
.flat();
|
||||
|
||||
// Helper to build filters for a single query, give priority to group by filters
|
||||
const buildQueryFilters = (
|
||||
queryData: IBuilderQuery,
|
||||
groupByFilters: TagFilterItem[],
|
||||
): { filters: TagFilterItem[]; dataSource?: string } => {
|
||||
const existingFilters = queryData.filters?.items || [];
|
||||
const uniqueFilters = existingFilters.filter(
|
||||
(filter) =>
|
||||
!groupByFilters.some(
|
||||
(groupFilter) => groupFilter.key?.key === filter?.key?.key,
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
filters: [...uniqueFilters, ...groupByFilters],
|
||||
dataSource: queryData.dataSource,
|
||||
};
|
||||
};
|
||||
|
||||
// Main function to build filters
|
||||
export const buildFilters = (
|
||||
query: Query,
|
||||
requestData?: GraphClickMetaData,
|
||||
): {
|
||||
[queryName: string]: { filters: TagFilterItem[]; dataSource?: string };
|
||||
} => {
|
||||
// Handle specific query navigation
|
||||
if (requestData?.queryName) {
|
||||
const queryData = query.builder.queryData.find(
|
||||
(q) => q.queryName === requestData.queryName,
|
||||
);
|
||||
|
||||
// Direct query match
|
||||
if (queryData) {
|
||||
const groupByFilters = createGroupByFilters(queryData.groupBy, requestData);
|
||||
return {
|
||||
[requestData.queryName]: buildQueryFilters(queryData, groupByFilters),
|
||||
};
|
||||
}
|
||||
|
||||
// Formula query handling
|
||||
const formulaQuery = query.builder.queryFormulas.find(
|
||||
(q) => q.queryName === requestData.queryName,
|
||||
);
|
||||
|
||||
if (!formulaQuery) return {};
|
||||
|
||||
const queryNames = extractQueryNamesFromExpression(formulaQuery.expression);
|
||||
const filteredQueryData = query.builder.queryData.filter((q) =>
|
||||
queryNames.includes(q.queryName),
|
||||
);
|
||||
|
||||
const returnObject: {
|
||||
[queryName: string]: { filters: TagFilterItem[]; dataSource?: string };
|
||||
} = {};
|
||||
|
||||
filteredQueryData.forEach((q) => {
|
||||
const groupByFilters = createGroupByFilters(q.groupBy, requestData);
|
||||
returnObject[q.queryName] = buildQueryFilters(q, groupByFilters);
|
||||
});
|
||||
|
||||
return returnObject;
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook for handling navigation to explorer pages with query data
|
||||
* @returns A function to handle navigation with query processing
|
||||
*/
|
||||
function useNavigateToExplorerPages(): (
|
||||
props: NavigateToExplorerPagesProps,
|
||||
) => Promise<{
|
||||
[queryName: string]: { filters: TagFilterItem[]; dataSource?: string };
|
||||
}> {
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
return useCallback(
|
||||
async ({ widget, requestData }: NavigateToExplorerPagesProps) => {
|
||||
try {
|
||||
// Return the finalFilters
|
||||
return buildFilters(
|
||||
widget.query,
|
||||
requestData ?? { queryName: '', inFocusOrNot: false },
|
||||
);
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: 'Error navigating to explorer',
|
||||
description:
|
||||
error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
});
|
||||
// Return empty object in case of error
|
||||
return {};
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[selectedDashboard, notifications],
|
||||
);
|
||||
}
|
||||
|
||||
export default useNavigateToExplorerPages;
|
66
frontend/src/container/GridCardLayout/useResolveQuery.ts
Normal file
66
frontend/src/container/GridCardLayout/useResolveQuery.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { getQueryRangeFormat } from 'api/dashboard/queryRangeFormat';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import { prepareQueryRangePayload } from 'lib/dashboard/prepareQueryRangePayload';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import { useCallback } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getGraphType } from 'utils/getGraphType';
|
||||
|
||||
interface UseUpdatedQueryOptions {
|
||||
widgetConfig: {
|
||||
query: Query;
|
||||
panelTypes: PANEL_TYPES;
|
||||
timePreferance: timePreferenceType;
|
||||
};
|
||||
selectedDashboard?: any;
|
||||
}
|
||||
|
||||
interface UseUpdatedQueryResult {
|
||||
getUpdatedQuery: (options: UseUpdatedQueryOptions) => Promise<Query>;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
function useUpdatedQuery(): UseUpdatedQueryResult {
|
||||
const { selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const queryRangeMutation = useMutation(getQueryRangeFormat);
|
||||
|
||||
const getUpdatedQuery = useCallback(
|
||||
async ({
|
||||
widgetConfig,
|
||||
selectedDashboard,
|
||||
}: UseUpdatedQueryOptions): Promise<Query> => {
|
||||
// Prepare query payload with resolved variables
|
||||
const { queryPayload } = prepareQueryRangePayload({
|
||||
query: widgetConfig.query,
|
||||
graphType: getGraphType(widgetConfig.panelTypes),
|
||||
selectedTime: widgetConfig.timePreferance,
|
||||
globalSelectedInterval,
|
||||
variables: getDashboardVariables(selectedDashboard?.data?.variables),
|
||||
});
|
||||
|
||||
// Execute query and process results
|
||||
const queryResult = await queryRangeMutation.mutateAsync(queryPayload);
|
||||
|
||||
// Map query data from API response
|
||||
return mapQueryDataFromApi(queryResult.compositeQuery, widgetConfig?.query);
|
||||
},
|
||||
[globalSelectedInterval, queryRangeMutation],
|
||||
);
|
||||
|
||||
return {
|
||||
getUpdatedQuery,
|
||||
isLoading: queryRangeMutation.isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
export default useUpdatedQuery;
|
@ -1,3 +1,4 @@
|
||||
import { FORMULA_REGEXP } from 'constants/regExp';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
|
||||
export const removeUndefinedValuesFromLayout = (layout: Layout[]): Layout[] =>
|
||||
@ -6,3 +7,21 @@ export const removeUndefinedValuesFromLayout = (layout: Layout[]): Layout[] =>
|
||||
Object.entries(obj).filter(([, value]) => value !== undefined),
|
||||
),
|
||||
) as Layout[];
|
||||
|
||||
export const isFormula = (queryName: string): boolean =>
|
||||
FORMULA_REGEXP.test(queryName);
|
||||
|
||||
/**
|
||||
* Extracts query names from a formula expression
|
||||
* Specifically targets capital letters A-Z as query names, as after Z we dont have any query names
|
||||
*/
|
||||
export function extractQueryNamesFromExpression(expression: string): string[] {
|
||||
if (!expression) return [];
|
||||
|
||||
// Use regex to match standalone capital letters
|
||||
// Uses word boundaries to ensure we only get standalone letters
|
||||
const queryNameRegex = /\b[A-Z]\b/g;
|
||||
|
||||
// Extract matches and deduplicate
|
||||
return [...new Set(expression.match(queryNameRegex) || [])];
|
||||
}
|
||||
|
@ -1,8 +1,13 @@
|
||||
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
|
||||
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
|
||||
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
|
||||
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
@ -22,6 +27,7 @@ import { UpdateTimeInterval } from 'store/actions';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
function WidgetGraph({
|
||||
selectedWidget,
|
||||
@ -88,6 +94,43 @@ function WidgetGraph({
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
// context redirection to explorer pages
|
||||
const graphClick = useGraphClickToShowButton({
|
||||
graphRef,
|
||||
isButtonEnabled: (selectedWidget?.query?.builder?.queryData ?? []).some(
|
||||
(q) =>
|
||||
q.dataSource === DataSource.TRACES || q.dataSource === DataSource.LOGS,
|
||||
),
|
||||
buttonClassName: 'view-onclick-show-button',
|
||||
});
|
||||
|
||||
const navigateToExplorer = useNavigateToExplorer();
|
||||
const navigateToExplorerPages = useNavigateToExplorerPages();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const graphClickHandler = (
|
||||
xValue: number,
|
||||
yValue: number,
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
metric?: { [key: string]: string },
|
||||
queryData?: { queryName: string; inFocusOrNot: boolean },
|
||||
): void => {
|
||||
handleGraphClick({
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
metric,
|
||||
queryData,
|
||||
widget: selectedWidget,
|
||||
navigateToExplorerPages,
|
||||
navigateToExplorer,
|
||||
notifications,
|
||||
graphClick,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={graphRef}
|
||||
@ -110,6 +153,7 @@ function WidgetGraph({
|
||||
setRequestData={setRequestData}
|
||||
onDragSelect={onDragSelect}
|
||||
selectedGraph={selectedGraph}
|
||||
onClickHandler={graphClickHandler}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -9,6 +9,10 @@ export interface OnClickPluginOpts {
|
||||
data?: {
|
||||
[key: string]: string;
|
||||
},
|
||||
queryData?: {
|
||||
queryName: string;
|
||||
inFocusOrNot: boolean;
|
||||
},
|
||||
) => void;
|
||||
apiResponse?: MetricRangePayloadProps;
|
||||
}
|
||||
@ -31,6 +35,10 @@ function onClickPlugin(opts: OnClickPluginOpts): uPlot.Plugin {
|
||||
let metric = {};
|
||||
const { series } = u;
|
||||
const apiResult = opts.apiResponse?.data?.result || [];
|
||||
const outputMetric = {
|
||||
queryName: '',
|
||||
inFocusOrNot: false,
|
||||
};
|
||||
|
||||
// this is to get the metric value of the focused series
|
||||
if (Array.isArray(series) && series.length > 0) {
|
||||
@ -38,13 +46,15 @@ function onClickPlugin(opts: OnClickPluginOpts): uPlot.Plugin {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if (item?.show && item?._focus) {
|
||||
const { metric: focusedMetric } = apiResult[index - 1] || [];
|
||||
const { metric: focusedMetric, queryName } = apiResult[index - 1] || [];
|
||||
metric = focusedMetric;
|
||||
outputMetric.queryName = queryName;
|
||||
outputMetric.inFocusOrNot = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
opts.onClick(xValue, yValue, mouseX, mouseY, metric);
|
||||
opts.onClick(xValue, yValue, mouseX, mouseY, metric, outputMetric);
|
||||
};
|
||||
u.over.addEventListener('click', handleClick);
|
||||
},
|
||||
|
@ -2,7 +2,7 @@ import { Color } from '@signozhq/design-tokens';
|
||||
import { Card } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useGetGraphCustomSeries } from 'components/CeleryTask/useGetGraphCustomSeries';
|
||||
import { useNavigateToTraces } from 'components/CeleryTask/useNavigateToTraces';
|
||||
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { ViewMenuAction } from 'container/GridCardLayout/config';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
@ -18,6 +18,7 @@ import { AppState } from 'store/reducers';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import {
|
||||
@ -82,7 +83,7 @@ export default function OverviewRightPanelGraph({
|
||||
setSelectedTimeStamp(selectTime);
|
||||
}, []);
|
||||
|
||||
const navigateToTraces = useNavigateToTraces();
|
||||
const navigateToExplorer = useNavigateToExplorer();
|
||||
|
||||
const onGraphClickHandler = useGraphClickHandler(handleSetTimeStamp);
|
||||
|
||||
@ -100,13 +101,14 @@ export default function OverviewRightPanelGraph({
|
||||
const goToTraces = useCallback(
|
||||
(widget: Widgets) => {
|
||||
const { stepInterval } = widget?.query?.builder?.queryData?.[0] ?? {};
|
||||
navigateToTraces(
|
||||
filters ?? [],
|
||||
selectedTimeStamp,
|
||||
selectedTimeStamp + (stepInterval ?? 60),
|
||||
);
|
||||
navigateToExplorer({
|
||||
filters: filters ?? [],
|
||||
dataSource: DataSource.TRACES,
|
||||
startTime: selectedTimeStamp,
|
||||
endTime: selectedTimeStamp + (stepInterval ?? 60),
|
||||
});
|
||||
},
|
||||
[navigateToTraces, filters, selectedTimeStamp],
|
||||
[navigateToExplorer, filters, selectedTimeStamp],
|
||||
);
|
||||
|
||||
const { getCustomSeries } = useGetGraphCustomSeries({
|
||||
|
@ -3,7 +3,7 @@ import './ValueInfo.styles.scss';
|
||||
import { FileSearchOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Col, Row } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useNavigateToTraces } from 'components/CeleryTask/useNavigateToTraces';
|
||||
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
@ -15,6 +15,7 @@ import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@ -82,7 +83,7 @@ export default function ValueInfo({
|
||||
[isLoading, getValues],
|
||||
);
|
||||
|
||||
const navigateToTrace = useNavigateToTraces();
|
||||
const navigateToExplorer = useNavigateToExplorer();
|
||||
|
||||
const avgLatencyInMs = useMemo(() => {
|
||||
if (avgLatency === 'NaN') return 'NaN';
|
||||
@ -144,7 +145,12 @@ export default function ValueInfo({
|
||||
maxTime,
|
||||
source: 'request rate',
|
||||
});
|
||||
navigateToTrace(filters ?? []);
|
||||
navigateToExplorer({
|
||||
filters: filters ?? [],
|
||||
dataSource: DataSource.TRACES,
|
||||
startTime: minTime,
|
||||
endTime: maxTime,
|
||||
});
|
||||
}}
|
||||
>
|
||||
View Traces
|
||||
@ -174,7 +180,8 @@ export default function ValueInfo({
|
||||
maxTime,
|
||||
source: 'error rate',
|
||||
});
|
||||
navigateToTrace([
|
||||
navigateToExplorer({
|
||||
filters: [
|
||||
...(filters ?? []),
|
||||
{
|
||||
id: uuidv4(),
|
||||
@ -189,7 +196,11 @@ export default function ValueInfo({
|
||||
op: '=',
|
||||
value: 'true',
|
||||
},
|
||||
]);
|
||||
],
|
||||
dataSource: DataSource.TRACES,
|
||||
startTime: minTime,
|
||||
endTime: maxTime,
|
||||
});
|
||||
}}
|
||||
>
|
||||
View Traces
|
||||
@ -219,7 +230,12 @@ export default function ValueInfo({
|
||||
maxTime,
|
||||
source: 'average latency',
|
||||
});
|
||||
navigateToTrace(filters ?? []);
|
||||
navigateToExplorer({
|
||||
filters: filters ?? [],
|
||||
dataSource: DataSource.TRACES,
|
||||
startTime: minTime,
|
||||
endTime: maxTime,
|
||||
});
|
||||
}}
|
||||
>
|
||||
View Traces
|
||||
|
@ -143,7 +143,7 @@ body {
|
||||
// =================================================================
|
||||
// AntD style overrides
|
||||
.ant-dropdown-menu {
|
||||
margin-top: 2px !important;
|
||||
margin-top: 2px;
|
||||
min-width: 160px;
|
||||
|
||||
border-radius: 4px;
|
||||
@ -180,6 +180,14 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
// these are default styles but are overridden by above dropdown styles
|
||||
.ant-dropdown-menu-submenu-popup {
|
||||
padding: 0 !important;
|
||||
z-index: 1050 !important;
|
||||
box-shadow: none !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/41307
|
||||
.ant-picker-panels > *:first-child button.ant-picker-header-next-btn {
|
||||
visibility: visible !important;
|
||||
|
Loading…
x
Reference in New Issue
Block a user