feat: added Messaging queue detail page (#5690)

* feat: added Messaging queue detail page

* feat: added MQDetails - tables - consumer, producer & network latency

* feat: added MQConfigOption - with dummy responses

* feat: configured query-range and autocomplete against the staging setup

* feat: added queryparams and linked config options with graph

* feat: added shareable link, cleanup code and connected details table with graph

* feat: fixed comments

* Messaging queue overview (#5782)

* feat: added messaging queue overview page

* feat: added get-started links

* feat: fixed comments

* feat: messaging queue misc tasks (#5785)

* feat: added lightMode styles

* feat: misc fix

* feat: misc fix

* feat: added customer tooltip info text

* feat: removed reset btn until the funcitonality is clear

* feat: fixed comments

* feat: fixed comments and added onDragSelect

* feat: added placeholder doc link for get-started for non-cloud
This commit is contained in:
SagarRajput-7 2024-08-29 16:36:56 +05:30 committed by GitHub
parent 532f274bd6
commit 140533b790
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 1893 additions and 6 deletions

View File

@ -49,5 +49,6 @@
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
"DEFAULT": "Open source Observability Platform | SigNoz",
"SHORTCUTS": "SigNoz | Shortcuts",
"INTEGRATIONS": "SigNoz | Integrations"
"INTEGRATIONS": "SigNoz | Integrations",
"MESSAGING_QUEUES": "SigNoz | Messaging Queues"
}

View File

@ -204,3 +204,15 @@ export const InstalledIntegrations = Loadable(
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
),
);
export const MessagingQueues = Loadable(
() =>
import(/* webpackChunkName: "MessagingQueues" */ 'pages/MessagingQueues'),
);
export const MQDetailPage = Loadable(
() =>
import(
/* webpackChunkName: "MQDetailPage" */ 'pages/MessagingQueues/MQDetailPage'
),
);

View File

@ -23,6 +23,8 @@ import {
LogsExplorer,
LogsIndexToFields,
LogsSaveViews,
MessagingQueues,
MQDetailPage,
MySettings,
NewDashboardPage,
OldLogsExplorer,
@ -351,6 +353,20 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'INTEGRATIONS',
},
{
path: ROUTES.MESSAGING_QUEUES,
exact: true,
component: MessagingQueues,
key: 'MESSAGING_QUEUES',
isPrivate: true,
},
{
path: ROUTES.MESSAGING_QUEUES_DETAIL,
exact: true,
component: MQDetailPage,
key: 'MESSAGING_QUEUES_DETAIL',
isPrivate: true,
},
];
export const SUPPORT_ROUTE: AppRoutes = {

View File

@ -32,4 +32,8 @@ export enum QueryParams {
relativeTime = 'relativeTime',
alertType = 'alertType',
ruleId = 'ruleId',
consumerGrp = 'consumerGrp',
topic = 'topic',
partition = 'partition',
selectedTimelineQuery = 'selectedTimelineQuery',
}

View File

@ -8,4 +8,5 @@ export const REACT_QUERY_KEY = {
GET_FEATURES_FLAGS: 'GET_FEATURES_FLAGS',
DELETE_DASHBOARD: 'DELETE_DASHBOARD',
LOGS_PIPELINE_PREVIEW: 'LOGS_PIPELINE_PREVIEW',
GET_CONSUMER_LAG_DETAILS: 'GET_CONSUMER_LAG_DETAILS',
};

View File

@ -54,6 +54,8 @@ const ROUTES = {
WORKSPACE_LOCKED: '/workspace-locked',
SHORTCUTS: '/shortcuts',
INTEGRATIONS: '/integrations',
MESSAGING_QUEUES: '/messaging-queues',
MESSAGING_QUEUES_DETAIL: '/messaging-queues/detail',
} as const;
export default ROUTES;

View File

@ -9,6 +9,7 @@ export const GlobalShortcuts = {
NavigateToDashboards: 'd+shift',
NavigateToAlerts: 'a+shift',
NavigateToExceptions: 'e+shift',
NavigateToMessagingQueues: 'm+shift',
};
export const GlobalShortcutsName = {
@ -19,6 +20,7 @@ export const GlobalShortcutsName = {
NavigateToDashboards: 'shift+d',
NavigateToAlerts: 'shift+a',
NavigateToExceptions: 'shift+e',
NavigateToMessagingQueues: 'shift+m',
};
export const GlobalShortcutsDescription = {
@ -29,4 +31,5 @@ export const GlobalShortcutsDescription = {
NavigateToDashboards: 'Navigate to dashboards page',
NavigateToAlerts: 'Navigate to alerts page',
NavigateToExceptions: 'Navigate to Exceptions page',
NavigateToMessagingQueues: 'Navigate to Messaging Queues page',
};

View File

@ -241,6 +241,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const isTracesView = (): boolean =>
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
const isMessagingQueues = (): boolean =>
routeKey === 'MESSAGING_QUEUES' || routeKey === 'MESSAGING_QUEUES_DETAIL';
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
const isDashboardView = (): boolean => {
/**
@ -329,7 +332,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isTracesView() ||
isDashboardView() ||
isDashboardWidgetView() ||
isDashboardListView()
isDashboardListView() ||
isMessagingQueues()
? 0
: '0 1rem',
}}

View File

@ -47,6 +47,7 @@ function WidgetGraphComponent({
setRequestData,
onClickHandler,
onDragSelect,
customTooltipElement,
}: WidgetGraphComponentProps): JSX.Element {
const [deleteModal, setDeleteModal] = useState(false);
const [hovered, setHovered] = useState(false);
@ -335,6 +336,7 @@ function WidgetGraphComponent({
onClickHandler={onClickHandler}
onDragSelect={onDragSelect}
tableProcessedDataRef={tableProcessedDataRef}
customTooltipElement={customTooltipElement}
/>
</div>
)}

View File

@ -33,6 +33,7 @@ function GridCardGraph({
version,
onClickHandler,
onDragSelect,
customTooltipElement,
}: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>();
@ -215,6 +216,7 @@ function GridCardGraph({
setRequestData={setRequestData}
onClickHandler={onClickHandler}
onDragSelect={onDragSelect}
customTooltipElement={customTooltipElement}
/>
)}
</div>

View File

@ -31,6 +31,7 @@ export interface WidgetGraphComponentProps {
setRequestData?: Dispatch<SetStateAction<GetQueryResultsProps>>;
onClickHandler?: OnClickPluginOpts['onClick'];
onDragSelect: (start: number, end: number) => void;
customTooltipElement?: HTMLDivElement;
}
export interface GridCardGraphProps {
@ -42,6 +43,7 @@ export interface GridCardGraphProps {
variables?: Dashboard['data']['variables'];
version?: string;
onDragSelect: (start: number, end: number) => void;
customTooltipElement?: HTMLDivElement;
}
export interface GetGraphVisibilityStateOnLegendClickProps {

View File

@ -15,6 +15,7 @@ function PanelWrapper({
onDragSelect,
selectedGraph,
tableProcessedDataRef,
customTooltipElement,
}: PanelWrapperProps): JSX.Element {
const Component = PanelTypeVsPanelWrapper[
selectedGraph || widget.panelTypes
@ -37,6 +38,7 @@ function PanelWrapper({
onDragSelect={onDragSelect}
selectedGraph={selectedGraph}
tableProcessedDataRef={tableProcessedDataRef}
customTooltipElement={customTooltipElement}
/>
);
}

View File

@ -30,6 +30,7 @@ function UplotPanelWrapper({
onClickHandler,
onDragSelect,
selectedGraph,
customTooltipElement,
}: PanelWrapperProps): JSX.Element {
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const isDarkMode = useIsDarkMode();
@ -126,6 +127,7 @@ function UplotPanelWrapper({
stackBarChart: widget?.stackedBarChart,
hiddenGraph,
setHiddenGraph,
customTooltipElement,
}),
[
widget?.id,
@ -147,6 +149,7 @@ function UplotPanelWrapper({
selectedGraph,
currentQuery,
hiddenGraph,
customTooltipElement,
],
);

View File

@ -23,6 +23,7 @@ export type PanelWrapperProps = {
onDragSelect: (start: number, end: number) => void;
selectedGraph?: PANEL_TYPES;
tableProcessedDataRef?: React.MutableRefObject<RowData[]>;
customTooltipElement?: HTMLDivElement;
};
export type TooltipData = {

View File

@ -347,6 +347,10 @@ function SideNav({
onClickHandler(ROUTES.ALL_DASHBOARD, null),
);
registerShortcut(GlobalShortcuts.NavigateToMessagingQueues, () =>
onClickHandler(ROUTES.MESSAGING_QUEUES, null),
);
registerShortcut(GlobalShortcuts.NavigateToAlerts, () =>
onClickHandler(ROUTES.LIST_ALL_ALERT, null),
);
@ -362,6 +366,7 @@ function SideNav({
deregisterShortcut(GlobalShortcuts.NavigateToDashboards);
deregisterShortcut(GlobalShortcuts.NavigateToAlerts);
deregisterShortcut(GlobalShortcuts.NavigateToExceptions);
deregisterShortcut(GlobalShortcuts.NavigateToMessagingQueues);
};
}, [deregisterShortcut, onClickHandler, onCollapse, registerShortcut]);

View File

@ -48,4 +48,6 @@ export const routeConfig: Record<string, QueryParams[]> = {
[ROUTES.TRACE_EXPLORER]: [QueryParams.resourceAttributes],
[ROUTES.LOGS_PIPELINES]: [QueryParams.resourceAttributes],
[ROUTES.WORKSPACE_LOCKED]: [QueryParams.resourceAttributes],
[ROUTES.MESSAGING_QUEUES]: [QueryParams.resourceAttributes],
[ROUTES.MESSAGING_QUEUES_DETAIL]: [QueryParams.resourceAttributes],
};

View File

@ -10,6 +10,7 @@ import {
FileKey2,
Layers2,
LayoutGrid,
ListMinus,
MessageSquare,
Receipt,
Route,
@ -86,6 +87,11 @@ const menuItems: SidebarItem[] = [
label: 'Dashboards',
icon: <LayoutGrid size={16} />,
},
{
key: ROUTES.MESSAGING_QUEUES,
label: 'Messaging Queues',
icon: <ListMinus size={16} />,
},
{
key: ROUTES.LIST_ALL_ALERT,
label: 'Alerts',

View File

@ -27,6 +27,7 @@ const breadcrumbNameMap: Record<string, string> = {
[ROUTES.BILLING]: 'Billing',
[ROUTES.SUPPORT]: 'Support',
[ROUTES.WORKSPACE_LOCKED]: 'Workspace Locked',
[ROUTES.MESSAGING_QUEUES]: 'Messaging Queues',
};
function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element {

View File

@ -208,6 +208,8 @@ export const routesToSkip = [
ROUTES.DASHBOARD,
ROUTES.DASHBOARD_WIDGET,
ROUTES.SERVICE_TOP_LEVEL_OPERATIONS,
ROUTES.MESSAGING_QUEUES,
ROUTES.MESSAGING_QUEUES_DETAIL,
];
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];

View File

@ -53,6 +53,7 @@ export interface GetUPlotChartOptions {
[key: string]: boolean;
}>
>;
customTooltipElement?: HTMLDivElement;
}
/** the function converts series A , series B , series C to
@ -154,6 +155,7 @@ export const getUPlotChartOptions = ({
stackBarChart: stackChart,
hiddenGraph,
setHiddenGraph,
customTooltipElement,
}: GetUPlotChartOptions): uPlot.Options => {
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
@ -209,9 +211,16 @@ export const getUPlotChartOptions = ({
},
},
plugins: [
tooltipPlugin({ apiResponse, yAxisUnit, stackBarChart, isDarkMode }),
tooltipPlugin({
apiResponse,
yAxisUnit,
stackBarChart,
isDarkMode,
customTooltipElement,
}),
onClickPlugin({
onClick: onClickHandler,
apiResponse,
}),
],
hooks: {

View File

@ -1,10 +1,16 @@
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
export interface OnClickPluginOpts {
onClick: (
xValue: number,
yValue: number,
mouseX: number,
mouseY: number,
data?: {
[key: string]: string;
},
) => void;
apiResponse?: MetricRangePayloadProps;
}
function onClickPlugin(opts: OnClickPluginOpts): uPlot.Plugin {
@ -22,9 +28,24 @@ function onClickPlugin(opts: OnClickPluginOpts): uPlot.Plugin {
const xValue = u.posToVal(event.offsetX, 'x');
const yValue = u.posToVal(event.offsetY, 'y');
opts.onClick(xValue, yValue, mouseX, mouseY);
};
let metric = {};
const { series } = u;
const apiResult = opts.apiResponse?.data?.result || [];
// this is to get the metric value of the focused series
if (Array.isArray(series) && series.length > 0) {
series.forEach((item, index) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (item?.show && item?._focus) {
const { metric: focusedMetric } = apiResult[index - 1] || [];
metric = focusedMetric;
}
});
}
opts.onClick(xValue, yValue, mouseX, mouseY, metric);
};
u.over.addEventListener('click', handleClick);
},
destroy: (u: uPlot) => {

View File

@ -222,6 +222,7 @@ type ToolTipPluginProps = {
isMergedSeries?: boolean;
stackBarChart?: boolean;
isDarkMode: boolean;
customTooltipElement?: HTMLDivElement;
};
const tooltipPlugin = ({
@ -232,7 +233,9 @@ const tooltipPlugin = ({
isMergedSeries,
stackBarChart,
isDarkMode,
}: ToolTipPluginProps): any => {
customTooltipElement,
}: // eslint-disable-next-line sonarjs/cognitive-complexity
ToolTipPluginProps): any => {
let over: HTMLElement;
let bound: HTMLElement;
let bLeft: any;
@ -298,6 +301,9 @@ const tooltipPlugin = ({
isMergedSeries,
stackBarChart,
);
if (customTooltipElement) {
content.appendChild(customTooltipElement);
}
overlay.appendChild(content);
placement(overlay, anchor, 'right', 'start', { bound });
}

View File

@ -0,0 +1,34 @@
.coming-soon {
display: inline-flex;
padding: 4px 8px;
border-radius: 20px;
border: 1px solid rgba(173, 127, 88, 0.2);
background: rgba(173, 127, 88, 0.1);
justify-content: center;
align-items: center;
gap: 5px;
&__text {
color: var(--text-sienna-400);
font-size: 10px;
font-weight: 500;
letter-spacing: -0.05px;
line-height: normal;
}
&__icon {
display: flex;
}
}
.tooltip-overlay {
text-wrap: nowrap;
.ant-tooltip-inner {
width: max-content;
}
}
.select-label-with-coming-soon {
display: flex;
align-items: center;
justify-content: space-between;
}

View File

@ -0,0 +1,46 @@
/* eslint-disable react/destructuring-assignment */
import './MQCommon.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Tooltip } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import { Info } from 'lucide-react';
export function ComingSoon(): JSX.Element {
return (
<Tooltip
title="Contact us at cloud-support@signoz.io for more details."
placement="top"
overlayClassName="tooltip-overlay"
>
<div className="coming-soon">
<div className="coming-soon__text">Coming Soon</div>
<div className="coming-soon__icon">
<Info size={10} color={Color.BG_SIENNA_400} />
</div>
</div>
</Tooltip>
);
}
export function SelectMaxTagPlaceholder(
omittedValues: Partial<DefaultOptionType>[],
): JSX.Element {
return (
<Tooltip title={omittedValues.map(({ value }) => value).join(', ')}>
<span>+ {omittedValues.length} </span>
</Tooltip>
);
}
export function SelectLabelWithComingSoon({
label,
}: {
label: string;
}): JSX.Element {
return (
<div className="select-label-with-coming-soon">
{label} <ComingSoon />
</div>
);
}

View File

@ -0,0 +1,72 @@
import '../MessagingQueues.styles.scss';
import { Select, Typography } from 'antd';
import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { ListMinus } from 'lucide-react';
import { useHistory } from 'react-router-dom';
import { SelectLabelWithComingSoon } from '../MQCommon/MQCommon';
import MessagingQueuesDetails from '../MQDetails/MQDetails';
import MessagingQueuesConfigOptions from '../MQGraph/MQConfigOptions';
import MessagingQueuesGraph from '../MQGraph/MQGraph';
enum MessagingQueueViewType {
consumerLag = 'consumerLag',
avgPartitionLatency = 'avgPartitionLatency',
avgProducerLatency = 'avgProducerLatency',
}
function MQDetailPage(): JSX.Element {
const history = useHistory();
return (
<div className="messaging-queue-container">
<div className="messaging-breadcrumb">
<ListMinus size={16} />
<Typography.Text
onClick={(): void => history.push(ROUTES.MESSAGING_QUEUES)}
className="message-queue-text"
>
Messaging Queues
</Typography.Text>
</div>
<div className="messaging-header">
<div className="header-config">
Kafka / views /
<Select
className="messaging-queue-options"
defaultValue="consumerLag"
popupClassName="messaging-queue-options-popup"
options={[
{
label: 'Consumer Lag view',
value: MessagingQueueViewType.consumerLag,
},
{
label: <SelectLabelWithComingSoon label="Avg. Partition latency" />,
value: MessagingQueueViewType.avgPartitionLatency,
disabled: true,
},
{
label: <SelectLabelWithComingSoon label="Avg. Producer latency" />,
value: MessagingQueueViewType.avgProducerLatency,
disabled: true,
},
]}
/>
</div>
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
</div>
<div className="messaging-queue-main-graph">
<MessagingQueuesConfigOptions />
<MessagingQueuesGraph />
</div>
<div className="messaging-queue-details">
<MessagingQueuesDetails />
</div>
</div>
);
}
export default MQDetailPage;

View File

@ -0,0 +1,3 @@
import MQDetailPage from './MQDetailPage';
export default MQDetailPage;

View File

@ -0,0 +1,6 @@
.mq-details {
width: 100%;
display: flex;
flex-direction: column;
gap: 24px;
}

View File

@ -0,0 +1,67 @@
import './MQDetails.style.scss';
import { Radio } from 'antd';
import { Dispatch, SetStateAction, useState } from 'react';
import {
ConsumerLagDetailTitle,
ConsumerLagDetailType,
} from '../MessagingQueuesUtils';
import { ComingSoon } from '../MQCommon/MQCommon';
import MessagingQueuesTable from './MQTables/MQTables';
function MessagingQueuesOptions({
currentTab,
setCurrentTab,
}: {
currentTab: ConsumerLagDetailType;
setCurrentTab: Dispatch<SetStateAction<ConsumerLagDetailType>>;
}): JSX.Element {
const [option, setOption] = useState<ConsumerLagDetailType>(currentTab);
return (
<Radio.Group
onChange={(value): void => {
setOption(value.target.value);
setCurrentTab(value.target.value);
}}
value={option}
className="mq-details-options"
>
<Radio.Button value={ConsumerLagDetailType.ConsumerDetails} checked>
{ConsumerLagDetailTitle[ConsumerLagDetailType.ConsumerDetails]}
</Radio.Button>
<Radio.Button value={ConsumerLagDetailType.ProducerDetails}>
{ConsumerLagDetailTitle[ConsumerLagDetailType.ProducerDetails]}
</Radio.Button>
<Radio.Button value={ConsumerLagDetailType.NetworkLatency}>
{ConsumerLagDetailTitle[ConsumerLagDetailType.NetworkLatency]}
</Radio.Button>
<Radio.Button
value={ConsumerLagDetailType.PartitionHostMetrics}
disabled
className="disabled-option"
>
{ConsumerLagDetailTitle[ConsumerLagDetailType.PartitionHostMetrics]}
<ComingSoon />
</Radio.Button>
</Radio.Group>
);
}
function MessagingQueuesDetails(): JSX.Element {
const [currentTab, setCurrentTab] = useState<ConsumerLagDetailType>(
ConsumerLagDetailType.ConsumerDetails,
);
return (
<div className="mq-details">
<MessagingQueuesOptions
currentTab={currentTab}
setCurrentTab={setCurrentTab}
/>
<MessagingQueuesTable currentTab={currentTab} />
</div>
);
}
export default MessagingQueuesDetails;

View File

@ -0,0 +1,99 @@
.mq-tables-container {
.mq-table-title {
display: flex;
align-items: center;
gap: 12px;
padding-bottom: 16px;
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 18px;
font-style: normal;
font-weight: 500;
line-height: 28px;
letter-spacing: -0.09px;
.mq-table-subtitle {
color: var(--bg-vanilla-400);
font-size: 14px;
}
}
.mq-table {
width: 100%;
.ant-table-content {
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.1);
}
.ant-table-tbody {
.ant-table-cell {
max-width: 250px;
background-color: var(--bg-ink-400);
border-bottom: none;
}
}
.ant-table-thead {
.ant-table-cell {
background-color: var(--bg-ink-500);
border-bottom: 1px solid var(--bg-slate-500);
}
}
}
.no-data-style {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
flex-direction: column;
gap: 24px;
padding: 24px;
border: 1px solid var(--bg-slate-500);
border-radius: 6px;
.ant-typography {
font-size: 14px;
}
}
}
.lightMode {
.mq-tables-container {
.mq-table-title {
color: var(--bg-slate-200);
.mq-table-subtitle {
color: var(--bg-slate-300);
}
}
.mq-table {
.ant-table-content {
border: 1px solid var(--bg-vanilla-300);
}
.ant-table-tbody {
.ant-table-cell {
background-color: var(--bg-vanilla-100);
}
}
.ant-table-thead {
.ant-table-cell {
background-color: var(--bg-vanilla-100);
border-bottom: 1px solid var(--bg-vanilla-300);
}
}
}
.no-data-style {
border: 1px solid var(--bg-vanilla-300);
}
}
}

View File

@ -0,0 +1,210 @@
import './MQTables.styles.scss';
import { Skeleton, Table, Typography } from 'antd';
import axios from 'axios';
import { isNumber } from 'chart.js/helpers';
import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { History } from 'history';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import { isEmpty } from 'lodash-es';
import {
ConsumerLagDetailTitle,
ConsumerLagDetailType,
convertToTitleCase,
RowData,
SelectedTimelineQuery,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useEffect, useMemo, useState } from 'react';
import { useMutation } from 'react-query';
import { useHistory } from 'react-router-dom';
import {
ConsumerLagPayload,
getConsumerLagDetails,
MessagingQueuesPayloadProps,
} from './getConsumerLagDetails';
// eslint-disable-next-line sonarjs/cognitive-complexity
export function getColumns(
data: MessagingQueuesPayloadProps['payload'],
history: History<unknown>,
): RowData[] {
console.log(data);
if (data?.result?.length === 0) {
return [];
}
const columns: {
title: string;
dataIndex: string;
key: string;
}[] = data?.result?.[0]?.table?.columns.map((column) => ({
title: convertToTitleCase(column.name),
dataIndex: column.name,
key: column.name,
render: [
'p99',
'error_rate',
'throughput',
'avg_msg_size',
'error_percentage',
].includes(column.name)
? (value: number | string): string => {
if (!isNumber(value)) return value.toString();
return (typeof value === 'string' ? parseFloat(value) : value).toFixed(3);
}
: (text: string): ColumnTypeRender<Record<string, unknown>> => ({
children:
column.name === 'service_name' ? (
<Typography.Link
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
history.push(`/services/${encodeURIComponent(text)}`);
}}
>
{text}
</Typography.Link>
) : (
<Typography.Text>{text}</Typography.Text>
),
}),
}));
return columns;
}
export function getTableData(
data: MessagingQueuesPayloadProps['payload'],
): RowData[] {
if (data?.result?.length === 0) {
return [];
}
const tableData: RowData[] =
data?.result?.[0]?.table?.rows?.map(
(row, index: number): RowData => ({
...row.data,
key: index,
}),
) || [];
return tableData;
}
const showPaginationItem = (total: number, range: number[]): JSX.Element => (
<>
<Typography.Text className="numbers">
{range[0]} &#8212; {range[1]}
</Typography.Text>
<Typography.Text className="total"> of {total}</Typography.Text>
</>
);
function MessagingQueuesTable({
currentTab,
}: {
currentTab: ConsumerLagDetailType;
}): JSX.Element {
const [columns, setColumns] = useState<any[]>([]);
const [tableData, setTableData] = useState<any[]>([]);
const { notifications } = useNotifications();
const urlQuery = useUrlQuery();
const history = useHistory();
const timelineQuery = decodeURIComponent(
urlQuery.get(QueryParams.selectedTimelineQuery) || '',
);
const timelineQueryData: SelectedTimelineQuery = useMemo(
() => (timelineQuery ? JSON.parse(timelineQuery) : {}),
[timelineQuery],
);
const paginationConfig = useMemo(
() =>
tableData?.length > 20 && {
pageSize: 20,
showTotal: showPaginationItem,
showSizeChanger: false,
hideOnSinglePage: true,
},
[tableData],
);
const props: ConsumerLagPayload = useMemo(
() => ({
start: (timelineQueryData?.start || 0) * 1e9,
end: (timelineQueryData?.end || 0) * 1e9,
variables: {
partition: timelineQueryData?.partition,
topic: timelineQueryData?.topic,
consumer_group: timelineQueryData?.group,
},
detailType: currentTab,
}),
[currentTab, timelineQueryData],
);
const handleConsumerDetailsOnError = (error: Error): void => {
notifications.error({
message: axios.isAxiosError(error) ? error?.message : SOMETHING_WENT_WRONG,
});
};
const { mutate: getConsumerDetails, isLoading } = useMutation(
getConsumerLagDetails,
{
onSuccess: (data) => {
if (data.payload) {
setColumns(getColumns(data?.payload, history));
setTableData(getTableData(data?.payload));
}
},
onError: handleConsumerDetailsOnError,
},
);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => getConsumerDetails(props), [currentTab, props]);
const isEmptyDetails = (timelineQueryData: SelectedTimelineQuery): boolean =>
isEmpty(timelineQueryData) ||
(!timelineQueryData?.group &&
!timelineQueryData?.topic &&
!timelineQueryData?.partition);
return (
<div className="mq-tables-container">
{isEmptyDetails(timelineQueryData) ? (
<div className="no-data-style">
<Typography.Text>
Click on a co-ordinate above to see the details
</Typography.Text>
<Skeleton />
</div>
) : (
<>
<div className="mq-table-title">
{ConsumerLagDetailTitle[currentTab]}
<div className="mq-table-subtitle">{`${timelineQueryData?.group || ''} ${
timelineQueryData?.topic || ''
} ${timelineQueryData?.partition || ''}`}</div>
</div>
<Table
className="mq-table"
pagination={paginationConfig}
size="middle"
columns={columns}
dataSource={tableData}
bordered={false}
loading={isLoading}
/>
</>
)}
</div>
);
}
export default MessagingQueuesTable;

View File

@ -0,0 +1,61 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ConsumerLagDetailType } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { ErrorResponse, SuccessResponse } from 'types/api';
export interface ConsumerLagPayload {
start?: number | string;
end?: number | string;
variables: {
partition?: string;
topic?: string;
consumer_group?: string;
};
detailType: ConsumerLagDetailType;
}
export interface MessagingQueuesPayloadProps {
status: string;
payload: {
resultType: string;
result: {
table: {
columns: {
name: string;
queryName: string;
isValueColumn: boolean;
}[];
rows: {
data: Record<string, string>;
}[];
};
}[];
};
}
export const getConsumerLagDetails = async (
props: ConsumerLagPayload,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => {
const { detailType, ...restProps } = props;
try {
const response = await axios.post(
`/messaging-queues/kafka/consumer-lag/${props.detailType}`,
{
...restProps,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
}
};

View File

@ -0,0 +1,4 @@
.mq-config {
display: flex;
justify-content: space-between;
}

View File

@ -0,0 +1,235 @@
import './MQConfigOptions.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Select, Spin, Tooltip } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import { QueryParams } from 'constants/query';
import { History, Location } from 'history';
import useDebouncedFn from 'hooks/useDebouncedFunction';
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 { SelectMaxTagPlaceholder } from '../MQCommon/MQCommon';
import { useGetAllConfigOptions } from './useGetAllConfigOptions';
type ConfigOptionType = 'group' | 'topic' | 'partition';
const getPlaceholder = (type: ConfigOptionType): string => {
switch (type) {
case 'group':
return 'Consumer Groups';
case 'topic':
return 'Topics';
case 'partition':
return 'Partitions';
default:
return '';
}
};
const useConfigOptions = (
type: ConfigOptionType,
): {
searchText: string;
handleSearch: (value: string) => void;
isFetching: boolean;
options: DefaultOptionType[];
} => {
const [searchText, setSearchText] = useState<string>('');
const { isFetching, options } = useGetAllConfigOptions({
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 };
};
function setQueryParamsForConfigOptions(
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);
}
function getConfigValuesFromQueryParams(
queryParams: QueryParams,
urlQuery: URLSearchParams,
): string[] {
const value = urlQuery.get(queryParams);
return value ? value.split(',') : [];
}
function MessagingQueuesConfigOptions(): JSX.Element {
const urlQuery = useUrlQuery();
const location = useLocation();
const history = useHistory();
const resetTabularConfigDetailsOnChange = (): void => {
urlQuery.delete(QueryParams.selectedTimelineQuery);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
};
const {
handleSearch: handleConsumerGrpSearch,
isFetching: isFetchingConsumerGrp,
options: consumerGrpOptions,
} = useConfigOptions('group');
const {
handleSearch: handleTopicSearch,
isFetching: isFetchingTopic,
options: topicOptions,
} = useConfigOptions('topic');
const {
handleSearch: handlePartitionSearch,
isFetching: isFetchingPartition,
options: partitionOptions,
} = useConfigOptions('partition');
const [isURLCopied, setIsURLCopied] = useState(false);
const [, handleCopyToClipboard] = useCopyToClipboard();
return (
<div className="mq-config">
<div className="config-options">
<Select
placeholder={getPlaceholder('group')}
showSearch
mode="multiple"
options={consumerGrpOptions}
loading={isFetchingConsumerGrp}
className="config-select-option"
onSearch={handleConsumerGrpSearch}
maxTagCount={4}
maxTagPlaceholder={SelectMaxTagPlaceholder}
value={
getConfigValuesFromQueryParams(QueryParams.consumerGrp, urlQuery) || []
}
notFoundContent={
isFetchingConsumerGrp ? (
<span>
<Spin size="small" /> Loading...
</span>
) : (
<span>No Consumer Groups found</span>
)
}
onChange={(value): void => {
handleConsumerGrpSearch('');
setQueryParamsForConfigOptions(
value,
urlQuery,
history,
location,
QueryParams.consumerGrp,
);
resetTabularConfigDetailsOnChange();
}}
/>
<Select
placeholder={getPlaceholder('topic')}
showSearch
mode="multiple"
options={topicOptions}
loading={isFetchingTopic}
onSearch={handleTopicSearch}
className="config-select-option"
maxTagCount={4}
value={getConfigValuesFromQueryParams(QueryParams.topic, urlQuery) || []}
maxTagPlaceholder={SelectMaxTagPlaceholder}
notFoundContent={
isFetchingTopic ? (
<span>
<Spin size="small" /> Loading...
</span>
) : (
<span>No Topics found</span>
)
}
onChange={(value): void => {
handleTopicSearch('');
setQueryParamsForConfigOptions(
value,
urlQuery,
history,
location,
QueryParams.topic,
);
resetTabularConfigDetailsOnChange();
}}
/>
<Select
placeholder={getPlaceholder('partition')}
showSearch
mode="multiple"
options={partitionOptions}
loading={isFetchingPartition}
className="config-select-option"
onSearch={handlePartitionSearch}
maxTagCount={4}
value={
getConfigValuesFromQueryParams(QueryParams.partition, urlQuery) || []
}
maxTagPlaceholder={SelectMaxTagPlaceholder}
notFoundContent={
isFetchingPartition ? (
<span>
<Spin size="small" /> Loading...
</span>
) : (
<span>No Partitions found</span>
)
}
onChange={(value): void => {
handlePartitionSearch('');
setQueryParamsForConfigOptions(
value,
urlQuery,
history,
location,
QueryParams.partition,
);
resetTabularConfigDetailsOnChange();
}}
/>
</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 MessagingQueuesConfigOptions;

View File

@ -0,0 +1,88 @@
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 { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import {
getFiltersFromConfigOptions,
getWidgetQuery,
setSelectedTimelineQuery,
} from '../MessagingQueuesUtils';
function MessagingQueuesGraph(): JSX.Element {
const isDarkMode = useIsDarkMode();
const urlQuery = useUrlQuery();
const consumerGrp = urlQuery.get(QueryParams.consumerGrp) || '';
const topic = urlQuery.get(QueryParams.topic) || '';
const partition = urlQuery.get(QueryParams.partition) || '';
const filterItems = useMemo(
() => getFiltersFromConfigOptions(consumerGrp, topic, partition),
[consumerGrp, topic, partition],
);
const widgetData = useMemo(
() => getWidgetQueryBuilder(getWidgetQuery({ filterItems })),
[filterItems],
);
const history = useHistory();
const location = useLocation();
const messagingQueueCustomTooltipText = (): HTMLDivElement => {
const customText = document.createElement('div');
customText.textContent = 'Click on co-ordinate to view details';
customText.style.paddingTop = '8px';
customText.style.paddingBottom = '2px';
customText.style.color = '#fff';
return customText;
};
const { pathname } = useLocation();
const dispatch = useDispatch();
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}
className="mq-graph"
>
<GridCard
widget={widgetData}
headerMenuList={[...ViewMenuAction]}
onClickHandler={(xValue, _yValue, _mouseX, _mouseY, data): void => {
setSelectedTimelineQuery(urlQuery, xValue, location, history, data);
}}
onDragSelect={onDragSelect}
customTooltipElement={messagingQueueCustomTooltipText()}
/>
</Card>
);
}
export default MessagingQueuesGraph;

View File

@ -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 interface ConfigOptions {
attributeKey: string;
searchText?: string;
}
export interface GetAllConfigOptionsResponse {
options: DefaultOptionType[];
isFetching: boolean;
}
export function useGetAllConfigOptions(
props: ConfigOptions,
): GetAllConfigOptionsResponse {
const { attributeKey, searchText } = props;
const { data, isLoading } = useQuery(
['attributesValues', attributeKey, searchText],
async () => {
const { payload } = await getAttributesValues({
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
aggregateAttribute: 'kafka_consumer_group_lag',
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 };
}

View File

@ -0,0 +1,424 @@
.messaging-queue-container {
.messaging-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);
.message-queue-text {
cursor: pointer;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
}
}
.messaging-header {
display: flex;
min-height: 48px;
padding: 10px 16px;
justify-content: space-between;
align-items: center;
color: var(--bg-vanilla-400);
font-family: 'Geist Mono';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: 0.25px;
border-bottom: 1px solid var(--bg-slate-500);
.header-config {
display: flex;
gap: 10px;
align-items: center;
.messaging-queue-options {
.ant-select-selector {
display: flex;
height: 32px;
padding: 6px 6px 6px 8px;
align-items: center;
gap: 4px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
}
}
}
}
.messaging-queue-main-graph {
display: flex;
padding: 24px 16px;
flex-direction: column;
gap: 16px;
.config-options {
display: flex;
align-items: center;
gap: 8px;
.config-select-option {
.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);
}
}
}
.mq-graph {
height: 420px;
padding: 24px 24px 0 24px;
}
border-bottom: 1px solid var(--bg-slate-500);
}
.messaging-queue-details {
display: flex;
padding: 16px;
.mq-details-options {
letter-spacing: -0.06px;
.ant-radio-button-wrapper {
border-color: var(--bg-slate-400);
color: var(--bg-vanilla-400);
}
.ant-radio-button-wrapper-checked {
background: var(--bg-slate-400);
color: var(--bg-vanilla-100);
}
.ant-radio-button-wrapper-disabled {
background: var(--bg-ink-400);
color: var(--bg-slate-200);
}
.ant-radio-button-wrapper::before {
width: 0px;
}
.disabled-option {
.coming-soon {
margin-left: 8px;
}
}
}
}
}
.messaging-queue-options-popup {
width: 260px !important;
}
.messaging-overview {
padding: 24px 16px 10px 16px;
.overview-text {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 24px;
letter-spacing: -0.08px;
margin: 0;
}
.overview-subtext {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
margin: 0;
margin-top: 4px;
}
.overview-doc-area {
margin: 16px 0 28px 0;
display: flex;
.middle-card {
border-left: none !important;
border-right: none !important;
}
.overview-info-card {
display: flex;
width: 376px;
min-height: 176px;
padding: 18px 20px 20px 20px;
flex-direction: column;
justify-content: space-between;
border: 1px solid var(--bg-slate-500);
border-radius: 2px;
.card-title {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 600;
line-height: 22px;
letter-spacing: 0.52px;
text-transform: uppercase;
margin: 0;
}
.card-info-text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
margin: 0;
margin-top: 4px;
}
.button-grp {
display: flex;
gap: 8px;
.ant-btn {
min-width: 80px;
}
.ant-btn-default {
background-color: var(--bg-slate-400);
border: none;
box-shadow: none;
}
}
}
}
.summary-section {
display: flex;
.summary-card {
display: flex;
padding: 12px;
flex-direction: column;
align-items: flex-start;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
width: 337px;
height: 283px;
border-radius: 2px;
.summary-title {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 24px;
> p {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 500; /* 169.231% */
letter-spacing: 0.52px;
text-transform: uppercase;
margin: 0;
}
.time-value {
display: flex;
gap: 4px;
align-items: center;
> p {
color: var(--bg-slate-200);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 600;
line-height: 22px;
letter-spacing: 0.48px;
text-transform: uppercase;
}
}
}
.view-detail-btn {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
}
}
.coming-soon-card {
background: var(--bg-ink-500) !important;
border-left: none !important;
}
}
.overview-confirm-modal {
background-color: var(--bg-ink-500);
padding: 0;
border-radius: 4px;
.ant-modal-content {
background-color: var(--bg-ink-300);
.ant-modal-confirm-content {
color: var(--bg-vanilla-100);
}
.ant-modal-confirm-body-wrapper {
display: flex;
flex-direction: column;
height: 150px;
justify-content: space-between;
}
}
}
.lightMode {
.messaging-queue-container {
.messaging-breadcrumb {
color: var(--bg-ink-400);
border-bottom: 1px solid var(--bg-vanilla-300);
}
.messaging-header {
color: var(--bg-ink-400);
border-bottom: 1px solid var(--bg-vanilla-300);
.header-config {
.messaging-queue-options {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}
}
}
.messaging-queue-main-graph {
.config-options {
.config-select-option {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}
}
border-bottom: 1px solid var(--bg-vanilla-300);
}
.messaging-queue-details {
.mq-details-options {
.ant-radio-button-wrapper {
border-color: var(--bg-vanilla-300);
color: var(--bg-slate-200);
}
.ant-radio-button-wrapper-checked {
color: var(--bg-slate-200);
background: var(--bg-vanilla-300);
}
.ant-radio-button-wrapper-disabled {
background: var(--bg-vanilla-100);
color: var(--bg-vanilla-400);
}
}
}
}
.messaging-overview {
.overview-text {
color: var(--bg-slate-200);
}
.overview-subtext {
color: var(--bg-slate-300);
}
.overview-doc-area {
.overview-info-card {
border: 1px solid var(--bg-vanilla-300);
.card-title {
color: var(--bg-slate-200);
}
.card-info-text {
color: var(--bg-slate-300);
}
.button-grp {
.ant-btn-default {
background-color: var(--bg-vanilla-100);
}
}
}
}
.summary-section {
.summary-card {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.summary-title {
> p {
color: var(--bg-slate-200);
}
.time-value {
> p {
color: var(--bg-slate-200);
}
}
}
}
}
.coming-soon-card {
background: var(--bg-vanilla-200) !important;
}
}
.overview-confirm-modal {
background-color: var(--bg-vanilla-100);
.ant-modal-content {
background-color: var(--bg-vanilla-100);
.ant-modal-confirm-content {
color: var(--bg-slate-200);
}
}
}
}

View File

@ -0,0 +1,171 @@
import './MessagingQueues.styles.scss';
import { ExclamationCircleFilled } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Button, Modal } from 'antd';
import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { Calendar, ListMinus } from 'lucide-react';
import { useHistory } from 'react-router-dom';
import { isCloudUser } from 'utils/app';
import { KAFKA_SETUP_DOC_LINK } from './MessagingQueuesUtils';
import { ComingSoon } from './MQCommon/MQCommon';
function MessagingQueues(): JSX.Element {
const history = useHistory();
const { confirm } = Modal;
const showConfirm = (): void => {
confirm({
icon: <ExclamationCircleFilled />,
content:
'Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.',
className: 'overview-confirm-modal',
onOk() {
history.push(ROUTES.MESSAGING_QUEUES_DETAIL);
},
okText: 'Proceed',
});
};
const isCloudUserVal = isCloudUser();
const getStartedRedirect = (link: string): void => {
if (isCloudUserVal) {
history.push(link);
} else {
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
}
};
return (
<div className="messaging-queue-container">
<div className="messaging-breadcrumb">
<ListMinus size={16} />
Messaging Queues
</div>
<div className="messaging-header">
<div className="header-config">Kafka / Overview</div>
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
</div>
<div className="messaging-overview">
<p className="overview-text">
Start sending data in as little as 20 minutes
</p>
<p className="overview-subtext">Connect and Monitor Your Data Streams</p>
<div className="overview-doc-area">
<div className="overview-info-card">
<div>
<p className="card-title">Configure Consumer</p>
<p className="card-info-text">
Connect your consumer and producer data sources to start monitoring.
</p>
</div>
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
getStartedRedirect(ROUTES.GET_STARTED_APPLICATION_MONITORING)
}
>
Get Started
</Button>
</div>
</div>
<div className="overview-info-card middle-card">
<div>
<p className="card-title">Configure Producer</p>
<p className="card-info-text">
Connect your consumer and producer data sources to start monitoring.
</p>
</div>
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
getStartedRedirect(ROUTES.GET_STARTED_APPLICATION_MONITORING)
}
>
Get Started
</Button>
</div>
</div>
<div className="overview-info-card">
<div>
<p className="card-title">Monitor kafka</p>
<p className="card-info-text">
Set up your Kafka monitoring to track consumer and producer activities.
</p>
</div>
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
getStartedRedirect(ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING)
}
>
Get Started
</Button>
</div>
</div>
</div>
<div className="summary-section">
<div className="summary-card">
<div className="summary-title">
<p>Consumer Lag</p>
<div className="time-value">
<Calendar size={14} color={Color.BG_SLATE_200} />
<p className="time-value">1D</p>
</div>
</div>
<div className="view-detail-btn">
<Button type="primary" onClick={showConfirm}>
View Details
</Button>
</div>
</div>
<div className="summary-card coming-soon-card">
<div className="summary-title">
<p>Avg. Partition latency</p>
<div className="time-value">
<Calendar size={14} color={Color.BG_SLATE_200} />
<p className="time-value">1D</p>
</div>
</div>
<div className="view-detail-btn">
<ComingSoon />
</div>
</div>
<div className="summary-card coming-soon-card">
<div className="summary-title">
<p>Avg. Partition latency</p>
<div className="time-value">
<Calendar size={14} color={Color.BG_SLATE_200} />
<p className="time-value">1D</p>
</div>
</div>
<div className="view-detail-btn">
<ComingSoon />
</div>
</div>
<div className="summary-card coming-soon-card">
<div className="summary-title">
<p>Avg. Partition latency</p>
<div className="time-value">
<Calendar size={14} color={Color.BG_SLATE_200} />
<p className="time-value">1D</p>
</div>
</div>
<div className="view-detail-btn">
<ComingSoon />
</div>
</div>
</div>
</div>
</div>
);
}
export default MessagingQueues;

View File

@ -0,0 +1,206 @@
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types';
import { History, Location } from 'history';
import { isEmpty } from 'lodash-es';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
export const KAFKA_SETUP_DOC_LINK =
'https://github.com/shivanshuraj1333/kafka-opentelemetry-instrumentation/tree/master';
export function convertToTitleCase(text: string): string {
return text
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
}
export type RowData = {
key: string | number;
[key: string]: string | number;
};
export enum ConsumerLagDetailType {
ConsumerDetails = 'consumer-details',
ProducerDetails = 'producer-details',
NetworkLatency = 'network-latency',
PartitionHostMetrics = 'partition-host-metric',
}
export const ConsumerLagDetailTitle: Record<ConsumerLagDetailType, string> = {
'consumer-details': 'Consumer Groups Details',
'producer-details': 'Producer Details',
'network-latency': 'Network Latency',
'partition-host-metric': 'Partition Host Metrics',
};
export function createWidgetFilterItem(
key: string,
value: string,
): TagFilterItem {
const id = `${key}--string--tag--false`;
return {
id: uuid(),
key: {
key,
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
id,
},
op: '=',
value,
};
}
export function getFiltersFromConfigOptions(
consumerGrp?: string,
topic?: string,
partition?: string,
): TagFilterItem[] {
const configOptions = [
{ key: 'group', values: consumerGrp?.split(',') },
{ key: 'topic', values: topic?.split(',') },
{ key: 'partition', values: partition?.split(',') },
];
return configOptions.reduce<TagFilterItem[]>(
(accumulator, { key, values }) => {
if (values && !isEmpty(values.filter((item) => item !== ''))) {
accumulator.push(
...values.map((value) => createWidgetFilterItem(key, value)),
);
}
return accumulator;
},
[],
);
}
export function getWidgetQuery({
filterItems,
}: {
filterItems: TagFilterItem[];
}): GetWidgetQueryBuilderProps {
return {
title: 'Consumer Lag',
panelTypes: PANEL_TYPES.TIME_SERIES,
fillSpans: false,
yAxisUnit: 'none',
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: {
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'kafka_consumer_group_lag--float64--Gauge--true',
isColumn: true,
isJSON: false,
key: 'kafka_consumer_group_lag',
type: 'Gauge',
},
aggregateOperator: 'max',
dataSource: DataSource.METRICS,
disabled: false,
expression: 'A',
filters: {
items: filterItems || [],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'group--string--tag--false',
isColumn: false,
isJSON: false,
key: 'group',
type: 'tag',
},
{
dataType: DataTypes.String,
id: 'topic--string--tag--false',
isColumn: false,
isJSON: false,
key: 'topic',
type: 'tag',
},
{
dataType: DataTypes.String,
id: 'partition--string--tag--false',
isColumn: false,
isJSON: false,
key: 'partition',
type: 'tag',
},
],
having: [],
legend: '{{group}}-{{topic}}-{{partition}}',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'avg',
stepInterval: 60,
timeAggregation: 'max',
},
],
queryFormulas: [],
},
clickhouse_sql: [],
id: uuid(),
},
};
}
export const convertToNanoseconds = (timestamp: number): bigint =>
BigInt((timestamp * 1e9).toFixed(0));
export const getStartAndEndTimesInMilliseconds = (
timestamp: number,
): { start: number; end: number } => {
const FIVE_MINUTES_IN_MILLISECONDS = 5 * 60 * 1000; // 5 minutes in milliseconds - check with Shivanshu once
const start = Math.floor(timestamp);
const end = Math.floor(start + FIVE_MINUTES_IN_MILLISECONDS);
return { start, end };
};
export interface SelectedTimelineQuery {
group?: string;
partition?: string;
topic?: string;
start?: number;
end?: number;
}
export function setSelectedTimelineQuery(
urlQuery: URLSearchParams,
timestamp: number,
location: Location<unknown>,
history: History<unknown>,
data?: {
[key: string]: string;
},
): void {
const selectedTimelineQuery: SelectedTimelineQuery = {
group: data?.group,
partition: data?.partition,
topic: data?.topic,
...getStartAndEndTimesInMilliseconds(timestamp),
};
urlQuery.set(
QueryParams.selectedTimelineQuery,
encodeURIComponent(JSON.stringify(selectedTimelineQuery)),
);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
}

View File

@ -0,0 +1,3 @@
import MessagingQueues from './MessagingQueues';
export default MessagingQueues;

View File

@ -52,6 +52,8 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
ALL_CHANNELS: ['ADMIN', 'EDITOR', 'VIEWER'],
INGESTION_SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'],
ALL_DASHBOARD: ['ADMIN', 'EDITOR', 'VIEWER'],
MESSAGING_QUEUES: ['ADMIN', 'EDITOR', 'VIEWER'],
MESSAGING_QUEUES_DETAIL: ['ADMIN', 'EDITOR', 'VIEWER'],
ALL_ERROR: ['ADMIN', 'EDITOR', 'VIEWER'],
APPLICATION: ['ADMIN', 'EDITOR', 'VIEWER'],
CHANNELS_EDIT: ['ADMIN'],