mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 07:25:53 +08:00
feat: added new tab for infra metrics in logs detailed page (#5771)
* feat: added new tab for infra metrics in logs detailed page * feat: added yaxis unit for the charts * chore: cleanup query_range params * fix: clusterName, podName variables not working * feat: added skeleton for each charts in infra metrics tab * change card height to 300px * fix: updated the test cases * feat: added new sub-tabs node and pod for infra metrics tab * feat: added new components for node and pod metrics * feat: added card titles for host metrics and handled empty state * fix: updated the constant for host name * feat: added vertical dotted line to all panels and updated y axis units for all panels * feat: removed other panel types other than graph from host metrics query payload * fix: updated the query payload for node metrics * feat: moved the label of vertical dotted line to top * feat: added console statement to check query payload * fix: added pod name instead of node name in pod query payload * fix: added key as pod name instead of node name in file system usage * fix: updated query payload for file system usage in pod metrics and removed label from dotted line * fix: updated the y axis units for network io * fix: custom date time issue while plotting the graph * feat: compare end time and current time update the end time accordingly * feat: added the start and end time in query payloads * refactor: removed the comments and unused variables * chore: added a todo to make common component for sub-tabs * fix: addressed review comments --------- Co-authored-by: Ankit Nayan <ankit@signoz.io>
This commit is contained in:
parent
f3c01a5155
commit
c5b5bfe540
@ -12,6 +12,20 @@ beforeAll(() => {
|
|||||||
matchMedia();
|
matchMedia();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.mock('uplot', () => {
|
||||||
|
const paths = {
|
||||||
|
spline: jest.fn(),
|
||||||
|
bars: jest.fn(),
|
||||||
|
};
|
||||||
|
const uplotMock = jest.fn(() => ({
|
||||||
|
paths,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
paths,
|
||||||
|
default: uplotMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
jest.mock('react-dnd', () => ({
|
jest.mock('react-dnd', () => ({
|
||||||
useDrop: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
|
useDrop: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
|
||||||
useDrag: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
|
useDrag: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
|
||||||
|
@ -2,6 +2,14 @@ export const VIEW_TYPES = {
|
|||||||
OVERVIEW: 'OVERVIEW',
|
OVERVIEW: 'OVERVIEW',
|
||||||
JSON: 'JSON',
|
JSON: 'JSON',
|
||||||
CONTEXT: 'CONTEXT',
|
CONTEXT: 'CONTEXT',
|
||||||
|
INFRAMETRICS: 'INFRAMETRICS',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type VIEWS = typeof VIEW_TYPES[keyof typeof VIEW_TYPES];
|
export type VIEWS = typeof VIEW_TYPES[keyof typeof VIEW_TYPES];
|
||||||
|
|
||||||
|
export const RESOURCE_KEYS = {
|
||||||
|
CLUSTER_NAME: 'k8s.cluster.name',
|
||||||
|
POD_NAME: 'k8s.pod.name',
|
||||||
|
NODE_NAME: 'k8s.node.name',
|
||||||
|
HOST_NAME: 'host.name',
|
||||||
|
} as const;
|
||||||
|
@ -9,6 +9,7 @@ import cx from 'classnames';
|
|||||||
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
|
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
|
||||||
|
import InfraMetrics from 'container/LogDetailedView/InfraMetrics/InfraMetrics';
|
||||||
import JSONView from 'container/LogDetailedView/JsonView';
|
import JSONView from 'container/LogDetailedView/JsonView';
|
||||||
import Overview from 'container/LogDetailedView/Overview';
|
import Overview from 'container/LogDetailedView/Overview';
|
||||||
import {
|
import {
|
||||||
@ -22,6 +23,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
|||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import {
|
import {
|
||||||
|
BarChart2,
|
||||||
Braces,
|
Braces,
|
||||||
Copy,
|
Copy,
|
||||||
Filter,
|
Filter,
|
||||||
@ -36,7 +38,7 @@ import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
|||||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||||
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
||||||
|
|
||||||
import { VIEW_TYPES, VIEWS } from './constants';
|
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
|
||||||
import { LogDetailProps } from './LogDetail.interfaces';
|
import { LogDetailProps } from './LogDetail.interfaces';
|
||||||
import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper';
|
import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper';
|
||||||
|
|
||||||
@ -192,6 +194,17 @@ function LogDetail({
|
|||||||
Context
|
Context
|
||||||
</div>
|
</div>
|
||||||
</Radio.Button>
|
</Radio.Button>
|
||||||
|
<Radio.Button
|
||||||
|
className={
|
||||||
|
selectedView === VIEW_TYPES.INFRAMETRICS ? 'selected_view tab' : 'tab'
|
||||||
|
}
|
||||||
|
value={VIEW_TYPES.INFRAMETRICS}
|
||||||
|
>
|
||||||
|
<div className="view-title">
|
||||||
|
<BarChart2 size={14} />
|
||||||
|
Metrics
|
||||||
|
</div>
|
||||||
|
</Radio.Button>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
|
|
||||||
{selectedView === VIEW_TYPES.JSON && (
|
{selectedView === VIEW_TYPES.JSON && (
|
||||||
@ -246,6 +259,15 @@ function LogDetail({
|
|||||||
isEdit={isEdit}
|
isEdit={isEdit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{selectedView === VIEW_TYPES.INFRAMETRICS && (
|
||||||
|
<InfraMetrics
|
||||||
|
clusterName={log.resources_string?.[RESOURCE_KEYS.CLUSTER_NAME] || ''}
|
||||||
|
podName={log.resources_string?.[RESOURCE_KEYS.POD_NAME] || ''}
|
||||||
|
nodeName={log.resources_string?.[RESOURCE_KEYS.NODE_NAME] || ''}
|
||||||
|
hostName={log.resources_string?.[RESOURCE_KEYS.HOST_NAME] || ''}
|
||||||
|
logLineTimestamp={log.timestamp.toString()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
.empty-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infra-metrics-container {
|
||||||
|
.views-tabs {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.infra-metrics-card {
|
||||||
|
margin: 1rem 0;
|
||||||
|
height: 300px;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
import './InfraMetrics.styles.scss';
|
||||||
|
|
||||||
|
import { Empty, Radio } from 'antd';
|
||||||
|
import { RadioChangeEvent } from 'antd/lib';
|
||||||
|
import { History, Table } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { VIEW_TYPES } from './constants';
|
||||||
|
import NodeMetrics from './NodeMetrics';
|
||||||
|
import PodMetrics from './PodMetrics';
|
||||||
|
|
||||||
|
interface MetricsDataProps {
|
||||||
|
podName: string;
|
||||||
|
nodeName: string;
|
||||||
|
hostName: string;
|
||||||
|
clusterName: string;
|
||||||
|
logLineTimestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfraMetrics({
|
||||||
|
podName,
|
||||||
|
nodeName,
|
||||||
|
hostName,
|
||||||
|
clusterName,
|
||||||
|
logLineTimestamp,
|
||||||
|
}: MetricsDataProps): JSX.Element {
|
||||||
|
const [selectedView, setSelectedView] = useState<string>(() =>
|
||||||
|
podName ? VIEW_TYPES.POD : VIEW_TYPES.NODE,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleModeChange = (e: RadioChangeEvent): void => {
|
||||||
|
setSelectedView(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!podName && !nodeName && !hostName) {
|
||||||
|
return (
|
||||||
|
<div className="empty-container">
|
||||||
|
<Empty
|
||||||
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||||
|
description="No data available. Please select a valid log line containing a pod, node, or host attributes to view metrics."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="infra-metrics-container">
|
||||||
|
<Radio.Group
|
||||||
|
className="views-tabs"
|
||||||
|
onChange={handleModeChange}
|
||||||
|
value={selectedView}
|
||||||
|
>
|
||||||
|
<Radio.Button
|
||||||
|
className={selectedView === VIEW_TYPES.NODE ? 'selected_view tab' : 'tab'}
|
||||||
|
value={VIEW_TYPES.NODE}
|
||||||
|
>
|
||||||
|
<div className="view-title">
|
||||||
|
<Table size={14} />
|
||||||
|
Node
|
||||||
|
</div>
|
||||||
|
</Radio.Button>
|
||||||
|
{podName && (
|
||||||
|
<Radio.Button
|
||||||
|
className={selectedView === VIEW_TYPES.POD ? 'selected_view tab' : 'tab'}
|
||||||
|
value={VIEW_TYPES.POD}
|
||||||
|
>
|
||||||
|
<div className="view-title">
|
||||||
|
<History size={14} />
|
||||||
|
Pod
|
||||||
|
</div>
|
||||||
|
</Radio.Button>
|
||||||
|
)}
|
||||||
|
</Radio.Group>
|
||||||
|
{/* TODO(Rahul): Make a common config driven component for this and other infra metrics components */}
|
||||||
|
{selectedView === VIEW_TYPES.NODE && (
|
||||||
|
<NodeMetrics
|
||||||
|
nodeName={nodeName}
|
||||||
|
clusterName={clusterName}
|
||||||
|
hostName={hostName}
|
||||||
|
logLineTimestamp={logLineTimestamp}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedView === VIEW_TYPES.POD && podName && (
|
||||||
|
<PodMetrics
|
||||||
|
podName={podName}
|
||||||
|
clusterName={clusterName}
|
||||||
|
logLineTimestamp={logLineTimestamp}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InfraMetrics;
|
@ -0,0 +1,140 @@
|
|||||||
|
import { Card, Col, Row, Skeleton, Typography } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import Uplot from 'components/Uplot';
|
||||||
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
|
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||||
|
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||||
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
|
import { useMemo, useRef } from 'react';
|
||||||
|
import { useQueries, UseQueryResult } from 'react-query';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getHostQueryPayload,
|
||||||
|
getNodeQueryPayload,
|
||||||
|
hostWidgetInfo,
|
||||||
|
nodeWidgetInfo,
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
|
function NodeMetrics({
|
||||||
|
nodeName,
|
||||||
|
clusterName,
|
||||||
|
hostName,
|
||||||
|
logLineTimestamp,
|
||||||
|
}: {
|
||||||
|
nodeName: string;
|
||||||
|
clusterName: string;
|
||||||
|
hostName: string;
|
||||||
|
logLineTimestamp: string;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { start, end, verticalLineTimestamp } = useMemo(() => {
|
||||||
|
const logTimestamp = dayjs(logLineTimestamp);
|
||||||
|
const now = dayjs();
|
||||||
|
const startTime = logTimestamp.subtract(3, 'hour');
|
||||||
|
|
||||||
|
const endTime = logTimestamp.add(3, 'hour').isBefore(now)
|
||||||
|
? logTimestamp.add(3, 'hour')
|
||||||
|
: now;
|
||||||
|
|
||||||
|
return {
|
||||||
|
start: startTime.unix(),
|
||||||
|
end: endTime.unix(),
|
||||||
|
verticalLineTimestamp: logTimestamp.unix(),
|
||||||
|
};
|
||||||
|
}, [logLineTimestamp]);
|
||||||
|
|
||||||
|
const queryPayloads = useMemo(() => {
|
||||||
|
if (nodeName) {
|
||||||
|
return getNodeQueryPayload(clusterName, nodeName, start, end);
|
||||||
|
}
|
||||||
|
return getHostQueryPayload(hostName, start, end);
|
||||||
|
}, [nodeName, hostName, clusterName, start, end]);
|
||||||
|
|
||||||
|
const widgetInfo = nodeName ? nodeWidgetInfo : hostWidgetInfo;
|
||||||
|
const queries = useQueries(
|
||||||
|
queryPayloads.map((payload) => ({
|
||||||
|
queryKey: ['metrics', payload, ENTITY_VERSION_V4, 'NODE'],
|
||||||
|
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||||
|
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||||
|
enabled: !!payload,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
|
const dimensions = useResizeObserver(graphRef);
|
||||||
|
|
||||||
|
const chartData = useMemo(
|
||||||
|
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
|
||||||
|
[queries],
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() =>
|
||||||
|
queries.map(({ data }, idx) =>
|
||||||
|
getUPlotChartOptions({
|
||||||
|
apiResponse: data?.payload,
|
||||||
|
isDarkMode,
|
||||||
|
dimensions,
|
||||||
|
yAxisUnit: widgetInfo[idx].yAxisUnit,
|
||||||
|
softMax: null,
|
||||||
|
softMin: null,
|
||||||
|
minTimeScale: start,
|
||||||
|
maxTimeScale: end,
|
||||||
|
verticalLineTimestamp,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
[
|
||||||
|
queries,
|
||||||
|
isDarkMode,
|
||||||
|
dimensions,
|
||||||
|
widgetInfo,
|
||||||
|
start,
|
||||||
|
verticalLineTimestamp,
|
||||||
|
end,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderCardContent = (
|
||||||
|
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
|
||||||
|
idx: number,
|
||||||
|
): JSX.Element => {
|
||||||
|
if (query.isLoading) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.error) {
|
||||||
|
const errorMessage =
|
||||||
|
(query.error as Error)?.message || 'Something went wrong';
|
||||||
|
return <div>{errorMessage}</div>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx('chart-container', {
|
||||||
|
'no-data-container':
|
||||||
|
!query.isLoading && !query?.data?.payload?.data?.result?.length,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Uplot options={options[idx]} data={chartData[idx]} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Row gutter={24}>
|
||||||
|
{queries.map((query, idx) => (
|
||||||
|
<Col span={12} key={widgetInfo[idx].title}>
|
||||||
|
<Typography.Text>{widgetInfo[idx].title}</Typography.Text>
|
||||||
|
<Card bordered className="infra-metrics-card" ref={graphRef}>
|
||||||
|
{renderCardContent(query, idx)}
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NodeMetrics;
|
@ -0,0 +1,121 @@
|
|||||||
|
import { Card, Col, Row, Skeleton, Typography } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import Uplot from 'components/Uplot';
|
||||||
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
|
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||||
|
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||||
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
|
import { useMemo, useRef } from 'react';
|
||||||
|
import { useQueries, UseQueryResult } from 'react-query';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
|
||||||
|
import { getPodQueryPayload, podWidgetInfo } from './constants';
|
||||||
|
|
||||||
|
function PodMetrics({
|
||||||
|
podName,
|
||||||
|
clusterName,
|
||||||
|
logLineTimestamp,
|
||||||
|
}: {
|
||||||
|
podName: string;
|
||||||
|
clusterName: string;
|
||||||
|
logLineTimestamp: string;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { start, end, verticalLineTimestamp } = useMemo(() => {
|
||||||
|
const logTimestamp = dayjs(logLineTimestamp);
|
||||||
|
const now = dayjs();
|
||||||
|
const startTime = logTimestamp.subtract(3, 'hour');
|
||||||
|
|
||||||
|
const endTime = logTimestamp.add(3, 'hour').isBefore(now)
|
||||||
|
? logTimestamp.add(3, 'hour')
|
||||||
|
: now;
|
||||||
|
|
||||||
|
return {
|
||||||
|
start: startTime.unix(),
|
||||||
|
end: endTime.unix(),
|
||||||
|
verticalLineTimestamp: logTimestamp.unix(),
|
||||||
|
};
|
||||||
|
}, [logLineTimestamp]);
|
||||||
|
const queryPayloads = useMemo(
|
||||||
|
() => getPodQueryPayload(clusterName, podName, start, end),
|
||||||
|
[clusterName, end, podName, start],
|
||||||
|
);
|
||||||
|
const queries = useQueries(
|
||||||
|
queryPayloads.map((payload) => ({
|
||||||
|
queryKey: ['metrics', payload, ENTITY_VERSION_V4, 'POD'],
|
||||||
|
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||||
|
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||||
|
enabled: !!payload,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
|
const dimensions = useResizeObserver(graphRef);
|
||||||
|
|
||||||
|
const chartData = useMemo(
|
||||||
|
() => queries.map(({ data }) => getUPlotChartData(data?.payload)),
|
||||||
|
[queries],
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() =>
|
||||||
|
queries.map(({ data }, idx) =>
|
||||||
|
getUPlotChartOptions({
|
||||||
|
apiResponse: data?.payload,
|
||||||
|
isDarkMode,
|
||||||
|
dimensions,
|
||||||
|
yAxisUnit: podWidgetInfo[idx].yAxisUnit,
|
||||||
|
softMax: null,
|
||||||
|
softMin: null,
|
||||||
|
minTimeScale: start,
|
||||||
|
maxTimeScale: end,
|
||||||
|
verticalLineTimestamp,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
[queries, isDarkMode, dimensions, start, verticalLineTimestamp, end],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderCardContent = (
|
||||||
|
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
|
||||||
|
idx: number,
|
||||||
|
): JSX.Element => {
|
||||||
|
if (query.isLoading) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.error) {
|
||||||
|
const errorMessage =
|
||||||
|
(query.error as Error)?.message || 'Something went wrong';
|
||||||
|
return <div>{errorMessage}</div>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx('chart-container', {
|
||||||
|
'no-data-container':
|
||||||
|
!query.isLoading && !query?.data?.payload?.data?.result?.length,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Uplot options={options[idx]} data={chartData[idx]} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row gutter={24}>
|
||||||
|
{queries.map((query, idx) => (
|
||||||
|
<Col span={12} key={podWidgetInfo[idx].title}>
|
||||||
|
<Typography.Text>{podWidgetInfo[idx].title}</Typography.Text>
|
||||||
|
<Card bordered className="infra-metrics-card" ref={graphRef}>
|
||||||
|
{renderCardContent(query, idx)}
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PodMetrics;
|
3033
frontend/src/container/LogDetailedView/InfraMetrics/constants.ts
Normal file
3033
frontend/src/container/LogDetailedView/InfraMetrics/constants.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -28,6 +28,20 @@ const lodsQueryServerRequest = (): void =>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
jest.mock('uplot', () => {
|
||||||
|
const paths = {
|
||||||
|
spline: jest.fn(),
|
||||||
|
bars: jest.fn(),
|
||||||
|
};
|
||||||
|
const uplotMock = jest.fn(() => ({
|
||||||
|
paths,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
paths,
|
||||||
|
default: uplotMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// mocking the graph components in this test as this should be handled separately
|
// mocking the graph components in this test as this should be handled separately
|
||||||
jest.mock(
|
jest.mock(
|
||||||
'container/TimeSeriesView/TimeSeriesView',
|
'container/TimeSeriesView/TimeSeriesView',
|
||||||
|
@ -9,6 +9,20 @@ import store from 'store';
|
|||||||
import ChangeHistory from '../index';
|
import ChangeHistory from '../index';
|
||||||
import { pipelineData, pipelineDataHistory } from './testUtils';
|
import { pipelineData, pipelineDataHistory } from './testUtils';
|
||||||
|
|
||||||
|
jest.mock('uplot', () => {
|
||||||
|
const paths = {
|
||||||
|
spline: jest.fn(),
|
||||||
|
bars: jest.fn(),
|
||||||
|
};
|
||||||
|
const uplotMock = jest.fn(() => ({
|
||||||
|
paths,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
paths,
|
||||||
|
default: uplotMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
|
@ -9,6 +9,20 @@ import store from 'store';
|
|||||||
import { pipelineMockData } from '../mocks/pipeline';
|
import { pipelineMockData } from '../mocks/pipeline';
|
||||||
import AddNewPipeline from '../PipelineListsView/AddNewPipeline';
|
import AddNewPipeline from '../PipelineListsView/AddNewPipeline';
|
||||||
|
|
||||||
|
jest.mock('uplot', () => {
|
||||||
|
const paths = {
|
||||||
|
spline: jest.fn(),
|
||||||
|
bars: jest.fn(),
|
||||||
|
};
|
||||||
|
const uplotMock = jest.fn(() => ({
|
||||||
|
paths,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
paths,
|
||||||
|
default: uplotMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
export function matchMedia(): void {
|
export function matchMedia(): void {
|
||||||
Object.defineProperty(window, 'matchMedia', {
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
writable: true,
|
writable: true,
|
||||||
|
@ -9,6 +9,20 @@ import { pipelineMockData } from '../mocks/pipeline';
|
|||||||
import AddNewProcessor from '../PipelineListsView/AddNewProcessor';
|
import AddNewProcessor from '../PipelineListsView/AddNewProcessor';
|
||||||
import { matchMedia } from './AddNewPipeline.test';
|
import { matchMedia } from './AddNewPipeline.test';
|
||||||
|
|
||||||
|
jest.mock('uplot', () => {
|
||||||
|
const paths = {
|
||||||
|
spline: jest.fn(),
|
||||||
|
bars: jest.fn(),
|
||||||
|
};
|
||||||
|
const uplotMock = jest.fn(() => ({
|
||||||
|
paths,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
paths,
|
||||||
|
default: uplotMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
matchMedia();
|
matchMedia();
|
||||||
});
|
});
|
||||||
|
@ -6,6 +6,20 @@ import { MemoryRouter } from 'react-router-dom';
|
|||||||
import i18n from 'ReactI18';
|
import i18n from 'ReactI18';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
|
|
||||||
|
jest.mock('uplot', () => {
|
||||||
|
const paths = {
|
||||||
|
spline: jest.fn(),
|
||||||
|
bars: jest.fn(),
|
||||||
|
};
|
||||||
|
const uplotMock = jest.fn(() => ({
|
||||||
|
paths,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
paths,
|
||||||
|
default: uplotMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('PipelinePage container test', () => {
|
describe('PipelinePage container test', () => {
|
||||||
it('should render DeleteAction section', () => {
|
it('should render DeleteAction section', () => {
|
||||||
const { asFragment } = render(
|
const { asFragment } = render(
|
||||||
|
@ -6,6 +6,20 @@ import { MemoryRouter } from 'react-router-dom';
|
|||||||
import i18n from 'ReactI18';
|
import i18n from 'ReactI18';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
|
|
||||||
|
jest.mock('uplot', () => {
|
||||||
|
const paths = {
|
||||||
|
spline: jest.fn(),
|
||||||
|
bars: jest.fn(),
|
||||||
|
};
|
||||||
|
const uplotMock = jest.fn(() => ({
|
||||||
|
paths,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
paths,
|
||||||
|
default: uplotMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('PipelinePage container test', () => {
|
describe('PipelinePage container test', () => {
|
||||||
it('should render DragAction section', () => {
|
it('should render DragAction section', () => {
|
||||||
const { asFragment } = render(
|
const { asFragment } = render(
|
||||||
|
@ -6,6 +6,20 @@ import { MemoryRouter } from 'react-router-dom';
|
|||||||
import i18n from 'ReactI18';
|
import i18n from 'ReactI18';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
|
|
||||||
|
jest.mock('uplot', () => {
|
||||||
|
const paths = {
|
||||||
|
spline: jest.fn(),
|
||||||
|
bars: jest.fn(),
|
||||||
|
};
|
||||||
|
const uplotMock = jest.fn(() => ({
|
||||||
|
paths,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
paths,
|
||||||
|
default: uplotMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('PipelinePage container test', () => {
|
describe('PipelinePage container test', () => {
|
||||||
it('should render EditAction section', () => {
|
it('should render EditAction section', () => {
|
||||||
const { asFragment } = render(
|
const { asFragment } = render(
|
||||||
|
@ -8,6 +8,20 @@ import store from 'store';
|
|||||||
import { pipelineMockData } from '../mocks/pipeline';
|
import { pipelineMockData } from '../mocks/pipeline';
|
||||||
import PipelineActions from '../PipelineListsView/TableComponents/PipelineActions';
|
import PipelineActions from '../PipelineListsView/TableComponents/PipelineActions';
|
||||||
|
|
||||||
|
jest.mock('uplot', () => {
|
||||||
|
const paths = {
|
||||||
|
spline: jest.fn(),
|
||||||
|
bars: jest.fn(),
|
||||||
|
};
|
||||||
|
const uplotMock = jest.fn(() => ({
|
||||||
|
paths,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
paths,
|
||||||
|
default: uplotMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('PipelinePage container test', () => {
|
describe('PipelinePage container test', () => {
|
||||||
it('should render PipelineActions section', () => {
|
it('should render PipelineActions section', () => {
|
||||||
const { asFragment } = render(
|
const { asFragment } = render(
|
||||||
|
@ -9,6 +9,20 @@ import { pipelineMockData } from '../mocks/pipeline';
|
|||||||
import PipelineExpandView from '../PipelineListsView/PipelineExpandView';
|
import PipelineExpandView from '../PipelineListsView/PipelineExpandView';
|
||||||
import { matchMedia } from './AddNewPipeline.test';
|
import { matchMedia } from './AddNewPipeline.test';
|
||||||
|
|
||||||
|
jest.mock('uplot', () => {
|
||||||
|
const paths = {
|
||||||
|
spline: jest.fn(),
|
||||||
|
bars: jest.fn(),
|
||||||
|
};
|
||||||
|
const uplotMock = jest.fn(() => ({
|
||||||
|
paths,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
paths,
|
||||||
|
default: uplotMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
matchMedia();
|
matchMedia();
|
||||||
});
|
});
|
||||||
|
@ -11,6 +11,20 @@ import store from 'store';
|
|||||||
import { pipelineApiResponseMockData } from '../mocks/pipeline';
|
import { pipelineApiResponseMockData } from '../mocks/pipeline';
|
||||||
import PipelineListsView from '../PipelineListsView';
|
import PipelineListsView from '../PipelineListsView';
|
||||||
|
|
||||||
|
jest.mock('uplot', () => {
|
||||||
|
const paths = {
|
||||||
|
spline: jest.fn(),
|
||||||
|
bars: jest.fn(),
|
||||||
|
};
|
||||||
|
const uplotMock = jest.fn(() => ({
|
||||||
|
paths,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
paths,
|
||||||
|
default: uplotMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const samplePipelinePreviewResponse = {
|
const samplePipelinePreviewResponse = {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
logs: [
|
logs: [
|
||||||
|
@ -11,6 +11,20 @@ import { v4 } from 'uuid';
|
|||||||
import PipelinePageLayout from '../Layouts/Pipeline';
|
import PipelinePageLayout from '../Layouts/Pipeline';
|
||||||
import { matchMedia } from './AddNewPipeline.test';
|
import { matchMedia } from './AddNewPipeline.test';
|
||||||
|
|
||||||
|
jest.mock('uplot', () => {
|
||||||
|
const paths = {
|
||||||
|
spline: jest.fn(),
|
||||||
|
bars: jest.fn(),
|
||||||
|
};
|
||||||
|
const uplotMock = jest.fn(() => ({
|
||||||
|
paths,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
paths,
|
||||||
|
default: uplotMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
matchMedia();
|
matchMedia();
|
||||||
});
|
});
|
||||||
|
@ -7,6 +7,20 @@ import store from 'store';
|
|||||||
|
|
||||||
import TagInput from '../components/TagInput';
|
import TagInput from '../components/TagInput';
|
||||||
|
|
||||||
|
jest.mock('uplot', () => {
|
||||||
|
const paths = {
|
||||||
|
spline: jest.fn(),
|
||||||
|
bars: jest.fn(),
|
||||||
|
};
|
||||||
|
const uplotMock = jest.fn(() => ({
|
||||||
|
paths,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
paths,
|
||||||
|
default: uplotMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('Pipeline Page', () => {
|
describe('Pipeline Page', () => {
|
||||||
it('should render TagInput section', () => {
|
it('should render TagInput section', () => {
|
||||||
const { asFragment } = render(
|
const { asFragment } = render(
|
||||||
|
@ -11,6 +11,20 @@ import {
|
|||||||
getTableColumn,
|
getTableColumn,
|
||||||
} from '../PipelineListsView/utils';
|
} from '../PipelineListsView/utils';
|
||||||
|
|
||||||
|
jest.mock('uplot', () => {
|
||||||
|
const paths = {
|
||||||
|
spline: jest.fn(),
|
||||||
|
bars: jest.fn(),
|
||||||
|
};
|
||||||
|
const uplotMock = jest.fn(() => ({
|
||||||
|
paths,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
paths,
|
||||||
|
default: uplotMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('Utils testing of Pipeline Page', () => {
|
describe('Utils testing of Pipeline Page', () => {
|
||||||
test('it should be check form field of add pipeline', () => {
|
test('it should be check form field of add pipeline', () => {
|
||||||
expect(pipelineFields.length).toBe(3);
|
expect(pipelineFields.length).toBe(3);
|
||||||
|
@ -26,13 +26,13 @@ export async function GetMetricQueryRange(
|
|||||||
headers?: Record<string, string>,
|
headers?: Record<string, string>,
|
||||||
): Promise<SuccessResponse<MetricRangePayloadProps>> {
|
): Promise<SuccessResponse<MetricRangePayloadProps>> {
|
||||||
const { legendMap, queryPayload } = prepareQueryRangePayload(props);
|
const { legendMap, queryPayload } = prepareQueryRangePayload(props);
|
||||||
|
|
||||||
const response = await getMetricsQueryRange(
|
const response = await getMetricsQueryRange(
|
||||||
queryPayload,
|
queryPayload,
|
||||||
version || 'v3',
|
version || 'v3',
|
||||||
signal,
|
signal,
|
||||||
headers,
|
headers,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
if (response.statusCode >= 400) {
|
if (response.statusCode >= 400) {
|
||||||
let error = `API responded with ${response.statusCode} - ${response.error} status: ${response.message}`;
|
let error = `API responded with ${response.statusCode} - ${response.error} status: ${response.message}`;
|
||||||
@ -78,7 +78,7 @@ export interface GetQueryResultsProps {
|
|||||||
query: Query;
|
query: Query;
|
||||||
graphType: PANEL_TYPES;
|
graphType: PANEL_TYPES;
|
||||||
selectedTime: timePreferenceType;
|
selectedTime: timePreferenceType;
|
||||||
globalSelectedInterval: Time | TimeV2 | CustomTimeType;
|
globalSelectedInterval?: Time | TimeV2 | CustomTimeType;
|
||||||
variables?: Record<string, unknown>;
|
variables?: Record<string, unknown>;
|
||||||
params?: Record<string, unknown>;
|
params?: Record<string, unknown>;
|
||||||
fillGaps?: boolean;
|
fillGaps?: boolean;
|
||||||
@ -87,4 +87,6 @@ export interface GetQueryResultsProps {
|
|||||||
pagination?: Pagination;
|
pagination?: Pagination;
|
||||||
selectColumns?: any;
|
selectColumns?: any;
|
||||||
};
|
};
|
||||||
|
start?: number;
|
||||||
|
end?: number;
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,8 @@ export const prepareQueryRangePayload = ({
|
|||||||
variables = {},
|
variables = {},
|
||||||
params = {},
|
params = {},
|
||||||
fillGaps = false,
|
fillGaps = false,
|
||||||
|
start: startTime,
|
||||||
|
end: endTime,
|
||||||
}: GetQueryResultsProps): PrepareQueryRangePayload => {
|
}: GetQueryResultsProps): PrepareQueryRangePayload => {
|
||||||
let legendMap: Record<string, string> = {};
|
let legendMap: Record<string, string> = {};
|
||||||
const {
|
const {
|
||||||
@ -100,8 +102,8 @@ export const prepareQueryRangePayload = ({
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const queryPayload: QueryRangePayload = {
|
const queryPayload: QueryRangePayload = {
|
||||||
start: parseInt(start, 10) * 1e3,
|
start: startTime ? startTime * 1e3 : parseInt(start, 10) * 1e3,
|
||||||
end: endLogTimeStamp || parseInt(end, 10) * 1e3,
|
end: endTime ? endTime * 1e3 : endLogTimeStamp || parseInt(end, 10) * 1e3,
|
||||||
step: getStep({
|
step: getStep({
|
||||||
start: allowSelectedIntervalForStepGen
|
start: allowSelectedIntervalForStepGen
|
||||||
? start
|
? start
|
||||||
|
@ -54,6 +54,7 @@ export interface GetUPlotChartOptions {
|
|||||||
}>
|
}>
|
||||||
>;
|
>;
|
||||||
customTooltipElement?: HTMLDivElement;
|
customTooltipElement?: HTMLDivElement;
|
||||||
|
verticalLineTimestamp?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** the function converts series A , series B , series C to
|
/** the function converts series A , series B , series C to
|
||||||
@ -156,6 +157,7 @@ export const getUPlotChartOptions = ({
|
|||||||
hiddenGraph,
|
hiddenGraph,
|
||||||
setHiddenGraph,
|
setHiddenGraph,
|
||||||
customTooltipElement,
|
customTooltipElement,
|
||||||
|
verticalLineTimestamp,
|
||||||
}: GetUPlotChartOptions): uPlot.Options => {
|
}: GetUPlotChartOptions): uPlot.Options => {
|
||||||
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
|
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
|
||||||
|
|
||||||
@ -222,6 +224,29 @@ export const getUPlotChartOptions = ({
|
|||||||
onClick: onClickHandler,
|
onClick: onClickHandler,
|
||||||
apiResponse,
|
apiResponse,
|
||||||
}),
|
}),
|
||||||
|
{
|
||||||
|
hooks: {
|
||||||
|
draw: [
|
||||||
|
(u): void => {
|
||||||
|
if (verticalLineTimestamp) {
|
||||||
|
const { ctx } = u;
|
||||||
|
ctx.save();
|
||||||
|
ctx.setLineDash([4, 2]);
|
||||||
|
ctx.strokeStyle = 'white';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
const x = u.valToPos(verticalLineTimestamp, 'x', true);
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, u.bbox.top);
|
||||||
|
ctx.lineTo(x, u.bbox.top + u.bbox.height);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
hooks: {
|
hooks: {
|
||||||
draw: [
|
draw: [
|
||||||
|
@ -26,6 +26,21 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
|||||||
import LogsExplorer from '../index';
|
import LogsExplorer from '../index';
|
||||||
|
|
||||||
const queryRangeURL = 'http://localhost/api/v3/query_range';
|
const queryRangeURL = 'http://localhost/api/v3/query_range';
|
||||||
|
|
||||||
|
jest.mock('uplot', () => {
|
||||||
|
const paths = {
|
||||||
|
spline: jest.fn(),
|
||||||
|
bars: jest.fn(),
|
||||||
|
};
|
||||||
|
const uplotMock = jest.fn(() => ({
|
||||||
|
paths,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
paths,
|
||||||
|
default: uplotMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// mocking the graph components in this test as this should be handled separately
|
// mocking the graph components in this test as this should be handled separately
|
||||||
jest.mock(
|
jest.mock(
|
||||||
'container/TimeSeriesView/TimeSeriesView',
|
'container/TimeSeriesView/TimeSeriesView',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user