feat: added right panel graphs in overview page (#6944)

* feat: added right panel graphs in overview page

* feat: added right panel trace navigation

* feat: implement search column wise and global table wise

* feat: implemented filter on table with api filtering

* feat: added allow clear to table filters

* feat: fixed empty table issue

* feat: fixed celery state - count display
This commit is contained in:
SagarRajput-7 2025-01-28 10:04:06 +05:30 committed by GitHub
parent 7c85befc17
commit 35c61045c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1666 additions and 116 deletions

View File

@ -409,10 +409,10 @@ const routes: AppRoutes[] = [
isPrivate: true,
},
{
path: ROUTES.MESSAGING_QUEUES_CELERY_OVERVIEW,
path: ROUTES.MESSAGING_QUEUES_OVERVIEW,
exact: true,
component: MessagingQueues,
key: 'MESSAGING_QUEUES_CELERY_OVERVIEW',
key: 'MESSAGING_QUEUES_OVERVIEW',
isPrivate: true,
},
{

View File

@ -10,6 +10,9 @@
.config-select-option {
width: 100%;
min-width: 160px;
max-width: fit-content;
.ant-select-selector {
display: flex;
min-height: 32px;
@ -23,6 +26,10 @@
}
}
}
.copy-url-btn {
flex-shrink: 0;
}
}
.lightMode {

View File

@ -1,7 +1,7 @@
import './CeleryOverviewConfigOptions.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Select, Spin, Tooltip } from 'antd';
import { Button, Row, Select, Spin, Tooltip } from 'antd';
import {
getValuesFromQueryParams,
setQueryParamsFromOptions,
@ -18,7 +18,7 @@ import { useCopyToClipboard } from 'react-use';
interface SelectOptionConfig {
placeholder: string;
queryParam: QueryParams;
filterType: 'serviceName' | 'spanName' | 'msgSystem';
filterType: string | string[];
}
function FilterSelect({
@ -29,13 +29,14 @@ function FilterSelect({
const { handleSearch, isFetching, options } = useCeleryFilterOptions(
filterType,
);
const urlQuery = useUrlQuery();
const history = useHistory();
const location = useLocation();
return (
<Select
key={filterType}
key={filterType.toString()}
placeholder={placeholder}
showSearch
mode="multiple"
@ -44,6 +45,7 @@ function FilterSelect({
className="config-select-option"
onSearch={handleSearch}
maxTagCount={4}
allowClear
maxTagPlaceholder={SelectMaxTagPlaceholder}
value={getValuesFromQueryParams(queryParam, urlQuery) || []}
notFoundContent={
@ -74,16 +76,26 @@ function CeleryOverviewConfigOptions(): JSX.Element {
queryParam: QueryParams.service,
filterType: 'serviceName',
},
// {
// placeholder: 'Span Name',
// queryParam: QueryParams.spanName,
// filterType: 'spanName',
// },
// {
// placeholder: 'Msg System',
// queryParam: QueryParams.msgSystem,
// filterType: 'msgSystem',
// },
{
placeholder: 'Span Name',
queryParam: QueryParams.spanName,
filterType: 'name',
},
{
placeholder: 'Msg System',
queryParam: QueryParams.msgSystem,
filterType: 'messaging.system',
},
{
placeholder: 'Destination',
queryParam: QueryParams.destination,
filterType: ['messaging.destination.name', 'messaging.destination'],
},
{
placeholder: 'Kind',
queryParam: QueryParams.kindString,
filterType: 'kind_string',
},
];
const handleShareURL = (): void => {
@ -96,16 +108,16 @@ function CeleryOverviewConfigOptions(): JSX.Element {
return (
<div className="celery-overview-filters">
<div className="celery-filters">
<Row className="celery-filters">
{selectConfigs.map((config) => (
<FilterSelect
key={config.filterType}
key={config.filterType.toString()}
placeholder={config.placeholder}
queryParam={config.queryParam}
filterType={config.filterType}
/>
))}
</div>
</Row>
<Tooltip title="Share this" arrow={false}>
<Button
className="periscope-btn copy-url-btn"

View File

@ -1,8 +1,20 @@
import './CeleryOverviewTable.styles.scss';
import { LoadingOutlined } from '@ant-design/icons';
import { LoadingOutlined, SearchOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Progress, Spin, TableColumnsType, Tooltip, Typography } from 'antd';
import {
Button,
Input,
InputRef,
Progress,
Space,
Spin,
TableColumnsType,
TableColumnType,
Tooltip,
Typography,
} from 'antd';
import { FilterDropdownProps } from 'antd/lib/table/interface';
import {
getQueueOverview,
QueueOverviewResponse,
@ -10,10 +22,12 @@ import {
import { isNumber } from 'chart.js/helpers';
import { ResizeTable } from 'components/ResizeTable';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
import useDragColumns from 'hooks/useDragColumns';
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { isEmpty } from 'lodash-es';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useMutation } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@ -55,6 +69,74 @@ function ProgressRender(item: string | number): JSX.Element {
);
}
const getColumnSearchProps = (
searchInput: React.RefObject<InputRef>,
handleReset: (
clearFilters: () => void,
confirm: FilterDropdownProps['confirm'],
) => void,
handleSearch: (selectedKeys: string[], confirm: () => void) => void,
dataIndex?: string,
): TableColumnType<RowData> => ({
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
close,
}): JSX.Element => (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div style={{ padding: 8 }} onKeyDown={(e): void => e.stopPropagation()}>
<Input
ref={searchInput}
placeholder={`Search ${dataIndex}`}
value={selectedKeys[0]}
onChange={(e): void =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={(): void => handleSearch(selectedKeys as string[], confirm)}
style={{ marginBottom: 8, display: 'block' }}
/>
<Space>
<Button
type="primary"
size="small"
onClick={(): void => handleSearch(selectedKeys as string[], confirm)}
icon={<SearchOutlined />}
>
Search
</Button>
<Button
onClick={(): void => clearFilters && handleReset(clearFilters, confirm)}
size="small"
style={{ width: 90 }}
>
Reset
</Button>
<Button
type="link"
size="small"
onClick={(): void => {
close();
}}
>
close
</Button>
</Space>
</div>
),
filterIcon: (filtered: boolean): JSX.Element => (
<SearchOutlined
style={{ color: filtered ? Color.BG_ROBIN_500 : undefined }}
/>
),
onFilter: (value, record): boolean =>
record[dataIndex || '']
.toString()
.toLowerCase()
.includes((value as string).toLowerCase()),
});
function getColumns(data: RowData[]): TableColumnsType<RowData> {
if (data?.length === 0) {
return [];
@ -235,12 +317,11 @@ type FilterConfig = {
function makeFilters(urlQuery: URLSearchParams): Filter[] {
const filterConfigs: FilterConfig[] = [
{ paramName: 'destination', key: 'destination', operator: 'in' },
{ paramName: 'queue', key: 'queue', operator: 'in' },
{ paramName: 'kind_string', key: 'kind_string', operator: 'in' },
{ paramName: 'service', key: 'service.name', operator: 'in' },
{ paramName: 'span_name', key: 'span_name', operator: 'in' },
{ paramName: 'messaging_system', key: 'messaging_system', operator: 'in' },
{ paramName: QueryParams.destination, key: 'destination', operator: 'in' },
{ paramName: QueryParams.msgSystem, key: 'queue', operator: 'in' },
{ paramName: QueryParams.kindString, key: 'kind_string', operator: 'in' },
{ paramName: QueryParams.service, key: 'service.name', operator: 'in' },
{ paramName: QueryParams.spanName, key: 'name', operator: 'in' },
];
return filterConfigs
@ -260,7 +341,11 @@ function makeFilters(urlQuery: URLSearchParams): Filter[] {
.filter((filter): filter is Filter => filter !== null);
}
export default function CeleryOverviewTable(): JSX.Element {
export default function CeleryOverviewTable({
onRowClick,
}: {
onRowClick: (record: RowData) => void;
}): JSX.Element {
const [tableData, setTableData] = useState<RowData[]>([]);
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
@ -271,6 +356,8 @@ export default function CeleryOverviewTable(): JSX.Element {
onSuccess: (data) => {
if (data?.payload) {
setTableData(getTableData(data?.payload));
} else if (isEmpty(data?.payload)) {
setTableData([]);
}
},
});
@ -293,11 +380,42 @@ export default function CeleryOverviewTable(): JSX.Element {
LOCALSTORAGE.CELERY_OVERVIEW_COLUMNS,
);
const [searchText, setSearchText] = useState('');
const searchInput = useRef<InputRef>(null);
const handleSearch = (
selectedKeys: string[],
confirm: FilterDropdownProps['confirm'],
): void => {
confirm();
setSearchText(selectedKeys[0]);
};
const handleReset = (
clearFilters: () => void,
confirm: FilterDropdownProps['confirm'],
): void => {
clearFilters();
setSearchText('');
confirm();
};
const columns = useMemo(
() => getDraggedColumns<RowData>(getColumns(tableData), draggedColumns),
() =>
getDraggedColumns<RowData>(
getColumns(tableData).map((item) => ({
...item,
...getColumnSearchProps(
searchInput,
handleReset,
handleSearch,
item.key?.toString(),
),
})),
draggedColumns,
),
[tableData, draggedColumns],
);
const handleDragColumn = useCallback(
(fromIndex: number, toIndex: number) =>
onDragColumns(columns, fromIndex, toIndex),
@ -316,17 +434,44 @@ export default function CeleryOverviewTable(): JSX.Element {
);
const handleRowClick = (record: RowData): void => {
console.log(record);
onRowClick(record);
};
const getFilteredData = useCallback(
(data: RowData[]): RowData[] => {
if (!searchText) return data;
const searchLower = searchText.toLowerCase();
return data.filter((record) =>
Object.values(record).some(
(value) =>
value !== undefined &&
value.toString().toLowerCase().includes(searchLower),
),
);
},
[searchText],
);
const filteredData = useMemo(() => getFilteredData(tableData), [
getFilteredData,
tableData,
]);
return (
<div style={{ width: '100%' }}>
<Input.Search
placeholder="Search across all columns"
onChange={(e): void => setSearchText(e.target.value)}
value={searchText}
allowClear
/>
<ResizeTable
className="celery-overview-table"
pagination={paginationConfig}
size="middle"
columns={columns}
dataSource={tableData}
dataSource={filteredData}
bordered={false}
loading={{
spinning: isLoading,

View File

@ -7,7 +7,7 @@ import { DataSource } from 'types/common/queryBuilder';
export interface Filters {
searchText: string;
attributeKey: string;
attributeKey: string | string[];
}
export interface GetAllFiltersResponse {
@ -19,27 +19,36 @@ export function useGetAllFilters(props: Filters): GetAllFiltersResponse {
const { searchText, attributeKey } = props;
const { data, isLoading } = useQuery(
['attributesValues', searchText],
['attributesValues', attributeKey, searchText],
async () => {
const { payload } = await getAttributesValues({
aggregateOperator: 'noop',
dataSource: DataSource.TRACES,
aggregateAttribute: '',
attributeKey,
searchText: searchText ?? '',
filterAttributeKeyDataType: DataTypes.String,
tagType: 'tag',
});
const keys = Array.isArray(attributeKey) ? attributeKey : [attributeKey];
if (payload) {
const values = Object.values(payload).find((el) => !!el) || [];
const options: DefaultOptionType[] = values.map((val: string) => ({
label: val,
value: val,
}));
return options;
}
return [];
const responses = await Promise.all(
keys.map((key) =>
getAttributesValues({
aggregateOperator: 'noop',
dataSource: DataSource.TRACES,
aggregateAttribute: '',
attributeKey: key,
searchText: searchText ?? '',
filterAttributeKeyDataType: DataTypes.String,
tagType: 'tag',
}),
),
);
const uniqueValues = [
...new Set(
responses.flatMap(
({ payload }) => Object.values(payload || {}).find((el) => !!el) || [],
),
),
];
return uniqueValues.map((val: string) => ({
label: val,
value: val,
}));
},
);

View File

@ -4,14 +4,11 @@ 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 { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
@ -19,12 +16,11 @@ 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';
import { useNavigateToTraces } from '../useNavigateToTraces';
export type CeleryTaskData = {
entity: string;
@ -143,46 +139,7 @@ export default function CeleryTaskDetail({
// 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');
};
const navigateToTrace = useNavigateToTraces();
return (
<Drawer
@ -223,7 +180,13 @@ export default function CeleryTaskDetail({
panelType={PANEL_TYPES.TABLE}
queryEnabled
openTracesButton
onOpenTraceBtnClick={navigateToTrace}
onOpenTraceBtnClick={(rowData): void => {
const filters = createFiltersFromData({
...rowData,
[taskData.entity]: taskData.value,
});
navigateToTrace(filters);
}}
/>
</Drawer>
);

View File

@ -140,7 +140,6 @@ function CeleryTaskBar({
});
const customSeries = (data: QueryData[]): uPlot.Series[] => {
console.log(data);
const configurations: uPlot.Series[] = [
{ label: 'Timestamp', stroke: 'purple' },
];

View File

@ -24,7 +24,7 @@
.widget-graph-container {
&.bar {
height: calc(100% - 85px);
height: calc(100% - 105px);
}
}
}
@ -45,9 +45,12 @@
height: calc(100% - 18px);
.widget-graph-container {
&.bar,
&.bar {
height: calc(100% - 105px);
}
&.graph {
height: calc(100% - 85px);
height: calc(100% - 80px);
}
}
}
@ -93,7 +96,10 @@
}
&__label-wrapper {
margin-bottom: 8px;
margin-bottom: 4px;
display: flex;
flex-direction: column;
gap: 8px;
}
&__label {

View File

@ -917,3 +917,215 @@ export const celeryTimeSeriesTablesWidgetData = (
columnUnits: { A: 'ns' },
}),
);
// State Count Widget
export const celeryAllStateCountWidgetData = getWidgetQueryBuilder(
getWidgetQuery({
title: 'All State Count',
description: 'Represents the all state count.',
panelTypes: PANEL_TYPES.VALUE,
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.EMPTY,
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: [],
having: [],
legend: '',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
},
],
}),
);
export const celerySuccessStateCountWidgetData = getWidgetQueryBuilder(
getWidgetQuery({
title: 'Successful State Count',
description: 'Represents the successful state count.',
panelTypes: PANEL_TYPES.VALUE,
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.EMPTY,
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.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
op: '=',
value: 'SUCCESS',
},
],
op: 'AND',
},
functions: [],
groupBy: [],
having: [],
legend: '',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
},
],
}),
);
export const celeryFailedStateCountWidgetData = getWidgetQueryBuilder(
getWidgetQuery({
title: 'Failed State Count',
description: 'Represents the failed state count.',
panelTypes: PANEL_TYPES.VALUE,
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.EMPTY,
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.state--string--tag--false',
isColumn: false,
isJSON: false,
key: 'celery.state',
type: 'tag',
},
op: '=',
value: 'FAILURE',
},
],
op: 'AND',
},
functions: [],
groupBy: [],
having: [],
legend: '',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
},
],
}),
);
export const celeryRetryStateCountWidgetData = getWidgetQueryBuilder(
getWidgetQuery({
title: 'Retry State Count',
description: 'Represents the retry state count.',
panelTypes: PANEL_TYPES.VALUE,
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.EMPTY,
id: '------false',
isColumn: false,
key: '',
type: '',
},
aggregateOperator: 'count',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: uuidv4(),
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: [],
having: [],
legend: '',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
},
],
}),
);

View File

@ -1,8 +1,17 @@
/* eslint-disable no-nested-ternary */
import './CeleryTaskGraph.style.scss';
import { Col, Row } from 'antd';
import { Dispatch, SetStateAction } from 'react';
import {
celeryAllStateCountWidgetData,
celeryFailedStateCountWidgetData,
celeryRetryStateCountWidgetData,
celerySuccessStateCountWidgetData,
} from './CeleryTaskGraphUtils';
import { useGetValueFromWidget } from './useGetValueFromWidget';
interface TabData {
label: string;
key: string;
@ -33,6 +42,16 @@ function CeleryTaskStateGraphConfig({
setBarState(key as CeleryTaskState);
};
const { values, isLoading, isError } = useGetValueFromWidget(
[
celeryAllStateCountWidgetData,
celeryFailedStateCountWidgetData,
celeryRetryStateCountWidgetData,
celerySuccessStateCountWidgetData,
],
['celery-task-states'],
);
return (
<Row className="celery-task-states">
{tabs.map((tab, index) => (
@ -46,6 +65,9 @@ function CeleryTaskStateGraphConfig({
>
<div className="celery-task-states__label-wrapper">
<div className="celery-task-states__label">{tab.label}</div>
<div className="celery-task-states__value">
{isLoading ? '-' : isError ? '-' : values[index]}
</div>
</div>
{tab.key === barState && <div className="celery-task-states__indicator" />}
</Col>

View File

@ -0,0 +1,63 @@
import { ENTITY_VERSION_V4 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { getQueryPayloadFromWidgetsData } from 'pages/Celery/CeleryOverview/CeleryOverviewUtils';
import { useCallback } from 'react';
import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { GlobalReducer } from 'types/reducer/globalTime';
interface UseGetValueResult {
values: string[];
isLoading: boolean;
isError: boolean;
}
export const useGetValueFromWidget = (
widgetsData: Widgets | Widgets[],
queryKey: string[],
): UseGetValueResult => {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const queryPayloads = useCallback(
() =>
getQueryPayloadFromWidgetsData({
start: Math.floor(minTime / 1000000000),
end: Math.floor(maxTime / 1000000000),
widgetsData: Array.isArray(widgetsData) ? widgetsData : [widgetsData],
panelType: PANEL_TYPES.VALUE,
}),
[minTime, maxTime, widgetsData],
);
const queries = useQueries(
queryPayloads().map((payload) => ({
queryKey: [...queryKey, payload, ENTITY_VERSION_V4],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
enabled: !!payload,
})),
);
const isLoading = queries.some((query) => query.isLoading);
const isError = queries.some((query) => query.isError);
const values = queries.map((query) => {
if (query.isLoading) return 'Loading...';
if (query.isError) return 'Error';
const value = parseFloat(
query.data?.payload?.data?.newResult?.data?.result?.[0]?.series?.[0]
?.values?.[0]?.value || 'NaN',
);
return Number.isNaN(value) ? 'NaN' : value.toFixed(2);
});
return { values, isLoading, isError };
};

View File

@ -5,7 +5,7 @@ import { useState } from 'react';
import { useGetAllFilters } from './CeleryTaskConfigOptions/useGetCeleryFilters';
export const useCeleryFilterOptions = (
type: string,
type: string | string[],
): {
searchText: string;
handleSearch: (value: string) => void;

View File

@ -0,0 +1,65 @@
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
export function useNavigateToTraces(): (
filters: TagFilterItem[],
startTime?: number,
endTime?: number,
) => void {
const { currentQuery } = useQueryBuilder();
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const prepareQuery = useCallback(
(selectedFilters: TagFilterItem[]): Query => ({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item) => ({
...item,
dataSource: DataSource.TRACES,
aggregateOperator: MetricAggregateOperator.NOOP,
filters: {
...item.filters,
items: selectedFilters,
},
})),
},
}),
[currentQuery],
);
return useCallback(
(filters: TagFilterItem[], startTime?: number, endTime?: number): void => {
const urlParams = new URLSearchParams();
if (startTime && endTime) {
urlParams.set(QueryParams.startTime, startTime.toString());
urlParams.set(QueryParams.endTime, endTime.toString());
} else {
urlParams.set(QueryParams.startTime, (minTime / 1000000).toString());
urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString());
}
const JSONCompositeQuery = encodeURIComponent(
JSON.stringify(prepareQuery(filters)),
);
const newTraceExplorerPath = `${
ROUTES.TRACES_EXPLORER
}?${urlParams.toString()}&${
QueryParams.compositeQuery
}=${JSONCompositeQuery}`;
window.open(newTraceExplorerPath, '_blank');
},
[minTime, maxTime, prepareQuery],
);
}

View File

@ -44,4 +44,6 @@ export enum QueryParams {
taskName = 'taskName',
spanName = 'spanName',
msgSystem = 'msgSystem',
destination = 'destination',
kindString = 'kindString',
}

View File

@ -64,7 +64,7 @@ const ROUTES = {
INFRASTRUCTURE_MONITORING_HOSTS: '/infrastructure-monitoring/hosts',
INFRASTRUCTURE_MONITORING_KUBERNETES: '/infrastructure-monitoring/kubernetes',
MESSAGING_QUEUES_CELERY_TASK: '/messaging-queues/celery-task',
MESSAGING_QUEUES_CELERY_OVERVIEW: '/messaging-queues/celery-overview',
MESSAGING_QUEUES_OVERVIEW: '/messaging-queues/overview',
} as const;
export default ROUTES;

View File

@ -290,7 +290,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
routeKey === 'MESSAGING_QUEUES' ||
routeKey === 'MESSAGING_QUEUES_DETAIL' ||
routeKey === 'MESSAGING_QUEUES_CELERY_TASK' ||
routeKey === 'MESSAGING_QUEUES_CELERY_OVERVIEW';
routeKey === 'MESSAGING_QUEUES_OVERVIEW';
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';

View File

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

View File

@ -215,7 +215,7 @@ export const routesToSkip = [
ROUTES.MESSAGING_QUEUES,
ROUTES.MESSAGING_QUEUES_DETAIL,
ROUTES.MESSAGING_QUEUES_CELERY_TASK,
ROUTES.MESSAGING_QUEUES_CELERY_OVERVIEW,
ROUTES.MESSAGING_QUEUES_OVERVIEW,
ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
ROUTES.SOMETHING_WENT_WRONG,
ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES,

View File

@ -1,20 +1,41 @@
import './CeleryOverview.styles.scss';
import CeleryOverviewConfigOptions from 'components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions';
import CeleryOverviewTable from 'components/CeleryOverview/CeleryOverviewTable/CeleryOverviewTable';
import CeleryOverviewTable, {
RowData,
} from 'components/CeleryOverview/CeleryOverviewTable/CeleryOverviewTable';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useState } from 'react';
import CeleryOverviewDetails from './CeleryOverviewDetail/CeleryOverviewDetails';
export default function CeleryOverview(): JSX.Element {
const [details, setDetails] = useState<RowData | null>(null);
const onRowClick = (record: RowData): void => {
setDetails(record);
};
return (
<div className="celery-overview-container">
<div className="celery-overview-content">
<div className="celery-overview-content-header">
<p className="celery-overview-content-header-title">Celery Overview</p>
<p className="celery-overview-content-header-title">
Messaging Queue Overview
</p>
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
</div>
<CeleryOverviewConfigOptions />
<CeleryOverviewTable />
<CeleryOverviewTable onRowClick={onRowClick} />
</div>
{details && (
<CeleryOverviewDetails
details={details}
onClose={(): void => {
setDetails(null);
}}
/>
)}
</div>
);
}

View File

@ -0,0 +1,30 @@
.celery-overview-detail-container {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px 8px;
}
.overview-right-panel-graph-card {
border-radius: 8px;
border: 1px solid #262626 !important;
background: #0a0a0a;
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.1),
0px 1px 2px -1px rgba(0, 0, 0, 0.1);
.request-rate-card,
.error-rate-card,
.avg-latency-card {
height: 320px !important;
padding: 10px;
width: 100%;
box-sizing: border-box;
border-bottom: 0.75px solid #262626;
margin-bottom: 12px;
position: relative;
.widget-graph-container {
height: 260px;
}
}
}

View File

@ -0,0 +1,99 @@
import './CeleryOverviewDetails.styles.scss';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Divider, Drawer, Typography } from 'antd';
import { RowData } from 'components/CeleryOverview/CeleryOverviewTable/CeleryOverviewTable';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { X } from 'lucide-react';
import { useMemo } from 'react';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { getFiltersFromKeyValue } from '../CeleryOverviewUtils';
import OverviewRightPanelGraph from './OverviewRightPanelGraph';
import ValueInfo from './ValueInfo';
export default function CeleryOverviewDetails({
details,
onClose,
}: {
details: RowData;
onClose: () => void;
}): JSX.Element {
const isDarkMode = useIsDarkMode();
const filters: TagFilterItem[] = useMemo(() => {
const keyValues = Object.entries(details).map(([key, value]) => {
switch (key) {
case 'service_name':
return getFiltersFromKeyValue('service.name', value, 'resource');
case 'span_name':
return getFiltersFromKeyValue('name', value, '');
case 'messaging_system':
return getFiltersFromKeyValue('messaging.system', value, 'tag');
case 'destination':
return getFiltersFromKeyValue(
details.messaging_system === 'celery'
? 'messaging.destination'
: 'messaging.destination.name',
value,
'tag',
);
case 'kind_string':
return getFiltersFromKeyValue('kind_string', value, '');
default:
return undefined;
}
});
return keyValues.filter((item) => item !== undefined) as TagFilterItem[];
}, [details]);
const groupByFilter = useMemo(
() =>
getFiltersFromKeyValue('messaging.destination.partition.id', '', 'tag').key,
[],
);
return (
<Drawer
width="60%"
title={
<div>
<Typography.Text className="title">{details.service_name}</Typography.Text>
<div>
<Typography.Text className="subtitle">
{details.span_name}
</Typography.Text>
<Divider type="vertical" />
<Typography.Text className="subtitle">
{details.messaging_system}
</Typography.Text>
<Divider type="vertical" />
<Typography.Text className="subtitle">
{details.destination}
</Typography.Text>
<Divider type="vertical" />
<Typography.Text className="subtitle">
{details.kind_string}
</Typography.Text>
</div>
</div>
}
placement="right"
onClose={onClose}
open={!!details}
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 }} />}
>
<div className="celery-overview-detail-container">
<ValueInfo filters={filters} />
<OverviewRightPanelGraph filters={filters} groupByFilter={groupByFilter} />
</div>
</Drawer>
);
}

View File

@ -0,0 +1,169 @@
import { Card } from 'antd';
import { useNavigateToTraces } from 'components/CeleryTask/useNavigateToTraces';
import { QueryParams } from 'constants/query';
import { ViewMenuAction } from 'container/GridCardLayout/config';
import GridCard from 'container/GridCardLayout/GridCard';
import { Button } from 'container/MetricsApplication/Tabs/styles';
import { onGraphClickHandler } from 'container/MetricsApplication/Tabs/util';
import useUrlQuery from 'hooks/useUrlQuery';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
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 { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import {
celeryOverviewAvgLatencyGraphData,
celeryOverviewErrorRateGraphData,
celeryOverviewRequestRateGraphData,
} from '../CeleryOverviewGraphUtils';
export default function OverviewRightPanelGraph({
groupByFilter,
filters,
}: {
groupByFilter?: BaseAutocompleteData;
filters?: TagFilterItem[];
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
const dispatch = useDispatch();
const urlQuery = useUrlQuery();
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 { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const requestRateWidget = useMemo(
() =>
celeryOverviewRequestRateGraphData(minTime, maxTime, filters, groupByFilter),
[minTime, maxTime, filters, groupByFilter],
);
const errorRateWidget = useMemo(
() =>
celeryOverviewErrorRateGraphData(minTime, maxTime, filters, groupByFilter),
[minTime, maxTime, filters, groupByFilter],
);
const avgLatencyWidget = useMemo(
() =>
celeryOverviewAvgLatencyGraphData(minTime, maxTime, filters, groupByFilter),
[minTime, maxTime, filters, groupByFilter],
);
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
const handleSetTimeStamp = useCallback((selectTime: number) => {
setSelectedTimeStamp(selectTime);
}, []);
const navigateToTraces = useNavigateToTraces();
const handleGraphClick = useCallback(
(type: string): OnClickPluginOpts['onClick'] => (
xValue,
yValue,
mouseX,
mouseY,
): Promise<void> =>
onGraphClickHandler(handleSetTimeStamp)(
xValue,
yValue,
mouseX,
mouseY,
type,
),
[handleSetTimeStamp],
);
const goToTraces = useCallback(
(widget: Widgets) => {
const { stepInterval } = widget?.query?.builder?.queryData?.[0] ?? {};
navigateToTraces(
filters ?? [],
selectedTimeStamp,
selectedTimeStamp + (stepInterval ?? 60),
);
},
[navigateToTraces, filters, selectedTimeStamp],
);
return (
<Card className="overview-right-panel-graph-card">
<div className="request-rate-card">
<Button
type="default"
size="small"
id="Celery_request_rate_button"
onClick={(): void => goToTraces(requestRateWidget)}
>
View Traces
</Button>
<GridCard
widget={requestRateWidget}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={handleGraphClick('Celery_request_rate')}
/>
</div>
<div className="error-rate-card">
<Button
type="default"
size="small"
id="Celery_error_rate_button"
onClick={(): void => goToTraces(errorRateWidget)}
>
View Traces
</Button>
<GridCard
widget={errorRateWidget}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={handleGraphClick('Celery_error_rate')}
/>
</div>
<div className="avg-latency-card">
<Button
type="default"
size="small"
id="Celery_avg_latency_button"
onClick={(): void => goToTraces(avgLatencyWidget)}
>
View Traces
</Button>
<GridCard
widget={avgLatencyWidget}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
onClickHandler={handleGraphClick('Celery_avg_latency')}
/>
</div>
</Card>
);
}
OverviewRightPanelGraph.defaultProps = {
groupByFilter: undefined,
filters: undefined,
};

View File

@ -0,0 +1,63 @@
.value-info-card {
border-radius: 8px;
border: 1px solid #262626 !important;
background: #0a0a0a;
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.1),
0px 1px 2px -1px rgba(0, 0, 0, 0.1);
.metric-column {
.metric-title {
color: #fafafa;
font-size: 14px;
font-weight: 500;
}
.metric-value-container {
display: flex;
align-items: baseline;
gap: 6px;
}
.metric-value {
font-size: 24px;
font-weight: 400;
&.red {
color: #f87171;
}
&.green {
color: #4ade80;
}
&.loading {
opacity: 0.5;
}
}
.metric-unit {
color: #a3a3a3;
font-size: 14px;
}
.metric-reference {
color: #a3a3a3;
font-size: 12px;
}
.trace-button {
margin-top: 8px;
background: #262626;
border: none;
&:hover {
background: #404040;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
}
}

View File

@ -0,0 +1,209 @@
import './ValueInfo.styles.scss';
import { FileSearchOutlined } from '@ant-design/icons';
import { Button, Card, Col, Row } from 'antd';
import { useNavigateToTraces } from 'components/CeleryTask/useNavigateToTraces';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useCallback, useMemo } from 'react';
import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuidv4 } from 'uuid';
import {
celeryOverviewAvgLatencyWidgetData,
celeryOverviewErrorRateWidgetData,
celeryOverviewRequestRateWidgetData,
} from '../CeleryOverviewGraphUtils';
import { getQueryPayloadFromWidgetsData } from '../CeleryOverviewUtils';
export default function ValueInfo({
filters,
}: {
filters?: TagFilterItem[];
}): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
// get data from api
const queryPayloads = useMemo(
() =>
getQueryPayloadFromWidgetsData({
start: Math.floor(minTime / 1000000000),
end: Math.floor(maxTime / 1000000000),
widgetsData: [
celeryOverviewRequestRateWidgetData(filters),
celeryOverviewErrorRateWidgetData(filters),
celeryOverviewAvgLatencyWidgetData(filters),
],
panelType: PANEL_TYPES.VALUE,
}),
[minTime, maxTime, filters],
);
const queries = useQueries(
queryPayloads.map((payload) => ({
queryKey: [
'overview-detail',
payload,
ENTITY_VERSION_V4,
'overview-right-panel',
],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
enabled: !!payload,
})),
);
const getValues = useCallback(
() =>
queries.map((query) => {
const value = parseFloat(
query.data?.payload?.data?.newResult?.data?.result?.[0]?.series?.[0]
?.values?.[0]?.value || 'NaN',
);
return Number.isNaN(value) ? 'NaN' : value.toFixed(2);
}),
[queries],
);
const isLoading = queries.some((query) => query.isLoading);
const [requestRate, errorRate, avgLatency] = useMemo(
() => (isLoading ? ['0', '0', '0'] : getValues()),
[isLoading, getValues],
);
const navigateToTrace = useNavigateToTraces();
const avgLatencyInMs = useMemo(() => {
if (avgLatency === 'NaN') return 'NaN';
const numericValue = parseFloat(avgLatency);
return (numericValue / 1000000).toFixed(2);
}, [avgLatency]);
const getColorBasedOnValue = (value: string): string => {
const numericValue = parseFloat(value);
if (value === 'NaN') return 'gray';
if (numericValue < 3) return 'green';
if (numericValue < 8) return 'yellow';
return 'red';
};
const getColorForLatency = (value: string): string => {
const numericValue = parseFloat(value);
if (value === 'NaN') return 'gray';
if (numericValue < 100) return 'green';
if (numericValue < 200) return 'yellow';
return 'red';
};
const getColorForErrorRate = (value: string): string => {
const numericValue = parseFloat(value);
if (value === 'NaN') return 'gray';
if (numericValue < 60) return 'green';
if (numericValue < 90) return 'yellow';
return 'red';
};
return (
<Card className="value-info-card">
<Row gutter={16}>
<Col span={8} className="metric-column">
<div className="metric-title">Request Rate</div>
<div className="metric-value-container">
<div
className={`metric-value ${getColorBasedOnValue(requestRate)} ${
isLoading ? 'loading' : ''
}`}
>
{requestRate === 'NaN' ? '0' : requestRate}
</div>
<div className="metric-unit">req/s</div>
</div>
<Button
type="primary"
icon={<FileSearchOutlined />}
className="trace-button"
disabled={isLoading}
onClick={(): void => navigateToTrace(filters ?? [])}
>
View Traces
</Button>
</Col>
<Col span={8} className="metric-column">
<div className="metric-title">Error Rate</div>
<div className="metric-value-container">
<div
className={`metric-value ${getColorForErrorRate(errorRate)} ${
isLoading ? 'loading' : ''
}`}
>
{errorRate === 'NaN' ? '0' : errorRate}
</div>
<div className="metric-unit">%</div>
</div>
<Button
type="primary"
icon={<FileSearchOutlined />}
className="trace-button"
disabled={isLoading}
onClick={(): void =>
navigateToTrace([
...(filters ?? []),
{
id: uuidv4(),
key: {
dataType: DataTypes.bool,
id: 'has_error--bool----true',
isColumn: true,
isJSON: false,
key: 'has_error',
type: '',
},
op: '=',
value: 'true',
},
])
}
>
View Traces
</Button>
</Col>
<Col span={8} className="metric-column">
<div className="metric-title">Average Latency</div>
<div className="metric-value-container">
<div
className={`metric-value ${getColorForLatency(avgLatencyInMs)} ${
isLoading ? 'loading' : ''
}`}
>
{avgLatencyInMs === 'NaN' ? '0' : avgLatencyInMs}
</div>
<div className="metric-unit">ms</div>
</div>
<Button
type="primary"
icon={<FileSearchOutlined />}
className="trace-button"
disabled={isLoading}
onClick={(): void => navigateToTrace(filters ?? [])}
>
View Traces
</Button>
</Col>
</Row>
</Card>
);
}
ValueInfo.defaultProps = {
filters: undefined,
};

View File

@ -0,0 +1,407 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { getStepInterval } from 'components/CeleryTask/CeleryTaskGraph/CeleryTaskGraphUtils';
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 {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuidv4 } from 'uuid';
export const celeryOverviewRequestRateWidgetData = (
filters?: TagFilterItem[],
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
title: 'Request Rate',
description: 'Represents request rate of the service',
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.EMPTY,
id: '------false',
isColumn: false,
isJSON: false,
key: '',
type: '',
},
aggregateOperator: 'rate',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: filters ?? [],
op: 'AND',
},
functions: [],
groupBy: [],
having: [],
legend: '',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
},
],
panelTypes: PANEL_TYPES.VALUE,
}),
);
export const celeryOverviewErrorRateWidgetData = (
filters?: TagFilterItem[],
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
title: 'Error',
description: 'Represents Error in the service',
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.EMPTY,
id: '------false',
isColumn: false,
isJSON: false,
key: '',
type: '',
},
aggregateOperator: 'rate',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [
...(filters ?? []),
{
id: uuidv4(),
key: {
dataType: DataTypes.bool,
id: 'has_error--bool----true',
isColumn: true,
isJSON: false,
key: 'has_error',
type: '',
},
op: '=',
value: 'true',
},
],
op: 'AND',
},
functions: [],
groupBy: [],
having: [],
legend: '',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
},
],
panelTypes: PANEL_TYPES.VALUE,
}),
);
export const celeryOverviewAvgLatencyWidgetData = (
filters?: TagFilterItem[],
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
title: 'Avg Latency',
description: 'Represents Avg Latency of the service',
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
aggregateOperator: 'p95',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: filters ?? [],
op: 'AND',
},
functions: [],
groupBy: [],
having: [],
legend: 'p95',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'p95',
},
],
panelTypes: PANEL_TYPES.VALUE,
}),
);
export const celeryOverviewRequestRateGraphData = (
startTime: number,
endTime: number,
filters?: TagFilterItem[],
groupByFilter?: BaseAutocompleteData,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
title: 'Request rate',
description: 'Represents Request rate of the service',
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.EMPTY,
id: '------false',
isColumn: false,
isJSON: false,
key: '',
type: '',
},
aggregateOperator: 'rate',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: filters ?? [],
op: 'AND',
},
functions: [],
groupBy: groupByFilter ? [groupByFilter] : [],
having: [],
legend: 'Request Rate',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'rate',
},
],
panelTypes: PANEL_TYPES.BAR,
}),
);
export const celeryOverviewErrorRateGraphData = (
startTime: number,
endTime: number,
filters?: TagFilterItem[],
groupByFilter?: BaseAutocompleteData,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
title: 'Error',
description: 'Represents Error in the service',
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.EMPTY,
id: '------false',
isColumn: false,
isJSON: false,
key: '',
type: '',
},
aggregateOperator: 'rate',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [
...(filters ?? []),
{
id: uuidv4(),
key: {
dataType: DataTypes.bool,
id: 'has_error--bool----true',
isColumn: true,
isJSON: false,
key: 'has_error',
type: '',
},
op: '=',
value: 'true',
},
],
op: 'AND',
},
functions: [],
groupBy: groupByFilter ? [groupByFilter] : [],
having: [],
legend: 'True',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'rate',
},
{
aggregateAttribute: {
dataType: DataTypes.EMPTY,
id: '------false',
isColumn: false,
isJSON: false,
key: '',
type: '',
},
aggregateOperator: 'rate',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'B',
filters: {
items: [
...(filters ?? []),
{
id: uuidv4(),
key: {
dataType: DataTypes.bool,
id: 'has_error--bool----true',
isColumn: true,
isJSON: false,
key: 'has_error',
type: '',
},
op: '=',
value: 'false',
},
],
op: 'AND',
},
functions: [],
groupBy: groupByFilter ? [groupByFilter] : [],
having: [],
legend: 'False',
limit: null,
orderBy: [],
queryName: 'B',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'rate',
},
],
panelTypes: PANEL_TYPES.BAR,
}),
);
export const celeryOverviewAvgLatencyGraphData = (
startTime: number,
endTime: number,
filters?: TagFilterItem[],
groupByFilter?: BaseAutocompleteData,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
title: 'Latency',
description: 'Represents Latency of the service',
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.ArrayFloat64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
aggregateOperator: 'p90',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'C',
filters: {
items: filters ?? [],
op: 'AND',
},
functions: [],
groupBy: groupByFilter ? [groupByFilter] : [],
having: [],
legend: 'p90',
limit: null,
orderBy: [],
queryName: 'C',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'p90',
},
{
aggregateAttribute: {
dataType: DataTypes.ArrayFloat64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
aggregateOperator: 'p95',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'D',
filters: {
items: filters ?? [],
op: 'AND',
},
functions: [],
groupBy: groupByFilter ? [groupByFilter] : [],
having: [],
legend: 'p95',
limit: null,
orderBy: [],
queryName: 'D',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'p95',
},
{
aggregateAttribute: {
dataType: DataTypes.ArrayFloat64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
aggregateOperator: 'p99',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'E',
filters: {
items: filters ?? [],
op: 'AND',
},
functions: [],
groupBy: groupByFilter ? [groupByFilter] : [],
having: [],
legend: 'p99',
limit: null,
orderBy: [],
queryName: 'E',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: getStepInterval(startTime, endTime),
timeAggregation: 'p99',
},
],
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ns',
}),
);

View File

@ -0,0 +1,47 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
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 const getQueryPayloadFromWidgetsData = ({
start,
end,
widgetsData,
panelType,
}: {
start: number;
end: number;
widgetsData: Widgets[];
panelType: PANEL_TYPES;
}): GetQueryResultsProps[] =>
widgetsData.map((widget) => ({
start,
end,
graphType: panelType,
query: widget.query,
selectedTime: 'GLOBAL_TIME',
formatForWeb: false,
variables: {},
}));
export const getFiltersFromKeyValue = (
key: string,
value: string | number,
type?: string,
op?: string,
dataType?: DataTypes,
): TagFilterItem => ({
id: uuidv4(),
key: {
key,
dataType: dataType || DataTypes.String,
type: type || 'tag',
isColumn: false,
isJSON: false,
id: `${key}--${dataType || DataTypes.String}--${type || 'tag'}--false`,
},
op: op || '=',
value: value.toString(),
});

View File

@ -37,11 +37,11 @@ export const Overview: TabRoutes = {
Component: CeleryOverview,
name: (
<div className="tab-item">
<Rows3 size={16} /> Celery Overview
<Rows3 size={16} /> Overview
</div>
),
route: ROUTES.MESSAGING_QUEUES_CELERY_OVERVIEW,
key: ROUTES.MESSAGING_QUEUES_CELERY_OVERVIEW,
route: ROUTES.MESSAGING_QUEUES_OVERVIEW,
key: ROUTES.MESSAGING_QUEUES_OVERVIEW,
};
export default function MessagingQueuesMainPage(): JSX.Element {

View File

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