mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-11 22:39:01 +08:00
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:
parent
532f274bd6
commit
140533b790
@ -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"
|
||||
}
|
||||
|
@ -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'
|
||||
),
|
||||
);
|
||||
|
@ -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 = {
|
||||
|
@ -32,4 +32,8 @@ export enum QueryParams {
|
||||
relativeTime = 'relativeTime',
|
||||
alertType = 'alertType',
|
||||
ruleId = 'ruleId',
|
||||
consumerGrp = 'consumerGrp',
|
||||
topic = 'topic',
|
||||
partition = 'partition',
|
||||
selectedTimelineQuery = 'selectedTimelineQuery',
|
||||
}
|
||||
|
@ -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',
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
};
|
||||
|
@ -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',
|
||||
}}
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -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 = {
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -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],
|
||||
};
|
||||
|
@ -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',
|
||||
|
@ -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 {
|
||||
|
@ -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];
|
||||
|
@ -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: {
|
||||
|
@ -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) => {
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
46
frontend/src/pages/MessagingQueues/MQCommon/MQCommon.tsx
Normal file
46
frontend/src/pages/MessagingQueues/MQCommon/MQCommon.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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;
|
@ -0,0 +1,3 @@
|
||||
import MQDetailPage from './MQDetailPage';
|
||||
|
||||
export default MQDetailPage;
|
@ -0,0 +1,6 @@
|
||||
.mq-details {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
67
frontend/src/pages/MessagingQueues/MQDetails/MQDetails.tsx
Normal file
67
frontend/src/pages/MessagingQueues/MQDetails/MQDetails.tsx
Normal 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;
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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]} — {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;
|
@ -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);
|
||||
}
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
.mq-config {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
235
frontend/src/pages/MessagingQueues/MQGraph/MQConfigOptions.tsx
Normal file
235
frontend/src/pages/MessagingQueues/MQGraph/MQConfigOptions.tsx
Normal 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;
|
88
frontend/src/pages/MessagingQueues/MQGraph/MQGraph.tsx
Normal file
88
frontend/src/pages/MessagingQueues/MQGraph/MQGraph.tsx
Normal 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;
|
@ -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 };
|
||||
}
|
424
frontend/src/pages/MessagingQueues/MessagingQueues.styles.scss
Normal file
424
frontend/src/pages/MessagingQueues/MessagingQueues.styles.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
171
frontend/src/pages/MessagingQueues/MessagingQueues.tsx
Normal file
171
frontend/src/pages/MessagingQueues/MessagingQueues.tsx
Normal 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;
|
206
frontend/src/pages/MessagingQueues/MessagingQueuesUtils.ts
Normal file
206
frontend/src/pages/MessagingQueues/MessagingQueuesUtils.ts
Normal 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);
|
||||
}
|
3
frontend/src/pages/MessagingQueues/index.tsx
Normal file
3
frontend/src/pages/MessagingQueues/index.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
import MessagingQueues from './MessagingQueues';
|
||||
|
||||
export default MessagingQueues;
|
@ -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'],
|
||||
|
Loading…
x
Reference in New Issue
Block a user