diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts
index 0857ea4664..55edfc54a7 100644
--- a/frontend/src/AppRoutes/pageComponents.ts
+++ b/frontend/src/AppRoutes/pageComponents.ts
@@ -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'
+ ),
+);
diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts
index dda546167f..5a259e3968 100644
--- a/frontend/src/AppRoutes/routes.ts
+++ b/frontend/src/AppRoutes/routes.ts
@@ -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,
diff --git a/frontend/src/components/CeleryTask/CeleryTaskConfigOptions/CeleryTaskConfigOptions.styles.scss b/frontend/src/components/CeleryTask/CeleryTaskConfigOptions/CeleryTaskConfigOptions.styles.scss
new file mode 100644
index 0000000000..1ffd4af3ff
--- /dev/null
+++ b/frontend/src/components/CeleryTask/CeleryTaskConfigOptions/CeleryTaskConfigOptions.styles.scss
@@ -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);
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/src/components/CeleryTask/CeleryTaskConfigOptions/CeleryTaskConfigOptions.tsx b/frontend/src/components/CeleryTask/CeleryTaskConfigOptions/CeleryTaskConfigOptions.tsx
new file mode 100644
index 0000000000..ebb514c40a
--- /dev/null
+++ b/frontend/src/components/CeleryTask/CeleryTaskConfigOptions/CeleryTaskConfigOptions.tsx
@@ -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 (
+
+
+
+ Task Name
+
+
+
+
+
+ );
+}
+
+export default CeleryTaskConfigOptions;
diff --git a/frontend/src/components/CeleryTask/CeleryTaskConfigOptions/useGetCeleryFilters.ts b/frontend/src/components/CeleryTask/CeleryTaskConfigOptions/useGetCeleryFilters.ts
new file mode 100644
index 0000000000..4d75115002
--- /dev/null
+++ b/frontend/src/components/CeleryTask/CeleryTaskConfigOptions/useGetCeleryFilters.ts
@@ -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 };
+}
diff --git a/frontend/src/components/CeleryTask/CeleryTaskDetail/CeleryTaskDetail.style.scss b/frontend/src/components/CeleryTask/CeleryTaskDetail/CeleryTaskDetail.style.scss
new file mode 100644
index 0000000000..7275b00cfb
--- /dev/null
+++ b/frontend/src/components/CeleryTask/CeleryTaskDetail/CeleryTaskDetail.style.scss
@@ -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;
+ }
+ }
+}
diff --git a/frontend/src/components/CeleryTask/CeleryTaskDetail/CeleryTaskDetail.tsx b/frontend/src/components/CeleryTask/CeleryTaskDetail/CeleryTaskDetail.tsx
new file mode 100644
index 0000000000..20989c57fc
--- /dev/null
+++ b/frontend/src/components/CeleryTask/CeleryTaskDetail/CeleryTaskDetail.tsx
@@ -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,
+): 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 (
+
+ {`Details - ${taskData.entity}`}
+
+
+ {`${formatTimestamp(taskData.timeRange[0])} ${
+ taskData.timeRange[1]
+ ? `- ${formatTimestamp(taskData.timeRange[1])}`
+ : ''
+ }`}
+
+
+
{taskData.value}
+
+
+ }
+ 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={}
+ footer={
+ {`Total Task: ${totalTask}`}
+ }
+ >
+
+
+ );
+}
diff --git a/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskBar.tsx b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskBar.tsx
new file mode 100644
index 0000000000..278cb31494
--- /dev/null
+++ b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskBar.tsx
@@ -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(
+ (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.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 (
+
+
+
+ {barState === CeleryTaskState.All && (
+
+ onGraphClick(celerySlowestTasksTableWidgetData, ...args)
+ }
+ customSeries={customSeries}
+ />
+ )}
+ {barState === CeleryTaskState.Failed && (
+
+ onGraphClick(celeryFailedTasksTableWidgetData, ...args)
+ }
+ customSeries={customSeries}
+ />
+ )}
+ {barState === CeleryTaskState.Retry && (
+
+ onGraphClick(celeryRetryTasksTableWidgetData, ...args)
+ }
+ customSeries={customSeries}
+ />
+ )}
+ {barState === CeleryTaskState.Successful && (
+
+ onGraphClick(celerySuccessTasksTableWidgetData, ...args)
+ }
+ customSeries={customSeries}
+ />
+ )}
+
+
+ );
+}
+
+CeleryTaskBar.defaultProps = {
+ onClick: (): void => {},
+};
+
+export default CeleryTaskBar;
diff --git a/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskGraph.style.scss b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskGraph.style.scss
new file mode 100644
index 0000000000..dfb66e2f49
--- /dev/null
+++ b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskGraph.style.scss
@@ -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);
+ }
+}
diff --git a/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskGraph.tsx b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskGraph.tsx
new file mode 100644
index 0000000000..340edb813b
--- /dev/null
+++ b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskGraph.tsx
@@ -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 (
+
+ {
+ 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}
+ />
+
+ );
+}
+
+CeleryTaskGraph.defaultProps = {
+ getGraphData: undefined,
+ onClick: undefined,
+ rightPanelTitle: undefined,
+ panelType: PANEL_TYPES.TIME_SERIES,
+ openTracesButton: false,
+ onOpenTraceBtnClick: undefined,
+ applyCeleryTaskFilter: false,
+};
+
+export default CeleryTaskGraph;
diff --git a/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskGraphGrid.tsx b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskGraphGrid.tsx
new file mode 100644
index 0000000000..e4accd0d5a
--- /dev/null
+++ b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskGraphGrid.tsx
@@ -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(
+ (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 (
+
+
+
+
+
+
+
+
+
+
+ {bottomWidgetData.map((widgetData, index) => (
+
+ ))}
+
+
+ );
+}
diff --git a/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskGraphUtils.ts b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskGraphUtils.ts
new file mode 100644
index 0000000000..2e963c9740
--- /dev/null
+++ b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskGraphUtils.ts
@@ -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' },
+ }),
+ );
diff --git a/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskLatencyGraph.tsx b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskLatencyGraph.tsx
new file mode 100644
index 0000000000..b3d8ac1c1a
--- /dev/null
+++ b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskLatencyGraph.tsx
@@ -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.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(
+ (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 (
+
+
+ {tabs.map((tab, index) => (
+ 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}
+ >
+
+
+ {tab.label.toUpperCase()}
+
+
+ {tab.key === graphState && (
+
+ )}
+
+ ))}
+
+
+ {graphState === CeleryTaskGraphState.P99 && (
+
+ )}
+
+ {graphState === CeleryTaskGraphState.P95 && (
+
+ )}
+ {graphState === CeleryTaskGraphState.P90 && (
+
+ )}
+
+
+ );
+}
+
+export default CeleryTaskLatencyGraph;
diff --git a/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskStateGraphConfig.tsx b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskStateGraphConfig.tsx
new file mode 100644
index 0000000000..5ae07a341d
--- /dev/null
+++ b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskStateGraphConfig.tsx
@@ -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>;
+ 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 (
+
+ {tabs.map((tab, index) => (
+ 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}
+ >
+
+ {tab.key === barState && }
+
+ ))}
+
+ );
+}
+
+export { CeleryTaskStateGraphConfig };
diff --git a/frontend/src/components/CeleryTask/CeleryUtils.ts b/frontend/src/components/CeleryTask/CeleryUtils.ts
new file mode 100644
index 0000000000..93f1ef7629
--- /dev/null
+++ b/frontend/src/components/CeleryTask/CeleryUtils.ts
@@ -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,
+ location: Location,
+ 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);
+};
diff --git a/frontend/src/components/CeleryTask/useCeleryFilterOptions.ts b/frontend/src/components/CeleryTask/useCeleryFilterOptions.ts
new file mode 100644
index 0000000000..dcc46a771d
--- /dev/null
+++ b/frontend/src/components/CeleryTask/useCeleryFilterOptions.ts
@@ -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('');
+ 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 };
+};
diff --git a/frontend/src/constants/query.ts b/frontend/src/constants/query.ts
index 56fbd737b5..757d49b99b 100644
--- a/frontend/src/constants/query.ts
+++ b/frontend/src/constants/query.ts
@@ -41,4 +41,5 @@ export enum QueryParams {
getStartedSource = 'getStartedSource',
getStartedSourceService = 'getStartedSourceService',
mqServiceView = 'mqServiceView',
+ taskName = 'taskName',
}
diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts
index a5a5212eb7..a4c279937b 100644
--- a/frontend/src/constants/routes.ts
+++ b/frontend/src/constants/routes.ts
@@ -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;
diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx
index 8bb91d48c3..c5143752b4 100644
--- a/frontend/src/container/AppLayout/index.tsx
+++ b/frontend/src/container/AppLayout/index.tsx
@@ -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';
diff --git a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx
index 6cb3581749..073c331b17 100644
--- a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx
+++ b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx
@@ -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}
/>
)}
diff --git a/frontend/src/container/GridCardLayout/GridCard/index.tsx b/frontend/src/container/GridCardLayout/GridCard/index.tsx
index 09b6b65e1a..86a4d81fe8 100644
--- a/frontend/src/container/GridCardLayout/GridCard/index.tsx
+++ b/frontend/src/container/GridCardLayout/GridCard/index.tsx
@@ -37,6 +37,10 @@ function GridCardGraph({
onDragSelect,
customTooltipElement,
dataAvailable,
+ getGraphData,
+ openTracesButton,
+ onOpenTraceBtnClick,
+ customSeries,
}: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState();
@@ -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}
/>
)}
diff --git a/frontend/src/container/GridCardLayout/GridCard/types.ts b/frontend/src/container/GridCardLayout/GridCard/types.ts
index 05d3368096..54c9d44c1c 100644
--- a/frontend/src/container/GridCardLayout/GridCard/types.ts
+++ b/frontend/src/container/GridCardLayout/GridCard/types.ts
@@ -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 {
diff --git a/frontend/src/container/GridTableComponent/GridTableComponent.styles.scss b/frontend/src/container/GridTableComponent/GridTableComponent.styles.scss
index 80491e991a..bdc21e164d 100644
--- a/frontend/src/container/GridTableComponent/GridTableComponent.styles.scss
+++ b/frontend/src/container/GridTableComponent/GridTableComponent.styles.scss
@@ -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;
+}
diff --git a/frontend/src/container/GridTableComponent/index.tsx b/frontend/src/container/GridTableComponent/index.tsx
index 63084be5f3..db3d72a974 100644
--- a/frontend/src/container/GridTableComponent/index.tsx
+++ b/frontend/src/container/GridTableComponent/index.tsx
@@ -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 = (
+
+ );
+ if (index !== 0) {
+ return {LineClampedTextComponent}
;
+ }
+
+ return (
+
+ {LineClampedTextComponent}
+
+
+
+
+ );
+ },
+ })),
+ [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 => ({
+ onClick: (): void => {
+ onOpenTraceBtnClick?.(record);
+ },
+ })
+ : undefined
+ }
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
diff --git a/frontend/src/container/GridTableComponent/types.ts b/frontend/src/container/GridTableComponent/types.ts
index 883e280b38..7e930d51c3 100644
--- a/frontend/src/container/GridTableComponent/types.ts
+++ b/frontend/src/container/GridTableComponent/types.ts
@@ -15,6 +15,8 @@ export type GridTableComponentProps = {
tableProcessedDataRef?: React.MutableRefObject;
sticky?: TableProps['sticky'];
searchTerm?: string;
+ openTracesButton?: boolean;
+ onOpenTraceBtnClick?: (record: RowData) => void;
} & Pick &
Omit, 'columns' | 'dataSource'>;
diff --git a/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts b/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts
index 672bc11812..15eff76f8b 100644
--- a/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts
+++ b/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts
@@ -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,
});
diff --git a/frontend/src/container/MetricsApplication/types.ts b/frontend/src/container/MetricsApplication/types.ts
index 4b4cc2f4f1..60e7f26934 100644
--- a/frontend/src/container/MetricsApplication/types.ts
+++ b/frontend/src/container/MetricsApplication/types.ts
@@ -11,6 +11,7 @@ export interface GetWidgetQueryBuilderProps {
yAxisUnit?: Widgets['yAxisUnit'];
id?: Widgets['id'];
fillSpans?: Widgets['fillSpans'];
+ columnUnits?: Widgets['columnUnits'];
}
export interface NavigateToTraceProps {
diff --git a/frontend/src/container/PanelWrapper/HistogramPanelWrapper.tsx b/frontend/src/container/PanelWrapper/HistogramPanelWrapper.tsx
index a6c80b01bd..9f69a7eef9 100644
--- a/frontend/src/container/PanelWrapper/HistogramPanelWrapper.tsx
+++ b/frontend/src/container/PanelWrapper/HistogramPanelWrapper.tsx
@@ -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(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,
],
);
diff --git a/frontend/src/container/PanelWrapper/PanelWrapper.tsx b/frontend/src/container/PanelWrapper/PanelWrapper.tsx
index 2f5b35485e..99a1f23e8d 100644
--- a/frontend/src/container/PanelWrapper/PanelWrapper.tsx
+++ b/frontend/src/container/PanelWrapper/PanelWrapper.tsx
@@ -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}
/>
);
}
diff --git a/frontend/src/container/PanelWrapper/TablePanelWrapper.tsx b/frontend/src/container/PanelWrapper/TablePanelWrapper.tsx
index c5222e8d53..58ddaef5a8 100644
--- a/frontend/src/container/PanelWrapper/TablePanelWrapper.tsx
+++ b/frontend/src/container/PanelWrapper/TablePanelWrapper.tsx
@@ -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}
/>
diff --git a/frontend/src/container/PanelWrapper/UplotPanelWrapper.tsx b/frontend/src/container/PanelWrapper/UplotPanelWrapper.tsx
index b297668cc4..07589af9e9 100644
--- a/frontend/src/container/PanelWrapper/UplotPanelWrapper.tsx
+++ b/frontend/src/container/PanelWrapper/UplotPanelWrapper.tsx
@@ -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,
],
);
diff --git a/frontend/src/container/PanelWrapper/panelWrapper.types.ts b/frontend/src/container/PanelWrapper/panelWrapper.types.ts
index 4778ffdb97..e1983a5b69 100644
--- a/frontend/src/container/PanelWrapper/panelWrapper.types.ts
+++ b/frontend/src/container/PanelWrapper/panelWrapper.types.ts
@@ -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;
searchTerm?: string;
customTooltipElement?: HTMLDivElement;
+ openTracesButton?: boolean;
+ onOpenTraceBtnClick?: (record: RowData) => void;
+ customSeries?: (data: QueryData[]) => uPlot.Series[];
};
export type TooltipData = {
diff --git a/frontend/src/container/SideNav/config.ts b/frontend/src/container/SideNav/config.ts
index 6f8cb07f1f..8e627e0bdd 100644
--- a/frontend/src/container/SideNav/config.ts
+++ b/frontend/src/container/SideNav/config.ts
@@ -51,6 +51,7 @@ export const routeConfig: Record = {
[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,
diff --git a/frontend/src/container/TopNav/DateTimeSelectionV2/config.ts b/frontend/src/container/TopNav/DateTimeSelectionV2/config.ts
index a0ac267052..10e71b3e0d 100644
--- a/frontend/src/container/TopNav/DateTimeSelectionV2/config.ts
+++ b/frontend/src/container/TopNav/DateTimeSelectionV2/config.ts
@@ -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,
diff --git a/frontend/src/lib/uPlotLib/getUplotChartOptions.ts b/frontend/src/lib/uPlotLib/getUplotChartOptions.ts
index ad8bf682e7..df7d6547e3 100644
--- a/frontend/src/lib/uPlotLib/getUplotChartOptions.ts
+++ b/frontend/src/lib/uPlotLib/getUplotChartOptions.ts
@@ -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,19 +372,21 @@ export const getUPlotChartOptions = ({
},
],
},
- series: getSeries({
- series:
- stackBarChart && isUndefined(hiddenGraph)
- ? series
- : apiResponse?.data?.result,
- widgetMetaData: apiResponse?.data.result,
- graphsVisibilityStates,
- panelType,
- currentQuery,
- stackBarChart,
- hiddenGraph,
- isDarkMode,
- }),
+ series: customSeries
+ ? customSeries(apiResponse?.data?.result || [])
+ : getSeries({
+ series:
+ stackBarChart && isUndefined(hiddenGraph)
+ ? series || []
+ : apiResponse?.data?.result || [],
+ widgetMetaData: apiResponse?.data?.result || [],
+ graphsVisibilityStates,
+ panelType,
+ currentQuery,
+ stackBarChart,
+ hiddenGraph,
+ isDarkMode,
+ }),
axes: getAxes(isDarkMode, yAxisUnit),
};
};
diff --git a/frontend/src/lib/uPlotLib/getUplotHistogramChartOptions.ts b/frontend/src/lib/uPlotLib/getUplotHistogramChartOptions.ts
index 2ff0f3051e..81750ff857 100644
--- a/frontend/src/lib/uPlotLib/getUplotHistogramChartOptions.ts
+++ b/frontend/src/lib/uPlotLib/getUplotHistogramChartOptions.ts
@@ -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>;
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: {
diff --git a/frontend/src/lib/uPlotLib/utils/generateColor.ts b/frontend/src/lib/uPlotLib/utils/generateColor.ts
index c58fde4e62..8e7b96997d 100644
--- a/frontend/src/lib/uPlotLib/utils/generateColor.ts
+++ b/frontend/src/lib/uPlotLib/utils/generateColor.ts
@@ -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++) {
diff --git a/frontend/src/pages/Celery/CeleryTask/CeleryTask.styles.scss b/frontend/src/pages/Celery/CeleryTask/CeleryTask.styles.scss
new file mode 100644
index 0000000000..03d53af448
--- /dev/null
+++ b/frontend/src/pages/Celery/CeleryTask/CeleryTask.styles.scss
@@ -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;
+ }
+ }
+ }
+}
diff --git a/frontend/src/pages/Celery/CeleryTask/CeleryTask.tsx b/frontend/src/pages/Celery/CeleryTask/CeleryTask.tsx
new file mode 100644
index 0000000000..fccd934a61
--- /dev/null
+++ b/frontend/src/pages/Celery/CeleryTask/CeleryTask.tsx
@@ -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(null);
+
+ const onTaskClick = (captureData: CaptureDataProps): void => {
+ setTask(captureData);
+ };
+
+ return (
+
+
+ {!!task && (
+
{
+ setTask(null);
+ }}
+ widgetData={task.widgetData}
+ taskData={task}
+ drawerOpen={!!task}
+ />
+ )}
+
+ );
+}
diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MetricPage/MetricPageUtil.ts b/frontend/src/pages/MessagingQueues/MQDetails/MetricPage/MetricPageUtil.ts
index e52b4e2d03..f2f84c0f88 100644
--- a/frontend/src/pages/MessagingQueues/MQDetails/MetricPage/MetricPageUtil.ts
+++ b/frontend/src/pages/MessagingQueues/MQDetails/MetricPage/MetricPageUtil.ts
@@ -12,11 +12,15 @@ interface GetWidgetQueryProps {
title: string;
description: string;
queryData: IBuilderQuery[];
+ panelTypes?: PANEL_TYPES;
+ yAxisUnit?: string;
+ columnUnits?: Record;
}
interface GetWidgetQueryPropsReturn extends GetWidgetQueryBuilderProps {
description?: string;
nullZeroValues: string;
+ columnUnits?: Record;
}
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: [],
diff --git a/frontend/src/pages/MessagingQueues/MessagingQueues.styles.scss b/frontend/src/pages/MessagingQueues/MessagingQueues.styles.scss
index 9edcd928a3..6a37ed959b 100644
--- a/frontend/src/pages/MessagingQueues/MessagingQueues.styles.scss
+++ b/frontend/src/pages/MessagingQueues/MessagingQueues.styles.scss
@@ -93,7 +93,7 @@
}
.mq-graph {
- height: 420px;
+ height: 420px !important;
padding: 24px 24px 0 24px;
}
diff --git a/frontend/src/pages/MessagingQueues/MessagingQueues.tsx b/frontend/src/pages/MessagingQueues/MessagingQueues.tsx
index a7e8681e9c..9829ffc6e3 100644
--- a/frontend/src/pages/MessagingQueues/MessagingQueues.tsx
+++ b/frontend/src/pages/MessagingQueues/MessagingQueues.tsx
@@ -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 (
-
-
- {t('breadcrumb')}
-
{t('header')} /
diff --git a/frontend/src/pages/MessagingQueues/MessagingQueuesMainPage.styles.scss b/frontend/src/pages/MessagingQueues/MessagingQueuesMainPage.styles.scss
new file mode 100644
index 0000000000..e1cda241d6
--- /dev/null
+++ b/frontend/src/pages/MessagingQueues/MessagingQueuesMainPage.styles.scss
@@ -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;
+ }
+ }
+ }
+}
diff --git a/frontend/src/pages/MessagingQueues/MessagingQueuesMainPage.tsx b/frontend/src/pages/MessagingQueues/MessagingQueuesMainPage.tsx
new file mode 100644
index 0000000000..8fe5976318
--- /dev/null
+++ b/frontend/src/pages/MessagingQueues/MessagingQueuesMainPage.tsx
@@ -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: (
+
+ Kafka
+
+ ),
+ route: ROUTES.MESSAGING_QUEUES,
+ key: ROUTES.MESSAGING_QUEUES,
+};
+
+export const Celery: TabRoutes = {
+ Component: CeleryTask,
+ name: (
+
+ Celery Task
+
+ ),
+ 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 (
+
+
+
+ );
+}
diff --git a/frontend/src/pages/MessagingQueues/index.tsx b/frontend/src/pages/MessagingQueues/index.tsx
index cc59152b17..fbde0ac648 100644
--- a/frontend/src/pages/MessagingQueues/index.tsx
+++ b/frontend/src/pages/MessagingQueues/index.tsx
@@ -1,3 +1,3 @@
-import MessagingQueues from './MessagingQueues';
+import MessagingQueuesMainPage from './MessagingQueuesMainPage';
-export default MessagingQueues;
+export default MessagingQueuesMainPage;
diff --git a/frontend/src/utils/permission/index.ts b/frontend/src/utils/permission/index.ts
index 4d0a04b569..a664477538 100644
--- a/frontend/src/utils/permission/index.ts
+++ b/frontend/src/utils/permission/index.ts
@@ -108,4 +108,5 @@ export const routePermission: Record
= {
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'],
};