feat: added celery task feature - with task graphs and details (#6840)

* feat: added celery task feature - with task garphs and details

* feat: added celery bar graph toggle states UI

* feat: added histogram charts and right panel

* feat: added task latency graph with different states

* feat: added right panel trace navigation

* feat: added navigateToTrace logic

* feat: added value graph and global filter logic

* feat: added dynamic stepinterval based on timerange

* feat: changed histogram occurences to bar

* feat: onclick right panels for celery state bar graphs

* feat: pagesetup and tabs with kafka setup

* feat: custom series for bar for color generation

* feat: fixed test cases

* feat: update styles
This commit is contained in:
SagarRajput-7 2025-01-27 08:53:19 +05:30 committed by GitHub
parent c30c882aae
commit e83e691ef5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 2741 additions and 28 deletions

View File

@ -229,7 +229,7 @@ export const InstalledIntegrations = Loadable(
),
);
export const MessagingQueues = Loadable(
export const MessagingQueuesMainPage = Loadable(
() =>
import(/* webpackChunkName: "MessagingQueues" */ 'pages/MessagingQueues'),
);
@ -247,3 +247,10 @@ export const InfrastructureMonitoring = Loadable(
/* webpackChunkName: "InfrastructureMonitoring" */ 'pages/InfrastructureMonitoring'
),
);
export const CeleryTask = Loadable(
() =>
import(
/* webpackChunkName: "CeleryTask" */ 'pages/Celery/CeleryTask/CeleryTask'
),
);

View File

@ -1,4 +1,5 @@
import ROUTES from 'constants/routes';
import MessagingQueues from 'pages/MessagingQueues';
import { RouteProps } from 'react-router-dom';
import {
@ -27,7 +28,6 @@ import {
LogsExplorer,
LogsIndexToFields,
LogsSaveViews,
MessagingQueues,
MQDetailPage,
MySettings,
NewDashboardPage,
@ -401,6 +401,13 @@ const routes: AppRoutes[] = [
key: 'MESSAGING_QUEUES',
isPrivate: true,
},
{
path: ROUTES.MESSAGING_QUEUES_CELERY_TASK,
exact: true,
component: MessagingQueues,
key: 'MESSAGING_QUEUES_CELERY_TASK',
isPrivate: true,
},
{
path: ROUTES.MESSAGING_QUEUES_DETAIL,
exact: true,

View File

@ -0,0 +1,39 @@
.celery-task-filters {
display: flex;
justify-content: space-between;
width: 100%;
.celery-filters {
display: flex;
align-items: center;
gap: 8px;
.config-select-option {
width: 100%;
.ant-select-selector {
display: flex;
min-height: 32px;
align-items: center;
gap: 16px;
min-width: 164px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
}
}
}
}
.lightMode {
.celery-task-filters {
.celery-filters {
.config-select-option {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}
}
}
}

View File

@ -0,0 +1,92 @@
import './CeleryTaskConfigOptions.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Select, Spin, Tooltip, Typography } from 'antd';
import { SelectMaxTagPlaceholder } from 'components/MessagingQueues/MQCommon/MQCommon';
import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
import { Check, Share2 } from 'lucide-react';
import { useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import {
getValuesFromQueryParams,
setQueryParamsFromOptions,
} from '../CeleryUtils';
import { useCeleryFilterOptions } from '../useCeleryFilterOptions';
function CeleryTaskConfigOptions(): JSX.Element {
const { handleSearch, isFetching, options } = useCeleryFilterOptions(
'celery.task_name',
);
const history = useHistory();
const location = useLocation();
const [isURLCopied, setIsURLCopied] = useState(false);
const urlQuery = useUrlQuery();
const [, handleCopyToClipboard] = useCopyToClipboard();
return (
<div className="celery-task-filters">
<div className="celery-filters">
<Typography.Text style={{ whiteSpace: 'nowrap' }}>
Task Name
</Typography.Text>
<Select
placeholder="Task Name"
showSearch
mode="multiple"
options={options}
loading={isFetching}
className="config-select-option"
onSearch={handleSearch}
maxTagCount={4}
maxTagPlaceholder={SelectMaxTagPlaceholder}
value={getValuesFromQueryParams(QueryParams.taskName, urlQuery) || []}
notFoundContent={
isFetching ? (
<span>
<Spin size="small" /> Loading...
</span>
) : (
<span>No Task Name found</span>
)
}
onChange={(value): void => {
handleSearch('');
setQueryParamsFromOptions(
value,
urlQuery,
history,
location,
QueryParams.taskName,
);
}}
/>
</div>
<Tooltip title="Share this" arrow={false}>
<Button
className="periscope-btn copy-url-btn"
onClick={(): void => {
handleCopyToClipboard(window.location.href);
setIsURLCopied(true);
setTimeout(() => {
setIsURLCopied(false);
}, 1000);
}}
icon={
isURLCopied ? (
<Check size={14} color={Color.BG_FOREST_500} />
) : (
<Share2 size={14} />
)
}
/>
</Tooltip>
</div>
);
}
export default CeleryTaskConfigOptions;

View File

@ -0,0 +1,49 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { DefaultOptionType } from 'antd/es/select';
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
import { useQuery } from 'react-query';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
export type FilterOptionType = 'celery.task_name';
export interface Filters {
searchText: string;
attributeKey: FilterOptionType;
}
export interface GetAllFiltersResponse {
options: DefaultOptionType[];
isFetching: boolean;
}
export function useGetAllFilters(props: Filters): GetAllFiltersResponse {
const { searchText, attributeKey } = props;
const { data, isLoading } = useQuery(
['attributesValues', searchText],
async () => {
const { payload } = await getAttributesValues({
aggregateOperator: 'noop',
dataSource: DataSource.TRACES,
aggregateAttribute: '',
attributeKey,
searchText: searchText ?? '',
filterAttributeKeyDataType: DataTypes.String,
tagType: 'tag',
});
if (payload) {
const values = Object.values(payload).find((el) => !!el) || [];
const options: DefaultOptionType[] = values.map((val: string) => ({
label: val,
value: val,
}));
return options;
}
return [];
},
);
return { options: data ?? [], isFetching: isLoading };
}

View File

@ -0,0 +1,74 @@
.celery-task-detail-drawer {
.ant-drawer-wrapper-body {
background: var(--bg-ink-500);
border: 1px solid var(--bg-ink-300);
}
.ant-drawer-body {
padding: 0px;
.ant-card {
border: none;
.ant-card-body {
height: 100%;
background: var(--bg-ink-500);
.ant-table {
background: var(--bg-ink-500);
}
}
}
}
.ant-drawer-header {
border-bottom: 1px solid var(--bg-ink-300);
.ant-drawer-header-title {
.ant-drawer-close {
position: absolute;
right: 0;
}
button > svg {
color: var(--bg-vanilla-100);
}
.ant-drawer-title {
display: flex;
flex-direction: column;
align-items: flex-start;
.title {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 18px;
font-style: normal;
font-weight: 600;
line-height: 18px;
letter-spacing: -0.45px;
}
.subtitle {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
}
}
}
.ant-drawer-footer {
border-top: 1px solid var(--bg-ink-300);
.footer-text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
}
}

View File

@ -0,0 +1,230 @@
import './CeleryTaskDetail.style.scss';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Divider, Drawer, Typography } from 'antd';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import dayjs from 'dayjs';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { X } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuidv4 } from 'uuid';
import CeleryTaskGraph from '../CeleryTaskGraph/CeleryTaskGraph';
export type CeleryTaskData = {
entity: string;
value: string | number;
timeRange: [number, number];
};
export interface CaptureDataProps extends CeleryTaskData {
widgetData: Widgets;
}
export type CeleryTaskDetailProps = {
onClose: () => void;
widgetData: Widgets;
taskData: CeleryTaskData;
drawerOpen: boolean;
};
const createFiltersFromData = (
data: Record<string, any>,
): Array<{
id: string;
key: {
key: string;
dataType: DataTypes;
type: string;
isColumn: boolean;
isJSON: boolean;
id: string;
};
op: string;
value: string;
}> => {
const excludeKeys = ['A', 'A_without_unit'];
return Object.entries(data)
.filter(([key]) => !excludeKeys.includes(key))
.map(([key, value]) => ({
id: uuidv4(),
key: {
key,
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: `${key}--string--tag--false`,
},
op: '=',
value: value.toString(),
}));
};
export default function CeleryTaskDetail({
widgetData,
taskData,
onClose,
drawerOpen,
}: CeleryTaskDetailProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const shouldShowDrawer =
!!taskData.entity && !!taskData.timeRange[0] && drawerOpen;
const formatTimestamp = (timestamp: number): string =>
dayjs(timestamp * 1000).format('MM-DD-YYYY hh:mm A');
const [totalTask, setTotalTask] = useState(0);
const getGraphData = (graphData?: MetricRangePayloadProps['data']): void => {
setTotalTask((graphData?.result?.[0] as any)?.table?.rows.length);
};
// set time range
const { minTime, maxTime, selectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const startTime = taskData.timeRange[0];
const endTime = taskData.timeRange[1];
const urlQuery = useUrlQuery();
const location = useLocation();
const history = useHistory();
const dispatch = useDispatch();
useEffect(() => {
urlQuery.delete(QueryParams.relativeTime);
urlQuery.set(QueryParams.startTime, startTime.toString());
urlQuery.set(QueryParams.endTime, endTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
if (startTime !== endTime) {
dispatch(UpdateTimeInterval('custom', [startTime, endTime]));
}
return (): void => {
urlQuery.delete(QueryParams.relativeTime);
urlQuery.delete(QueryParams.startTime);
urlQuery.delete(QueryParams.endTime);
if (selectedTime !== 'custom') {
dispatch(UpdateTimeInterval(selectedTime));
urlQuery.set(QueryParams.relativeTime, selectedTime);
} else {
dispatch(UpdateTimeInterval('custom', [minTime / 1e6, maxTime / 1e6]));
urlQuery.set(QueryParams.startTime, Math.floor(minTime / 1e6).toString());
urlQuery.set(QueryParams.endTime, Math.floor(maxTime / 1e6).toString());
}
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { currentQuery } = useQueryBuilder();
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],
);
const navigateToTrace = (data: RowData): void => {
const { entity, value } = taskData;
const selectedFilters = createFiltersFromData({ ...data, [entity]: value });
const urlParams = new URLSearchParams();
urlParams.set(QueryParams.startTime, (minTime / 1000000).toString());
urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString());
const JSONCompositeQuery = encodeURIComponent(
JSON.stringify(prepareQuery(selectedFilters)),
);
const newTraceExplorerPath = `${
ROUTES.TRACES_EXPLORER
}?${urlParams.toString()}&${
QueryParams.compositeQuery
}=${JSONCompositeQuery}`;
window.open(newTraceExplorerPath, '_blank');
};
return (
<Drawer
width="45%"
title={
<div>
<Typography.Text className="title">{`Details - ${taskData.entity}`}</Typography.Text>
<div>
<Typography.Text className="subtitle">
{`${formatTimestamp(taskData.timeRange[0])} ${
taskData.timeRange[1]
? `- ${formatTimestamp(taskData.timeRange[1])}`
: ''
}`}
</Typography.Text>
<Divider type="vertical" />
<Typography.Text className="subtitle">{taskData.value}</Typography.Text>
</div>
</div>
}
placement="right"
onClose={onClose}
open={shouldShowDrawer}
style={{
overscrollBehavior: 'contain',
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
}}
className="celery-task-detail-drawer"
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
footer={
<Typography.Text className="footer-text">{`Total Task: ${totalTask}`}</Typography.Text>
}
>
<CeleryTaskGraph
widgetData={widgetData}
getGraphData={getGraphData}
panelType={PANEL_TYPES.TABLE}
queryEnabled
openTracesButton
onOpenTraceBtnClick={navigateToTrace}
/>
</Drawer>
);
}

View File

@ -0,0 +1,235 @@
import './CeleryTaskGraph.style.scss';
import { Color } from '@signozhq/design-tokens';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { themeColors } from 'constants/theme';
import { ViewMenuAction } from 'container/GridCardLayout/config';
import GridCard from 'container/GridCardLayout/GridCard';
import { Card } from 'container/GridCardLayout/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import getLabelName from 'lib/getLabelName';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { QueryData } from 'types/api/widgets/getQuery';
import { GlobalReducer } from 'types/reducer/globalTime';
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';
import { paths } from '../CeleryUtils';
import {
celeryAllStateWidgetData,
celeryFailedStateWidgetData,
celeryFailedTasksTableWidgetData,
celeryRetryStateWidgetData,
celeryRetryTasksTableWidgetData,
celerySlowestTasksTableWidgetData,
celerySuccessStateWidgetData,
celerySuccessTasksTableWidgetData,
} from './CeleryTaskGraphUtils';
import {
CeleryTaskState,
CeleryTaskStateGraphConfig,
} from './CeleryTaskStateGraphConfig';
function CeleryTaskBar({
onClick,
queryEnabled,
}: {
onClick?: (task: CaptureDataProps) => void;
queryEnabled: boolean;
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
const dispatch = useDispatch();
const urlQuery = useUrlQuery();
const isDarkMode = useIsDarkMode();
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const onDragSelect = useCallback(
(start: number, end: number) => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
},
[dispatch, history, pathname, urlQuery],
);
const [barState, setBarState] = useState<CeleryTaskState>(CeleryTaskState.All);
const celeryAllStateData = useMemo(
() => celeryAllStateWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const celeryFailedStateData = useMemo(
() => celeryFailedStateWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const celeryRetryStateData = useMemo(
() => celeryRetryStateWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const celerySuccessStateData = useMemo(
() => celerySuccessStateWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const onGraphClick = (
widgetData: Widgets,
xValue: number,
_yValue: number,
_mouseX: number,
_mouseY: number,
data?: {
[key: string]: string;
},
): void => {
const { start, end } = getStartAndEndTimesInMilliseconds(xValue);
// Extract entity and value from data
const [firstDataPoint] = Object.entries(data || {});
const [entity, value] = (firstDataPoint || ([] as unknown)) as [
string,
string,
];
onClick?.({
entity,
value,
timeRange: [start, end],
widgetData,
});
};
const getGraphSeries = (color: string, label: string): any => ({
drawStyle: 'bars',
paths,
lineInterpolation: 'spline',
show: true,
label,
fill: `${color}90`,
stroke: color,
width: 2,
spanGaps: true,
points: {
size: 5,
show: false,
stroke: color,
},
});
const customSeries = (data: QueryData[]): uPlot.Series[] => {
console.log(data);
const configurations: uPlot.Series[] = [
{ label: 'Timestamp', stroke: 'purple' },
];
for (let i = 0; i < data.length; i += 1) {
const { metric = {}, queryName = '', legend = '' } = data[i] || {};
const label = getLabelName(metric, queryName || '', legend || '');
let color = generateColor(
label,
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
);
if (label === 'SUCCESS') {
color = Color.BG_FOREST_500;
}
if (label === 'FAILURE') {
color = Color.BG_CHERRY_500;
}
if (label === 'RETRY') {
color = Color.BG_AMBER_400;
}
const series = getGraphSeries(color, label);
configurations.push(series);
}
return configurations;
};
return (
<Card
isDarkMode={isDarkMode}
$panelType={PANEL_TYPES.BAR}
className="celery-task-graph-bar"
>
<CeleryTaskStateGraphConfig barState={barState} setBarState={setBarState} />
<div className="celery-task-graph-grid-content">
{barState === CeleryTaskState.All && (
<GridCard
widget={celeryAllStateData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
isQueryEnabled={queryEnabled}
onClickHandler={(...args): void =>
onGraphClick(celerySlowestTasksTableWidgetData, ...args)
}
customSeries={customSeries}
/>
)}
{barState === CeleryTaskState.Failed && (
<GridCard
widget={celeryFailedStateData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
isQueryEnabled={queryEnabled}
onClickHandler={(...args): void =>
onGraphClick(celeryFailedTasksTableWidgetData, ...args)
}
customSeries={customSeries}
/>
)}
{barState === CeleryTaskState.Retry && (
<GridCard
widget={celeryRetryStateData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
isQueryEnabled={queryEnabled}
onClickHandler={(...args): void =>
onGraphClick(celeryRetryTasksTableWidgetData, ...args)
}
customSeries={customSeries}
/>
)}
{barState === CeleryTaskState.Successful && (
<GridCard
widget={celerySuccessStateData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
isQueryEnabled={queryEnabled}
onClickHandler={(...args): void =>
onGraphClick(celerySuccessTasksTableWidgetData, ...args)
}
customSeries={customSeries}
/>
)}
</div>
</Card>
);
}
CeleryTaskBar.defaultProps = {
onClick: (): void => {},
};
export default CeleryTaskBar;

View File

@ -0,0 +1,122 @@
.celery-task-graph-grid-container {
width: 100%;
display: grid;
grid-template-rows: 1fr;
gap: 10px;
padding-bottom: 16px;
margin-bottom: 16px;
.celery-task-graph-grid {
display: grid;
grid-template-columns: 60% 40%;
align-items: flex-start;
gap: 10px;
width: 100%;
.celery-task-graph {
height: 380px !important;
padding: 6px;
width: 100%;
box-sizing: border-box;
.ant-card-body {
height: calc(100% - 18px);
.widget-graph-container {
&.bar {
height: calc(100% - 85px);
}
}
}
}
.celery-task-graph-bar,
.celery-task-graph-task-latency {
height: 380px !important;
width: 100%;
box-sizing: border-box;
.celery-task-graph-grid-content {
padding: 6px;
height: 100%;
}
.ant-card-body {
height: calc(100% - 18px);
.widget-graph-container {
&.bar,
&.graph {
height: calc(100% - 85px);
}
}
}
}
}
.celery-task-graph-grid-bottom {
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: flex-start;
gap: 10px;
width: 100%;
.celery-task-graph {
height: 380px !important;
padding: 10px;
width: 100%;
box-sizing: border-box;
.ant-card-body {
height: calc(100% - 18px);
}
}
}
}
.celery-task-states {
border-bottom: 1px solid var(--bg-ink-200);
&__tab {
min-width: 140px;
padding: 12px 13px 12px 12px;
cursor: pointer;
position: relative;
&:not([data-last-tab='true']) {
border-right: 1px solid var(--bg-ink-200);
}
&--selected {
background-color: rgba(38, 38, 38, 0.5);
}
}
&__label-wrapper {
margin-bottom: 8px;
}
&__label {
font-family: 'Inter';
font-size: 14px;
color: var(--bg-vanilla-400);
line-height: 20px;
font-weight: 500;
}
&__value {
font-family: 'Geist Mono';
font-size: 24px;
color: var(--bg-vanilla-100);
line-height: 32px;
}
&__indicator {
position: absolute;
width: 100%;
height: 2px;
bottom: 0;
left: 0;
background-color: var(--bg-vanilla-100);
}
}

View File

@ -0,0 +1,134 @@
import { ENTITY_VERSION_V4 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ViewMenuAction } from 'container/GridCardLayout/config';
import GridCard from 'container/GridCardLayout/GridCard';
import { Card } from 'container/GridCardLayout/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';
import {
applyCeleryFilterOnWidgetData,
getFiltersFromQueryParams,
} from '../CeleryUtils';
import { celeryTimeSeriesTablesWidgetData } from './CeleryTaskGraphUtils';
function CeleryTaskGraph({
widgetData,
onClick,
getGraphData,
queryEnabled,
rightPanelTitle,
panelType,
openTracesButton,
onOpenTraceBtnClick,
applyCeleryTaskFilter,
}: {
widgetData: Widgets;
onClick?: (task: CaptureDataProps) => void;
getGraphData?: (graphData?: MetricRangePayloadProps['data']) => void;
queryEnabled: boolean;
rightPanelTitle?: string;
panelType?: PANEL_TYPES;
openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void;
applyCeleryTaskFilter?: boolean;
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
const dispatch = useDispatch();
const urlQuery = useUrlQuery();
const isDarkMode = useIsDarkMode();
const selectedFilters = useMemo(
() =>
getFiltersFromQueryParams(
QueryParams.taskName,
urlQuery,
'celery.task_name',
),
[urlQuery],
);
const updatedWidgetData = useMemo(
() => applyCeleryFilterOnWidgetData(selectedFilters || [], widgetData),
[selectedFilters, widgetData],
);
const onDragSelect = useCallback(
(start: number, end: number) => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
},
[dispatch, history, pathname, urlQuery],
);
return (
<Card
isDarkMode={isDarkMode}
$panelType={PANEL_TYPES.TIME_SERIES || panelType}
className="celery-task-graph"
>
<GridCard
widget={applyCeleryTaskFilter ? updatedWidgetData : widgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={(xValue, _yValue, _mouseX, _mouseY, data): void => {
const { start, end } = getStartAndEndTimesInMilliseconds(xValue);
// Extract entity and value from data
const [firstDataPoint] = Object.entries(data || {});
const [entity, value] = firstDataPoint || [];
const widgetData = celeryTimeSeriesTablesWidgetData(
entity,
value,
rightPanelTitle || '',
);
onClick?.({
entity,
value,
timeRange: [start, end],
widgetData,
});
}}
getGraphData={getGraphData}
isQueryEnabled={queryEnabled}
openTracesButton={openTracesButton}
onOpenTraceBtnClick={onOpenTraceBtnClick}
version={ENTITY_VERSION_V4}
/>
</Card>
);
}
CeleryTaskGraph.defaultProps = {
getGraphData: undefined,
onClick: undefined,
rightPanelTitle: undefined,
panelType: PANEL_TYPES.TIME_SERIES,
openTracesButton: false,
onOpenTraceBtnClick: undefined,
applyCeleryTaskFilter: false,
};
export default CeleryTaskGraph;

View File

@ -0,0 +1,100 @@
import './CeleryTaskGraph.style.scss';
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';
import CeleryTaskBar from './CeleryTaskBar';
import CeleryTaskGraph from './CeleryTaskGraph';
import {
celeryActiveTasksWidgetData,
celeryErrorByWorkerWidgetData,
celeryLatencyByWorkerWidgetData,
celeryTasksByWorkerWidgetData,
celeryWorkerOnlineWidgetData,
} from './CeleryTaskGraphUtils';
import CeleryTaskLatencyGraph from './CeleryTaskLatencyGraph';
export default function CeleryTaskGraphGrid({
onClick,
queryEnabled,
}: {
onClick: (task: CaptureDataProps) => void;
queryEnabled: boolean;
}): JSX.Element {
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const celeryWorkerOnlineData = useMemo(
() => celeryWorkerOnlineWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const celeryActiveTasksData = useMemo(
() => celeryActiveTasksWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const celeryErrorByWorkerData = useMemo(
() => celeryErrorByWorkerWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const celeryLatencyByWorkerData = useMemo(
() => celeryLatencyByWorkerWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const celeryTasksByWorkerData = useMemo(
() => celeryTasksByWorkerWidgetData(minTime, maxTime),
[minTime, maxTime],
);
const bottomWidgetData = [
celeryTasksByWorkerData,
celeryErrorByWorkerData,
celeryLatencyByWorkerData,
];
const rightPanelTitle = [
'Tasks/s by worker',
'Error% by worker',
'Latency by worker',
];
return (
<div className="celery-task-graph-grid-container">
<div className="celery-task-graph-grid">
<CeleryTaskBar queryEnabled={queryEnabled} onClick={onClick} />
<CeleryTaskGraph
key={celeryWorkerOnlineData.id}
widgetData={celeryWorkerOnlineData}
queryEnabled={queryEnabled}
/>
</div>
<div className="celery-task-graph-grid">
<CeleryTaskLatencyGraph onClick={onClick} queryEnabled={queryEnabled} />
<CeleryTaskGraph
key={celeryActiveTasksData.id}
widgetData={celeryActiveTasksData}
queryEnabled={queryEnabled}
/>
</div>
<div className="celery-task-graph-grid-bottom">
{bottomWidgetData.map((widgetData, index) => (
<CeleryTaskGraph
key={widgetData.id}
widgetData={widgetData}
onClick={onClick}
queryEnabled={queryEnabled}
rightPanelTitle={rightPanelTitle[index]}
applyCeleryTaskFilter
/>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,919 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { PANEL_TYPES } from 'constants/queryBuilder';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { getWidgetQuery } from 'pages/MessagingQueues/MQDetails/MetricPage/MetricPageUtil';
import { Widgets } from 'types/api/dashboard/getAll';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuidv4 } from 'uuid';
// dynamic step interval
export const getStepInterval = (startTime: number, endTime: number): number => {
const diffInMinutes = (endTime - startTime) / 1000000 / (60 * 1000); // Convert to minutes
if (diffInMinutes <= 15) return 60; // 15 min or less
if (diffInMinutes <= 30) return 60; // 30 min or less
if (diffInMinutes <= 60) return 120; // 1 hour or less
if (diffInMinutes <= 360) return 520; // 6 hours or less
if (diffInMinutes <= 1440) return 2440; // 1 day or less
if (diffInMinutes <= 10080) return 10080; // 1 week or less
return 54000; // More than a week (use monthly interval)
};
// State Graphs
export const celeryAllStateWidgetData = (
startTime: number,
endTime: number,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
isJSON: false,
key: '',
type: '',
},
aggregateOperator: 'count',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
op: '=',
value: 'tasks.tasks.divide',
},
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
],
having: [],
legend: '{{celery.state}}',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'rate',
},
],
title: 'All',
description:
'Represents all states of task, including success, failed, and retry.',
panelTypes: PANEL_TYPES.BAR,
}),
);
export const celeryRetryStateWidgetData = (
startTime: number,
endTime: number,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
title: 'Retry',
description: 'Represents the number of retry tasks.',
panelTypes: PANEL_TYPES.BAR,
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
key: '',
type: '',
},
aggregateOperator: 'count',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: '6d97eed3',
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
op: '=',
value: 'RETRY',
},
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'celery.hostname--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
},
],
having: [],
legend: '{{celery.hostname}}',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'count',
},
],
}),
);
export const celeryFailedStateWidgetData = (
startTime: number,
endTime: number,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
title: 'Failed',
description: 'Represents the number of failed tasks.',
panelTypes: PANEL_TYPES.BAR,
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
isJSON: false,
key: '',
type: '',
},
aggregateOperator: 'count',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: '5983eae2',
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
op: '=',
value: 'FAILURE',
},
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'celery.hostname--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
},
],
having: [],
legend: '{{celery.hostname}}',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'rate',
},
],
}),
);
export const celerySuccessStateWidgetData = (
startTime: number,
endTime: number,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
title: 'Success',
description: 'Represents the number of successful tasks.',
panelTypes: PANEL_TYPES.BAR,
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
isJSON: false,
key: '',
type: '',
},
aggregateOperator: 'count',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: '000c5a93',
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
op: '=',
value: 'SUCCESS',
},
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'celery.hostname--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
},
],
having: [],
legend: '{{celery.hostname}}',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'rate',
},
],
}),
);
export const celeryTasksByWorkerWidgetData = (
startTime: number,
endTime: number,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
title: 'Tasks/s by worker',
description: 'Represents the number of tasks executed by each worker.',
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
key: '',
type: '',
},
aggregateOperator: 'rate',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'celery.hostname--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
},
],
having: [],
legend: '{{celery.hostname}}',
limit: 10,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'rate',
},
],
}),
);
export const celeryErrorByWorkerWidgetData = (
startTime: number,
endTime: number,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
title: 'Error% by worker',
description: 'Represents the number of errors by each worker.',
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
isJSON: false,
key: '',
type: '',
},
aggregateOperator: 'rate',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'celery.hostname--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
},
],
having: [],
legend: '{{celery.hostname}}',
limit: 10,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'rate',
},
],
}),
);
export const celeryLatencyByWorkerWidgetData = (
startTime: number,
endTime: number,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
title: 'Latency by Worker',
description: 'Represents the latency of tasks by each worker.',
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
aggregateOperator: 'p99',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'celery.hostname--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.hostname',
type: 'tag',
},
],
having: [],
legend: '{{celery.hostname}}',
limit: 10,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'p99',
},
],
yAxisUnit: 'ns',
}),
);
export const celeryActiveTasksWidgetData = (
startTime: number,
endTime: number,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
title: 'Tasks/ worker (Active tasks)',
description: 'Represents the number of active tasks.',
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id:
'flower_worker_number_of_currently_executing_tasks--float64--Gauge--true',
isColumn: true,
isJSON: false,
key: 'flower_worker_number_of_currently_executing_tasks',
type: 'Gauge',
},
aggregateOperator: 'latest',
dataSource: DataSource.METRICS,
disabled: false,
expression: 'A',
filters: {
items: [],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'worker--string--tag--false',
isColumn: false,
isJSON: false,
key: 'worker',
type: 'tag',
},
],
having: [],
legend: '{{worker}}',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'avg',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'latest',
},
],
}),
);
export const celeryWorkerOnlineWidgetData = (
startTime: number,
endTime: number,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
title: 'Worker Online',
description: 'Represents the number of workers online.',
panelTypes: PANEL_TYPES.VALUE,
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'flower_task_runtime_seconds_sum--float64--Sum--true',
isColumn: true,
isJSON: false,
key: 'flower_task_runtime_seconds_sum',
type: 'Sum',
},
aggregateOperator: 'rate',
dataSource: DataSource.METRICS,
disabled: false,
expression: 'A',
filters: {
items: [],
op: 'AND',
},
functions: [],
groupBy: [],
having: [],
legend: '',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'rate',
},
],
}),
);
export const celeryTaskLatencyWidgetData = (
type: string,
startTime: number,
endTime: number,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
title: 'Task Latency',
description: 'Represents the latency of task execution.',
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
aggregateOperator: type || 'p99',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
],
having: [],
legend: '{{celery.task_name}}',
limit: null,
orderBy: [
{
columnName: '#SIGNOZ_VALUE',
order: 'asc',
},
],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'p99',
},
],
yAxisUnit: 'ns',
}),
);
// Tables
export const celerySlowestTasksTableWidgetData = getWidgetQueryBuilder(
getWidgetQuery({
title: 'Slowest Tasks',
description: 'Represents the slowest tasks.',
panelTypes: PANEL_TYPES.TABLE,
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
aggregateOperator: 'avg',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
],
having: [],
legend: '',
limit: 10,
orderBy: [
{
columnName: '#SIGNOZ_VALUE',
order: 'desc',
},
],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
},
],
columnUnits: { A: 'ns' },
}),
);
export const celeryRetryTasksTableWidgetData = getWidgetQueryBuilder(
getWidgetQuery({
title: 'Top 10 tasks in retry state',
description: 'Represents the top 10 tasks in retry state.',
panelTypes: PANEL_TYPES.TABLE,
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
aggregateOperator: 'avg',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: '9e09c9ed',
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
op: '=',
value: 'RETRY',
},
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
],
having: [],
legend: '',
limit: 10,
orderBy: [
{
columnName: '#SIGNOZ_VALUE',
order: 'desc',
},
],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
},
],
columnUnits: { A: 'ns' },
}),
);
export const celeryFailedTasksTableWidgetData = getWidgetQueryBuilder(
getWidgetQuery({
title: 'Top 10 tasks in FAILED state',
description: 'Represents the top 10 tasks in failed state.',
panelTypes: PANEL_TYPES.TABLE,
columnUnits: { A: 'ns' },
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
aggregateOperator: 'avg',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: '2330f906',
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
op: '=',
value: 'FAILURE',
},
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
],
having: [],
legend: '',
limit: null,
orderBy: [
{
columnName: '#SIGNOZ_VALUE',
order: 'desc',
},
],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
},
],
}),
);
export const celerySuccessTasksTableWidgetData = getWidgetQueryBuilder(
getWidgetQuery({
title: 'Top 10 tasks in SUCCESS state',
description: 'Represents the top 10 tasks in success state.',
panelTypes: PANEL_TYPES.TABLE,
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
aggregateOperator: 'avg',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: 'ec3df7b7',
key: {
dataType: DataTypes.String,
id: 'celery.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
op: '=',
value: 'SUCCESS',
},
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
],
having: [],
legend: '',
limit: null,
orderBy: [
{
columnName: '#SIGNOZ_VALUE',
order: 'desc',
},
],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
},
],
columnUnits: { A: 'ns' },
}),
);
export const celeryTimeSeriesTablesWidgetData = (
entity: string,
value: string | number,
rightPanelTitle: string,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
title: rightPanelTitle,
description: '',
panelTypes: PANEL_TYPES.TABLE,
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
aggregateOperator: 'avg',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: uuidv4(),
key: {
dataType: DataTypes.String,
id: `${entity}--string--tag--false`,
isColumn: false,
isJSON: false,
key: `${entity}`,
type: 'tag',
},
op: '=',
value,
},
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'celery.task_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.task_name',
type: 'tag',
},
],
having: [],
legend: '',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
},
],
columnUnits: { A: 'ns' },
}),
);

View File

@ -0,0 +1,196 @@
import './CeleryTaskGraph.style.scss';
import { Col, Row } from 'antd';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ViewMenuAction } from 'container/GridCardLayout/config';
import GridCard from 'container/GridCardLayout/GridCard';
import { Card } from 'container/GridCardLayout/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';
import {
applyCeleryFilterOnWidgetData,
getFiltersFromQueryParams,
} from '../CeleryUtils';
import {
celeryTaskLatencyWidgetData,
celeryTimeSeriesTablesWidgetData,
} from './CeleryTaskGraphUtils';
interface TabData {
label: string;
key: string;
}
export enum CeleryTaskGraphState {
P99 = 'p99',
P95 = 'p95',
P90 = 'p90',
}
function CeleryTaskLatencyGraph({
onClick,
queryEnabled,
}: {
onClick: (task: CaptureDataProps) => void;
queryEnabled: boolean;
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
const dispatch = useDispatch();
const urlQuery = useUrlQuery();
const isDarkMode = useIsDarkMode();
const tabs: TabData[] = [
{ label: CeleryTaskGraphState.P99, key: CeleryTaskGraphState.P99 },
{ label: CeleryTaskGraphState.P95, key: CeleryTaskGraphState.P95 },
{ label: CeleryTaskGraphState.P90, key: CeleryTaskGraphState.P90 },
];
const [graphState, setGraphState] = useState<CeleryTaskGraphState>(
CeleryTaskGraphState.P99,
);
const handleTabClick = (key: CeleryTaskGraphState): void => {
setGraphState(key as CeleryTaskGraphState);
};
const onDragSelect = useCallback(
(start: number, end: number) => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
},
[dispatch, history, pathname, urlQuery],
);
const selectedFilters = useMemo(
() =>
getFiltersFromQueryParams(
QueryParams.taskName,
urlQuery,
'celery.task_name',
),
[urlQuery],
);
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const celeryTaskLatencyData = useMemo(
() => celeryTaskLatencyWidgetData(graphState, minTime, maxTime),
[graphState, minTime, maxTime],
);
const updatedWidgetData = useMemo(
() =>
applyCeleryFilterOnWidgetData(selectedFilters || [], celeryTaskLatencyData),
[celeryTaskLatencyData, selectedFilters],
);
const onGraphClick = (
xValue: number,
_yValue: number,
_mouseX: number,
_mouseY: number,
data?: {
[key: string]: string;
},
): void => {
const { start, end } = getStartAndEndTimesInMilliseconds(xValue);
// Extract entity and value from data
const [firstDataPoint] = Object.entries(data || {});
const [entity, value] = (firstDataPoint || ([] as unknown)) as [
string,
string,
];
onClick?.({
entity,
value,
timeRange: [start, end],
widgetData: celeryTimeSeriesTablesWidgetData(entity, value, 'Task Latency'),
});
};
return (
<Card
isDarkMode={isDarkMode}
$panelType={PANEL_TYPES.TIME_SERIES}
className="celery-task-graph-task-latency"
>
<Row className="celery-task-states">
{tabs.map((tab, index) => (
<Col
key={tab.key}
onClick={(): void => handleTabClick(tab.key as CeleryTaskGraphState)}
className={`celery-task-states__tab ${
tab.key === graphState ? 'celery-task-states__tab--selected' : ''
}`}
data-last-tab={index === tabs.length - 1}
>
<div className="celery-task-states__label-wrapper">
<div className="celery-task-states__label">
{tab.label.toUpperCase()}
</div>
</div>
{tab.key === graphState && (
<div className="celery-task-states__indicator" />
)}
</Col>
))}
</Row>
<div className="celery-task-graph-grid-content">
{graphState === CeleryTaskGraphState.P99 && (
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick}
isQueryEnabled={queryEnabled}
/>
)}
{graphState === CeleryTaskGraphState.P95 && (
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick}
isQueryEnabled={queryEnabled}
/>
)}
{graphState === CeleryTaskGraphState.P90 && (
<GridCard
widget={updatedWidgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={onGraphClick}
isQueryEnabled={queryEnabled}
/>
)}
</div>
</Card>
);
}
export default CeleryTaskLatencyGraph;

View File

@ -0,0 +1,57 @@
import './CeleryTaskGraph.style.scss';
import { Col, Row } from 'antd';
import { Dispatch, SetStateAction } from 'react';
interface TabData {
label: string;
key: string;
}
export enum CeleryTaskState {
All = 'all',
Failed = 'failed',
Retry = 'retry',
Successful = 'successful',
}
function CeleryTaskStateGraphConfig({
barState,
setBarState,
}: {
setBarState: Dispatch<SetStateAction<CeleryTaskState>>;
barState: CeleryTaskState;
}): JSX.Element {
const tabs: TabData[] = [
{ label: 'All Tasks', key: CeleryTaskState.All },
{ label: 'Failed', key: CeleryTaskState.Failed },
{ label: 'Retry', key: CeleryTaskState.Retry },
{ label: 'Successful', key: CeleryTaskState.Successful },
];
const handleTabClick = (key: CeleryTaskState): void => {
setBarState(key as CeleryTaskState);
};
return (
<Row className="celery-task-states">
{tabs.map((tab, index) => (
<Col
key={tab.key}
onClick={(): void => handleTabClick(tab.key as CeleryTaskState)}
className={`celery-task-states__tab ${
tab.key === barState ? 'celery-task-states__tab--selected' : ''
}`}
data-last-tab={index === tabs.length - 1}
>
<div className="celery-task-states__label-wrapper">
<div className="celery-task-states__label">{tab.label}</div>
</div>
{tab.key === barState && <div className="celery-task-states__indicator" />}
</Col>
))}
</Row>
);
}
export { CeleryTaskStateGraphConfig };

View File

@ -0,0 +1,92 @@
import { QueryParams } from 'constants/query';
import { History, Location } from 'history';
import getRenderer from 'lib/uPlotLib/utils/getRenderer';
import { Widgets } from 'types/api/dashboard/getAll';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuidv4 } from 'uuid';
export function getValuesFromQueryParams(
queryParams: QueryParams,
urlQuery: URLSearchParams,
): string[] {
const value = urlQuery.get(queryParams);
return value ? value.split(',') : [];
}
export function setQueryParamsFromOptions(
value: string[],
urlQuery: URLSearchParams,
history: History<unknown>,
location: Location<unknown>,
queryParams: QueryParams,
): void {
urlQuery.set(queryParams, value.join(','));
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
}
export function getFiltersFromQueryParams(
queryParams: QueryParams,
urlQuery: URLSearchParams,
key: string,
): TagFilterItem[] {
const value = urlQuery.get(queryParams);
const filters = value ? value.split(',') : [];
return filters.map((value) => ({
id: uuidv4(),
key: {
key,
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id: `${key}--string--tag--false`,
},
op: '=',
value: value.toString(),
}));
}
export function applyCeleryFilterOnWidgetData(
filters: TagFilterItem[],
widgetData: Widgets,
): Widgets {
return {
...widgetData,
query: {
...widgetData.query,
builder: {
...widgetData.query.builder,
queryData: widgetData.query.builder.queryData.map((queryItem, index) =>
index === 0
? {
...queryItem,
filters: {
...queryItem.filters,
items: [...queryItem.filters.items, ...filters],
},
}
: queryItem,
),
},
},
};
}
export const paths = (
u: any,
seriesIdx: number,
idx0: number,
idx1: number,
extendGap: boolean,
buildClip: boolean,
): uPlot.Series.PathBuilder => {
const s = u.series[seriesIdx];
const style = s.drawStyle;
const interp = s.lineInterpolation;
const renderer = getRenderer(style, interp);
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
};

View File

@ -0,0 +1,32 @@
import { DefaultOptionType } from 'antd/es/select';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useState } from 'react';
import {
FilterOptionType,
useGetAllFilters,
} from './CeleryTaskConfigOptions/useGetCeleryFilters';
export const useCeleryFilterOptions = (
type: FilterOptionType,
): {
searchText: string;
handleSearch: (value: string) => void;
isFetching: boolean;
options: DefaultOptionType[];
} => {
const [searchText, setSearchText] = useState<string>('');
const { isFetching, options } = useGetAllFilters({
attributeKey: type,
searchText,
});
const handleDebouncedSearch = useDebouncedFn((searchText): void => {
setSearchText(searchText as string);
}, 500);
const handleSearch = (value: string): void => {
handleDebouncedSearch(value || '');
};
return { searchText, handleSearch, isFetching, options };
};

View File

@ -41,4 +41,5 @@ export enum QueryParams {
getStartedSource = 'getStartedSource',
getStartedSourceService = 'getStartedSourceService',
mqServiceView = 'mqServiceView',
taskName = 'taskName',
}

View File

@ -63,6 +63,7 @@ const ROUTES = {
MESSAGING_QUEUES_DETAIL: '/messaging-queues/detail',
INFRASTRUCTURE_MONITORING_HOSTS: '/infrastructure-monitoring/hosts',
INFRASTRUCTURE_MONITORING_KUBERNETES: '/infrastructure-monitoring/kubernetes',
MESSAGING_QUEUES_CELERY_TASK: '/messaging-queues/celery-task',
} as const;
export default ROUTES;

View File

@ -287,7 +287,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
const isMessagingQueues = (): boolean =>
routeKey === 'MESSAGING_QUEUES' || routeKey === 'MESSAGING_QUEUES_DETAIL';
routeKey === 'MESSAGING_QUEUES' ||
routeKey === 'MESSAGING_QUEUES_DETAIL' ||
routeKey === 'MESSAGING_QUEUES_CELERY_TASK';
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';

View File

@ -45,6 +45,9 @@ function WidgetGraphComponent({
onClickHandler,
onDragSelect,
customTooltipElement,
openTracesButton,
onOpenTraceBtnClick,
customSeries,
}: WidgetGraphComponentProps): JSX.Element {
const [deleteModal, setDeleteModal] = useState(false);
const [hovered, setHovered] = useState(false);
@ -333,6 +336,9 @@ function WidgetGraphComponent({
tableProcessedDataRef={tableProcessedDataRef}
customTooltipElement={customTooltipElement}
searchTerm={searchTerm}
openTracesButton={openTracesButton}
onOpenTraceBtnClick={onOpenTraceBtnClick}
customSeries={customSeries}
/>
</div>
)}

View File

@ -37,6 +37,10 @@ function GridCardGraph({
onDragSelect,
customTooltipElement,
dataAvailable,
getGraphData,
openTracesButton,
onOpenTraceBtnClick,
customSeries,
}: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>();
@ -209,6 +213,7 @@ function GridCardGraph({
dataAvailable?.(
isDataAvailableByPanelType(data?.payload?.data, widget?.panelTypes),
);
getGraphData?.(data?.payload?.data);
setDashboardQueryRangeCalled(true);
},
},
@ -248,6 +253,9 @@ function GridCardGraph({
onClickHandler={onClickHandler}
onDragSelect={onDragSelect}
customTooltipElement={customTooltipElement}
openTracesButton={openTracesButton}
onOpenTraceBtnClick={onOpenTraceBtnClick}
customSeries={customSeries}
/>
)}
</div>

View File

@ -1,11 +1,13 @@
import { ToggleGraphProps } from 'components/Graph/types';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { Dispatch, MutableRefObject, ReactNode, SetStateAction } from 'react';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { QueryData } from 'types/api/widgets/getQuery';
import uPlot from 'uplot';
import { MenuItemKeys } from '../WidgetHeader/contants';
@ -32,6 +34,9 @@ export interface WidgetGraphComponentProps {
onClickHandler?: OnClickPluginOpts['onClick'];
onDragSelect: (start: number, end: number) => void;
customTooltipElement?: HTMLDivElement;
openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void;
customSeries?: (data: QueryData[]) => uPlot.Series[];
}
export interface GridCardGraphProps {
@ -45,6 +50,10 @@ export interface GridCardGraphProps {
onDragSelect: (start: number, end: number) => void;
customTooltipElement?: HTMLDivElement;
dataAvailable?: (isDataAvailable: boolean) => void;
getGraphData?: (graphData?: MetricRangePayloadProps['data']) => void;
openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void;
customSeries?: (data: QueryData[]) => uPlot.Series[];
}
export interface GetGraphVisibilityStateOnLegendClickProps {

View File

@ -3,3 +3,14 @@
max-height: 500px;
overflow-y: auto;
}
.open-traces-button {
font-size: 11px;
border-radius: 4px;
border: none;
padding: 6px 8px;
cursor: pointer;
display: flex;
gap: 6px;
align-items: center;
}

View File

@ -1,3 +1,4 @@
/* eslint-disable sonarjs/no-duplicate-string */
import './GridTableComponent.styles.scss';
import { ExclamationCircleFilled } from '@ant-design/icons';
@ -7,9 +8,11 @@ import { Events } from 'constants/events';
import { QueryTable } from 'container/QueryTable';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { cloneDeep, get, isEmpty } from 'lodash-es';
import { Compass } from 'lucide-react';
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
import { memo, ReactNode, useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import { eventEmitter } from 'utils/getEventEmitter';
import { WrapperStyled } from './styles';
@ -20,6 +23,23 @@ import {
TableData,
} from './utils';
export const HoverButtonWrapper = styled.div`
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
opacity: 0;
transition: opacity 0.2s;
`;
const RelativeWrapper = styled.div`
position: relative;
&:hover ${HoverButtonWrapper} {
opacity: 1;
}
`;
function GridTableComponent({
data,
query,
@ -27,6 +47,8 @@ function GridTableComponent({
columnUnits,
tableProcessedDataRef,
sticky,
openTracesButton,
onOpenTraceBtnClick,
...props
}: GridTableComponentProps): JSX.Element {
const { t } = useTranslation(['valueGraph']);
@ -161,6 +183,42 @@ function GridTableComponent({
},
}));
const columnDataWithOpenTracesButton = useMemo(
() =>
newColumnData.map((column, index) => ({
...column,
render: (text: string): JSX.Element => {
const LineClampedTextComponent = (
<LineClampedText
text={text}
lines={3}
tooltipProps={{
placement: 'right',
autoAdjustOverflow: true,
overlayClassName: 'long-text-tooltip',
}}
/>
);
if (index !== 0) {
return <div>{LineClampedTextComponent}</div>;
}
return (
<RelativeWrapper>
{LineClampedTextComponent}
<HoverButtonWrapper className="hover-button">
<button type="button" className="open-traces-button">
<Compass size={12} />
Open Trace
</button>
</HoverButtonWrapper>
</RelativeWrapper>
);
},
})),
[newColumnData],
);
useEffect(() => {
eventEmitter.emit(Events.TABLE_COLUMNS_DATA, {
columns: newColumnData,
@ -174,9 +232,18 @@ function GridTableComponent({
query={query}
queryTableData={data}
loading={false}
columns={newColumnData}
columns={openTracesButton ? columnDataWithOpenTracesButton : newColumnData}
dataSource={dataSource}
sticky={sticky}
onRow={
openTracesButton
? (record): React.HTMLAttributes<HTMLElement> => ({
onClick: (): void => {
onOpenTraceBtnClick?.(record);
},
})
: undefined
}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>

View File

@ -15,6 +15,8 @@ export type GridTableComponentProps = {
tableProcessedDataRef?: React.MutableRefObject<RowData[]>;
sticky?: TableProps<RowData>['sticky'];
searchTerm?: string;
openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void;
} & Pick<LogsExplorerTableProps, 'data'> &
Omit<TableProps<RowData>, 'columns' | 'dataSource'>;

View File

@ -10,6 +10,7 @@ export const getWidgetQueryBuilder = ({
yAxisUnit = '',
fillSpans = false,
id,
columnUnits,
}: GetWidgetQueryBuilderProps): Widgets => ({
description: '',
id: id || v4(),
@ -26,4 +27,5 @@ export const getWidgetQueryBuilder = ({
selectedLogFields: [],
selectedTracesFields: [],
fillSpans,
columnUnits,
});

View File

@ -11,6 +11,7 @@ export interface GetWidgetQueryBuilderProps {
yAxisUnit?: Widgets['yAxisUnit'];
id?: Widgets['id'];
fillSpans?: Widgets['fillSpans'];
columnUnits?: Widgets['columnUnits'];
}
export interface NavigateToTraceProps {

View File

@ -5,6 +5,7 @@ import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/Gr
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { getUplotHistogramChartOptions } from 'lib/uPlotLib/getUplotHistogramChartOptions';
import _noop from 'lodash-es/noop';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useEffect, useMemo, useRef } from 'react';
@ -18,6 +19,7 @@ function HistogramPanelWrapper({
graphVisibility,
isFullViewMode,
onToggleModelHandler,
onClickHandler,
}: PanelWrapperProps): JSX.Element {
const graphRef = useRef<HTMLDivElement>(null);
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
@ -67,6 +69,7 @@ function HistogramPanelWrapper({
setGraphsVisibilityStates: setGraphVisibility,
graphsVisibilityStates: graphVisibility,
mergeAllQueries: widget.mergeAllActiveQueries,
onClickHandler: onClickHandler || _noop,
}),
[
containerDimensions,
@ -78,6 +81,7 @@ function HistogramPanelWrapper({
widget.id,
widget.mergeAllActiveQueries,
widget.panelTypes,
onClickHandler,
],
);

View File

@ -17,6 +17,9 @@ function PanelWrapper({
tableProcessedDataRef,
customTooltipElement,
searchTerm,
openTracesButton,
onOpenTraceBtnClick,
customSeries,
}: PanelWrapperProps): JSX.Element {
const Component = PanelTypeVsPanelWrapper[
selectedGraph || widget.panelTypes
@ -41,6 +44,9 @@ function PanelWrapper({
tableProcessedDataRef={tableProcessedDataRef}
customTooltipElement={customTooltipElement}
searchTerm={searchTerm}
openTracesButton={openTracesButton}
onOpenTraceBtnClick={onOpenTraceBtnClick}
customSeries={customSeries}
/>
);
}

View File

@ -9,6 +9,8 @@ function TablePanelWrapper({
queryResponse,
tableProcessedDataRef,
searchTerm,
openTracesButton,
onOpenTraceBtnClick,
}: PanelWrapperProps): JSX.Element {
const panelData =
(queryResponse.data?.payload?.data?.result?.[0] as any)?.table || [];
@ -22,6 +24,8 @@ function TablePanelWrapper({
tableProcessedDataRef={tableProcessedDataRef}
sticky={widget.panelTypes === PANEL_TYPES.TABLE}
searchTerm={searchTerm}
openTracesButton={openTracesButton}
onOpenTraceBtnClick={onOpenTraceBtnClick}
// eslint-disable-next-line react/jsx-props-no-spreading
{...GRID_TABLE_CONFIG}
/>

View File

@ -33,6 +33,7 @@ function UplotPanelWrapper({
onDragSelect,
selectedGraph,
customTooltipElement,
customSeries,
}: PanelWrapperProps): JSX.Element {
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const isDarkMode = useIsDarkMode();
@ -135,6 +136,7 @@ function UplotPanelWrapper({
tzDate: (timestamp: number) =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
timezone: timezone.value,
customSeries,
}),
[
widget?.id,
@ -158,6 +160,7 @@ function UplotPanelWrapper({
hiddenGraph,
customTooltipElement,
timezone.value,
customSeries,
],
);

View File

@ -7,6 +7,7 @@ import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { QueryData } from 'types/api/widgets/getQuery';
export type PanelWrapperProps = {
queryResponse: UseQueryResult<
@ -25,6 +26,9 @@ export type PanelWrapperProps = {
tableProcessedDataRef?: React.MutableRefObject<RowData[]>;
searchTerm?: string;
customTooltipElement?: HTMLDivElement;
openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void;
customSeries?: (data: QueryData[]) => uPlot.Series[];
};
export type TooltipData = {

View File

@ -51,6 +51,7 @@ export const routeConfig: Record<string, QueryParams[]> = {
[ROUTES.WORKSPACE_LOCKED]: [QueryParams.resourceAttributes],
[ROUTES.MESSAGING_QUEUES]: [QueryParams.resourceAttributes],
[ROUTES.MESSAGING_QUEUES_DETAIL]: [QueryParams.resourceAttributes],
[ROUTES.MESSAGING_QUEUES_CELERY_TASK]: [QueryParams.resourceAttributes],
[ROUTES.INFRASTRUCTURE_MONITORING_HOSTS]: [QueryParams.resourceAttributes],
[ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES]: [
QueryParams.resourceAttributes,

View File

@ -214,6 +214,7 @@ export const routesToSkip = [
ROUTES.ALERT_OVERVIEW,
ROUTES.MESSAGING_QUEUES,
ROUTES.MESSAGING_QUEUES_DETAIL,
ROUTES.MESSAGING_QUEUES_CELERY_TASK,
ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
ROUTES.SOMETHING_WENT_WRONG,
ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES,

View File

@ -57,6 +57,7 @@ export interface GetUPlotChartOptions {
verticalLineTimestamp?: number;
tzDate?: (timestamp: number) => Date;
timezone?: string;
customSeries?: (data: QueryData[]) => uPlot.Series[];
}
/** the function converts series A , series B , series C to
@ -162,6 +163,7 @@ export const getUPlotChartOptions = ({
verticalLineTimestamp,
tzDate,
timezone,
customSeries,
}: GetUPlotChartOptions): uPlot.Options => {
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
@ -370,12 +372,14 @@ export const getUPlotChartOptions = ({
},
],
},
series: getSeries({
series: customSeries
? customSeries(apiResponse?.data?.result || [])
: getSeries({
series:
stackBarChart && isUndefined(hiddenGraph)
? series
: apiResponse?.data?.result,
widgetMetaData: apiResponse?.data.result,
? series || []
: apiResponse?.data?.result || [],
widgetMetaData: apiResponse?.data?.result || [],
graphsVisibilityStates,
panelType,
currentQuery,

View File

@ -4,12 +4,14 @@ import { themeColors } from 'constants/theme';
import { saveLegendEntriesToLocalStorage } from 'container/GridCardLayout/GridCard/FullView/utils';
import { Dimensions } from 'hooks/useDimensions';
import getLabelName from 'lib/getLabelName';
import _noop from 'lodash-es/noop';
import { Dispatch, SetStateAction } from 'react';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { QueryData } from 'types/api/widgets/getQuery';
import uPlot from 'uplot';
import onClickPlugin, { OnClickPluginOpts } from './plugins/onClickPlugin';
import tooltipPlugin from './plugins/tooltipPlugin';
import { drawStyles } from './utils/constants';
import { generateColor } from './utils/generateColor';
@ -27,6 +29,7 @@ type GetUplotHistogramChartOptionsProps = {
graphsVisibilityStates?: boolean[];
setGraphsVisibilityStates?: Dispatch<SetStateAction<boolean[]>>;
mergeAllQueries?: boolean;
onClickHandler?: OnClickPluginOpts['onClick'];
};
type GetHistogramSeriesProps = {
@ -119,6 +122,7 @@ export const getUplotHistogramChartOptions = ({
graphsVisibilityStates,
setGraphsVisibilityStates,
mergeAllQueries,
onClickHandler = _noop,
}: GetUplotHistogramChartOptionsProps): uPlot.Options =>
({
id,
@ -140,6 +144,10 @@ export const getUplotHistogramChartOptions = ({
isMergedSeries: mergeAllQueries,
isDarkMode,
}),
onClickPlugin({
onClick: onClickHandler,
apiResponse,
}),
],
scales: {
x: {

View File

@ -1,4 +1,5 @@
/* eslint-disable no-bitwise */
export function hashFn(str: string): number {
let hash = 5381;
for (let i = 0; i < str.length; i++) {

View File

@ -0,0 +1,44 @@
.celery-task-container {
.celery-task-breadcrumb {
display: flex;
padding: 0px 16px;
align-items: center;
gap: 8px;
padding-top: 10px;
padding-bottom: 8px;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
border-bottom: 1px solid var(--bg-slate-400);
}
.celery-content {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px;
align-items: flex-start;
.celery-content-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
.celery-content-header-title {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 24px;
font-style: normal;
font-weight: 700;
line-height: 32px;
margin: 0;
}
}
}
}

View File

@ -0,0 +1,40 @@
import './CeleryTask.styles.scss';
import CeleryTaskConfigOptions from 'components/CeleryTask/CeleryTaskConfigOptions/CeleryTaskConfigOptions';
import CeleryTaskDetail, {
CaptureDataProps,
} from 'components/CeleryTask/CeleryTaskDetail/CeleryTaskDetail';
import CeleryTaskGraphGrid from 'components/CeleryTask/CeleryTaskGraph/CeleryTaskGraphGrid';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useState } from 'react';
export default function CeleryTask(): JSX.Element {
const [task, setTask] = useState<CaptureDataProps | null>(null);
const onTaskClick = (captureData: CaptureDataProps): void => {
setTask(captureData);
};
return (
<div className="celery-task-container">
<div className="celery-content">
<div className="celery-content-header">
<p className="celery-content-header-title">Celery Task</p>
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
</div>
<CeleryTaskConfigOptions />
<CeleryTaskGraphGrid onClick={onTaskClick} queryEnabled={!task} />
</div>
{!!task && (
<CeleryTaskDetail
onClose={(): void => {
setTask(null);
}}
widgetData={task.widgetData}
taskData={task}
drawerOpen={!!task}
/>
)}
</div>
);
}

View File

@ -12,11 +12,15 @@ interface GetWidgetQueryProps {
title: string;
description: string;
queryData: IBuilderQuery[];
panelTypes?: PANEL_TYPES;
yAxisUnit?: string;
columnUnits?: Record<string, string>;
}
interface GetWidgetQueryPropsReturn extends GetWidgetQueryBuilderProps {
description?: string;
nullZeroValues: string;
columnUnits?: Record<string, string>;
}
export const getWidgetQueryBuilder = ({
@ -49,14 +53,15 @@ export const getWidgetQueryBuilder = ({
export function getWidgetQuery(
props: GetWidgetQueryProps,
): GetWidgetQueryPropsReturn {
const { title, description } = props;
const { title, description, panelTypes, yAxisUnit, columnUnits } = props;
return {
title,
yAxisUnit: 'none',
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: yAxisUnit || 'none',
panelTypes: panelTypes || PANEL_TYPES.TIME_SERIES,
fillSpans: false,
description,
nullZeroValues: 'zero',
columnUnits,
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],

View File

@ -93,7 +93,7 @@
}
.mq-graph {
height: 420px;
height: 420px !important;
padding: 24px 24px 0 24px;
}

View File

@ -8,7 +8,6 @@ import MessagingQueueHealthCheck from 'components/MessagingQueueHealthCheck/Mess
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { ListMinus } from 'lucide-react';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
@ -55,10 +54,6 @@ function MessagingQueues(): JSX.Element {
return (
<div className="messaging-queue-container">
<div className="messaging-breadcrumb">
<ListMinus size={16} />
{t('breadcrumb')}
</div>
<div className="messaging-header">
<div className="header-config">
{t('header')} /

View File

@ -0,0 +1,52 @@
.messaging-queues-module-container {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
.ant-tabs {
height: 100%;
}
.ant-tabs-nav {
padding: 0 8px;
margin-bottom: 0px;
&::before {
border-bottom: 1px solid var(--bg-slate-400) !important;
}
}
.ant-tabs-content-holder {
display: flex;
.ant-tabs-content {
flex: 1;
display: flex;
flex-direction: column;
.ant-tabs-tabpane {
flex: 1;
display: flex;
flex-direction: column;
}
}
}
.tab-item {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
}
.lightMode {
.messaging-queues-module-container {
.ant-tabs-nav {
&::before {
border-bottom: 1px solid var(--bg-vanilla-300) !important;
}
}
}
}

View File

@ -0,0 +1,45 @@
import './MessagingQueuesMainPage.styles.scss';
import RouteTab from 'components/RouteTab';
import { TabRoutes } from 'components/RouteTab/types';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { ListMinus, Rows3 } from 'lucide-react';
import { useLocation } from 'react-use';
import CeleryTask from '../Celery/CeleryTask/CeleryTask';
import MessagingQueues from './MessagingQueues';
export const Kafka: TabRoutes = {
Component: MessagingQueues,
name: (
<div className="tab-item">
<ListMinus size={16} /> Kafka
</div>
),
route: ROUTES.MESSAGING_QUEUES,
key: ROUTES.MESSAGING_QUEUES,
};
export const Celery: TabRoutes = {
Component: CeleryTask,
name: (
<div className="tab-item">
<Rows3 size={16} /> Celery Task
</div>
),
route: ROUTES.MESSAGING_QUEUES_CELERY_TASK,
key: ROUTES.MESSAGING_QUEUES_CELERY_TASK,
};
export default function MessagingQueuesMainPage(): JSX.Element {
const { pathname } = useLocation();
const routes: TabRoutes[] = [Kafka, Celery];
return (
<div className="messaging-queues-module-container">
<RouteTab routes={routes} activeKey={pathname} history={history} />
</div>
);
}

View File

@ -1,3 +1,3 @@
import MessagingQueues from './MessagingQueues';
import MessagingQueuesMainPage from './MessagingQueuesMainPage';
export default MessagingQueues;
export default MessagingQueuesMainPage;

View File

@ -108,4 +108,5 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
SERVICE_TOP_LEVEL_OPERATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
INFRASTRUCTURE_MONITORING_HOSTS: ['ADMIN', 'EDITOR', 'VIEWER'],
INFRASTRUCTURE_MONITORING_KUBERNETES: ['ADMIN', 'EDITOR', 'VIEWER'],
MESSAGING_QUEUES_CELERY_TASK: ['ADMIN', 'EDITOR', 'VIEWER'],
};