mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-13 05:29:04 +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'),
|
import(/* webpackChunkName: "MessagingQueues" */ 'pages/MessagingQueues'),
|
||||||
);
|
);
|
||||||
@ -247,3 +247,10 @@ export const InfrastructureMonitoring = Loadable(
|
|||||||
/* webpackChunkName: "InfrastructureMonitoring" */ 'pages/InfrastructureMonitoring'
|
/* 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 ROUTES from 'constants/routes';
|
||||||
|
import MessagingQueues from 'pages/MessagingQueues';
|
||||||
import { RouteProps } from 'react-router-dom';
|
import { RouteProps } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -27,7 +28,6 @@ import {
|
|||||||
LogsExplorer,
|
LogsExplorer,
|
||||||
LogsIndexToFields,
|
LogsIndexToFields,
|
||||||
LogsSaveViews,
|
LogsSaveViews,
|
||||||
MessagingQueues,
|
|
||||||
MQDetailPage,
|
MQDetailPage,
|
||||||
MySettings,
|
MySettings,
|
||||||
NewDashboardPage,
|
NewDashboardPage,
|
||||||
@ -401,6 +401,13 @@ const routes: AppRoutes[] = [
|
|||||||
key: 'MESSAGING_QUEUES',
|
key: 'MESSAGING_QUEUES',
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.MESSAGING_QUEUES_CELERY_TASK,
|
||||||
|
exact: true,
|
||||||
|
component: MessagingQueues,
|
||||||
|
key: 'MESSAGING_QUEUES_CELERY_TASK',
|
||||||
|
isPrivate: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ROUTES.MESSAGING_QUEUES_DETAIL,
|
path: ROUTES.MESSAGING_QUEUES_DETAIL,
|
||||||
exact: true,
|
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',
|
getStartedSource = 'getStartedSource',
|
||||||
getStartedSourceService = 'getStartedSourceService',
|
getStartedSourceService = 'getStartedSourceService',
|
||||||
mqServiceView = 'mqServiceView',
|
mqServiceView = 'mqServiceView',
|
||||||
|
taskName = 'taskName',
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,7 @@ const ROUTES = {
|
|||||||
MESSAGING_QUEUES_DETAIL: '/messaging-queues/detail',
|
MESSAGING_QUEUES_DETAIL: '/messaging-queues/detail',
|
||||||
INFRASTRUCTURE_MONITORING_HOSTS: '/infrastructure-monitoring/hosts',
|
INFRASTRUCTURE_MONITORING_HOSTS: '/infrastructure-monitoring/hosts',
|
||||||
INFRASTRUCTURE_MONITORING_KUBERNETES: '/infrastructure-monitoring/kubernetes',
|
INFRASTRUCTURE_MONITORING_KUBERNETES: '/infrastructure-monitoring/kubernetes',
|
||||||
|
MESSAGING_QUEUES_CELERY_TASK: '/messaging-queues/celery-task',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default ROUTES;
|
export default ROUTES;
|
||||||
|
@ -287,7 +287,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
|
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
|
||||||
|
|
||||||
const isMessagingQueues = (): boolean =>
|
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 isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
|
||||||
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';
|
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';
|
||||||
|
@ -45,6 +45,9 @@ function WidgetGraphComponent({
|
|||||||
onClickHandler,
|
onClickHandler,
|
||||||
onDragSelect,
|
onDragSelect,
|
||||||
customTooltipElement,
|
customTooltipElement,
|
||||||
|
openTracesButton,
|
||||||
|
onOpenTraceBtnClick,
|
||||||
|
customSeries,
|
||||||
}: WidgetGraphComponentProps): JSX.Element {
|
}: WidgetGraphComponentProps): JSX.Element {
|
||||||
const [deleteModal, setDeleteModal] = useState(false);
|
const [deleteModal, setDeleteModal] = useState(false);
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
@ -333,6 +336,9 @@ function WidgetGraphComponent({
|
|||||||
tableProcessedDataRef={tableProcessedDataRef}
|
tableProcessedDataRef={tableProcessedDataRef}
|
||||||
customTooltipElement={customTooltipElement}
|
customTooltipElement={customTooltipElement}
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
|
openTracesButton={openTracesButton}
|
||||||
|
onOpenTraceBtnClick={onOpenTraceBtnClick}
|
||||||
|
customSeries={customSeries}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -37,6 +37,10 @@ function GridCardGraph({
|
|||||||
onDragSelect,
|
onDragSelect,
|
||||||
customTooltipElement,
|
customTooltipElement,
|
||||||
dataAvailable,
|
dataAvailable,
|
||||||
|
getGraphData,
|
||||||
|
openTracesButton,
|
||||||
|
onOpenTraceBtnClick,
|
||||||
|
customSeries,
|
||||||
}: GridCardGraphProps): JSX.Element {
|
}: GridCardGraphProps): JSX.Element {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [errorMessage, setErrorMessage] = useState<string>();
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
@ -209,6 +213,7 @@ function GridCardGraph({
|
|||||||
dataAvailable?.(
|
dataAvailable?.(
|
||||||
isDataAvailableByPanelType(data?.payload?.data, widget?.panelTypes),
|
isDataAvailableByPanelType(data?.payload?.data, widget?.panelTypes),
|
||||||
);
|
);
|
||||||
|
getGraphData?.(data?.payload?.data);
|
||||||
setDashboardQueryRangeCalled(true);
|
setDashboardQueryRangeCalled(true);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -248,6 +253,9 @@ function GridCardGraph({
|
|||||||
onClickHandler={onClickHandler}
|
onClickHandler={onClickHandler}
|
||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
customTooltipElement={customTooltipElement}
|
customTooltipElement={customTooltipElement}
|
||||||
|
openTracesButton={openTracesButton}
|
||||||
|
onOpenTraceBtnClick={onOpenTraceBtnClick}
|
||||||
|
customSeries={customSeries}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { ToggleGraphProps } from 'components/Graph/types';
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||||
|
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||||
import { Dispatch, MutableRefObject, ReactNode, SetStateAction } from 'react';
|
import { Dispatch, MutableRefObject, ReactNode, SetStateAction } from 'react';
|
||||||
import { UseQueryResult } from 'react-query';
|
import { UseQueryResult } from 'react-query';
|
||||||
import { SuccessResponse } from 'types/api';
|
import { SuccessResponse } from 'types/api';
|
||||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
import { QueryData } from 'types/api/widgets/getQuery';
|
||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
import { MenuItemKeys } from '../WidgetHeader/contants';
|
import { MenuItemKeys } from '../WidgetHeader/contants';
|
||||||
@ -32,6 +34,9 @@ export interface WidgetGraphComponentProps {
|
|||||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||||
onDragSelect: (start: number, end: number) => void;
|
onDragSelect: (start: number, end: number) => void;
|
||||||
customTooltipElement?: HTMLDivElement;
|
customTooltipElement?: HTMLDivElement;
|
||||||
|
openTracesButton?: boolean;
|
||||||
|
onOpenTraceBtnClick?: (record: RowData) => void;
|
||||||
|
customSeries?: (data: QueryData[]) => uPlot.Series[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GridCardGraphProps {
|
export interface GridCardGraphProps {
|
||||||
@ -45,6 +50,10 @@ export interface GridCardGraphProps {
|
|||||||
onDragSelect: (start: number, end: number) => void;
|
onDragSelect: (start: number, end: number) => void;
|
||||||
customTooltipElement?: HTMLDivElement;
|
customTooltipElement?: HTMLDivElement;
|
||||||
dataAvailable?: (isDataAvailable: boolean) => void;
|
dataAvailable?: (isDataAvailable: boolean) => void;
|
||||||
|
getGraphData?: (graphData?: MetricRangePayloadProps['data']) => void;
|
||||||
|
openTracesButton?: boolean;
|
||||||
|
onOpenTraceBtnClick?: (record: RowData) => void;
|
||||||
|
customSeries?: (data: QueryData[]) => uPlot.Series[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetGraphVisibilityStateOnLegendClickProps {
|
export interface GetGraphVisibilityStateOnLegendClickProps {
|
||||||
|
@ -3,3 +3,14 @@
|
|||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
overflow-y: auto;
|
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 './GridTableComponent.styles.scss';
|
||||||
|
|
||||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||||
@ -7,9 +8,11 @@ import { Events } from 'constants/events';
|
|||||||
import { QueryTable } from 'container/QueryTable';
|
import { QueryTable } from 'container/QueryTable';
|
||||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
import { cloneDeep, get, isEmpty } from 'lodash-es';
|
import { cloneDeep, get, isEmpty } from 'lodash-es';
|
||||||
|
import { Compass } from 'lucide-react';
|
||||||
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
||||||
import { memo, ReactNode, useCallback, useEffect, useMemo } from 'react';
|
import { memo, ReactNode, useCallback, useEffect, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import styled from 'styled-components';
|
||||||
import { eventEmitter } from 'utils/getEventEmitter';
|
import { eventEmitter } from 'utils/getEventEmitter';
|
||||||
|
|
||||||
import { WrapperStyled } from './styles';
|
import { WrapperStyled } from './styles';
|
||||||
@ -20,6 +23,23 @@ import {
|
|||||||
TableData,
|
TableData,
|
||||||
} from './utils';
|
} 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({
|
function GridTableComponent({
|
||||||
data,
|
data,
|
||||||
query,
|
query,
|
||||||
@ -27,6 +47,8 @@ function GridTableComponent({
|
|||||||
columnUnits,
|
columnUnits,
|
||||||
tableProcessedDataRef,
|
tableProcessedDataRef,
|
||||||
sticky,
|
sticky,
|
||||||
|
openTracesButton,
|
||||||
|
onOpenTraceBtnClick,
|
||||||
...props
|
...props
|
||||||
}: GridTableComponentProps): JSX.Element {
|
}: GridTableComponentProps): JSX.Element {
|
||||||
const { t } = useTranslation(['valueGraph']);
|
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(() => {
|
useEffect(() => {
|
||||||
eventEmitter.emit(Events.TABLE_COLUMNS_DATA, {
|
eventEmitter.emit(Events.TABLE_COLUMNS_DATA, {
|
||||||
columns: newColumnData,
|
columns: newColumnData,
|
||||||
@ -174,9 +232,18 @@ function GridTableComponent({
|
|||||||
query={query}
|
query={query}
|
||||||
queryTableData={data}
|
queryTableData={data}
|
||||||
loading={false}
|
loading={false}
|
||||||
columns={newColumnData}
|
columns={openTracesButton ? columnDataWithOpenTracesButton : newColumnData}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
sticky={sticky}
|
sticky={sticky}
|
||||||
|
onRow={
|
||||||
|
openTracesButton
|
||||||
|
? (record): React.HTMLAttributes<HTMLElement> => ({
|
||||||
|
onClick: (): void => {
|
||||||
|
onOpenTraceBtnClick?.(record);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
@ -15,6 +15,8 @@ export type GridTableComponentProps = {
|
|||||||
tableProcessedDataRef?: React.MutableRefObject<RowData[]>;
|
tableProcessedDataRef?: React.MutableRefObject<RowData[]>;
|
||||||
sticky?: TableProps<RowData>['sticky'];
|
sticky?: TableProps<RowData>['sticky'];
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
|
openTracesButton?: boolean;
|
||||||
|
onOpenTraceBtnClick?: (record: RowData) => void;
|
||||||
} & Pick<LogsExplorerTableProps, 'data'> &
|
} & Pick<LogsExplorerTableProps, 'data'> &
|
||||||
Omit<TableProps<RowData>, 'columns' | 'dataSource'>;
|
Omit<TableProps<RowData>, 'columns' | 'dataSource'>;
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ export const getWidgetQueryBuilder = ({
|
|||||||
yAxisUnit = '',
|
yAxisUnit = '',
|
||||||
fillSpans = false,
|
fillSpans = false,
|
||||||
id,
|
id,
|
||||||
|
columnUnits,
|
||||||
}: GetWidgetQueryBuilderProps): Widgets => ({
|
}: GetWidgetQueryBuilderProps): Widgets => ({
|
||||||
description: '',
|
description: '',
|
||||||
id: id || v4(),
|
id: id || v4(),
|
||||||
@ -26,4 +27,5 @@ export const getWidgetQueryBuilder = ({
|
|||||||
selectedLogFields: [],
|
selectedLogFields: [],
|
||||||
selectedTracesFields: [],
|
selectedTracesFields: [],
|
||||||
fillSpans,
|
fillSpans,
|
||||||
|
columnUnits,
|
||||||
});
|
});
|
||||||
|
@ -11,6 +11,7 @@ export interface GetWidgetQueryBuilderProps {
|
|||||||
yAxisUnit?: Widgets['yAxisUnit'];
|
yAxisUnit?: Widgets['yAxisUnit'];
|
||||||
id?: Widgets['id'];
|
id?: Widgets['id'];
|
||||||
fillSpans?: Widgets['fillSpans'];
|
fillSpans?: Widgets['fillSpans'];
|
||||||
|
columnUnits?: Widgets['columnUnits'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NavigateToTraceProps {
|
export interface NavigateToTraceProps {
|
||||||
|
@ -5,6 +5,7 @@ import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/Gr
|
|||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useResizeObserver } from 'hooks/useDimensions';
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
import { getUplotHistogramChartOptions } from 'lib/uPlotLib/getUplotHistogramChartOptions';
|
import { getUplotHistogramChartOptions } from 'lib/uPlotLib/getUplotHistogramChartOptions';
|
||||||
|
import _noop from 'lodash-es/noop';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { useEffect, useMemo, useRef } from 'react';
|
import { useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
@ -18,6 +19,7 @@ function HistogramPanelWrapper({
|
|||||||
graphVisibility,
|
graphVisibility,
|
||||||
isFullViewMode,
|
isFullViewMode,
|
||||||
onToggleModelHandler,
|
onToggleModelHandler,
|
||||||
|
onClickHandler,
|
||||||
}: PanelWrapperProps): JSX.Element {
|
}: PanelWrapperProps): JSX.Element {
|
||||||
const graphRef = useRef<HTMLDivElement>(null);
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
|
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
|
||||||
@ -67,6 +69,7 @@ function HistogramPanelWrapper({
|
|||||||
setGraphsVisibilityStates: setGraphVisibility,
|
setGraphsVisibilityStates: setGraphVisibility,
|
||||||
graphsVisibilityStates: graphVisibility,
|
graphsVisibilityStates: graphVisibility,
|
||||||
mergeAllQueries: widget.mergeAllActiveQueries,
|
mergeAllQueries: widget.mergeAllActiveQueries,
|
||||||
|
onClickHandler: onClickHandler || _noop,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
containerDimensions,
|
containerDimensions,
|
||||||
@ -78,6 +81,7 @@ function HistogramPanelWrapper({
|
|||||||
widget.id,
|
widget.id,
|
||||||
widget.mergeAllActiveQueries,
|
widget.mergeAllActiveQueries,
|
||||||
widget.panelTypes,
|
widget.panelTypes,
|
||||||
|
onClickHandler,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -17,6 +17,9 @@ function PanelWrapper({
|
|||||||
tableProcessedDataRef,
|
tableProcessedDataRef,
|
||||||
customTooltipElement,
|
customTooltipElement,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
|
openTracesButton,
|
||||||
|
onOpenTraceBtnClick,
|
||||||
|
customSeries,
|
||||||
}: PanelWrapperProps): JSX.Element {
|
}: PanelWrapperProps): JSX.Element {
|
||||||
const Component = PanelTypeVsPanelWrapper[
|
const Component = PanelTypeVsPanelWrapper[
|
||||||
selectedGraph || widget.panelTypes
|
selectedGraph || widget.panelTypes
|
||||||
@ -41,6 +44,9 @@ function PanelWrapper({
|
|||||||
tableProcessedDataRef={tableProcessedDataRef}
|
tableProcessedDataRef={tableProcessedDataRef}
|
||||||
customTooltipElement={customTooltipElement}
|
customTooltipElement={customTooltipElement}
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
|
openTracesButton={openTracesButton}
|
||||||
|
onOpenTraceBtnClick={onOpenTraceBtnClick}
|
||||||
|
customSeries={customSeries}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,8 @@ function TablePanelWrapper({
|
|||||||
queryResponse,
|
queryResponse,
|
||||||
tableProcessedDataRef,
|
tableProcessedDataRef,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
|
openTracesButton,
|
||||||
|
onOpenTraceBtnClick,
|
||||||
}: PanelWrapperProps): JSX.Element {
|
}: PanelWrapperProps): JSX.Element {
|
||||||
const panelData =
|
const panelData =
|
||||||
(queryResponse.data?.payload?.data?.result?.[0] as any)?.table || [];
|
(queryResponse.data?.payload?.data?.result?.[0] as any)?.table || [];
|
||||||
@ -22,6 +24,8 @@ function TablePanelWrapper({
|
|||||||
tableProcessedDataRef={tableProcessedDataRef}
|
tableProcessedDataRef={tableProcessedDataRef}
|
||||||
sticky={widget.panelTypes === PANEL_TYPES.TABLE}
|
sticky={widget.panelTypes === PANEL_TYPES.TABLE}
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
|
openTracesButton={openTracesButton}
|
||||||
|
onOpenTraceBtnClick={onOpenTraceBtnClick}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...GRID_TABLE_CONFIG}
|
{...GRID_TABLE_CONFIG}
|
||||||
/>
|
/>
|
||||||
|
@ -33,6 +33,7 @@ function UplotPanelWrapper({
|
|||||||
onDragSelect,
|
onDragSelect,
|
||||||
selectedGraph,
|
selectedGraph,
|
||||||
customTooltipElement,
|
customTooltipElement,
|
||||||
|
customSeries,
|
||||||
}: PanelWrapperProps): JSX.Element {
|
}: PanelWrapperProps): JSX.Element {
|
||||||
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
|
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
@ -135,6 +136,7 @@ function UplotPanelWrapper({
|
|||||||
tzDate: (timestamp: number) =>
|
tzDate: (timestamp: number) =>
|
||||||
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
||||||
timezone: timezone.value,
|
timezone: timezone.value,
|
||||||
|
customSeries,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
widget?.id,
|
widget?.id,
|
||||||
@ -158,6 +160,7 @@ function UplotPanelWrapper({
|
|||||||
hiddenGraph,
|
hiddenGraph,
|
||||||
customTooltipElement,
|
customTooltipElement,
|
||||||
timezone.value,
|
timezone.value,
|
||||||
|
customSeries,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import { UseQueryResult } from 'react-query';
|
|||||||
import { SuccessResponse } from 'types/api';
|
import { SuccessResponse } from 'types/api';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
import { QueryData } from 'types/api/widgets/getQuery';
|
||||||
|
|
||||||
export type PanelWrapperProps = {
|
export type PanelWrapperProps = {
|
||||||
queryResponse: UseQueryResult<
|
queryResponse: UseQueryResult<
|
||||||
@ -25,6 +26,9 @@ export type PanelWrapperProps = {
|
|||||||
tableProcessedDataRef?: React.MutableRefObject<RowData[]>;
|
tableProcessedDataRef?: React.MutableRefObject<RowData[]>;
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
customTooltipElement?: HTMLDivElement;
|
customTooltipElement?: HTMLDivElement;
|
||||||
|
openTracesButton?: boolean;
|
||||||
|
onOpenTraceBtnClick?: (record: RowData) => void;
|
||||||
|
customSeries?: (data: QueryData[]) => uPlot.Series[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TooltipData = {
|
export type TooltipData = {
|
||||||
|
@ -51,6 +51,7 @@ export const routeConfig: Record<string, QueryParams[]> = {
|
|||||||
[ROUTES.WORKSPACE_LOCKED]: [QueryParams.resourceAttributes],
|
[ROUTES.WORKSPACE_LOCKED]: [QueryParams.resourceAttributes],
|
||||||
[ROUTES.MESSAGING_QUEUES]: [QueryParams.resourceAttributes],
|
[ROUTES.MESSAGING_QUEUES]: [QueryParams.resourceAttributes],
|
||||||
[ROUTES.MESSAGING_QUEUES_DETAIL]: [QueryParams.resourceAttributes],
|
[ROUTES.MESSAGING_QUEUES_DETAIL]: [QueryParams.resourceAttributes],
|
||||||
|
[ROUTES.MESSAGING_QUEUES_CELERY_TASK]: [QueryParams.resourceAttributes],
|
||||||
[ROUTES.INFRASTRUCTURE_MONITORING_HOSTS]: [QueryParams.resourceAttributes],
|
[ROUTES.INFRASTRUCTURE_MONITORING_HOSTS]: [QueryParams.resourceAttributes],
|
||||||
[ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES]: [
|
[ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES]: [
|
||||||
QueryParams.resourceAttributes,
|
QueryParams.resourceAttributes,
|
||||||
|
@ -214,6 +214,7 @@ export const routesToSkip = [
|
|||||||
ROUTES.ALERT_OVERVIEW,
|
ROUTES.ALERT_OVERVIEW,
|
||||||
ROUTES.MESSAGING_QUEUES,
|
ROUTES.MESSAGING_QUEUES,
|
||||||
ROUTES.MESSAGING_QUEUES_DETAIL,
|
ROUTES.MESSAGING_QUEUES_DETAIL,
|
||||||
|
ROUTES.MESSAGING_QUEUES_CELERY_TASK,
|
||||||
ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
|
ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
|
||||||
ROUTES.SOMETHING_WENT_WRONG,
|
ROUTES.SOMETHING_WENT_WRONG,
|
||||||
ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES,
|
ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES,
|
||||||
|
@ -57,6 +57,7 @@ export interface GetUPlotChartOptions {
|
|||||||
verticalLineTimestamp?: number;
|
verticalLineTimestamp?: number;
|
||||||
tzDate?: (timestamp: number) => Date;
|
tzDate?: (timestamp: number) => Date;
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
|
customSeries?: (data: QueryData[]) => uPlot.Series[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** the function converts series A , series B , series C to
|
/** the function converts series A , series B , series C to
|
||||||
@ -162,6 +163,7 @@ export const getUPlotChartOptions = ({
|
|||||||
verticalLineTimestamp,
|
verticalLineTimestamp,
|
||||||
tzDate,
|
tzDate,
|
||||||
timezone,
|
timezone,
|
||||||
|
customSeries,
|
||||||
}: GetUPlotChartOptions): uPlot.Options => {
|
}: GetUPlotChartOptions): uPlot.Options => {
|
||||||
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
|
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
|
||||||
|
|
||||||
@ -370,19 +372,21 @@ export const getUPlotChartOptions = ({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
series: getSeries({
|
series: customSeries
|
||||||
series:
|
? customSeries(apiResponse?.data?.result || [])
|
||||||
stackBarChart && isUndefined(hiddenGraph)
|
: getSeries({
|
||||||
? series
|
series:
|
||||||
: apiResponse?.data?.result,
|
stackBarChart && isUndefined(hiddenGraph)
|
||||||
widgetMetaData: apiResponse?.data.result,
|
? series || []
|
||||||
graphsVisibilityStates,
|
: apiResponse?.data?.result || [],
|
||||||
panelType,
|
widgetMetaData: apiResponse?.data?.result || [],
|
||||||
currentQuery,
|
graphsVisibilityStates,
|
||||||
stackBarChart,
|
panelType,
|
||||||
hiddenGraph,
|
currentQuery,
|
||||||
isDarkMode,
|
stackBarChart,
|
||||||
}),
|
hiddenGraph,
|
||||||
|
isDarkMode,
|
||||||
|
}),
|
||||||
axes: getAxes(isDarkMode, yAxisUnit),
|
axes: getAxes(isDarkMode, yAxisUnit),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -4,12 +4,14 @@ import { themeColors } from 'constants/theme';
|
|||||||
import { saveLegendEntriesToLocalStorage } from 'container/GridCardLayout/GridCard/FullView/utils';
|
import { saveLegendEntriesToLocalStorage } from 'container/GridCardLayout/GridCard/FullView/utils';
|
||||||
import { Dimensions } from 'hooks/useDimensions';
|
import { Dimensions } from 'hooks/useDimensions';
|
||||||
import getLabelName from 'lib/getLabelName';
|
import getLabelName from 'lib/getLabelName';
|
||||||
|
import _noop from 'lodash-es/noop';
|
||||||
import { Dispatch, SetStateAction } from 'react';
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { QueryData } from 'types/api/widgets/getQuery';
|
import { QueryData } from 'types/api/widgets/getQuery';
|
||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
|
import onClickPlugin, { OnClickPluginOpts } from './plugins/onClickPlugin';
|
||||||
import tooltipPlugin from './plugins/tooltipPlugin';
|
import tooltipPlugin from './plugins/tooltipPlugin';
|
||||||
import { drawStyles } from './utils/constants';
|
import { drawStyles } from './utils/constants';
|
||||||
import { generateColor } from './utils/generateColor';
|
import { generateColor } from './utils/generateColor';
|
||||||
@ -27,6 +29,7 @@ type GetUplotHistogramChartOptionsProps = {
|
|||||||
graphsVisibilityStates?: boolean[];
|
graphsVisibilityStates?: boolean[];
|
||||||
setGraphsVisibilityStates?: Dispatch<SetStateAction<boolean[]>>;
|
setGraphsVisibilityStates?: Dispatch<SetStateAction<boolean[]>>;
|
||||||
mergeAllQueries?: boolean;
|
mergeAllQueries?: boolean;
|
||||||
|
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||||
};
|
};
|
||||||
|
|
||||||
type GetHistogramSeriesProps = {
|
type GetHistogramSeriesProps = {
|
||||||
@ -119,6 +122,7 @@ export const getUplotHistogramChartOptions = ({
|
|||||||
graphsVisibilityStates,
|
graphsVisibilityStates,
|
||||||
setGraphsVisibilityStates,
|
setGraphsVisibilityStates,
|
||||||
mergeAllQueries,
|
mergeAllQueries,
|
||||||
|
onClickHandler = _noop,
|
||||||
}: GetUplotHistogramChartOptionsProps): uPlot.Options =>
|
}: GetUplotHistogramChartOptionsProps): uPlot.Options =>
|
||||||
({
|
({
|
||||||
id,
|
id,
|
||||||
@ -140,6 +144,10 @@ export const getUplotHistogramChartOptions = ({
|
|||||||
isMergedSeries: mergeAllQueries,
|
isMergedSeries: mergeAllQueries,
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
}),
|
}),
|
||||||
|
onClickPlugin({
|
||||||
|
onClick: onClickHandler,
|
||||||
|
apiResponse,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
/* eslint-disable no-bitwise */
|
/* eslint-disable no-bitwise */
|
||||||
|
|
||||||
export function hashFn(str: string): number {
|
export function hashFn(str: string): number {
|
||||||
let hash = 5381;
|
let hash = 5381;
|
||||||
for (let i = 0; i < str.length; i++) {
|
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;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
queryData: IBuilderQuery[];
|
queryData: IBuilderQuery[];
|
||||||
|
panelTypes?: PANEL_TYPES;
|
||||||
|
yAxisUnit?: string;
|
||||||
|
columnUnits?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetWidgetQueryPropsReturn extends GetWidgetQueryBuilderProps {
|
interface GetWidgetQueryPropsReturn extends GetWidgetQueryBuilderProps {
|
||||||
description?: string;
|
description?: string;
|
||||||
nullZeroValues: string;
|
nullZeroValues: string;
|
||||||
|
columnUnits?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getWidgetQueryBuilder = ({
|
export const getWidgetQueryBuilder = ({
|
||||||
@ -49,14 +53,15 @@ export const getWidgetQueryBuilder = ({
|
|||||||
export function getWidgetQuery(
|
export function getWidgetQuery(
|
||||||
props: GetWidgetQueryProps,
|
props: GetWidgetQueryProps,
|
||||||
): GetWidgetQueryPropsReturn {
|
): GetWidgetQueryPropsReturn {
|
||||||
const { title, description } = props;
|
const { title, description, panelTypes, yAxisUnit, columnUnits } = props;
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
yAxisUnit: 'none',
|
yAxisUnit: yAxisUnit || 'none',
|
||||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
panelTypes: panelTypes || PANEL_TYPES.TIME_SERIES,
|
||||||
fillSpans: false,
|
fillSpans: false,
|
||||||
description,
|
description,
|
||||||
nullZeroValues: 'zero',
|
nullZeroValues: 'zero',
|
||||||
|
columnUnits,
|
||||||
query: {
|
query: {
|
||||||
queryType: EQueryType.QUERY_BUILDER,
|
queryType: EQueryType.QUERY_BUILDER,
|
||||||
promql: [],
|
promql: [],
|
||||||
|
@ -93,7 +93,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mq-graph {
|
.mq-graph {
|
||||||
height: 420px;
|
height: 420px !important;
|
||||||
padding: 24px 24px 0 24px;
|
padding: 24px 24px 0 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ import MessagingQueueHealthCheck from 'components/MessagingQueueHealthCheck/Mess
|
|||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||||
import { ListMinus } from 'lucide-react';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
@ -55,10 +54,6 @@ function MessagingQueues(): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="messaging-queue-container">
|
<div className="messaging-queue-container">
|
||||||
<div className="messaging-breadcrumb">
|
|
||||||
<ListMinus size={16} />
|
|
||||||
{t('breadcrumb')}
|
|
||||||
</div>
|
|
||||||
<div className="messaging-header">
|
<div className="messaging-header">
|
||||||
<div className="header-config">
|
<div className="header-config">
|
||||||
{t('header')} /
|
{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'],
|
SERVICE_TOP_LEVEL_OPERATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
INFRASTRUCTURE_MONITORING_HOSTS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
INFRASTRUCTURE_MONITORING_HOSTS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
INFRASTRUCTURE_MONITORING_KUBERNETES: ['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