mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-13 03:49:00 +08:00
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:
parent
c30c882aae
commit
e83e691ef5
@ -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'
|
||||
),
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
@ -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 };
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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;
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
@ -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>
|
||||
);
|
||||
}
|
@ -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' },
|
||||
}),
|
||||
);
|
@ -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;
|
@ -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 };
|
92
frontend/src/components/CeleryTask/CeleryUtils.ts
Normal file
92
frontend/src/components/CeleryTask/CeleryUtils.ts
Normal 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);
|
||||
};
|
32
frontend/src/components/CeleryTask/useCeleryFilterOptions.ts
Normal file
32
frontend/src/components/CeleryTask/useCeleryFilterOptions.ts
Normal 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 };
|
||||
};
|
@ -41,4 +41,5 @@ export enum QueryParams {
|
||||
getStartedSource = 'getStartedSource',
|
||||
getStartedSourceService = 'getStartedSourceService',
|
||||
mqServiceView = 'mqServiceView',
|
||||
taskName = 'taskName',
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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'>;
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -11,6 +11,7 @@ export interface GetWidgetQueryBuilderProps {
|
||||
yAxisUnit?: Widgets['yAxisUnit'];
|
||||
id?: Widgets['id'];
|
||||
fillSpans?: Widgets['fillSpans'];
|
||||
columnUnits?: Widgets['columnUnits'];
|
||||
}
|
||||
|
||||
export interface NavigateToTraceProps {
|
||||
|
@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -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 = {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
@ -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: {
|
||||
|
@ -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++) {
|
||||
|
44
frontend/src/pages/Celery/CeleryTask/CeleryTask.styles.scss
Normal file
44
frontend/src/pages/Celery/CeleryTask/CeleryTask.styles.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
40
frontend/src/pages/Celery/CeleryTask/CeleryTask.tsx
Normal file
40
frontend/src/pages/Celery/CeleryTask/CeleryTask.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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: [],
|
||||
|
@ -93,7 +93,7 @@
|
||||
}
|
||||
|
||||
.mq-graph {
|
||||
height: 420px;
|
||||
height: 420px !important;
|
||||
padding: 24px 24px 0 24px;
|
||||
}
|
||||
|
||||
|
@ -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')} /
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
import MessagingQueues from './MessagingQueues';
|
||||
import MessagingQueuesMainPage from './MessagingQueuesMainPage';
|
||||
|
||||
export default MessagingQueues;
|
||||
export default MessagingQueuesMainPage;
|
||||
|
@ -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'],
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user