mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 04:15:57 +08:00
feat: celery overview page (#6918)
* 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 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: added new celery overview page * feat: added table feat and column details * feat: improved table style and column configs * feat: added service name filter and common filter logic * feat: code fix * feat: code fix
This commit is contained in:
parent
e83e691ef5
commit
bd0c4beeee
@ -254,3 +254,10 @@ export const CeleryTask = Loadable(
|
|||||||
/* webpackChunkName: "CeleryTask" */ 'pages/Celery/CeleryTask/CeleryTask'
|
/* webpackChunkName: "CeleryTask" */ 'pages/Celery/CeleryTask/CeleryTask'
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const CeleryOverview = Loadable(
|
||||||
|
() =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "CeleryOverview" */ 'pages/Celery/CeleryOverview/CeleryOverview'
|
||||||
|
),
|
||||||
|
);
|
||||||
|
@ -408,6 +408,13 @@ const routes: AppRoutes[] = [
|
|||||||
key: 'MESSAGING_QUEUES_CELERY_TASK',
|
key: 'MESSAGING_QUEUES_CELERY_TASK',
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.MESSAGING_QUEUES_CELERY_OVERVIEW,
|
||||||
|
exact: true,
|
||||||
|
component: MessagingQueues,
|
||||||
|
key: 'MESSAGING_QUEUES_CELERY_OVERVIEW',
|
||||||
|
isPrivate: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ROUTES.MESSAGING_QUEUES_DETAIL,
|
path: ROUTES.MESSAGING_QUEUES_DETAIL,
|
||||||
exact: true,
|
exact: true,
|
||||||
|
53
frontend/src/api/messagingQueues/celery/getQueueOverview.ts
Normal file
53
frontend/src/api/messagingQueues/celery/getQueueOverview.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
|
||||||
|
export interface QueueOverviewPayload {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
filters: {
|
||||||
|
items: {
|
||||||
|
key: {
|
||||||
|
key: string;
|
||||||
|
dataType: string;
|
||||||
|
};
|
||||||
|
op: string;
|
||||||
|
value: string[];
|
||||||
|
}[];
|
||||||
|
op: 'AND' | 'OR';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueueOverviewResponse {
|
||||||
|
status: string;
|
||||||
|
data: {
|
||||||
|
timestamp?: string;
|
||||||
|
data: {
|
||||||
|
destination?: string;
|
||||||
|
error_percentage?: number;
|
||||||
|
kind_string?: string;
|
||||||
|
messaging_system?: string;
|
||||||
|
p95_latency?: number;
|
||||||
|
service_name?: string;
|
||||||
|
span_name?: string;
|
||||||
|
throughput?: number;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getQueueOverview = async (
|
||||||
|
props: QueueOverviewPayload,
|
||||||
|
): Promise<SuccessResponse<QueueOverviewResponse['data']> | ErrorResponse> => {
|
||||||
|
const { start, end, filters } = props;
|
||||||
|
const response = await axios.post(`messaging-queues/queue-overview`, {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
filters,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data.data,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,39 @@
|
|||||||
|
.celery-overview-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-overview-filters {
|
||||||
|
.celery-filters {
|
||||||
|
.config-select-option {
|
||||||
|
.ant-select-selector {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,126 @@
|
|||||||
|
import './CeleryOverviewConfigOptions.styles.scss';
|
||||||
|
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Button, Select, Spin, Tooltip } from 'antd';
|
||||||
|
import {
|
||||||
|
getValuesFromQueryParams,
|
||||||
|
setQueryParamsFromOptions,
|
||||||
|
} from 'components/CeleryTask/CeleryUtils';
|
||||||
|
import { useCeleryFilterOptions } from 'components/CeleryTask/useCeleryFilterOptions';
|
||||||
|
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';
|
||||||
|
|
||||||
|
interface SelectOptionConfig {
|
||||||
|
placeholder: string;
|
||||||
|
queryParam: QueryParams;
|
||||||
|
filterType: 'serviceName' | 'spanName' | 'msgSystem';
|
||||||
|
}
|
||||||
|
|
||||||
|
function FilterSelect({
|
||||||
|
placeholder,
|
||||||
|
queryParam,
|
||||||
|
filterType,
|
||||||
|
}: SelectOptionConfig): JSX.Element {
|
||||||
|
const { handleSearch, isFetching, options } = useCeleryFilterOptions(
|
||||||
|
filterType,
|
||||||
|
);
|
||||||
|
const urlQuery = useUrlQuery();
|
||||||
|
const history = useHistory();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
key={filterType}
|
||||||
|
placeholder={placeholder}
|
||||||
|
showSearch
|
||||||
|
mode="multiple"
|
||||||
|
options={options}
|
||||||
|
loading={isFetching}
|
||||||
|
className="config-select-option"
|
||||||
|
onSearch={handleSearch}
|
||||||
|
maxTagCount={4}
|
||||||
|
maxTagPlaceholder={SelectMaxTagPlaceholder}
|
||||||
|
value={getValuesFromQueryParams(queryParam, urlQuery) || []}
|
||||||
|
notFoundContent={
|
||||||
|
isFetching ? (
|
||||||
|
<span>
|
||||||
|
<Spin size="small" /> Loading...
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>No {placeholder} found</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onChange={(value): void => {
|
||||||
|
handleSearch('');
|
||||||
|
setQueryParamsFromOptions(value, urlQuery, history, location, queryParam);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CeleryOverviewConfigOptions(): JSX.Element {
|
||||||
|
const [isURLCopied, setIsURLCopied] = useState(false);
|
||||||
|
|
||||||
|
const [, handleCopyToClipboard] = useCopyToClipboard();
|
||||||
|
|
||||||
|
const selectConfigs: SelectOptionConfig[] = [
|
||||||
|
{
|
||||||
|
placeholder: 'Service Name',
|
||||||
|
queryParam: QueryParams.service,
|
||||||
|
filterType: 'serviceName',
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// placeholder: 'Span Name',
|
||||||
|
// queryParam: QueryParams.spanName,
|
||||||
|
// filterType: 'spanName',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// placeholder: 'Msg System',
|
||||||
|
// queryParam: QueryParams.msgSystem,
|
||||||
|
// filterType: 'msgSystem',
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleShareURL = (): void => {
|
||||||
|
handleCopyToClipboard(window.location.href);
|
||||||
|
setIsURLCopied(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsURLCopied(false);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="celery-overview-filters">
|
||||||
|
<div className="celery-filters">
|
||||||
|
{selectConfigs.map((config) => (
|
||||||
|
<FilterSelect
|
||||||
|
key={config.filterType}
|
||||||
|
placeholder={config.placeholder}
|
||||||
|
queryParam={config.queryParam}
|
||||||
|
filterType={config.filterType}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Tooltip title="Share this" arrow={false}>
|
||||||
|
<Button
|
||||||
|
className="periscope-btn copy-url-btn"
|
||||||
|
onClick={handleShareURL}
|
||||||
|
icon={
|
||||||
|
isURLCopied ? (
|
||||||
|
<Check size={14} color={Color.BG_FOREST_500} />
|
||||||
|
) : (
|
||||||
|
<Share2 size={14} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CeleryOverviewConfigOptions;
|
@ -0,0 +1,106 @@
|
|||||||
|
.progress-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable-row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.celery-overview-table {
|
||||||
|
.ant-table {
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
padding: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
border-bottom: none;
|
||||||
|
|
||||||
|
color: var(--Vanilla-400, #c0c1c3);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 18px; /* 163.636% */
|
||||||
|
letter-spacing: 0.44px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell {
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
.ant-progress-bg {
|
||||||
|
height: 8px !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr:hover > td {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:first-child {
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:nth-child(2) {
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:nth-child(n + 3) {
|
||||||
|
padding-right: 24px;
|
||||||
|
}
|
||||||
|
.column-header-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.column-header-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.ant-table-tbody > tr > td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-pagination {
|
||||||
|
position: relative;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
padding: 4px;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
// this is to offset intercom icon
|
||||||
|
padding-right: 72px;
|
||||||
|
|
||||||
|
.ant-pagination-item {
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&-active {
|
||||||
|
background: var(--bg-robin-500);
|
||||||
|
border-color: var(--bg-robin-500);
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--bg-ink-500) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,349 @@
|
|||||||
|
import './CeleryOverviewTable.styles.scss';
|
||||||
|
|
||||||
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Progress, Spin, TableColumnsType, Tooltip, Typography } from 'antd';
|
||||||
|
import {
|
||||||
|
getQueueOverview,
|
||||||
|
QueueOverviewResponse,
|
||||||
|
} from 'api/messagingQueues/celery/getQueueOverview';
|
||||||
|
import { isNumber } from 'chart.js/helpers';
|
||||||
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
import useDragColumns from 'hooks/useDragColumns';
|
||||||
|
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
|
||||||
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useMutation } from 'react-query';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
|
const INITIAL_PAGE_SIZE = 20;
|
||||||
|
|
||||||
|
const showPaginationItem = (total: number, range: number[]): JSX.Element => (
|
||||||
|
<>
|
||||||
|
<Typography.Text className="numbers">
|
||||||
|
{range[0]} — {range[1]}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text className="total"> of {total}</Typography.Text>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export type RowData = {
|
||||||
|
key: string | number;
|
||||||
|
[key: string]: string | number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ProgressRender(item: string | number): JSX.Element {
|
||||||
|
const percent = Number(Number(item).toFixed(1));
|
||||||
|
return (
|
||||||
|
<div className="progress-container">
|
||||||
|
<Progress
|
||||||
|
percent={percent}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
size="small"
|
||||||
|
strokeColor={((): string => {
|
||||||
|
const cpuPercent = percent;
|
||||||
|
if (cpuPercent >= 90) return Color.BG_SAKURA_500;
|
||||||
|
if (cpuPercent >= 60) return Color.BG_AMBER_500;
|
||||||
|
return Color.BG_FOREST_500;
|
||||||
|
})()}
|
||||||
|
className="progress-bar"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColumns(data: RowData[]): TableColumnsType<RowData> {
|
||||||
|
if (data?.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltipRender = (item: string): JSX.Element => (
|
||||||
|
<Tooltip placement="topLeft" title={item}>
|
||||||
|
{item}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: 'SERVICE NAME',
|
||||||
|
dataIndex: 'service_name',
|
||||||
|
key: 'service_name',
|
||||||
|
ellipsis: {
|
||||||
|
showTitle: false,
|
||||||
|
},
|
||||||
|
width: 200,
|
||||||
|
sorter: (a: RowData, b: RowData): number =>
|
||||||
|
String(a.service_name).localeCompare(String(b.service_name)),
|
||||||
|
render: tooltipRender,
|
||||||
|
fixed: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'SPAN NAME',
|
||||||
|
dataIndex: 'span_name',
|
||||||
|
key: 'span_name',
|
||||||
|
ellipsis: {
|
||||||
|
showTitle: false,
|
||||||
|
},
|
||||||
|
width: 200,
|
||||||
|
sorter: (a: RowData, b: RowData): number =>
|
||||||
|
String(a.span_name).localeCompare(String(b.span_name)),
|
||||||
|
render: tooltipRender,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'MESSAGING SYSTEM',
|
||||||
|
dataIndex: 'messaging_system',
|
||||||
|
key: 'messaging_system',
|
||||||
|
ellipsis: {
|
||||||
|
showTitle: false,
|
||||||
|
},
|
||||||
|
width: 200,
|
||||||
|
sorter: (a: RowData, b: RowData): number =>
|
||||||
|
String(a.messaging_system).localeCompare(String(b.messaging_system)),
|
||||||
|
render: tooltipRender,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'DESTINATION',
|
||||||
|
dataIndex: 'destination',
|
||||||
|
key: 'destination',
|
||||||
|
ellipsis: {
|
||||||
|
showTitle: false,
|
||||||
|
},
|
||||||
|
render: tooltipRender,
|
||||||
|
width: 200,
|
||||||
|
sorter: (a: RowData, b: RowData): number =>
|
||||||
|
String(a.destination).localeCompare(String(b.destination)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'KIND',
|
||||||
|
dataIndex: 'kind_string',
|
||||||
|
key: 'kind_string',
|
||||||
|
ellipsis: {
|
||||||
|
showTitle: false,
|
||||||
|
},
|
||||||
|
width: 100,
|
||||||
|
sorter: (a: RowData, b: RowData): number =>
|
||||||
|
String(a.kind_string).localeCompare(String(b.kind_string)),
|
||||||
|
render: tooltipRender,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ERROR %',
|
||||||
|
dataIndex: 'error_percentage',
|
||||||
|
key: 'error_percentage',
|
||||||
|
ellipsis: {
|
||||||
|
showTitle: false,
|
||||||
|
},
|
||||||
|
width: 200,
|
||||||
|
sorter: (a: RowData, b: RowData): number =>
|
||||||
|
String(a.error_percentage).localeCompare(String(b.error_percentage)),
|
||||||
|
render: ProgressRender,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'LATENCY (P95)',
|
||||||
|
dataIndex: 'p95_latency',
|
||||||
|
key: 'p95_latency',
|
||||||
|
ellipsis: {
|
||||||
|
showTitle: false,
|
||||||
|
},
|
||||||
|
width: 100,
|
||||||
|
sorter: (a: RowData, b: RowData): number =>
|
||||||
|
String(a.p95_latency).localeCompare(String(b.p95_latency)),
|
||||||
|
render: (value: number | string): string => {
|
||||||
|
if (!isNumber(value)) return value.toString();
|
||||||
|
return (typeof value === 'string' ? parseFloat(value) : value).toFixed(3);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'THROUGHPUT',
|
||||||
|
dataIndex: 'throughput',
|
||||||
|
key: 'throughput',
|
||||||
|
ellipsis: {
|
||||||
|
showTitle: false,
|
||||||
|
},
|
||||||
|
width: 100,
|
||||||
|
sorter: (a: RowData, b: RowData): number =>
|
||||||
|
String(a.throughput).localeCompare(String(b.throughput)),
|
||||||
|
render: (value: number | string): string => {
|
||||||
|
if (!isNumber(value)) return value.toString();
|
||||||
|
return (typeof value === 'string' ? parseFloat(value) : value).toFixed(3);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTableData(data: QueueOverviewResponse['data']): RowData[] {
|
||||||
|
if (data?.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnOrder = [
|
||||||
|
'service_name',
|
||||||
|
'span_name',
|
||||||
|
'messaging_system',
|
||||||
|
'destination',
|
||||||
|
'kind_string',
|
||||||
|
'error_percentage',
|
||||||
|
'p95_latency',
|
||||||
|
'throughput',
|
||||||
|
];
|
||||||
|
|
||||||
|
const tableData: RowData[] =
|
||||||
|
data?.map(
|
||||||
|
(row, index: number): RowData => {
|
||||||
|
const rowData: Record<string, string | number> = {};
|
||||||
|
columnOrder.forEach((key) => {
|
||||||
|
const value = row.data[key as keyof typeof row.data];
|
||||||
|
if (typeof value === 'string' || typeof value === 'number') {
|
||||||
|
rowData[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.entries(row.data).forEach(([key, value]) => {
|
||||||
|
if (
|
||||||
|
!columnOrder.includes(key) &&
|
||||||
|
(typeof value === 'string' || typeof value === 'number')
|
||||||
|
) {
|
||||||
|
rowData[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rowData,
|
||||||
|
key: index,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
return tableData;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Filter = {
|
||||||
|
key: {
|
||||||
|
key: string;
|
||||||
|
dataType: string;
|
||||||
|
};
|
||||||
|
op: string;
|
||||||
|
value: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type FilterConfig = {
|
||||||
|
paramName: string;
|
||||||
|
operator: string;
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
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' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return filterConfigs
|
||||||
|
.map(({ paramName, operator, key }) => {
|
||||||
|
const value = urlQuery.get(paramName);
|
||||||
|
if (!value) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: {
|
||||||
|
key,
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
op: operator,
|
||||||
|
value: value.split(','),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((filter): filter is Filter => filter !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CeleryOverviewTable(): JSX.Element {
|
||||||
|
const [tableData, setTableData] = useState<RowData[]>([]);
|
||||||
|
|
||||||
|
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutate: getOverviewData, isLoading } = useMutation(getQueueOverview, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data?.payload) {
|
||||||
|
setTableData(getTableData(data?.payload));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const urlQuery = useUrlQuery();
|
||||||
|
const filters = useMemo(() => makeFilters(urlQuery), [urlQuery]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getOverviewData({
|
||||||
|
start: minTime,
|
||||||
|
end: maxTime,
|
||||||
|
filters: {
|
||||||
|
items: filters,
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [getOverviewData, minTime, maxTime, filters]);
|
||||||
|
|
||||||
|
const { draggedColumns, onDragColumns } = useDragColumns<RowData>(
|
||||||
|
LOCALSTORAGE.CELERY_OVERVIEW_COLUMNS,
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => getDraggedColumns<RowData>(getColumns(tableData), draggedColumns),
|
||||||
|
[tableData, draggedColumns],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDragColumn = useCallback(
|
||||||
|
(fromIndex: number, toIndex: number) =>
|
||||||
|
onDragColumns(columns, fromIndex, toIndex),
|
||||||
|
[columns, onDragColumns],
|
||||||
|
);
|
||||||
|
|
||||||
|
const paginationConfig = useMemo(
|
||||||
|
() =>
|
||||||
|
tableData?.length > INITIAL_PAGE_SIZE && {
|
||||||
|
pageSize: INITIAL_PAGE_SIZE,
|
||||||
|
showTotal: showPaginationItem,
|
||||||
|
showSizeChanger: false,
|
||||||
|
hideOnSinglePage: true,
|
||||||
|
},
|
||||||
|
[tableData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRowClick = (record: RowData): void => {
|
||||||
|
console.log(record);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: '100%' }}>
|
||||||
|
<ResizeTable
|
||||||
|
className="celery-overview-table"
|
||||||
|
pagination={paginationConfig}
|
||||||
|
size="middle"
|
||||||
|
columns={columns}
|
||||||
|
dataSource={tableData}
|
||||||
|
bordered={false}
|
||||||
|
loading={{
|
||||||
|
spinning: isLoading,
|
||||||
|
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||||
|
}}
|
||||||
|
locale={{
|
||||||
|
emptyText: isLoading ? null : <Typography.Text>No data</Typography.Text>,
|
||||||
|
}}
|
||||||
|
scroll={{ x: true }}
|
||||||
|
showSorterTooltip
|
||||||
|
onDragColumn={handleDragColumn}
|
||||||
|
onRow={(record): { onClick: () => void; className: string } => ({
|
||||||
|
onClick: (): void => handleRowClick(record),
|
||||||
|
className: 'clickable-row',
|
||||||
|
})}
|
||||||
|
tableLayout="fixed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -5,11 +5,9 @@ import { useQuery } from 'react-query';
|
|||||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
export type FilterOptionType = 'celery.task_name';
|
|
||||||
|
|
||||||
export interface Filters {
|
export interface Filters {
|
||||||
searchText: string;
|
searchText: string;
|
||||||
attributeKey: FilterOptionType;
|
attributeKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetAllFiltersResponse {
|
export interface GetAllFiltersResponse {
|
||||||
|
@ -2,13 +2,10 @@ import { DefaultOptionType } from 'antd/es/select';
|
|||||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import {
|
import { useGetAllFilters } from './CeleryTaskConfigOptions/useGetCeleryFilters';
|
||||||
FilterOptionType,
|
|
||||||
useGetAllFilters,
|
|
||||||
} from './CeleryTaskConfigOptions/useGetCeleryFilters';
|
|
||||||
|
|
||||||
export const useCeleryFilterOptions = (
|
export const useCeleryFilterOptions = (
|
||||||
type: FilterOptionType,
|
type: string,
|
||||||
): {
|
): {
|
||||||
searchText: string;
|
searchText: string;
|
||||||
handleSearch: (value: string) => void;
|
handleSearch: (value: string) => void;
|
||||||
|
@ -24,4 +24,5 @@ export enum LOCALSTORAGE {
|
|||||||
USER_ID = 'USER_ID',
|
USER_ID = 'USER_ID',
|
||||||
PREFERRED_TIMEZONE = 'PREFERRED_TIMEZONE',
|
PREFERRED_TIMEZONE = 'PREFERRED_TIMEZONE',
|
||||||
UNAUTHENTICATED_ROUTE_HIT = 'UNAUTHENTICATED_ROUTE_HIT',
|
UNAUTHENTICATED_ROUTE_HIT = 'UNAUTHENTICATED_ROUTE_HIT',
|
||||||
|
CELERY_OVERVIEW_COLUMNS = 'CELERY_OVERVIEW_COLUMNS',
|
||||||
}
|
}
|
||||||
|
@ -42,4 +42,6 @@ export enum QueryParams {
|
|||||||
getStartedSourceService = 'getStartedSourceService',
|
getStartedSourceService = 'getStartedSourceService',
|
||||||
mqServiceView = 'mqServiceView',
|
mqServiceView = 'mqServiceView',
|
||||||
taskName = 'taskName',
|
taskName = 'taskName',
|
||||||
|
spanName = 'spanName',
|
||||||
|
msgSystem = 'msgSystem',
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,7 @@ const ROUTES = {
|
|||||||
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',
|
MESSAGING_QUEUES_CELERY_TASK: '/messaging-queues/celery-task',
|
||||||
|
MESSAGING_QUEUES_CELERY_OVERVIEW: '/messaging-queues/celery-overview',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default ROUTES;
|
export default ROUTES;
|
||||||
|
@ -289,7 +289,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
const isMessagingQueues = (): boolean =>
|
const isMessagingQueues = (): boolean =>
|
||||||
routeKey === 'MESSAGING_QUEUES' ||
|
routeKey === 'MESSAGING_QUEUES' ||
|
||||||
routeKey === 'MESSAGING_QUEUES_DETAIL' ||
|
routeKey === 'MESSAGING_QUEUES_DETAIL' ||
|
||||||
routeKey === 'MESSAGING_QUEUES_CELERY_TASK';
|
routeKey === 'MESSAGING_QUEUES_CELERY_TASK' ||
|
||||||
|
routeKey === 'MESSAGING_QUEUES_CELERY_OVERVIEW';
|
||||||
|
|
||||||
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
|
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
|
||||||
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';
|
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';
|
||||||
|
@ -52,6 +52,7 @@ export const routeConfig: Record<string, QueryParams[]> = {
|
|||||||
[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.MESSAGING_QUEUES_CELERY_TASK]: [QueryParams.resourceAttributes],
|
||||||
|
[ROUTES.MESSAGING_QUEUES_CELERY_OVERVIEW]: [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,
|
||||||
|
@ -215,6 +215,7 @@ export const routesToSkip = [
|
|||||||
ROUTES.MESSAGING_QUEUES,
|
ROUTES.MESSAGING_QUEUES,
|
||||||
ROUTES.MESSAGING_QUEUES_DETAIL,
|
ROUTES.MESSAGING_QUEUES_DETAIL,
|
||||||
ROUTES.MESSAGING_QUEUES_CELERY_TASK,
|
ROUTES.MESSAGING_QUEUES_CELERY_TASK,
|
||||||
|
ROUTES.MESSAGING_QUEUES_CELERY_OVERVIEW,
|
||||||
ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
|
ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
|
||||||
ROUTES.SOMETHING_WENT_WRONG,
|
ROUTES.SOMETHING_WENT_WRONG,
|
||||||
ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES,
|
ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES,
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
.celery-overview-container {
|
||||||
|
.celery-overview-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-overview-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.celery-overview-content-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.celery-overview-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
frontend/src/pages/Celery/CeleryOverview/CeleryOverview.tsx
Normal file
20
frontend/src/pages/Celery/CeleryOverview/CeleryOverview.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import './CeleryOverview.styles.scss';
|
||||||
|
|
||||||
|
import CeleryOverviewConfigOptions from 'components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions';
|
||||||
|
import CeleryOverviewTable from 'components/CeleryOverview/CeleryOverviewTable/CeleryOverviewTable';
|
||||||
|
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||||
|
|
||||||
|
export default function CeleryOverview(): JSX.Element {
|
||||||
|
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>
|
||||||
|
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
|
||||||
|
</div>
|
||||||
|
<CeleryOverviewConfigOptions />
|
||||||
|
<CeleryOverviewTable />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -5,6 +5,7 @@ import { TabRoutes } from 'components/RouteTab/types';
|
|||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { ListMinus, Rows3 } from 'lucide-react';
|
import { ListMinus, Rows3 } from 'lucide-react';
|
||||||
|
import CeleryOverview from 'pages/Celery/CeleryOverview/CeleryOverview';
|
||||||
import { useLocation } from 'react-use';
|
import { useLocation } from 'react-use';
|
||||||
|
|
||||||
import CeleryTask from '../Celery/CeleryTask/CeleryTask';
|
import CeleryTask from '../Celery/CeleryTask/CeleryTask';
|
||||||
@ -32,10 +33,21 @@ export const Celery: TabRoutes = {
|
|||||||
key: ROUTES.MESSAGING_QUEUES_CELERY_TASK,
|
key: ROUTES.MESSAGING_QUEUES_CELERY_TASK,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Overview: TabRoutes = {
|
||||||
|
Component: CeleryOverview,
|
||||||
|
name: (
|
||||||
|
<div className="tab-item">
|
||||||
|
<Rows3 size={16} /> Celery Overview
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
route: ROUTES.MESSAGING_QUEUES_CELERY_OVERVIEW,
|
||||||
|
key: ROUTES.MESSAGING_QUEUES_CELERY_OVERVIEW,
|
||||||
|
};
|
||||||
|
|
||||||
export default function MessagingQueuesMainPage(): JSX.Element {
|
export default function MessagingQueuesMainPage(): JSX.Element {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
const routes: TabRoutes[] = [Kafka, Celery];
|
const routes: TabRoutes[] = [Kafka, Celery, Overview];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="messaging-queues-module-container">
|
<div className="messaging-queues-module-container">
|
||||||
|
@ -109,4 +109,5 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
|||||||
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'],
|
MESSAGING_QUEUES_CELERY_TASK: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
|
MESSAGING_QUEUES_CELERY_OVERVIEW: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user