Compare commits

...

23 Commits

Author SHA1 Message Date
Nikhil Mantri
d594fa96e2 Merge branch 'main' into feat/infraM_list_apis_request_time_check 2026-03-03 15:21:58 +05:30
Srikanth Chekuri
1d967fadac chore(metrics-explorer): handle errors properly (#10474) 2026-03-03 09:27:09 +00:00
Abhi kumar
5af6ed6148 chore: updated config builder types (#10477)
* chore: made baseconfigbuilder generic to be used across different charts

* chore: updated baseconfigbuilder test

* chore: updated timezone types

* chore: fixed tsc + test

* chore: fixed tsc + test

* chore: fixed tsc + test

* chore: updated config builder types

* chore: updated comment
2026-03-03 14:17:57 +05:30
Abhi kumar
d094e9cb45 chore: made baseconfigbuilder generic to be used across different charts (#10451)
* chore: made baseconfigbuilder generic to be used across different charts

* chore: updated baseconfigbuilder test

* chore: updated timezone types

* chore: fixed tsc + test

* chore: fixed tsc + test

* chore: fixed tsc + test
2026-03-03 14:05:54 +05:30
Nikhil Mantri
aa2e0d8104 Merge branch 'main' into feat/infraM_list_apis_request_time_check 2026-02-26 20:12:42 +05:30
nikhilmantri0902
ea5d89e72c chore: added 3 cases handling for all list apis 2026-02-26 14:00:23 +05:30
nikhilmantri0902
a518b1688b chore: rename func 2026-02-26 13:17:05 +05:30
Nikhil Mantri
44a94e7746 Merge branch 'main' into feat/update_hosts_list_error_messaging 2026-02-26 11:04:38 +05:30
Nikhil Mantri
38d334b9aa Merge branch 'main' into feat/update_hosts_list_error_messaging 2026-02-24 20:36:14 +05:30
Nikhil Mantri
3f27e49eac Merge branch 'main' into feat/update_hosts_list_error_messaging 2026-02-20 12:57:03 +05:30
nikhilmantri0902
0b8e87ec96 chore: added test case and final comment resolve 2026-02-19 16:51:58 +05:30
nikhilmantri0902
df2916bf7f chore: review comments 1 2026-02-19 16:15:30 +05:30
Nikhil Mantri
0c62c075f9 Merge branch 'main' into feat/update_hosts_list_error_messaging 2026-02-19 15:54:14 +05:30
nikhilmantri0902
f42d95d5f5 chore: title updated for no host metrics found 2026-02-18 12:32:32 +05:30
nikhilmantri0902
32c0dfa28f chore: title for end time before earliest metadata time 2026-02-18 12:25:09 +05:30
Nikhil Mantri
68e831c4b0 Merge branch 'main' into feat/update_hosts_list_error_messaging 2026-02-17 15:51:27 +05:30
Nikhil Mantri
ed79d40492 Merge branch 'main' into feat/update_hosts_list_error_messaging 2026-02-17 14:16:13 +05:30
nikhilmantri0902
ad8223c792 chore: refactor and conditions combine 2026-02-17 13:46:55 +05:30
nikhilmantri0902
5d691476b1 chore: rearrangement 2026-02-17 12:46:38 +05:30
nikhilmantri0902
358977e203 chore: frontend messaging test fix 2026-02-16 14:37:03 +05:30
nikhilmantri0902
7ffb3e30b5 chore: improved messaging and comment 2026-02-16 14:19:12 +05:30
nikhilmantri0902
ded7b78360 chore: use named query 2026-02-16 13:43:43 +05:30
nikhilmantri0902
76e9074ca7 chore: initial logical changes 2026-02-15 19:04:44 +05:30
75 changed files with 1690 additions and 1136 deletions

View File

@@ -2,7 +2,7 @@ import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import APIError from 'types/api/error';
// Handles errors from generated API hooks (which use RenderErrorResponseDTO)
// @deprecated Use convertToApiError instead
export function ErrorResponseHandlerForGeneratedAPIs(
error: AxiosError<RenderErrorResponseDTO>,
): never {
@@ -46,3 +46,34 @@ export function ErrorResponseHandlerForGeneratedAPIs(
},
});
}
// convertToApiError converts an AxiosError from generated API
// hooks into an APIError.
export function convertToApiError(
error: AxiosError<RenderErrorResponseDTO> | null,
): APIError | undefined {
if (!error) {
return undefined;
}
const response = error.response;
const errorData = response?.data?.error;
return new APIError({
httpStatusCode: response?.status || error.status || 500,
error: {
code:
errorData?.code ||
String(response?.status || error.code || 'unknown_error'),
message:
errorData?.message ||
response?.statusText ||
error.message ||
'Something went wrong',
url: errorData?.url ?? '',
errors: (errorData?.errors ?? []).map((e) => ({
message: e.message ?? '',
})),
},
});
}

View File

@@ -37,8 +37,8 @@ export interface K8sClustersListResponse {
records: K8sClustersData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
sentAnyMetricsData: boolean;
endTimeBeforeRetention: boolean;
};
}

View File

@@ -43,8 +43,8 @@ export interface K8sDaemonSetsListResponse {
records: K8sDaemonSetsData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
sentAnyMetricsData: boolean;
endTimeBeforeRetention: boolean;
};
}

View File

@@ -43,8 +43,8 @@ export interface K8sDeploymentsListResponse {
records: K8sDeploymentsData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
sentAnyMetricsData: boolean;
endTimeBeforeRetention: boolean;
};
}

View File

@@ -45,8 +45,8 @@ export interface K8sJobsListResponse {
records: K8sJobsData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
sentAnyMetricsData: boolean;
endTimeBeforeRetention: boolean;
};
}

View File

@@ -35,8 +35,8 @@ export interface K8sNamespacesListResponse {
records: K8sNamespacesData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
sentAnyMetricsData: boolean;
endTimeBeforeRetention: boolean;
};
}

View File

@@ -38,8 +38,8 @@ export interface K8sNodesListResponse {
records: K8sNodesData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
sentAnyMetricsData: boolean;
endTimeBeforeRetention: boolean;
};
}

View File

@@ -66,8 +66,8 @@ export interface K8sPodsListResponse {
records: K8sPodsData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
sentAnyMetricsData: boolean;
endTimeBeforeRetention: boolean;
};
}

View File

@@ -44,8 +44,8 @@ export interface K8sVolumesListResponse {
records: K8sVolumesData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
sentAnyMetricsData: boolean;
endTimeBeforeRetention: boolean;
};
}

View File

@@ -42,8 +42,8 @@ export interface K8sStatefulSetsListResponse {
records: K8sStatefulSetsData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
sentAnyMetricsData: boolean;
endTimeBeforeRetention: boolean;
};
}

View File

@@ -1,54 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { TreemapViewType } from 'container/MetricsExplorer/Summary/types';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface MetricsTreeMapPayload {
filters: TagFilter;
limit?: number;
treemap?: TreemapViewType;
}
export interface MetricsTreeMapResponse {
status: string;
data: {
[TreemapViewType.TIMESERIES]: TimeseriesData[];
[TreemapViewType.SAMPLES]: SamplesData[];
};
}
export interface TimeseriesData {
percentage: number;
total_value: number;
metric_name: string;
}
export interface SamplesData {
percentage: number;
metric_name: string;
}
export const getMetricsTreeMap = async (
props: MetricsTreeMapPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricsTreeMapResponse> | ErrorResponse> => {
try {
const response = await axios.post('/metrics/treemap', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,36 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Temporality } from './getMetricDetails';
import { MetricType } from './getMetricsList';
export interface UpdateMetricMetadataProps {
description: string;
metricType: MetricType;
temporality?: Temporality;
isMonotonic?: boolean;
unit?: string;
}
export interface UpdateMetricMetadataResponse {
success: boolean;
message: string;
}
const updateMetricMetadata = async (
metricName: string,
props: UpdateMetricMetadataProps,
): Promise<SuccessResponse<UpdateMetricMetadataResponse> | ErrorResponse> => {
const response = await axios.post(`/metrics/${metricName}/metadata`, {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateMetricMetadata;

View File

@@ -49,7 +49,6 @@ export const REACT_QUERY_KEY = {
// Metrics Explorer Query Keys
GET_METRICS_LIST: 'GET_METRICS_LIST',
GET_METRICS_TREE_MAP: 'GET_METRICS_TREE_MAP',
GET_METRICS_LIST_FILTER_KEYS: 'GET_METRICS_LIST_FILTER_KEYS',
GET_METRICS_LIST_FILTER_VALUES: 'GET_METRICS_LIST_FILTER_VALUES',
GET_METRIC_DETAILS: 'GET_METRIC_DETAILS',

View File

@@ -1,3 +1,4 @@
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PrecisionOption } from 'components/Graph/types';
import { LegendConfig, TooltipRenderArgs } from 'lib/uPlotV2/components/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
@@ -8,7 +9,7 @@ interface BaseChartProps {
height: number;
showTooltip?: boolean;
showLegend?: boolean;
timezone: string;
timezone?: Timezone;
canPinTooltip?: boolean;
yAxisUnit?: string;
decimalPrecision?: PrecisionOption;

View File

@@ -129,12 +129,12 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
onDestroy={onPlotDestroy}
yAxisUnit={widget.yAxisUnit}
decimalPrecision={widget.decimalPrecision}
timezone={timezone.value}
data={chartData as uPlot.AlignedData}
width={containerDimensions.width}
height={containerDimensions.height}
layoutChildren={layoutChildren}
isStackedBarChart={widget.stackedBarChart ?? false}
timezone={timezone}
>
<ContextMenu
coordinates={coordinates}

View File

@@ -5,12 +5,7 @@ import { getInitialStackedBands } from 'container/DashboardContainer/visualizati
import { getLegend } from 'lib/dashboard/getQueryResults';
import getLabelName from 'lib/getLabelName';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import {
DrawStyle,
LineInterpolation,
LineStyle,
VisibilityMode,
} from 'lib/uPlotV2/config/types';
import { DrawStyle } from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { get } from 'lodash-es';
import { Widgets } from 'types/api/dashboard/getAll';
@@ -63,7 +58,12 @@ export function prepareBarPanelConfig({
const minStepInterval = Math.min(...Object.values(stepIntervals));
const builder = buildBaseConfig({
widget,
id: widget.id,
thresholds: widget.thresholds,
yAxisUnit: widget.yAxisUnit,
softMin: widget.softMin ?? undefined,
softMax: widget.softMax ?? undefined,
isLogScale: widget.isLogScale,
isDarkMode,
onClick,
onDragSelect,
@@ -98,14 +98,8 @@ export function prepareBarPanelConfig({
builder.addSeries({
scaleKey: 'y',
drawStyle: DrawStyle.Bar,
panelType: PANEL_TYPES.BAR,
label: label,
colorMapping: widget.customLegendColors ?? {},
spanGaps: false,
lineStyle: LineStyle.Solid,
lineInterpolation: LineInterpolation.Spline,
showPoints: VisibilityMode.Never,
pointSize: 5,
isDarkMode,
stepInterval: currentStepInterval,
});

View File

@@ -100,7 +100,7 @@ function HistogramPanel(props: PanelWrapperProps): JSX.Element {
yAxisUnit={widget.yAxisUnit}
decimalPrecision={widget.decimalPrecision}
syncMode={DashboardCursorSync.Crosshair}
timezone={timezone.value}
timezone={timezone}
data={chartData as uPlot.AlignedData}
width={containerDimensions.width}
height={containerDimensions.height}

View File

@@ -154,7 +154,12 @@ export function prepareHistogramPanelConfig({
isDarkMode: boolean;
}): UPlotConfigBuilder {
const builder = buildBaseConfig({
widget,
id: widget.id,
thresholds: widget.thresholds,
yAxisUnit: widget.yAxisUnit,
softMin: widget.softMin ?? undefined,
softMax: widget.softMax ?? undefined,
isLogScale: widget.isLogScale,
isDarkMode,
apiResponse,
panelMode,
@@ -191,10 +196,8 @@ export function prepareHistogramPanelConfig({
builder.addSeries({
label: '',
scaleKey: 'y',
drawStyle: DrawStyle.Bar,
panelType: PANEL_TYPES.HISTOGRAM,
drawStyle: DrawStyle.Histogram,
colorMapping: widget.customLegendColors ?? {},
spanGaps: false,
barWidthFactor: 1,
pointSize: 5,
lineColor: '#3f5ecc',
@@ -216,10 +219,8 @@ export function prepareHistogramPanelConfig({
builder.addSeries({
label: label,
scaleKey: 'y',
drawStyle: DrawStyle.Bar,
panelType: PANEL_TYPES.HISTOGRAM,
drawStyle: DrawStyle.Histogram,
colorMapping: widget.customLegendColors ?? {},
spanGaps: false,
barWidthFactor: 1,
pointSize: 5,
isDarkMode,

View File

@@ -118,7 +118,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
}}
yAxisUnit={widget.yAxisUnit}
decimalPrecision={widget.decimalPrecision}
timezone={timezone.value}
timezone={timezone}
data={chartData as uPlot.AlignedData}
width={containerDimensions.width}
height={containerDimensions.height}

View File

@@ -82,7 +82,12 @@ export const prepareUPlotConfig = ({
const minStepInterval = Math.min(...Object.values(stepIntervals));
const builder = buildBaseConfig({
widget,
id: widget.id,
thresholds: widget.thresholds,
yAxisUnit: widget.yAxisUnit,
softMin: widget.softMin ?? undefined,
softMax: widget.softMax ?? undefined,
isLogScale: widget.isLogScale,
isDarkMode,
onClick,
onDragSelect,
@@ -120,7 +125,6 @@ export const prepareUPlotConfig = ({
: VisibilityMode.Never,
pointSize: 5,
isDarkMode,
panelType: PANEL_TYPES.TIME_SERIES,
});
});

View File

@@ -1,11 +1,11 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { STEP_INTERVAL_MULTIPLIER } from 'lib/uPlotV2/constants';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { PanelMode } from '../../types';
import { buildBaseConfig } from '../baseConfigBuilder';
import { BaseConfigBuilderProps, buildBaseConfig } from '../baseConfigBuilder';
jest.mock(
'container/DashboardContainer/visualization/panels/utils/legendVisibilityUtils',
@@ -27,16 +27,25 @@ jest.mock('lib/uPlotLib/plugins/onClickPlugin', () => ({
default: jest.fn().mockReturnValue({ name: 'onClickPlugin' }),
}));
const createWidget = (overrides: Partial<Widgets> = {}): Widgets =>
({
id: 'widget-1',
yAxisUnit: 'ms',
isLogScale: false,
softMin: undefined,
softMax: undefined,
thresholds: [],
...overrides,
} as Widgets);
const createBaseConfigBuilderProps = (
overrides: Partial<
Pick<
BaseConfigBuilderProps,
'id' | 'yAxisUnit' | 'isLogScale' | 'softMin' | 'softMax' | 'thresholds'
>
> = {},
): Pick<
BaseConfigBuilderProps,
'id' | 'yAxisUnit' | 'isLogScale' | 'softMin' | 'softMax' | 'thresholds'
> => ({
id: 'widget-1',
yAxisUnit: 'ms',
isLogScale: false,
softMin: undefined,
softMax: undefined,
thresholds: [],
...overrides,
});
const createApiResponse = (
overrides: Partial<MetricRangePayloadProps> = {},
@@ -47,7 +56,7 @@ const createApiResponse = (
} as MetricRangePayloadProps);
const baseProps = {
widget: createWidget(),
...createBaseConfigBuilderProps(),
apiResponse: createApiResponse(),
isDarkMode: true,
panelMode: PanelMode.DASHBOARD_VIEW,
@@ -63,14 +72,14 @@ describe('buildBaseConfig', () => {
expect(typeof builder.getLegendItems).toBe('function');
});
it('configures builder with widgetId and DASHBOARD_VIEW preferences', () => {
it('configures builder with id and DASHBOARD_VIEW preferences', () => {
const builder = buildBaseConfig({
...baseProps,
panelMode: PanelMode.DASHBOARD_VIEW,
widget: createWidget({ id: 'my-widget' }),
...createBaseConfigBuilderProps({ id: 'my-widget' }),
});
expect(builder.getWidgetId()).toBe('my-widget');
expect(builder.getId()).toBe('my-widget');
expect(builder.getShouldSaveSelectionPreference()).toBe(true);
});
@@ -127,7 +136,7 @@ describe('buildBaseConfig', () => {
it('configures log scale on y axis when widget.isLogScale is true', () => {
const builder = buildBaseConfig({
...baseProps,
widget: createWidget({ isLogScale: true }),
...createBaseConfigBuilderProps({ isLogScale: true }),
});
const config = builder.getConfig();
@@ -171,7 +180,7 @@ describe('buildBaseConfig', () => {
it('adds thresholds from widget', () => {
const builder = buildBaseConfig({
...baseProps,
widget: createWidget({
...createBaseConfigBuilderProps({
thresholds: [
{
thresholdValue: 80,
@@ -179,7 +188,7 @@ describe('buildBaseConfig', () => {
thresholdUnit: 'ms',
thresholdLabel: 'High',
},
] as Widgets['thresholds'],
] as ThresholdProps[],
}),
});

View File

@@ -1,5 +1,6 @@
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import onClickPlugin, {
OnClickPluginOpts,
} from 'lib/uPlotLib/plugins/onClickPlugin';
@@ -9,28 +10,32 @@ import {
} from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { ThresholdsDrawHookOptions } from 'lib/uPlotV2/hooks/types';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { PanelMode } from '../types';
export interface BaseConfigBuilderProps {
widget: Widgets;
id: string;
thresholds?: ThresholdProps[];
apiResponse: MetricRangePayloadProps;
isDarkMode: boolean;
onClick?: OnClickPluginOpts['onClick'];
onDragSelect?: (startTime: number, endTime: number) => void;
timezone?: Timezone;
panelMode: PanelMode;
panelMode?: PanelMode;
panelType: PANEL_TYPES;
minTimeScale?: number;
maxTimeScale?: number;
stepInterval?: number;
isLogScale?: boolean;
yAxisUnit?: string;
softMin?: number;
softMax?: number;
}
export function buildBaseConfig({
widget,
id,
isDarkMode,
onClick,
onDragSelect,
@@ -38,9 +43,14 @@ export function buildBaseConfig({
timezone,
panelMode,
panelType,
thresholds,
minTimeScale,
maxTimeScale,
stepInterval,
isLogScale,
yAxisUnit,
softMin,
softMax,
}: BaseConfigBuilderProps): UPlotConfigBuilder {
const tzDate = timezone
? (timestamp: number): Date =>
@@ -48,28 +58,27 @@ export function buildBaseConfig({
: undefined;
const builder = new UPlotConfigBuilder({
id,
onDragSelect,
widgetId: widget.id,
tzDate,
shouldSaveSelectionPreference: panelMode === PanelMode.DASHBOARD_VIEW,
selectionPreferencesSource: [
PanelMode.DASHBOARD_VIEW,
PanelMode.STANDALONE_VIEW,
].includes(panelMode)
? SelectionPreferencesSource.LOCAL_STORAGE
selectionPreferencesSource: panelMode
? [PanelMode.DASHBOARD_VIEW, PanelMode.STANDALONE_VIEW].includes(panelMode)
? SelectionPreferencesSource.LOCAL_STORAGE
: SelectionPreferencesSource.IN_MEMORY
: SelectionPreferencesSource.IN_MEMORY,
stepInterval,
});
const thresholdOptions: ThresholdsDrawHookOptions = {
scaleKey: 'y',
thresholds: (widget.thresholds || []).map((threshold) => ({
thresholds: (thresholds || []).map((threshold) => ({
thresholdValue: threshold.thresholdValue ?? 0,
thresholdColor: threshold.thresholdColor,
thresholdUnit: threshold.thresholdUnit,
thresholdLabel: threshold.thresholdLabel,
})),
yAxisUnit: widget.yAxisUnit,
yAxisUnit: yAxisUnit,
};
builder.addThresholds(thresholdOptions);
@@ -79,8 +88,8 @@ export function buildBaseConfig({
time: true,
min: minTimeScale,
max: maxTimeScale,
logBase: widget.isLogScale ? 10 : undefined,
distribution: widget.isLogScale
logBase: isLogScale ? 10 : undefined,
distribution: isLogScale
? DistributionType.Logarithmic
: DistributionType.Linear,
});
@@ -91,11 +100,11 @@ export function buildBaseConfig({
time: false,
min: undefined,
max: undefined,
softMin: widget.softMin ?? undefined,
softMax: widget.softMax ?? undefined,
softMin: softMin,
softMax: softMax,
thresholds: thresholdOptions,
logBase: widget.isLogScale ? 10 : undefined,
distribution: widget.isLogScale
logBase: isLogScale ? 10 : undefined,
distribution: isLogScale
? DistributionType.Logarithmic
: DistributionType.Linear,
});
@@ -114,7 +123,7 @@ export function buildBaseConfig({
show: true,
side: 2,
isDarkMode,
isLogScale: widget.isLogScale,
isLogScale,
panelType,
});
@@ -123,8 +132,8 @@ export function buildBaseConfig({
show: true,
side: 3,
isDarkMode,
isLogScale: widget.isLogScale,
yAxisUnit: widget.yAxisUnit,
isLogScale,
yAxisUnit,
panelType,
});

View File

@@ -15,7 +15,7 @@ export const getRandomColor = (): string => {
};
export const DATASOURCE_VS_ROUTES: Record<DataSource, string> = {
[DataSource.METRICS]: ROUTES.METRICS_EXPLORER,
[DataSource.METRICS]: ROUTES.METRICS_EXPLORER_EXPLORER,
[DataSource.TRACES]: ROUTES.TRACES_EXPLORER,
[DataSource.LOGS]: ROUTES.LOGS_EXPLORER,
};

View File

@@ -32,6 +32,7 @@ import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from '../constants';
import { getK8sEmptyState } from '../K8sEmptyState';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import { usePageSize } from '../utils';
@@ -329,6 +330,9 @@ function K8sClustersList({
const clustersData = useMemo(() => data?.payload?.data?.records || [], [data]);
const totalCount = data?.payload?.data?.total || 0;
const sentAnyMetricsData = data?.payload?.data?.sentAnyMetricsData ?? false;
const endTimeBeforeRetention =
data?.payload?.data?.endTimeBeforeRetention ?? false;
const formattedClustersData = useMemo(
() => formatDataForTable(clustersData, groupBy),
@@ -653,6 +657,18 @@ function K8sClustersList({
const showTableLoadingState =
(isFetching || isLoading) && formattedClustersData.length === 0;
const emptyState = getK8sEmptyState({
sentAnyMetricsData,
endTimeBeforeRetention,
entityName: 'cluster',
isLoading,
isFetching,
hasRecords: formattedClustersData.length > 0,
hasFilters: queryFilters?.items?.length > 0,
isError,
errorMessage: data?.error ?? '',
});
return (
<div className="k8s-list">
<K8sHeader
@@ -667,54 +683,42 @@ function K8sClustersList({
entity={K8sCategory.NODES}
showAutoRefresh={!selectedClusterData}
/>
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
{emptyState}
<Table
className="k8s-list-table clusters-list-table"
dataSource={showTableLoadingState ? [] : formattedClustersData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{
spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText: showTableLoadingState ? null : (
<div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
),
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
{!emptyState && (
<Table
className="k8s-list-table clusters-list-table"
dataSource={showTableLoadingState ? [] : formattedClustersData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{
spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText: null,
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
)}
<ClusterDetails
cluster={selectedClusterData}

View File

@@ -33,6 +33,7 @@ import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from '../constants';
import { getK8sEmptyState } from '../K8sEmptyState';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import { usePageSize } from '../utils';
@@ -326,6 +327,9 @@ function K8sDaemonSetsList({
data,
]);
const totalCount = data?.payload?.data?.total || 0;
const sentAnyMetricsData = data?.payload?.data?.sentAnyMetricsData ?? false;
const endTimeBeforeRetention =
data?.payload?.data?.endTimeBeforeRetention ?? false;
const formattedDaemonSetsData = useMemo(
() => formatDataForTable(daemonSetsData, groupBy),
@@ -659,6 +663,18 @@ function K8sDaemonSetsList({
const showTableLoadingState =
(isFetching || isLoading) && formattedDaemonSetsData.length === 0;
const emptyState = getK8sEmptyState({
sentAnyMetricsData,
endTimeBeforeRetention,
entityName: 'daemon set',
isLoading,
isFetching,
hasRecords: formattedDaemonSetsData.length > 0,
hasFilters: queryFilters?.items?.length > 0,
isError,
errorMessage: data?.error ?? '',
});
return (
<div className="k8s-list">
<K8sHeader
@@ -673,56 +689,44 @@ function K8sDaemonSetsList({
entity={K8sCategory.DAEMONSETS}
showAutoRefresh={!selectedDaemonSetData}
/>
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
{emptyState}
<Table
className={classNames('k8s-list-table', 'daemonSets-list-table', {
'expanded-daemonsets-list-table': isGroupedByAttribute,
})}
dataSource={showTableLoadingState ? [] : formattedDaemonSetsData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{
spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText: showTableLoadingState ? null : (
<div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
),
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
{!emptyState && (
<Table
className={classNames('k8s-list-table', 'daemonSets-list-table', {
'expanded-daemonsets-list-table': isGroupedByAttribute,
})}
dataSource={showTableLoadingState ? [] : formattedDaemonSetsData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{
spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText: null,
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
)}
<DaemonSetDetails
daemonSet={selectedDaemonSetData}

View File

@@ -33,6 +33,7 @@ import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from '../constants';
import { getK8sEmptyState } from '../K8sEmptyState';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import { usePageSize } from '../utils';
@@ -327,6 +328,9 @@ function K8sDeploymentsList({
data,
]);
const totalCount = data?.payload?.data?.total || 0;
const sentAnyMetricsData = data?.payload?.data?.sentAnyMetricsData ?? false;
const endTimeBeforeRetention =
data?.payload?.data?.endTimeBeforeRetention ?? false;
const formattedDeploymentsData = useMemo(
() => formatDataForTable(deploymentsData, groupBy),
@@ -666,6 +670,18 @@ function K8sDeploymentsList({
const showTableLoadingState =
(isFetching || isLoading) && formattedDeploymentsData.length === 0;
const emptyState = getK8sEmptyState({
sentAnyMetricsData,
endTimeBeforeRetention,
entityName: 'deployment',
isLoading,
isFetching,
hasRecords: formattedDeploymentsData.length > 0,
hasFilters: queryFilters?.items?.length > 0,
isError,
errorMessage: data?.error ?? '',
});
return (
<div className="k8s-list">
<K8sHeader
@@ -680,56 +696,44 @@ function K8sDeploymentsList({
entity={K8sCategory.NODES}
showAutoRefresh={!selectedDeploymentData}
/>
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
{emptyState}
<Table
className={classNames('k8s-list-table', 'deployments-list-table', {
'expanded-deployments-list-table': isGroupedByAttribute,
})}
dataSource={showTableLoadingState ? [] : formattedDeploymentsData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{
spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText: showTableLoadingState ? null : (
<div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
),
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
{!emptyState && (
<Table
className={classNames('k8s-list-table', 'deployments-list-table', {
'expanded-deployments-list-table': isGroupedByAttribute,
})}
dataSource={showTableLoadingState ? [] : formattedDeploymentsData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{
spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText: null,
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
)}
<DeploymentDetails
deployment={selectedDeploymentData}

View File

@@ -33,6 +33,7 @@ import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from '../constants';
import { getK8sEmptyState } from '../K8sEmptyState';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import { usePageSize } from '../utils';
@@ -319,6 +320,9 @@ function K8sJobsList({
const jobsData = useMemo(() => data?.payload?.data?.records || [], [data]);
const totalCount = data?.payload?.data?.total || 0;
const sentAnyMetricsData = data?.payload?.data?.sentAnyMetricsData ?? false;
const endTimeBeforeRetention =
data?.payload?.data?.endTimeBeforeRetention ?? false;
const formattedJobsData = useMemo(
() => formatDataForTable(jobsData, groupBy),
@@ -627,6 +631,18 @@ function K8sJobsList({
});
};
const emptyState = getK8sEmptyState({
sentAnyMetricsData,
endTimeBeforeRetention,
entityName: 'job',
isLoading,
isFetching,
hasRecords: formattedJobsData.length > 0,
hasFilters: queryFilters?.items?.length > 0,
isError,
errorMessage: data?.error ?? '',
});
return (
<div className="k8s-list">
<K8sHeader
@@ -641,57 +657,44 @@ function K8sJobsList({
entity={K8sCategory.JOBS}
showAutoRefresh={!selectedJobData}
/>
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
{emptyState}
<Table
className={classNames('k8s-list-table', 'jobs-list-table', {
'expanded-jobs-list-table': isGroupedByAttribute,
})}
dataSource={isFetching || isLoading ? [] : formattedJobsData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{
spinning: isFetching || isLoading,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText:
isFetching || isLoading ? null : (
<div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
),
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
{!emptyState && (
<Table
className={classNames('k8s-list-table', 'jobs-list-table', {
'expanded-jobs-list-table': isGroupedByAttribute,
})}
dataSource={isFetching || isLoading ? [] : formattedJobsData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{
spinning: isFetching || isLoading,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText: null,
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
)}
<JobDetails
job={selectedJobData}

View File

@@ -0,0 +1,150 @@
import { LoadingOutlined } from '@ant-design/icons';
import { Skeleton, Spin, Typography } from 'antd';
interface K8sEmptyStateProps {
sentAnyMetricsData: boolean;
endTimeBeforeRetention: boolean;
entityName: string;
isLoading: boolean;
isFetching: boolean;
hasRecords: boolean;
hasFilters: boolean;
isError: boolean;
errorMessage: string;
}
export function getK8sEmptyState({
sentAnyMetricsData,
endTimeBeforeRetention,
entityName,
isLoading,
isFetching,
hasRecords,
hasFilters,
isError,
errorMessage,
}: K8sEmptyStateProps): React.ReactNode {
if (isError) {
return <Typography>{errorMessage || 'Something went wrong'}</Typography>;
}
const showTableLoadingState = (isLoading || isFetching) && !hasRecords;
if (showTableLoadingState) {
return (
<div className="hosts-list-loading-state">
<Skeleton.Input
className="hosts-list-loading-state-item"
size="large"
block
active
/>
<Skeleton.Input
className="hosts-list-loading-state-item"
size="large"
block
active
/>
<Skeleton.Input
className="hosts-list-loading-state-item"
size="large"
block
active
/>
</div>
);
}
const showEmptyState =
!isFetching &&
!isLoading &&
!hasRecords &&
!sentAnyMetricsData &&
!hasFilters;
if (showEmptyState) {
return (
<div className="hosts-empty-state-container">
<div className="hosts-empty-state-container-content">
<img className="eyes-emoji" src="/Images/eyesEmoji.svg" alt="eyes emoji" />
<div className="no-hosts-message">
<Typography.Title level={5} className="no-hosts-message-title">
No {entityName} metrics data received yet.
</Typography.Title>
<Typography.Text className="no-hosts-message-text">
Please refer to the{' '}
<a
href="https://signoz.io/docs/infrastructure-monitoring/k8s-metrics/"
target="_blank"
rel="noreferrer"
>
Kubernetes Infrastructure Monitoring docs
</a>{' '}
to learn how to send K8s metrics to SigNoz.
</Typography.Text>
</div>
</div>
</div>
);
}
const showEndTimeBeforeRetentionMessage =
!isFetching &&
!isLoading &&
!hasRecords &&
endTimeBeforeRetention &&
!hasFilters;
if (showEndTimeBeforeRetentionMessage) {
return (
<div className="hosts-empty-state-container">
<div className="hosts-empty-state-container-content">
<img className="eyes-emoji" src="/Images/eyesEmoji.svg" alt="eyes emoji" />
<div className="no-hosts-message">
<Typography.Title level={5} className="no-hosts-message-title">
Queried time range is before earliest {entityName} metrics
</Typography.Title>
<Typography.Text className="no-hosts-message-text">
Your requested end time is earlier than the earliest detected time of{' '}
{entityName} metrics data, please adjust your end time.
</Typography.Text>
</div>
</div>
</div>
);
}
const showNoRecordsMessage =
!isFetching &&
!isLoading &&
!hasRecords &&
!showEmptyState &&
!showEndTimeBeforeRetentionMessage;
if (showNoRecordsMessage) {
return (
<div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Title level={5} className="no-filtered-hosts-title">
No {entityName} metrics found
</Typography.Title>
<Typography.Text className="no-filtered-hosts-message">
No {entityName} metrics in the selected time range and filters. Please
adjust your time range or filters.
</Typography.Text>
</div>
</div>
);
}
return null;
}
export function K8sTableLoadingIndicator(): JSX.Element {
return <Spin indicator={<LoadingOutlined size={14} spin />} />;
}

View File

@@ -32,6 +32,7 @@ import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from '../constants';
import { getK8sEmptyState } from '../K8sEmptyState';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import { usePageSize } from '../utils';
@@ -325,6 +326,9 @@ function K8sNamespacesList({
data,
]);
const totalCount = data?.payload?.data?.total || 0;
const sentAnyMetricsData = data?.payload?.data?.sentAnyMetricsData ?? false;
const endTimeBeforeRetention =
data?.payload?.data?.endTimeBeforeRetention ?? false;
const formattedNamespacesData = useMemo(
() => formatDataForTable(namespacesData, groupBy),
@@ -662,6 +666,18 @@ function K8sNamespacesList({
const showTableLoadingState =
(isFetching || isLoading) && formattedNamespacesData.length === 0;
const emptyState = getK8sEmptyState({
sentAnyMetricsData,
endTimeBeforeRetention,
entityName: 'namespace',
isLoading,
isFetching,
hasRecords: formattedNamespacesData.length > 0,
hasFilters: queryFilters?.items?.length > 0,
isError,
errorMessage: data?.error ?? '',
});
return (
<div className="k8s-list">
<K8sHeader
@@ -676,54 +692,42 @@ function K8sNamespacesList({
entity={K8sCategory.NODES}
showAutoRefresh={!selectedNamespaceData}
/>
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
<Table
className="k8s-list-table namespaces-list-table"
dataSource={showTableLoadingState ? [] : formattedNamespacesData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{
spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText: showTableLoadingState ? null : (
<div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
),
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
{emptyState ? (
<>{emptyState}</>
) : (
<Table
className="k8s-list-table namespaces-list-table"
dataSource={showTableLoadingState ? [] : formattedNamespacesData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{
spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText: null,
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
)}
<NamespaceDetails
namespace={selectedNamespaceData}
isModalTimeSelection

View File

@@ -32,6 +32,7 @@ import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from '../constants';
import { getK8sEmptyState } from '../K8sEmptyState';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import { usePageSize } from '../utils';
@@ -324,6 +325,9 @@ function K8sNodesList({
const nodesData = useMemo(() => data?.payload?.data?.records || [], [data]);
const totalCount = data?.payload?.data?.total || 0;
const sentAnyMetricsData = data?.payload?.data?.sentAnyMetricsData ?? false;
const endTimeBeforeRetention =
data?.payload?.data?.endTimeBeforeRetention ?? false;
const formattedNodesData = useMemo(
() => formatDataForTable(nodesData, groupBy),
@@ -641,6 +645,18 @@ function K8sNodesList({
const showTableLoadingState =
(isFetching || isLoading) && formattedNodesData.length === 0;
const emptyState = getK8sEmptyState({
sentAnyMetricsData,
endTimeBeforeRetention,
entityName: 'node',
isLoading,
isFetching,
hasRecords: formattedNodesData.length > 0,
hasFilters: queryFilters?.items?.length > 0,
isError,
errorMessage: data?.error ?? '',
});
return (
<div className="k8s-list">
<K8sHeader
@@ -655,54 +671,42 @@ function K8sNodesList({
entity={K8sCategory.NODES}
showAutoRefresh={!selectedNodeData}
/>
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
<Table
className="k8s-list-table nodes-list-table"
dataSource={showTableLoadingState ? [] : formattedNodesData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{
spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText: showTableLoadingState ? null : (
<div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
),
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
{emptyState ? (
<>{emptyState}</>
) : (
<Table
className="k8s-list-table nodes-list-table"
dataSource={showTableLoadingState ? [] : formattedNodesData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{
spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText: null,
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
)}
<NodeDetails
node={selectedNodeData}

View File

@@ -35,6 +35,7 @@ import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from '../constants';
import { getK8sEmptyState } from '../K8sEmptyState';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import {
@@ -334,6 +335,9 @@ function K8sPodsList({
const podsData = useMemo(() => data?.payload?.data?.records || [], [data]);
const totalCount = data?.payload?.data?.total || 0;
const sentAnyMetricsData = data?.payload?.data?.sentAnyMetricsData ?? false;
const endTimeBeforeRetention =
data?.payload?.data?.endTimeBeforeRetention ?? false;
const nestedPodsData = useMemo(() => {
if (!selectedRowData || !groupedByRowData?.payload?.data.records) {
@@ -693,6 +697,18 @@ function K8sPodsList({
const showTableLoadingState =
(isFetching || isLoading) && formattedPodsData.length === 0;
const emptyState = getK8sEmptyState({
sentAnyMetricsData,
endTimeBeforeRetention,
entityName: 'pod',
isLoading,
isFetching,
hasRecords: formattedPodsData.length > 0,
hasFilters: queryFilters?.items?.length > 0,
isError,
errorMessage: data?.error ?? '',
});
return (
<div className="k8s-list">
<K8sHeader
@@ -711,56 +727,44 @@ function K8sPodsList({
entity={K8sCategory.PODS}
showAutoRefresh={!selectedPodData}
/>
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
<Table
className={classNames('k8s-list-table', {
'expanded-k8s-list-table': isGroupedByAttribute,
})}
dataSource={showTableLoadingState ? [] : formattedPodsData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
}}
loading={{
spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText: showTableLoadingState ? null : (
<div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
),
}}
scroll={{ x: true }}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
{emptyState ? (
<>{emptyState}</>
) : (
<Table
className={classNames('k8s-list-table', {
'expanded-k8s-list-table': isGroupedByAttribute,
})}
dataSource={showTableLoadingState ? [] : formattedPodsData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
}}
loading={{
spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText: null,
}}
scroll={{ x: true }}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
)}
{selectedPodData && (
<PodDetails

View File

@@ -33,6 +33,7 @@ import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from '../constants';
import { getK8sEmptyState } from '../K8sEmptyState';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import { usePageSize } from '../utils';
@@ -333,6 +334,9 @@ function K8sStatefulSetsList({
data,
]);
const totalCount = data?.payload?.data?.total || 0;
const sentAnyMetricsData = data?.payload?.data?.sentAnyMetricsData ?? false;
const endTimeBeforeRetention =
data?.payload?.data?.endTimeBeforeRetention ?? false;
const formattedStatefulSetsData = useMemo(
() => formatDataForTable(statefulSetsData, groupBy),
@@ -662,6 +666,18 @@ function K8sStatefulSetsList({
const showTableLoadingState =
(isFetching || isLoading) && formattedStatefulSetsData.length === 0;
const emptyState = getK8sEmptyState({
sentAnyMetricsData,
endTimeBeforeRetention,
entityName: 'stateful set',
isLoading,
isFetching,
hasRecords: formattedStatefulSetsData.length > 0,
hasFilters: queryFilters?.items?.length > 0,
isError,
errorMessage: data?.error ?? '',
});
return (
<div className="k8s-list">
<K8sHeader
@@ -676,56 +692,44 @@ function K8sStatefulSetsList({
entity={K8sCategory.STATEFULSETS}
showAutoRefresh={!selectedStatefulSetData}
/>
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
{emptyState}
<Table
className={classNames('k8s-list-table', 'statefulSets-list-table', {
'expanded-statefulsets-list-table': isGroupedByAttribute,
})}
dataSource={showTableLoadingState ? [] : formattedStatefulSetsData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{
spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText: showTableLoadingState ? null : (
<div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
),
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
{!emptyState && (
<Table
className={classNames('k8s-list-table', 'statefulSets-list-table', {
'expanded-statefulsets-list-table': isGroupedByAttribute,
})}
dataSource={showTableLoadingState ? [] : formattedStatefulSetsData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{
spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText: null,
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
)}
<StatefulSetDetails
statefulSet={selectedStatefulSetData}

View File

@@ -33,6 +33,7 @@ import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from '../constants';
import { getK8sEmptyState } from '../K8sEmptyState';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import { usePageSize } from '../utils';
@@ -272,6 +273,9 @@ function K8sVolumesList({
const volumesData = useMemo(() => data?.payload?.data?.records || [], [data]);
const totalCount = data?.payload?.data?.total || 0;
const sentAnyMetricsData = data?.payload?.data?.sentAnyMetricsData ?? false;
const endTimeBeforeRetention =
data?.payload?.data?.endTimeBeforeRetention ?? false;
const formattedVolumesData = useMemo(
() => formatDataForTable(volumesData, groupBy),
@@ -585,6 +589,18 @@ function K8sVolumesList({
const showTableLoadingState =
(isFetching || isLoading) && formattedVolumesData.length === 0;
const emptyState = getK8sEmptyState({
sentAnyMetricsData,
endTimeBeforeRetention,
entityName: 'volume',
isLoading,
isFetching,
hasRecords: formattedVolumesData.length > 0,
hasFilters: queryFilters?.items?.length > 0,
isError,
errorMessage: data?.error ?? '',
});
return (
<div className="k8s-list">
<K8sHeader
@@ -599,56 +615,44 @@ function K8sVolumesList({
entity={K8sCategory.NODES}
showAutoRefresh={!selectedVolumeData}
/>
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
{emptyState}
<Table
className={classNames('k8s-list-table', 'volumes-list-table', {
'expanded-volumes-list-table': isGroupedByAttribute,
})}
dataSource={showTableLoadingState ? [] : formattedVolumesData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{
spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText: showTableLoadingState ? null : (
<div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
),
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
{!emptyState && (
<Table
className={classNames('k8s-list-table', 'volumes-list-table', {
'expanded-volumes-list-table': isGroupedByAttribute,
})}
dataSource={showTableLoadingState ? [] : formattedVolumesData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{
spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText: null,
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
expandIcon: expandRowIconRenderer,
expandedRowKeys,
}}
/>
)}
<VolumeDetails
volume={selectedVolumeData}

View File

@@ -12,14 +12,21 @@ import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interface
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import {
ICurrentQueryData,
useHandleExplorerTabChange,
} from 'hooks/useHandleExplorerTabChange';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { isEmpty } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import { Warning } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { MetricAggregation } from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
import { explorerViewToPanelType } from 'utils/explorerUtils';
import { v4 as uuid } from 'uuid';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
@@ -42,15 +49,20 @@ function Explorer(): JSX.Element {
stagedQuery,
updateAllQueriesOperators,
currentQuery,
handleSetConfig,
} = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
const { handleExplorerTabChange } = useHandleExplorerTabChange();
const [isMetricDetailsOpen, setIsMetricDetailsOpen] = useState(false);
const metricNames = useMemo(() => {
const currentMetricNames: string[] = [];
stagedQuery?.builder.queryData.forEach((query) => {
if (query.aggregateAttribute?.key) {
currentMetricNames.push(query.aggregateAttribute?.key);
const metricName =
query.aggregateAttribute?.key ||
(query.aggregations?.[0] as MetricAggregation | undefined)?.metricName;
if (metricName) {
currentMetricNames.push(metricName);
}
});
return currentMetricNames;
@@ -176,6 +188,16 @@ function Explorer(): JSX.Element {
useShareBuilderUrl({ defaultValue: defaultQuery });
const handleChangeSelectedView = useCallback(
(view: ExplorerViews, querySearchParameters?: ICurrentQueryData): void => {
const nextPanelType =
explorerViewToPanelType[view] || PANEL_TYPES.TIME_SERIES;
handleSetConfig(nextPanelType, DataSource.METRICS);
handleExplorerTabChange(nextPanelType, querySearchParameters);
},
[handleSetConfig, handleExplorerTabChange],
);
const handleExport = useCallback(
(
dashboard: Dashboard | null,
@@ -348,6 +370,7 @@ function Explorer(): JSX.Element {
onExport={handleExport}
isOneChartPerQuery={showOneChartPerQuery}
splitedQueries={splitedQueries}
handleChangeSelectedView={handleChangeSelectedView}
/>
{isMetricDetailsOpen && selectedMetricName && (
<MetricDetails

View File

@@ -12,6 +12,7 @@ import { initialQueriesMap } from 'constants/queryBuilder';
import * as useOptionsMenuHooks from 'container/OptionsMenu';
import * as useUpdateDashboardHooks from 'hooks/dashboard/useUpdateDashboard';
import * as useQueryBuilderHooks from 'hooks/queryBuilder/useQueryBuilder';
import * as useHandleExplorerTabChangeHooks from 'hooks/useHandleExplorerTabChange';
import * as appContextHooks from 'providers/App/App';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import * as timezoneHooks from 'providers/Timezone';
@@ -29,6 +30,8 @@ const queryClient = new QueryClient();
const mockUpdateAllQueriesOperators = jest
.fn()
.mockReturnValue(initialQueriesMap[DataSource.METRICS]);
const mockHandleSetConfig = jest.fn();
const mockHandleExplorerTabChange = jest.fn();
const mockUseQueryBuilderData = {
handleRunQuery: jest.fn(),
stagedQuery: initialQueriesMap[DataSource.METRICS],
@@ -40,7 +43,7 @@ const mockUseQueryBuilderData = {
handleSetQueryData: jest.fn(),
handleSetFormulaData: jest.fn(),
handleSetQueryItemData: jest.fn(),
handleSetConfig: jest.fn(),
handleSetConfig: mockHandleSetConfig,
removeQueryBuilderEntityByIndex: jest.fn(),
removeQueryTypeItemByIndex: jest.fn(),
isDefaultQuery: jest.fn(),
@@ -135,6 +138,11 @@ jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue({
...mockUseQueryBuilderData,
} as any);
jest
.spyOn(useHandleExplorerTabChangeHooks, 'useHandleExplorerTabChange')
.mockReturnValue({
handleExplorerTabChange: mockHandleExplorerTabChange,
});
const Y_AXIS_UNIT_SELECTOR_TEST_ID = 'y-axis-unit-selector';
@@ -382,4 +390,109 @@ describe('Explorer', () => {
expect(oneChartPerQueryToggle).toBeEnabled();
expect(oneChartPerQueryToggle).not.toBeChecked();
});
describe('loading saved views with v5 query format', () => {
const EMPTY_STATE_TEXT = 'Select a metric and run a query to see the results';
it('should show empty state when no metric is selected', () => {
(useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams({}),
mockSetSearchParams,
]);
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [],
});
jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue({
...mockUseQueryBuilderData,
} as any);
renderExplorer();
expect(screen.getByText(EMPTY_STATE_TEXT)).toBeInTheDocument();
});
it('should not show empty state when saved view has v5 aggregations format', () => {
(useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams({}),
mockSetSearchParams,
]);
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [MOCK_METRIC_METADATA],
});
// saved view loaded back from v5 format
// aggregateAttribute.key is empty (lost in v3/v4 -> v5 -> v3/v4 round trip)
// but aggregations[0].metricName has metric name
// TODO(srikanthccv): remove this mess
const mockQueryData = {
...initialQueriesMap[DataSource.METRICS].builder.queryData[0],
aggregateAttribute: {
...(initialQueriesMap[DataSource.METRICS].builder.queryData[0]
.aggregateAttribute as BaseAutocompleteData),
key: '',
},
aggregations: [
{
metricName: 'http_requests_total',
temporality: 'cumulative',
timeAggregation: 'rate',
spaceAggregation: 'sum',
},
],
};
jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue(({
...mockUseQueryBuilderData,
stagedQuery: {
...initialQueriesMap[DataSource.METRICS],
builder: {
...initialQueriesMap[DataSource.METRICS].builder,
queryData: [mockQueryData],
},
},
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
renderExplorer();
expect(screen.queryByText(EMPTY_STATE_TEXT)).not.toBeInTheDocument();
});
it('should not show empty state when query uses v3 aggregateAttribute format', () => {
(useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams({}),
mockSetSearchParams,
]);
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [MOCK_METRIC_METADATA],
});
const mockQueryData = {
...initialQueriesMap[DataSource.METRICS].builder.queryData[0],
aggregateAttribute: {
...(initialQueriesMap[DataSource.METRICS].builder.queryData[0]
.aggregateAttribute as BaseAutocompleteData),
key: 'system_cpu_usage',
},
};
jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue(({
...mockUseQueryBuilderData,
stagedQuery: {
...initialQueriesMap[DataSource.METRICS],
builder: {
...initialQueriesMap[DataSource.METRICS].builder,
queryData: [mockQueryData],
},
},
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
renderExplorer();
expect(screen.queryByText(EMPTY_STATE_TEXT)).not.toBeInTheDocument();
});
});
});

View File

@@ -9,7 +9,6 @@ import {
} from 'api/generated/services/metrics';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import useUrlQuery from 'hooks/useUrlQuery';
import { Bell, Grid } from 'lucide-react';
import { pluralize } from 'utils/pluralize';
@@ -18,8 +17,6 @@ import { DashboardsAndAlertsPopoverProps } from './types';
function DashboardsAndAlertsPopover({
metricName,
}: DashboardsAndAlertsPopoverProps): JSX.Element | null {
const params = useUrlQuery();
const {
data: alertsData,
isLoading: isLoadingAlerts,
@@ -71,8 +68,10 @@ function DashboardsAndAlertsPopover({
<Typography.Link
key={alert.alertId}
onClick={(): void => {
params.set(QueryParams.ruleId, alert.alertId);
window.open(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, '_blank');
window.open(
`${ROUTES.ALERT_OVERVIEW}?${QueryParams.ruleId}=${alert.alertId}`,
'_blank',
);
}}
className="dashboards-popover-content-item"
>
@@ -82,7 +81,7 @@ function DashboardsAndAlertsPopover({
}));
}
return null;
}, [alerts, params]);
}, [alerts]);
const dashboardsPopoverContent = useMemo(() => {
if (dashboards && dashboards.length > 0) {

View File

@@ -16,15 +16,6 @@ import {
const mockWindowOpen = jest.fn();
Object.defineProperty(window, 'open', { value: mockWindowOpen });
const mockSetQuery = jest.fn();
const mockUrlQuery = {
set: mockSetQuery,
toString: jest.fn(),
};
jest.mock('hooks/useUrlQuery', () => ({
__esModule: true,
default: jest.fn(() => mockUrlQuery),
}));
const useGetMetricAlertsMock = jest.spyOn(
metricsExplorerHooks,
@@ -156,12 +147,10 @@ describe('DashboardsAndAlertsPopover', () => {
// Click on the first alert rule
await userEvent.click(screen.getByText(MOCK_ALERT_1.alertName));
// Should open alert in new tab
expect(mockSetQuery).toHaveBeenCalledWith(
QueryParams.ruleId,
MOCK_ALERT_1.alertId,
expect(mockWindowOpen).toHaveBeenCalledWith(
`/alerts/overview?${QueryParams.ruleId}=${MOCK_ALERT_1.alertId}`,
'_blank',
);
expect(mockWindowOpen).toHaveBeenCalled();
});
it('renders unique dashboards even when there are duplicates', async () => {

View File

@@ -1,76 +0,0 @@
import { useMemo } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Typography } from 'antd';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import {
BarChart,
BarChart2,
BarChartHorizontal,
Diff,
Gauge,
} from 'lucide-react';
import { METRIC_TYPE_LABEL_MAP } from './constants';
// TODO: @amlannandy Delete this component after API migration is complete
function MetricTypeRenderer({ type }: { type: MetricType }): JSX.Element {
const [icon, color] = useMemo(() => {
switch (type) {
case MetricType.SUM:
return [
<Diff key={type} size={12} color={Color.BG_ROBIN_500} />,
Color.BG_ROBIN_500,
];
case MetricType.GAUGE:
return [
<Gauge key={type} size={12} color={Color.BG_SAKURA_500} />,
Color.BG_SAKURA_500,
];
case MetricType.HISTOGRAM:
return [
<BarChart2 key={type} size={12} color={Color.BG_SIENNA_500} />,
Color.BG_SIENNA_500,
];
case MetricType.SUMMARY:
return [
<BarChartHorizontal key={type} size={12} color={Color.BG_FOREST_500} />,
Color.BG_FOREST_500,
];
case MetricType.EXPONENTIAL_HISTOGRAM:
return [
<BarChart key={type} size={12} color={Color.BG_AQUA_500} />,
Color.BG_AQUA_500,
];
default:
return [null, ''];
}
}, [type]);
const metricTypeRendererStyle = useMemo(
() => ({
backgroundColor: `${color}33`,
border: `1px solid ${color}`,
color,
}),
[color],
);
const metricTypeRendererTextStyle = useMemo(
() => ({
color,
fontSize: 12,
}),
[color],
);
return (
<div className="metric-type-renderer" style={metricTypeRendererStyle}>
{icon}
<Typography.Text style={metricTypeRendererTextStyle}>
{METRIC_TYPE_LABEL_MAP[type]}
</Typography.Text>
</div>
);
}
export default MetricTypeRenderer;

View File

@@ -47,7 +47,7 @@ function MetricsSearch({
}}
onRun={handleRunQuery}
showFilterSuggestionsWithoutMetric
placeholder="Try metric_name CONTAINS 'http.server' to view all HTTP Server metrics being sent"
placeholder="Search your metrics. Try service.name='api' to see all API service metrics, or http.client for HTTP client metrics."
/>
</div>
<RunQueryBtn

View File

@@ -10,6 +10,7 @@ import {
} from 'antd';
import { SorterResult } from 'antd/es/table/interface';
import { Querybuildertypesv5OrderDirectionDTO } from 'api/generated/services/sigNoz.schemas';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import { Info } from 'lucide-react';
import { MetricsListItemRowData, MetricsTableProps } from './types';
@@ -18,6 +19,7 @@ import { getMetricsTableColumns } from './utils';
function MetricsTable({
isLoading,
isError,
error,
data,
pageSize,
currentPage,
@@ -71,54 +73,54 @@ function MetricsTable({
<Info size={16} />
</Tooltip>
</div>
<Table
loading={{
spinning: isLoading,
indicator: (
<Spin
data-testid="metrics-table-loading-state"
indicator={<LoadingOutlined size={14} spin />}
/>
),
}}
dataSource={data}
columns={getMetricsTableColumns(queryFilterExpression, onFilterChange)}
locale={{
emptyText: isLoading ? null : (
<div
className="no-metrics-message-container"
data-testid={
isError ? 'metrics-table-error-state' : 'metrics-table-empty-state'
}
>
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
{isError && error ? (
<ErrorInPlace error={error} />
) : (
<Table
loading={{
spinning: isLoading,
indicator: (
<Spin
data-testid="metrics-table-loading-state"
indicator={<LoadingOutlined size={14} spin />}
/>
<Typography.Text className="no-metrics-message">
{isError
? 'Error fetching metrics. If the problem persists, please contact support.'
: 'This query had no results. Edit your query and try again!'}
</Typography.Text>
</div>
),
}}
tableLayout="fixed"
onChange={handleTableChange}
pagination={{
current: currentPage,
pageSize,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
total: totalCount,
}}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => openMetricDetails(record.key, 'list'),
className: 'clickable-row',
})}
/>
),
}}
dataSource={data}
columns={getMetricsTableColumns(queryFilterExpression, onFilterChange)}
locale={{
emptyText: isLoading ? null : (
<div
className="no-metrics-message-container"
data-testid="metrics-table-empty-state"
>
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-metrics-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
),
}}
tableLayout="fixed"
onChange={handleTableChange}
pagination={{
current: currentPage,
pageSize,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
total: totalCount,
}}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => openMetricDetails(record.key, 'list'),
className: 'clickable-row',
})}
/>
)}
</div>
);
}

View File

@@ -4,6 +4,7 @@ import { Group } from '@visx/group';
import { Treemap } from '@visx/hierarchy';
import { Empty, Select, Skeleton, Tooltip, Typography } from 'antd';
import { MetricsexplorertypesTreemapModeDTO } from 'api/generated/services/sigNoz.schemas';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import { HierarchyNode, stratify, treemapBinary } from 'd3-hierarchy';
import { Info } from 'lucide-react';
@@ -27,6 +28,7 @@ import {
function MetricsTreemapInternal({
isLoading,
isError,
error,
data,
viewType,
openMetricDetails,
@@ -91,6 +93,10 @@ function MetricsTreemapInternal({
);
}
if (isError && error) {
return <ErrorInPlace error={error} />;
}
if (isError) {
return (
<Empty
@@ -174,6 +180,7 @@ function MetricsTreemap({
data,
isLoading,
isError,
error,
openMetricDetails,
setHeatmapView,
}: MetricsTreemapProps): JSX.Element {
@@ -202,6 +209,7 @@ function MetricsTreemap({
<MetricsTreemapInternal
isLoading={isLoading}
isError={isError}
error={error}
data={data}
viewType={viewType}
openMetricDetails={openMetricDetails}

View File

@@ -4,6 +4,7 @@ import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import * as Sentry from '@sentry/react';
import logEvent from 'api/common/logEvent';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
useGetMetricsStats,
useGetMetricsTreemap,
@@ -63,13 +64,20 @@ function Summary(): JSX.Element {
MetricsexplorertypesTreemapModeDTO.samples,
);
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const {
currentQuery,
stagedQuery,
redirectWithQueryBuilderData,
} = useQueryBuilder();
useShareBuilderUrl({ defaultValue: initialQueriesMap[DataSource.METRICS] });
const query = useMemo(() => currentQuery?.builder?.queryData[0], [
currentQuery,
]);
const query = useMemo(
() =>
stagedQuery?.builder?.queryData?.[0] ||
initialQueriesMap[DataSource.METRICS].builder.queryData[0],
[stagedQuery],
);
const [searchParams, setSearchParams] = useSearchParams();
const [isMetricDetailsOpen, setIsMetricDetailsOpen] = useState(
@@ -86,14 +94,16 @@ function Summary(): JSX.Element {
(state) => state.globalTime,
);
const appliedFilterExpression = query?.filter?.expression || '';
const [
currentQueryFilterExpression,
setCurrentQueryFilterExpression,
] = useState<string>(query?.filter?.expression || '');
] = useState<string>(appliedFilterExpression);
const [appliedFilterExpression, setAppliedFilterExpression] = useState(
query?.filter?.expression || '',
);
useEffect(() => {
setCurrentQueryFilterExpression(appliedFilterExpression);
}, [appliedFilterExpression]);
const queryFilterExpression = useMemo(
() => ({ expression: appliedFilterExpression }),
@@ -150,6 +160,7 @@ function Summary(): JSX.Element {
mutate: getMetricsStats,
isLoading: isGetMetricsStatsLoading,
isError: isGetMetricsStatsError,
error: metricsStatsError,
} = useGetMetricsStats();
const {
@@ -157,8 +168,19 @@ function Summary(): JSX.Element {
mutate: getMetricsTreemap,
isLoading: isGetMetricsTreemapLoading,
isError: isGetMetricsTreemapError,
error: metricsTreemapError,
} = useGetMetricsTreemap();
const metricsStatsApiError = useMemo(
() => convertToApiError(metricsStatsError),
[metricsStatsError],
);
const metricsTreemapApiError = useMemo(
() => convertToApiError(metricsTreemapError),
[metricsTreemapError],
);
useEffect(() => {
getMetricsStats({
data: metricsListQuery,
@@ -192,8 +214,6 @@ function Summary(): JSX.Element {
],
},
});
setCurrentQueryFilterExpression(expression);
setAppliedFilterExpression(expression);
setCurrentPage(1);
if (expression) {
logEvent(MetricsExplorerEvents.FilterApplied, {
@@ -290,10 +310,14 @@ function Summary(): JSX.Element {
};
const isMetricsListDataEmpty =
formattedMetricsData.length === 0 && !isGetMetricsStatsLoading;
formattedMetricsData.length === 0 &&
!isGetMetricsStatsLoading &&
!isGetMetricsStatsError;
const isMetricsTreeMapDataEmpty =
!treeMapData?.data[heatmapView]?.length && !isGetMetricsTreemapLoading;
!treeMapData?.data[heatmapView]?.length &&
!isGetMetricsTreemapLoading &&
!isGetMetricsTreemapError;
const showFullScreenLoading =
(isGetMetricsStatsLoading || isGetMetricsTreemapLoading) &&
@@ -322,6 +346,7 @@ function Summary(): JSX.Element {
data={treeMapData?.data}
isLoading={isGetMetricsTreemapLoading}
isError={isGetMetricsTreemapError}
error={metricsTreemapApiError}
viewType={heatmapView}
openMetricDetails={openMetricDetails}
setHeatmapView={handleSetHeatmapView}
@@ -329,6 +354,7 @@ function Summary(): JSX.Element {
<MetricsTable
isLoading={isGetMetricsStatsLoading}
isError={isGetMetricsStatsError}
error={metricsStatsApiError}
data={formattedMetricsData}
pageSize={pageSize}
currentPage={currentPage}

View File

@@ -1,63 +0,0 @@
import { Color } from '@signozhq/design-tokens';
import { render, screen } from '@testing-library/react';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import MetricTypeRenderer from '../MetricTypeRenderer';
jest.mock('lucide-react', () => {
return {
__esModule: true,
Diff: (): JSX.Element => <svg data-testid="diff-icon" />,
Gauge: (): JSX.Element => <svg data-testid="gauge-icon" />,
BarChart2: (): JSX.Element => <svg data-testid="bar-chart-2-icon" />,
BarChartHorizontal: (): JSX.Element => (
<svg data-testid="bar-chart-horizontal-icon" />
),
BarChart: (): JSX.Element => <svg data-testid="bar-chart-icon" />,
};
});
describe('MetricTypeRenderer', () => {
it('should render correct icon and color for each metric type', () => {
const types = [
{
type: MetricType.SUM,
color: Color.BG_ROBIN_500,
iconTestId: 'diff-icon',
},
{
type: MetricType.GAUGE,
color: Color.BG_SAKURA_500,
iconTestId: 'gauge-icon',
},
{
type: MetricType.HISTOGRAM,
color: Color.BG_SIENNA_500,
iconTestId: 'bar-chart-2-icon',
},
{
type: MetricType.SUMMARY,
color: Color.BG_FOREST_500,
iconTestId: 'bar-chart-horizontal-icon',
},
{
type: MetricType.EXPONENTIAL_HISTOGRAM,
color: Color.BG_AQUA_500,
iconTestId: 'bar-chart-icon',
},
];
types.forEach(({ type, color, iconTestId }) => {
const { container } = render(<MetricTypeRenderer type={type} />);
const rendererDiv = container.firstChild as HTMLElement;
expect(rendererDiv).toHaveStyle({
backgroundColor: `${color}33`,
border: `1px solid ${color}`,
color,
});
expect(screen.getByTestId(iconTestId)).toBeInTheDocument();
});
});
});

View File

@@ -6,6 +6,7 @@ import { Filter } from 'api/v5/v5';
import * as useGetMetricsListFilterValues from 'hooks/metricsExplorer/useGetMetricsListFilterValues';
import * as useQueryBuilderOperationsHooks from 'hooks/queryBuilder/useQueryBuilderOperations';
import store from 'store';
import APIError from 'types/api/error';
import MetricsTable from '../MetricsTable';
import { MetricsListItemRowData } from '../types';
@@ -119,12 +120,23 @@ describe('MetricsTable', () => {
});
it('shows error state', () => {
const mockError = new APIError({
httpStatusCode: 400,
error: {
code: '400',
message: 'invalid filter expression',
url: '',
errors: [],
},
});
render(
<MemoryRouter>
<Provider store={store}>
<MetricsTable
isLoading={false}
isError
error={mockError}
data={[]}
pageSize={10}
currentPage={1}
@@ -139,12 +151,8 @@ describe('MetricsTable', () => {
</MemoryRouter>,
);
expect(screen.getByTestId('metrics-table-error-state')).toBeInTheDocument();
expect(
screen.getByText(
'Error fetching metrics. If the problem persists, please contact support.',
),
).toBeInTheDocument();
expect(screen.getByText('400')).toBeInTheDocument();
expect(screen.getByText('invalid filter expression')).toBeInTheDocument();
});
it('shows empty state when no data', () => {

View File

@@ -1,16 +1,12 @@
import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { Provider } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import * as metricsHooks from 'api/generated/services/metrics';
import { initialQueriesMap } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import * as useGetMetricsListHooks from 'hooks/metricsExplorer/useGetMetricsList';
import * as useGetMetricsTreeMapHooks from 'hooks/metricsExplorer/useGetMetricsTreeMap';
import store from 'store';
import { render, screen } from 'tests/test-utils';
import * as useQueryBuilderHooks from 'hooks/queryBuilder/useQueryBuilder';
import { render, screen, waitFor } from 'tests/test-utils';
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
import Summary from '../Summary';
import { TreemapViewType } from '../types';
jest.mock('d3-hierarchy', () => ({
stratify: jest.fn().mockReturnValue({
@@ -44,58 +40,135 @@ jest.mock('react-router-dom', () => ({
pathname: `${ROUTES.METRICS_EXPLORER_BASE}`,
}),
}));
jest.mock('hooks/queryBuilder/useShareBuilderUrl', () => ({
useShareBuilderUrl: jest.fn(),
}));
// so filter expression assertions easy
jest.mock('../MetricsSearch', () => {
return function MockMetricsSearch(props: {
currentQueryFilterExpression: string;
}): JSX.Element {
return (
<div data-testid="metrics-search-expression">
{props.currentQueryFilterExpression}
</div>
);
};
});
const queryClient = new QueryClient();
const mockMetricName = 'test-metric';
jest.spyOn(useGetMetricsListHooks, 'useGetMetricsList').mockReturnValue({
data: {
payload: {
status: 'success',
data: {
metrics: [
{
metric_name: mockMetricName,
description: 'description for a test metric',
type: MetricType.GAUGE,
unit: 'count',
lastReceived: '1715702400',
[TreemapViewType.TIMESERIES]: 100,
[TreemapViewType.SAMPLES]: 100,
},
],
},
},
},
isError: false,
isLoading: false,
} as any);
jest.spyOn(useGetMetricsTreeMapHooks, 'useGetMetricsTreeMap').mockReturnValue({
data: {
payload: {
status: 'success',
data: {
[TreemapViewType.TIMESERIES]: [
{
metric_name: mockMetricName,
percentage: 100,
total_value: 100,
},
],
[TreemapViewType.SAMPLES]: [
{
metric_name: mockMetricName,
percentage: 100,
},
],
},
},
},
isError: false,
isLoading: false,
} as any);
const mockSetSearchParams = jest.fn();
const mockGetMetricsStats = jest.fn();
const mockGetMetricsTreemap = jest.fn();
const mockUseQueryBuilderData = {
handleRunQuery: jest.fn(),
stagedQuery: initialQueriesMap[DataSource.METRICS],
updateAllQueriesOperators: jest.fn(),
currentQuery: initialQueriesMap[DataSource.METRICS],
resetQuery: jest.fn(),
redirectWithQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(),
handleSetQueryData: jest.fn(),
handleSetFormulaData: jest.fn(),
handleSetQueryItemData: jest.fn(),
handleSetConfig: jest.fn(),
removeQueryBuilderEntityByIndex: jest.fn(),
removeQueryTypeItemByIndex: jest.fn(),
isDefaultQuery: jest.fn(),
};
const useGetMetricsStatsSpy = jest.spyOn(metricsHooks, 'useGetMetricsStats');
const useGetMetricsTreemapSpy = jest.spyOn(
metricsHooks,
'useGetMetricsTreemap',
);
const useQueryBuilderSpy = jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder');
describe('Summary', () => {
beforeEach(() => {
jest.clearAllMocks();
(useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams(),
mockSetSearchParams,
]);
useGetMetricsStatsSpy.mockReturnValue({
data: null,
mutate: mockGetMetricsStats,
isLoading: true,
isError: false,
error: null,
isIdle: true,
isSuccess: false,
reset: jest.fn(),
status: 'idle',
} as any);
useGetMetricsTreemapSpy.mockReturnValue({
data: null,
mutate: mockGetMetricsTreemap,
isLoading: true,
isError: false,
error: null,
isIdle: true,
isSuccess: false,
reset: jest.fn(),
status: 'idle',
} as any);
useQueryBuilderSpy.mockReturnValue(({
...mockUseQueryBuilderData,
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
});
it('does not carry filter expression from a previous page', async () => {
const staleFilterExpression = "service.name = 'redis'";
// prev filter from logs explorer
const staleQuery = {
...initialQueriesMap[DataSource.METRICS],
builder: {
...initialQueriesMap[DataSource.METRICS].builder,
queryData: [
{
...initialQueriesMap[DataSource.METRICS].builder.queryData[0],
filter: { expression: staleFilterExpression },
},
],
},
};
// stagedQuery has stale filter (before QueryBuilder resets it)
useQueryBuilderSpy.mockReturnValue(({
...mockUseQueryBuilderData,
stagedQuery: staleQuery,
currentQuery: staleQuery,
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
const { rerender } = render(<Summary />);
expect(screen.getByTestId('metrics-search-expression')).toHaveTextContent(
staleFilterExpression,
);
// QB route change effect resets stagedQuery to null
useQueryBuilderSpy.mockReturnValue(({
...mockUseQueryBuilderData,
stagedQuery: null,
currentQuery: initialQueriesMap[DataSource.METRICS],
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
rerender(<Summary />);
await waitFor(() => {
expect(
screen.getByTestId('metrics-search-expression'),
).toBeEmptyDOMElement();
});
});
it('persists inspect modal open state across page refresh', () => {
(useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams({
@@ -105,13 +178,7 @@ describe('Summary', () => {
mockSetSearchParams,
]);
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<Summary />
</Provider>
</QueryClientProvider>,
);
render(<Summary />);
expect(screen.queryByText('Proportion View')).not.toBeInTheDocument();
});
@@ -120,18 +187,12 @@ describe('Summary', () => {
(useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams({
isMetricDetailsOpen: 'true',
selectedMetricName: mockMetricName,
selectedMetricName: 'test-metric',
}),
mockSetSearchParams,
]);
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<Summary />
</Provider>
</QueryClientProvider>,
);
render(<Summary />);
expect(screen.queryByText('Proportion View')).not.toBeInTheDocument();
});

View File

@@ -2,7 +2,6 @@ import {
MetricsexplorertypesTreemapModeDTO,
MetrictypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
export const METRICS_TABLE_PAGE_SIZE = 10;
@@ -19,15 +18,6 @@ export const TREEMAP_SQUARE_PADDING = 5;
export const TREEMAP_MARGINS = { TOP: 10, LEFT: 10, RIGHT: 10, BOTTOM: 10 };
// TODO: Remove this once API migration is complete
export const METRIC_TYPE_LABEL_MAP = {
[MetricType.SUM]: 'Sum',
[MetricType.GAUGE]: 'Gauge',
[MetricType.HISTOGRAM]: 'Histogram',
[MetricType.SUMMARY]: 'Summary',
[MetricType.EXPONENTIAL_HISTOGRAM]: 'Exp. Histogram',
};
export const METRIC_TYPE_VIEW_LABEL_MAP: Record<MetrictypesTypeDTO, string> = {
[MetrictypesTypeDTO.sum]: 'Sum',
[MetrictypesTypeDTO.gauge]: 'Gauge',
@@ -36,15 +26,6 @@ export const METRIC_TYPE_VIEW_LABEL_MAP: Record<MetrictypesTypeDTO, string> = {
[MetrictypesTypeDTO.exponentialhistogram]: 'Exp. Histogram',
};
// TODO(@amlannandy): To remove this once API migration is complete
export const METRIC_TYPE_VALUES_MAP: Record<MetricType, string> = {
[MetricType.SUM]: 'Sum',
[MetricType.GAUGE]: 'Gauge',
[MetricType.HISTOGRAM]: 'Histogram',
[MetricType.SUMMARY]: 'Summary',
[MetricType.EXPONENTIAL_HISTOGRAM]: 'ExponentialHistogram',
};
export const METRIC_TYPE_VIEW_VALUES_MAP: Record<MetrictypesTypeDTO, string> = {
[MetrictypesTypeDTO.sum]: 'Sum',
[MetrictypesTypeDTO.gauge]: 'Gauge',

View File

@@ -5,11 +5,13 @@ import {
Querybuildertypesv5OrderByDTO,
} from 'api/generated/services/sigNoz.schemas';
import { Filter } from 'api/v5/v5';
import APIError from 'types/api/error';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export interface MetricsTableProps {
isLoading: boolean;
isError: boolean;
error?: APIError;
data: MetricsListItemRowData[];
pageSize: number;
currentPage: number;
@@ -33,6 +35,7 @@ export interface MetricsTreemapProps {
data: MetricsexplorertypesTreemapResponseDTO | undefined;
isLoading: boolean;
isError: boolean;
error?: APIError;
viewType: MetricsexplorertypesTreemapModeDTO;
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
setHeatmapView: (value: MetricsexplorertypesTreemapModeDTO) => void;
@@ -41,6 +44,7 @@ export interface MetricsTreemapProps {
export interface MetricsTreemapInternalProps {
isLoading: boolean;
isError: boolean;
error?: APIError;
data: MetricsexplorertypesTreemapResponseDTO | undefined;
viewType: MetricsexplorertypesTreemapModeDTO;
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;

View File

@@ -1,5 +1,11 @@
import { useEffect, useState } from 'react';
import { fireEvent, render, screen, within } from '@testing-library/react';
import {
fireEvent,
render,
screen,
waitFor,
within,
} from '@testing-library/react';
import {
MetricsexplorertypesListMetricDTO,
MetrictypesTypeDTO,
@@ -221,6 +227,108 @@ describe('MetricNameSelector', () => {
expect(container.querySelector('.ant-spin-spinning')).toBeInTheDocument();
});
it('preserves metric search text for signalSource normalization transition (undefined -> empty)', async () => {
returnMetrics([makeMetric({ metricName: 'http_requests_total' })]);
const query = makeQuery({
aggregateAttribute: {
key: 'http_requests_total',
type: '',
dataType: DataTypes.Float64,
},
aggregations: [
{
metricName: 'http_requests_total',
timeAggregation: 'rate',
spaceAggregation: 'sum',
temporality: '',
},
] as MetricAggregation[],
});
const { rerender } = render(
<MetricNameSelector
query={query}
onChange={jest.fn()}
signalSource={undefined}
/>,
);
rerender(
<MetricNameSelector query={query} onChange={jest.fn()} signalSource="" />,
);
await waitFor(() => {
const lastCall =
mockUseListMetrics.mock.calls[mockUseListMetrics.mock.calls.length - 1];
expect(lastCall?.[0]).toMatchObject({
searchText: 'http_requests_total',
limit: 100,
});
});
});
it('updates search text when metric name is hydrated after initial mount', async () => {
returnMetrics([makeMetric({ metricName: 'signoz_latency.bucket' })]);
const emptyQuery = makeQuery({
aggregateAttribute: {
key: '',
type: '',
dataType: DataTypes.Float64,
},
aggregations: [
{
metricName: '',
timeAggregation: 'rate',
spaceAggregation: 'sum',
temporality: '',
},
] as MetricAggregation[],
});
const hydratedQuery = makeQuery({
aggregateAttribute: {
key: '',
type: '',
dataType: DataTypes.Float64,
},
aggregations: [
{
metricName: 'signoz_latency.bucket',
timeAggregation: 'rate',
spaceAggregation: 'sum',
temporality: '',
},
] as MetricAggregation[],
});
const { rerender } = render(
<MetricNameSelector
query={emptyQuery}
onChange={jest.fn()}
signalSource=""
/>,
);
rerender(
<MetricNameSelector
query={hydratedQuery}
onChange={jest.fn()}
signalSource=""
/>,
);
await waitFor(() => {
const lastCall =
mockUseListMetrics.mock.calls[mockUseListMetrics.mock.calls.length - 1];
expect(lastCall?.[0]).toMatchObject({
searchText: 'signoz_latency.bucket',
limit: 100,
});
});
});
});
describe('selecting a metric type updates the aggregation options', () => {

View File

@@ -98,15 +98,30 @@ export const MetricNameSelector = memo(function MetricNameSelector({
useEffect(() => {
setInputValue(currentMetricName || defaultValue || '');
if (currentMetricName) {
setSearchText(currentMetricName);
}
}, [defaultValue, currentMetricName]);
useEffect(() => {
if (prevSignalSourceRef.current !== signalSource) {
const previousSignalSource = prevSignalSourceRef.current;
prevSignalSourceRef.current = signalSource;
const isNormalizationTransition =
(previousSignalSource === undefined && signalSource === '') ||
(previousSignalSource === '' && signalSource === undefined);
if (isNormalizationTransition && currentMetricName) {
setSearchText(currentMetricName);
setInputValue(currentMetricName || defaultValue || '');
return;
}
setSearchText('');
setInputValue('');
}
}, [signalSource]);
}, [signalSource, currentMetricName, defaultValue]);
const debouncedValue = useDebounce(searchText, DEBOUNCE_DELAY);
@@ -152,7 +167,9 @@ export const MetricNameSelector = memo(function MetricNameSelector({
}, [metrics]);
useEffect(() => {
const metricName = (query.aggregations?.[0] as MetricAggregation)?.metricName;
const metricName =
(query.aggregations?.[0] as MetricAggregation)?.metricName ||
query.aggregateAttribute?.key;
const hasAggregateAttributeType = query.aggregateAttribute?.type;
if (metricName && !hasAggregateAttributeType && metrics.length > 0) {
@@ -164,7 +181,13 @@ export const MetricNameSelector = memo(function MetricNameSelector({
);
}
}
}, [metrics, query.aggregations, query.aggregateAttribute?.type, onChange]);
}, [
metrics,
query.aggregations,
query.aggregateAttribute?.key,
query.aggregateAttribute?.type,
onChange,
]);
const resolveMetricFromText = useCallback(
(text: string): BaseAutocompleteData => {

View File

@@ -1,50 +0,0 @@
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import {
getMetricsTreeMap,
MetricsTreeMapPayload,
MetricsTreeMapResponse,
} from 'api/metricsExplorer/getMetricsTreeMap';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseGetMetricsTreeMap = (
requestData: MetricsTreeMapPayload,
options?: UseQueryOptions<
SuccessResponse<MetricsTreeMapResponse> | ErrorResponse,
Error
>,
headers?: Record<string, string>,
) => UseQueryResult<
SuccessResponse<MetricsTreeMapResponse> | ErrorResponse,
Error
>;
export const useGetMetricsTreeMap: UseGetMetricsTreeMap = (
requestData,
options,
headers,
) => {
const queryKey = useMemo(() => {
if (options?.queryKey && Array.isArray(options.queryKey)) {
return [...options.queryKey];
}
if (options?.queryKey && typeof options.queryKey === 'string') {
return options.queryKey;
}
return [REACT_QUERY_KEY.GET_METRICS_TREE_MAP, requestData];
}, [options?.queryKey, requestData]);
return useQuery<
SuccessResponse<MetricsTreeMapResponse> | ErrorResponse,
Error
>({
queryFn: ({ signal }) => getMetricsTreeMap(requestData, signal, headers),
...options,
queryKey,
});
};

View File

@@ -1,26 +0,0 @@
import { useMutation, UseMutationResult } from 'react-query';
import updateMetricMetadata, {
UpdateMetricMetadataProps,
UpdateMetricMetadataResponse,
} from 'api/metricsExplorer/updateMetricMetadata';
import { ErrorResponse, SuccessResponse } from 'types/api';
export interface UseUpdateMetricMetadataProps {
metricName: string;
payload: UpdateMetricMetadataProps;
}
export function useUpdateMetricMetadata(): UseMutationResult<
SuccessResponse<UpdateMetricMetadataResponse> | ErrorResponse,
Error,
UseUpdateMetricMetadataProps
> {
return useMutation<
SuccessResponse<UpdateMetricMetadataResponse> | ErrorResponse,
Error,
UseUpdateMetricMetadataProps
>({
mutationFn: ({ metricName, payload }) =>
updateMetricMetadata(metricName, payload),
});
}

View File

@@ -4,6 +4,7 @@ import cx from 'classnames';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useTimezone } from 'providers/Timezone';
import { TooltipProps } from '../types';
@@ -22,6 +23,14 @@ export default function Tooltip({
const isDarkMode = useIsDarkMode();
const [listHeight, setListHeight] = useState(0);
const tooltipContent = content ?? [];
const { timezone: userTimezone } = useTimezone();
const resolvedTimezone = useMemo(() => {
if (!timezone) {
return userTimezone.value;
}
return timezone.value;
}, [timezone, userTimezone]);
const headerTitle = useMemo(() => {
if (!showTooltipHeader) {
@@ -33,10 +42,10 @@ export default function Tooltip({
return null;
}
return dayjs(data[0][cursorIdx] * 1000)
.tz(timezone)
.tz(resolvedTimezone)
.format(DATE_TIME_FORMATS.MONTH_DATETIME_SECONDS);
}, [
timezone,
resolvedTimezone,
uPlotInstance.data,
uPlotInstance.cursor.idx,
showTooltipHeader,

View File

@@ -83,7 +83,7 @@ function createUPlotInstance(cursorIdx: number | null): uPlot {
function renderTooltip(props: Partial<TooltipTestProps> = {}): RenderResult {
const defaultProps: TooltipTestProps = {
uPlotInstance: createUPlotInstance(null),
timezone: 'UTC',
timezone: { value: 'UTC', name: 'UTC', offset: '0', searchIndex: '0' },
content: [],
showTooltipHeader: true,
// TooltipRenderArgs (not used directly in component but required by type)

View File

@@ -92,7 +92,7 @@ export default function UPlotChart({
setPlotContextInitialState({
uPlotInstance: plot,
widgetId: config.getWidgetId(),
id: config.getId(),
shouldSaveSelectionPreference: config.getShouldSaveSelectionPreference(),
});

View File

@@ -84,7 +84,7 @@ const createMockConfig = (): UPlotConfigBuilder => {
hooks: {},
cursor: {},
}),
getWidgetId: jest.fn().mockReturnValue(undefined),
getId: jest.fn().mockReturnValue(undefined),
getShouldSaveSelectionPreference: jest.fn().mockReturnValue(false),
} as unknown) as UPlotConfigBuilder;
};

View File

@@ -1,4 +1,5 @@
import { ReactNode } from 'react';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PrecisionOption } from 'components/Graph/types';
import uPlot from 'uplot';
@@ -61,7 +62,7 @@ export interface TooltipRenderArgs {
export interface BaseTooltipProps {
showTooltipHeader?: boolean;
timezone: string;
timezone?: Timezone;
yAxisUnit?: string;
decimalPrecision?: PrecisionOption;
content?: TooltipContentItem[];

View File

@@ -74,23 +74,21 @@ export class UPlotConfigBuilder extends ConfigBuilder<
private tzDate: ((timestamp: number) => Date) | undefined;
private widgetId: string | undefined;
private id: string;
private onDragSelect: (startTime: number, endTime: number) => void;
constructor(args?: ConfigBuilderProps) {
super(args ?? {});
constructor(args: ConfigBuilderProps) {
super(args);
const {
widgetId,
id,
onDragSelect,
tzDate,
selectionPreferencesSource,
shouldSaveSelectionPreference,
stepInterval,
} = args ?? {};
if (widgetId) {
this.widgetId = widgetId;
}
this.id = id;
if (tzDate) {
this.tzDate = tzDate;
@@ -252,10 +250,10 @@ export class UPlotConfigBuilder extends ConfigBuilder<
*/
private getStoredVisibility(): SeriesVisibilityItem[] | null {
if (
this.widgetId &&
this.id &&
this.selectionPreferencesSource === SelectionPreferencesSource.LOCAL_STORAGE
) {
return getStoredSeriesVisibility(this.widgetId);
return getStoredSeriesVisibility(this.id);
}
return null;
}
@@ -378,10 +376,10 @@ export class UPlotConfigBuilder extends ConfigBuilder<
}
/**
* Get the widget id
* Get the id for the builder
*/
getWidgetId(): string | undefined {
return this.widgetId;
getId(): string {
return this.id;
}
/**

View File

@@ -1,4 +1,3 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { themeColors } from 'constants/theme';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { calculateWidthBasedOnStepInterval } from 'lib/uPlotV2/utils';
@@ -23,6 +22,9 @@ import {
* Path builders are static and shared across all instances of UPlotSeriesBuilder
*/
let builders: PathBuilders | null = null;
const DEFAULT_LINE_WIDTH = 2;
export const POINT_SIZE_FACTOR = 2.5;
export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
constructor(props: SeriesProps) {
super(props);
@@ -53,7 +55,7 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
const { lineWidth, lineStyle, lineCap, fillColor } = this.props;
const lineConfig: Partial<Series> = {
stroke: resolvedLineColor,
width: lineWidth ?? 2,
width: lineWidth ?? DEFAULT_LINE_WIDTH,
};
if (lineStyle === LineStyle.Dashed) {
@@ -66,9 +68,9 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
if (fillColor) {
lineConfig.fill = fillColor;
} else if (this.props.panelType === PANEL_TYPES.BAR) {
} else if (this.props.drawStyle === DrawStyle.Bar) {
lineConfig.fill = resolvedLineColor;
} else if (this.props.panelType === PANEL_TYPES.HISTOGRAM) {
} else if (this.props.drawStyle === DrawStyle.Histogram) {
lineConfig.fill = `${resolvedLineColor}40`;
}
@@ -137,10 +139,19 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
drawStyle,
showPoints,
} = this.props;
/**
* If pointSize is not provided, use the lineWidth * POINT_SIZE_FACTOR
* to determine the point size.
* POINT_SIZE_FACTOR is 2, so the point size will be 2x the line width.
*/
const resolvedPointSize =
pointSize ?? (lineWidth ?? DEFAULT_LINE_WIDTH) * POINT_SIZE_FACTOR;
const pointsConfig: Partial<Series.Points> = {
stroke: resolvedLineColor,
fill: resolvedLineColor,
size: !pointSize || pointSize < (lineWidth ?? 2) ? undefined : pointSize,
size: resolvedPointSize,
filter: pointsFilter || undefined,
};
@@ -231,7 +242,7 @@ function getPathBuilder({
throw new Error('Required uPlot path builders are not available');
}
if (drawStyle === DrawStyle.Bar) {
if (drawStyle === DrawStyle.Bar || drawStyle === DrawStyle.Histogram) {
const pathBuilders = uPlot.paths;
return getBarPathBuilder({
pathBuilders,

View File

@@ -1,4 +1,3 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import uPlot from 'uplot';
import {
@@ -43,27 +42,27 @@ describe('UPlotConfigBuilder', () => {
label: 'Requests',
colorMapping: {},
drawStyle: DrawStyle.Line,
panelType: PANEL_TYPES.TIME_SERIES,
...overrides,
});
it('returns correct save selection preference flag from constructor args', () => {
const builder = new UPlotConfigBuilder({
id: 'widget-123',
shouldSaveSelectionPreference: true,
});
expect(builder.getShouldSaveSelectionPreference()).toBe(true);
});
it('returns widgetId from constructor args', () => {
const builder = new UPlotConfigBuilder({ widgetId: 'widget-123' });
it('returns id from constructor args', () => {
const builder = new UPlotConfigBuilder({ id: 'widget-123' });
expect(builder.getWidgetId()).toBe('widget-123');
expect(builder.getId()).toBe('widget-123');
});
it('sets tzDate from constructor and includes it in config', () => {
const tzDate = (ts: number): Date => new Date(ts);
const builder = new UPlotConfigBuilder({ tzDate });
const builder = new UPlotConfigBuilder({ id: 'widget-123', tzDate });
const config = builder.getConfig();
@@ -72,7 +71,7 @@ describe('UPlotConfigBuilder', () => {
it('does not call onDragSelect for click without drag (width === 0)', () => {
const onDragSelect = jest.fn();
const builder = new UPlotConfigBuilder({ onDragSelect });
const builder = new UPlotConfigBuilder({ id: 'widget-123', onDragSelect });
const config = builder.getConfig();
const setSelectHooks = config.hooks?.setSelect ?? [];
@@ -85,14 +84,15 @@ describe('UPlotConfigBuilder', () => {
// Simulate uPlot calling the hook
const setSelectHook = setSelectHooks[0];
setSelectHook!(uplotInstance);
expect(setSelectHook).toBeDefined();
setSelectHook?.(uplotInstance);
expect(onDragSelect).not.toHaveBeenCalled();
});
it('calls onDragSelect with start and end times in milliseconds for a drag selection', () => {
const onDragSelect = jest.fn();
const builder = new UPlotConfigBuilder({ onDragSelect });
const builder = new UPlotConfigBuilder({ id: 'widget-123', onDragSelect });
const config = builder.getConfig();
const setSelectHooks = config.hooks?.setSelect ?? [];
@@ -111,7 +111,8 @@ describe('UPlotConfigBuilder', () => {
} as unknown) as uPlot;
const setSelectHook = setSelectHooks[0];
setSelectHook!(uplotInstance);
expect(setSelectHook).toBeDefined();
setSelectHook?.(uplotInstance);
expect(onDragSelect).toHaveBeenCalledTimes(1);
// 100 and 110 seconds converted to milliseconds
@@ -119,7 +120,7 @@ describe('UPlotConfigBuilder', () => {
});
it('adds and removes hooks via addHook, and exposes them through getConfig', () => {
const builder = new UPlotConfigBuilder();
const builder = new UPlotConfigBuilder({ id: 'widget-123' });
const drawHook = jest.fn();
const remove = builder.addHook('draw', drawHook as uPlot.Hooks.Defs['draw']);
@@ -134,7 +135,7 @@ describe('UPlotConfigBuilder', () => {
});
it('adds axes, scales, and series and wires them into the final config', () => {
const builder = new UPlotConfigBuilder();
const builder = new UPlotConfigBuilder({ id: 'widget-123' });
// Add axis and scale
builder.addAxis({ scaleKey: 'y', label: 'Requests' });
@@ -170,7 +171,7 @@ describe('UPlotConfigBuilder', () => {
});
it('merges axis when addAxis is called twice with same scaleKey', () => {
const builder = new UPlotConfigBuilder();
const builder = new UPlotConfigBuilder({ id: 'widget-123' });
builder.addAxis({ scaleKey: 'y', label: 'Requests' });
builder.addAxis({ scaleKey: 'y', label: 'Updated Label', show: false });
@@ -183,7 +184,7 @@ describe('UPlotConfigBuilder', () => {
});
it('merges scale when addScale is called twice with same scaleKey', () => {
const builder = new UPlotConfigBuilder();
const builder = new UPlotConfigBuilder({ id: 'widget-123' });
builder.addScale({ scaleKey: 'y', min: 0 });
builder.addScale({ scaleKey: 'y', max: 100 });
@@ -204,7 +205,7 @@ describe('UPlotConfigBuilder', () => {
]);
const builder = new UPlotConfigBuilder({
widgetId: 'widget-1',
id: 'widget-1',
selectionPreferencesSource: SelectionPreferencesSource.LOCAL_STORAGE,
});
@@ -231,7 +232,7 @@ describe('UPlotConfigBuilder', () => {
]);
const builder = new UPlotConfigBuilder({
widgetId: 'widget-1',
id: 'widget-1',
selectionPreferencesSource: SelectionPreferencesSource.LOCAL_STORAGE,
});
@@ -269,7 +270,7 @@ describe('UPlotConfigBuilder', () => {
]);
const builder = new UPlotConfigBuilder({
widgetId: 'widget-1',
id: 'widget-1',
selectionPreferencesSource: SelectionPreferencesSource.LOCAL_STORAGE,
});
@@ -302,7 +303,7 @@ describe('UPlotConfigBuilder', () => {
]);
const builder = new UPlotConfigBuilder({
widgetId: 'widget-dup',
id: 'widget-dup',
selectionPreferencesSource: SelectionPreferencesSource.LOCAL_STORAGE,
});
@@ -329,7 +330,7 @@ describe('UPlotConfigBuilder', () => {
it('does not attempt to read stored visibility when using in-memory preferences', () => {
const builder = new UPlotConfigBuilder({
widgetId: 'widget-1',
id: 'widget-1',
selectionPreferencesSource: SelectionPreferencesSource.IN_MEMORY,
});
@@ -344,7 +345,7 @@ describe('UPlotConfigBuilder', () => {
});
it('adds thresholds only once per scale key', () => {
const builder = new UPlotConfigBuilder();
const builder = new UPlotConfigBuilder({ id: 'widget-123' });
const thresholdsOptions = {
scaleKey: 'y',
@@ -362,7 +363,7 @@ describe('UPlotConfigBuilder', () => {
});
it('adds multiple thresholds when scale key is different', () => {
const builder = new UPlotConfigBuilder();
const builder = new UPlotConfigBuilder({ id: 'widget-123' });
const thresholdsOptions = {
scaleKey: 'y',
@@ -383,7 +384,7 @@ describe('UPlotConfigBuilder', () => {
});
it('merges cursor configuration with defaults instead of replacing them', () => {
const builder = new UPlotConfigBuilder();
const builder = new UPlotConfigBuilder({ id: 'widget-123' });
builder.setCursor({
drag: { setScale: false },
@@ -398,7 +399,7 @@ describe('UPlotConfigBuilder', () => {
describe('getCursorConfig', () => {
it('returns default cursor merged with custom cursor when no stepInterval', () => {
const builder = new UPlotConfigBuilder();
const builder = new UPlotConfigBuilder({ id: 'widget-123' });
builder.setCursor({
drag: { setScale: false },
@@ -412,7 +413,7 @@ describe('UPlotConfigBuilder', () => {
});
it('returns hover prox as DEFAULT_HOVER_PROXIMITY_VALUE when stepInterval is not set', () => {
const builder = new UPlotConfigBuilder();
const builder = new UPlotConfigBuilder({ id: 'widget-123' });
const cursorConfig = builder.getCursorConfig();
@@ -424,15 +425,15 @@ describe('UPlotConfigBuilder', () => {
const mockWidth = 100;
calculateWidthBasedOnStepIntervalMock.mockReturnValue(mockWidth);
const builder = new UPlotConfigBuilder({ stepInterval });
const builder = new UPlotConfigBuilder({ id: 'widget-123', stepInterval });
const cursorConfig = builder.getCursorConfig();
expect(typeof cursorConfig.hover?.prox).toBe('function');
const uPlotInstance = {} as uPlot;
const proxResult = (cursorConfig.hover!.prox as (u: uPlot) => number)(
uPlotInstance,
);
const prox = cursorConfig.hover?.prox as ((u: uPlot) => number) | undefined;
expect(prox).toBeDefined();
const proxResult = prox ? prox(uPlotInstance) : NaN;
expect(calculateWidthBasedOnStepIntervalMock).toHaveBeenCalledWith({
uPlotInstance,
@@ -443,7 +444,7 @@ describe('UPlotConfigBuilder', () => {
});
it('adds plugins and includes them in config', () => {
const builder = new UPlotConfigBuilder();
const builder = new UPlotConfigBuilder({ id: 'widget-123' });
const plugin: uPlot.Plugin = {
opts: (): void => {},
hooks: {},
@@ -458,7 +459,7 @@ describe('UPlotConfigBuilder', () => {
it('sets padding, legend, focus, select, tzDate, bands and includes them in config', () => {
const tzDate = (ts: number): Date => new Date(ts);
const builder = new UPlotConfigBuilder();
const builder = new UPlotConfigBuilder({ id: 'widget-123' });
const bands: uPlot.Band[] = [{ series: [1, 2], fill: (): string => '#000' }];
@@ -480,7 +481,7 @@ describe('UPlotConfigBuilder', () => {
});
it('does not include plugins when none added', () => {
const builder = new UPlotConfigBuilder();
const builder = new UPlotConfigBuilder({ id: 'widget-123' });
const config = builder.getConfig();
@@ -488,7 +489,7 @@ describe('UPlotConfigBuilder', () => {
});
it('does not include bands when empty', () => {
const builder = new UPlotConfigBuilder();
const builder = new UPlotConfigBuilder({ id: 'widget-123' });
const config = builder.getConfig();

View File

@@ -1,4 +1,3 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { themeColors } from 'constants/theme';
import uPlot from 'uplot';
@@ -9,7 +8,7 @@ import {
LineStyle,
VisibilityMode,
} from '../types';
import { UPlotSeriesBuilder } from '../UPlotSeriesBuilder';
import { POINT_SIZE_FACTOR, UPlotSeriesBuilder } from '../UPlotSeriesBuilder';
const createBaseProps = (
overrides: Partial<SeriesProps> = {},
@@ -19,7 +18,6 @@ const createBaseProps = (
colorMapping: {},
drawStyle: DrawStyle.Line,
isDarkMode: false,
panelType: PANEL_TYPES.TIME_SERIES,
...overrides,
});
@@ -137,7 +135,6 @@ describe('UPlotSeriesBuilder', () => {
const smallPointsBuilder = new UPlotSeriesBuilder(
createBaseProps({
lineWidth: 4,
pointSize: 2,
}),
);
const largePointsBuilder = new UPlotSeriesBuilder(
@@ -150,7 +147,7 @@ describe('UPlotSeriesBuilder', () => {
const smallConfig = smallPointsBuilder.getConfig();
const largeConfig = largePointsBuilder.getConfig();
expect(smallConfig.points?.size).toBeUndefined();
expect(smallConfig.points?.size).toBe(4 * POINT_SIZE_FACTOR); // should be lineWidth * POINT_SIZE_FACTOR, when pointSize is not provided
expect(largeConfig.points?.size).toBe(4);
});

View File

@@ -34,7 +34,7 @@ export enum SelectionPreferencesSource {
* Props for configuring the uPlot config builder
*/
export interface ConfigBuilderProps {
widgetId?: string;
id: string;
onDragSelect?: (startTime: number, endTime: number) => void;
tzDate?: uPlot.LocalDateFromUnix;
selectionPreferencesSource?: SelectionPreferencesSource;
@@ -112,6 +112,7 @@ export enum DrawStyle {
Line = 'line',
Points = 'points',
Bar = 'bar',
Histogram = 'histogram',
}
export enum LineInterpolation {
@@ -168,7 +169,6 @@ export interface PointsConfig {
export interface SeriesProps extends LineConfig, PointsConfig, BarConfig {
scaleKey: string;
label?: string;
panelType: PANEL_TYPES;
colorMapping: Record<string, string>;
drawStyle: DrawStyle;
pathBuilder?: Series.PathBuilder;

View File

@@ -13,7 +13,7 @@ import { updateSeriesVisibilityToLocalStorage } from 'container/DashboardContain
import type uPlot from 'uplot';
export interface PlotContextInitialState {
uPlotInstance: uPlot | null;
widgetId?: string;
id?: string;
shouldSaveSelectionPreference?: boolean;
}
export interface IPlotContext {
@@ -31,17 +31,17 @@ export const PlotContextProvider = ({
}: PropsWithChildren): JSX.Element => {
const uPlotInstanceRef = useRef<uPlot | null>(null);
const activeSeriesIndex = useRef<number | undefined>(undefined);
const widgetIdRef = useRef<string | undefined>(undefined);
const idRef = useRef<string | undefined>(undefined);
const shouldSavePreferencesRef = useRef<boolean>(false);
const setPlotContextInitialState = useCallback(
({
uPlotInstance,
widgetId,
id,
shouldSaveSelectionPreference,
}: PlotContextInitialState): void => {
uPlotInstanceRef.current = uPlotInstance;
widgetIdRef.current = widgetId;
idRef.current = id;
activeSeriesIndex.current = undefined;
shouldSavePreferencesRef.current = !!shouldSaveSelectionPreference;
},
@@ -50,7 +50,7 @@ export const PlotContextProvider = ({
const syncSeriesVisibilityToLocalStorage = useCallback((): void => {
const plot = uPlotInstanceRef.current;
if (!plot || !widgetIdRef.current) {
if (!plot || !idRef.current) {
return;
}
@@ -61,7 +61,7 @@ export const PlotContextProvider = ({
}),
);
updateSeriesVisibilityToLocalStorage(widgetIdRef.current, seriesVisibility);
updateSeriesVisibilityToLocalStorage(idRef.current, seriesVisibility);
}, []);
const onToggleSeriesVisibility = useCallback(
@@ -84,7 +84,7 @@ export const PlotContextProvider = ({
show: isReset || currentSeriesIndex === seriesIndex,
});
});
if (widgetIdRef.current && shouldSavePreferencesRef.current) {
if (idRef.current && shouldSavePreferencesRef.current) {
syncSeriesVisibilityToLocalStorage();
}
});
@@ -104,7 +104,7 @@ export const PlotContextProvider = ({
return;
}
plot.setSeries(seriesIndex, { show: !series.show });
if (widgetIdRef.current && shouldSavePreferencesRef.current) {
if (idRef.current && shouldSavePreferencesRef.current) {
syncSeriesVisibilityToLocalStorage();
}
},

View File

@@ -32,13 +32,13 @@ const createMockPlot = (series: MockSeries[] = []): uPlot =>
interface TestComponentProps {
plot?: uPlot;
widgetId?: string;
id?: string;
shouldSaveSelectionPreference?: boolean;
}
const TestComponent = ({
plot,
widgetId,
id,
shouldSaveSelectionPreference,
}: TestComponentProps): JSX.Element => {
const {
@@ -49,17 +49,13 @@ const TestComponent = ({
onFocusSeries,
} = usePlotContext();
const handleInit = (): void => {
if (
!plot ||
!widgetId ||
typeof shouldSaveSelectionPreference !== 'boolean'
) {
if (!plot || !id || typeof shouldSaveSelectionPreference !== 'boolean') {
return;
}
setPlotContextInitialState({
uPlotInstance: plot,
widgetId,
id,
shouldSaveSelectionPreference,
});
};
@@ -148,11 +144,7 @@ describe('PlotContext', () => {
render(
<PlotContextProvider>
<TestComponent
plot={plot}
widgetId="widget-123"
shouldSaveSelectionPreference
/>
<TestComponent plot={plot} id="widget-123" shouldSaveSelectionPreference />
</PlotContextProvider>,
);
@@ -199,7 +191,7 @@ describe('PlotContext', () => {
<PlotContextProvider>
<TestComponent
plot={plot}
widgetId="widget-visibility"
id="widget-visibility"
shouldSaveSelectionPreference
/>
</PlotContextProvider>,
@@ -240,7 +232,7 @@ describe('PlotContext', () => {
<PlotContextProvider>
<TestComponent
plot={plot}
widgetId="widget-reset"
id="widget-reset"
shouldSaveSelectionPreference
/>
</PlotContextProvider>,
@@ -290,7 +282,7 @@ describe('PlotContext', () => {
<PlotContextProvider>
<TestComponent
plot={plot}
widgetId="widget-toggle"
id="widget-toggle"
shouldSaveSelectionPreference
/>
</PlotContextProvider>,
@@ -316,7 +308,7 @@ describe('PlotContext', () => {
<PlotContextProvider>
<TestComponent
plot={plot}
widgetId="widget-missing-series"
id="widget-missing-series"
shouldSaveSelectionPreference
/>
</PlotContextProvider>,
@@ -341,7 +333,7 @@ describe('PlotContext', () => {
<PlotContextProvider>
<TestComponent
plot={plot}
widgetId="widget-no-persist"
id="widget-no-persist"
shouldSaveSelectionPreference={false}
/>
</PlotContextProvider>,
@@ -379,7 +371,7 @@ describe('PlotContext', () => {
<PlotContextProvider>
<TestComponent
plot={plot}
widgetId="widget-focus"
id="widget-focus"
shouldSaveSelectionPreference={false}
/>
</PlotContextProvider>,

View File

@@ -40,7 +40,7 @@ class TestConfigBuilder extends UPlotConfigBuilder {
type ConfigMock = TestConfigBuilder;
function createConfigMock(): ConfigMock {
return new TestConfigBuilder();
return new TestConfigBuilder({ id: 'test-widget' });
}
function getHandler(config: ConfigMock, hookName: string): HookHandler {

View File

@@ -202,6 +202,14 @@ func (p *ClustersRepo) getTopClusterGroups(ctx context.Context, orgID valuer.UUI
return topClusterGroups, allClusterGroups, nil
}
func (p *ClustersRepo) GetMetricsExistenceAndEarliestTime(ctx context.Context) (uint64, uint64, error) {
names := []string{}
for _, metricName := range metricNamesForNodes {
names = append(names, metricName)
}
return p.reader.GetMetricsExistenceAndEarliestTime(ctx, names)
}
func (p *ClustersRepo) GetClusterList(ctx context.Context, orgID valuer.UUID, req model.ClusterListRequest) (model.ClusterListResponse, error) {
resp := model.ClusterListResponse{}
@@ -220,6 +228,22 @@ func (p *ClustersRepo) GetClusterList(ctx context.Context, orgID valuer.UUID, re
resp.Type = model.ResponseTypeGroupedList
}
if count, minFirstReportedUnixMilli, err := p.GetMetricsExistenceAndEarliestTime(ctx); err == nil {
if count == 0 {
resp.SentAnyMetricsData = false
resp.Records = []model.ClusterListRecord{}
resp.Total = 0
return resp, nil
}
resp.SentAnyMetricsData = true
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []model.ClusterListRecord{}
resp.Total = 0
return resp, nil
}
}
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
query := NodesTableListQuery.Clone()

View File

@@ -269,6 +269,17 @@ func (d *DaemonSetsRepo) getTopDaemonSetGroups(ctx context.Context, orgID valuer
return topDaemonSetGroups, allDaemonSetGroups, nil
}
func (d *DaemonSetsRepo) GetMetricsExistenceAndEarliestTime(ctx context.Context) (uint64, uint64, error) {
names := []string{}
for _, metricName := range metricNamesForWorkloads {
names = append(names, metricName)
}
for _, metricName := range metricNamesForDaemonSets {
names = append(names, metricName)
}
return d.reader.GetMetricsExistenceAndEarliestTime(ctx, names)
}
func (d *DaemonSetsRepo) GetDaemonSetList(ctx context.Context, orgID valuer.UUID, req model.DaemonSetListRequest) (model.DaemonSetListResponse, error) {
resp := model.DaemonSetListResponse{}
@@ -287,6 +298,22 @@ func (d *DaemonSetsRepo) GetDaemonSetList(ctx context.Context, orgID valuer.UUID
resp.Type = model.ResponseTypeGroupedList
}
if count, minFirstReportedUnixMilli, err := d.GetMetricsExistenceAndEarliestTime(ctx); err == nil {
if count == 0 {
resp.SentAnyMetricsData = false
resp.Records = []model.DaemonSetListRecord{}
resp.Total = 0
return resp, nil
}
resp.SentAnyMetricsData = true
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []model.DaemonSetListRecord{}
resp.Total = 0
return resp, nil
}
}
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
query := WorkloadTableListQuery.Clone()

View File

@@ -269,6 +269,17 @@ func (d *DeploymentsRepo) getTopDeploymentGroups(ctx context.Context, orgID valu
return topDeploymentGroups, allDeploymentGroups, nil
}
func (d *DeploymentsRepo) GetMetricsExistenceAndEarliestTime(ctx context.Context) (uint64, uint64, error) {
names := []string{}
for _, metricName := range metricNamesForWorkloads {
names = append(names, metricName)
}
for _, metricName := range metricNamesForDeployments {
names = append(names, metricName)
}
return d.reader.GetMetricsExistenceAndEarliestTime(ctx, names)
}
func (d *DeploymentsRepo) GetDeploymentList(ctx context.Context, orgID valuer.UUID, req model.DeploymentListRequest) (model.DeploymentListResponse, error) {
resp := model.DeploymentListResponse{}
@@ -287,6 +298,22 @@ func (d *DeploymentsRepo) GetDeploymentList(ctx context.Context, orgID valuer.UU
resp.Type = model.ResponseTypeGroupedList
}
if count, minFirstReportedUnixMilli, err := d.GetMetricsExistenceAndEarliestTime(ctx); err == nil {
if count == 0 {
resp.SentAnyMetricsData = false
resp.Records = []model.DeploymentListRecord{}
resp.Total = 0
return resp, nil
}
resp.SentAnyMetricsData = true
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []model.DeploymentListRecord{}
resp.Total = 0
return resp, nil
}
}
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
query := WorkloadTableListQuery.Clone()

View File

@@ -313,6 +313,17 @@ func (d *JobsRepo) getTopJobGroups(ctx context.Context, orgID valuer.UUID, req m
return topJobGroups, allJobGroups, nil
}
func (d *JobsRepo) GetMetricsExistenceAndEarliestTime(ctx context.Context) (uint64, uint64, error) {
names := []string{}
for _, metricName := range metricNamesForWorkloads {
names = append(names, metricName)
}
for _, metricName := range metricNamesForJobs {
names = append(names, metricName)
}
return d.reader.GetMetricsExistenceAndEarliestTime(ctx, names)
}
func (d *JobsRepo) GetJobList(ctx context.Context, orgID valuer.UUID, req model.JobListRequest) (model.JobListResponse, error) {
resp := model.JobListResponse{}
@@ -331,6 +342,22 @@ func (d *JobsRepo) GetJobList(ctx context.Context, orgID valuer.UUID, req model.
resp.Type = model.ResponseTypeGroupedList
}
if count, minFirstReportedUnixMilli, err := d.GetMetricsExistenceAndEarliestTime(ctx); err == nil {
if count == 0 {
resp.SentAnyMetricsData = false
resp.Records = []model.JobListRecord{}
resp.Total = 0
return resp, nil
}
resp.SentAnyMetricsData = true
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []model.JobListRecord{}
resp.Total = 0
return resp, nil
}
}
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
query := WorkloadTableListQuery.Clone()

View File

@@ -196,6 +196,14 @@ func (p *NamespacesRepo) getTopNamespaceGroups(ctx context.Context, orgID valuer
return topNamespaceGroups, allNamespaceGroups, nil
}
func (p *NamespacesRepo) GetMetricsExistenceAndEarliestTime(ctx context.Context) (uint64, uint64, error) {
names := []string{}
for _, metricName := range metricNamesForPods {
names = append(names, metricName)
}
return p.reader.GetMetricsExistenceAndEarliestTime(ctx, names)
}
func (p *NamespacesRepo) GetNamespaceList(ctx context.Context, orgID valuer.UUID, req model.NamespaceListRequest) (model.NamespaceListResponse, error) {
resp := model.NamespaceListResponse{}
@@ -214,6 +222,22 @@ func (p *NamespacesRepo) GetNamespaceList(ctx context.Context, orgID valuer.UUID
resp.Type = model.ResponseTypeGroupedList
}
if count, minFirstReportedUnixMilli, err := p.GetMetricsExistenceAndEarliestTime(ctx); err == nil {
if count == 0 {
resp.SentAnyMetricsData = false
resp.Records = []model.NamespaceListRecord{}
resp.Total = 0
return resp, nil
}
resp.SentAnyMetricsData = true
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []model.NamespaceListRecord{}
resp.Total = 0
return resp, nil
}
}
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
query := PodsTableListQuery.Clone()

View File

@@ -226,6 +226,14 @@ func (p *NodesRepo) getTopNodeGroups(ctx context.Context, orgID valuer.UUID, req
return topNodeGroups, allNodeGroups, nil
}
func (p *NodesRepo) GetMetricsExistenceAndEarliestTime(ctx context.Context) (uint64, uint64, error) {
names := []string{}
for _, metricName := range metricNamesForNodes {
names = append(names, metricName)
}
return p.reader.GetMetricsExistenceAndEarliestTime(ctx, names)
}
func (p *NodesRepo) GetNodeList(ctx context.Context, orgID valuer.UUID, req model.NodeListRequest) (model.NodeListResponse, error) {
resp := model.NodeListResponse{}
@@ -244,6 +252,22 @@ func (p *NodesRepo) GetNodeList(ctx context.Context, orgID valuer.UUID, req mode
resp.Type = model.ResponseTypeGroupedList
}
if count, minFirstReportedUnixMilli, err := p.GetMetricsExistenceAndEarliestTime(ctx); err == nil {
if count == 0 {
resp.SentAnyMetricsData = false
resp.Records = []model.NodeListRecord{}
resp.Total = 0
return resp, nil
}
resp.SentAnyMetricsData = true
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []model.NodeListRecord{}
resp.Total = 0
return resp, nil
}
}
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
query := NodesTableListQuery.Clone()

View File

@@ -371,6 +371,14 @@ func (p *PodsRepo) getTopPodGroups(ctx context.Context, orgID valuer.UUID, req m
return topPodGroups, allPodGroups, nil
}
func (p *PodsRepo) GetMetricsExistenceAndEarliestTime(ctx context.Context) (uint64, uint64, error) {
names := []string{}
for _, metricName := range metricNamesForPods {
names = append(names, metricName)
}
return p.reader.GetMetricsExistenceAndEarliestTime(ctx, names)
}
func (p *PodsRepo) GetPodList(ctx context.Context, orgID valuer.UUID, req model.PodListRequest) (model.PodListResponse, error) {
resp := model.PodListResponse{}
@@ -389,6 +397,22 @@ func (p *PodsRepo) GetPodList(ctx context.Context, orgID valuer.UUID, req model.
resp.Type = model.ResponseTypeGroupedList
}
if count, minFirstReportedUnixMilli, err := p.GetMetricsExistenceAndEarliestTime(ctx); err == nil {
if count == 0 {
resp.SentAnyMetricsData = false
resp.Records = []model.PodListRecord{}
resp.Total = 0
return resp, nil
}
resp.SentAnyMetricsData = true
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []model.PodListRecord{}
resp.Total = 0
return resp, nil
}
}
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
query := PodsTableListQuery.Clone()

View File

@@ -210,6 +210,14 @@ func (p *ProcessesRepo) getTopProcessGroups(ctx context.Context, orgID valuer.UU
return topProcessGroups, allProcessGroups, nil
}
func (p *ProcessesRepo) GetMetricsExistenceAndEarliestTime(ctx context.Context) (uint64, uint64, error) {
names := []string{}
for _, metricName := range metricNamesForProcesses {
names = append(names, metricName)
}
return p.reader.GetMetricsExistenceAndEarliestTime(ctx, names)
}
func (p *ProcessesRepo) GetProcessList(ctx context.Context, orgID valuer.UUID, req model.ProcessListRequest) (model.ProcessListResponse, error) {
resp := model.ProcessListResponse{}
if req.Limit == 0 {
@@ -229,6 +237,22 @@ func (p *ProcessesRepo) GetProcessList(ctx context.Context, orgID valuer.UUID, r
resp.Type = model.ResponseTypeGroupedList
}
if count, minFirstReportedUnixMilli, err := p.GetMetricsExistenceAndEarliestTime(ctx); err == nil {
if count == 0 {
resp.SentAnyMetricsData = false
resp.Records = []model.ProcessListRecord{}
resp.Total = 0
return resp, nil
}
resp.SentAnyMetricsData = true
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []model.ProcessListRecord{}
resp.Total = 0
return resp, nil
}
}
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
query := ProcessesTableListQuery.Clone()

View File

@@ -229,6 +229,14 @@ func (p *PvcsRepo) getTopVolumeGroups(ctx context.Context, orgID valuer.UUID, re
return topVolumeGroups, allVolumeGroups, nil
}
func (p *PvcsRepo) GetMetricsExistenceAndEarliestTime(ctx context.Context) (uint64, uint64, error) {
names := []string{}
for _, metricName := range metricNamesForVolumes {
names = append(names, metricName)
}
return p.reader.GetMetricsExistenceAndEarliestTime(ctx, names)
}
func (p *PvcsRepo) GetPvcList(ctx context.Context, orgID valuer.UUID, req model.VolumeListRequest) (model.VolumeListResponse, error) {
resp := model.VolumeListResponse{}
@@ -247,6 +255,22 @@ func (p *PvcsRepo) GetPvcList(ctx context.Context, orgID valuer.UUID, req model.
resp.Type = model.ResponseTypeGroupedList
}
if count, minFirstReportedUnixMilli, err := p.GetMetricsExistenceAndEarliestTime(ctx); err == nil {
if count == 0 {
resp.SentAnyMetricsData = false
resp.Records = []model.VolumeListRecord{}
resp.Total = 0
return resp, nil
}
resp.SentAnyMetricsData = true
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []model.VolumeListRecord{}
resp.Total = 0
return resp, nil
}
}
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
query := PvcsTableListQuery.Clone()

View File

@@ -269,6 +269,17 @@ func (d *StatefulSetsRepo) getTopStatefulSetGroups(ctx context.Context, orgID va
return topStatefulSetGroups, allStatefulSetGroups, nil
}
func (d *StatefulSetsRepo) GetMetricsExistenceAndEarliestTime(ctx context.Context) (uint64, uint64, error) {
names := []string{}
for _, metricName := range metricNamesForWorkloads {
names = append(names, metricName)
}
for _, metricName := range metricNamesForStatefulSets {
names = append(names, metricName)
}
return d.reader.GetMetricsExistenceAndEarliestTime(ctx, names)
}
func (d *StatefulSetsRepo) GetStatefulSetList(ctx context.Context, orgID valuer.UUID, req model.StatefulSetListRequest) (model.StatefulSetListResponse, error) {
resp := model.StatefulSetListResponse{}
@@ -287,6 +298,22 @@ func (d *StatefulSetsRepo) GetStatefulSetList(ctx context.Context, orgID valuer.
resp.Type = model.ResponseTypeGroupedList
}
if count, minFirstReportedUnixMilli, err := d.GetMetricsExistenceAndEarliestTime(ctx); err == nil {
if count == 0 {
resp.SentAnyMetricsData = false
resp.Records = []model.StatefulSetListRecord{}
resp.Total = 0
return resp, nil
}
resp.SentAnyMetricsData = true
if req.End < int64(minFirstReportedUnixMilli) {
resp.EndTimeBeforeRetention = true
resp.Records = []model.StatefulSetListRecord{}
resp.Total = 0
return resp, nil
}
}
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
query := WorkloadTableListQuery.Clone()

View File

@@ -86,9 +86,11 @@ type ProcessListRequest struct {
}
type ProcessListResponse struct {
Type ResponseType `json:"type"`
Records []ProcessListRecord `json:"records"`
Total int `json:"total"`
Type ResponseType `json:"type"`
Records []ProcessListRecord `json:"records"`
Total int `json:"total"`
SentAnyMetricsData bool `json:"sentAnyMetricsData"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention"`
}
type ProcessListRecord struct {
@@ -112,9 +114,11 @@ type PodListRequest struct {
}
type PodListResponse struct {
Type ResponseType `json:"type"`
Records []PodListRecord `json:"records"`
Total int `json:"total"`
Type ResponseType `json:"type"`
Records []PodListRecord `json:"records"`
Total int `json:"total"`
SentAnyMetricsData bool `json:"sentAnyMetricsData"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention"`
}
func (r *PodListResponse) SortBy(orderBy *v3.OrderBy) {
@@ -190,9 +194,11 @@ type NodeListRequest struct {
}
type NodeListResponse struct {
Type ResponseType `json:"type"`
Records []NodeListRecord `json:"records"`
Total int `json:"total"`
Type ResponseType `json:"type"`
Records []NodeListRecord `json:"records"`
Total int `json:"total"`
SentAnyMetricsData bool `json:"sentAnyMetricsData"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention"`
}
func (r *NodeListResponse) SortBy(orderBy *v3.OrderBy) {
@@ -251,9 +257,11 @@ type NamespaceListRequest struct {
}
type NamespaceListResponse struct {
Type ResponseType `json:"type"`
Records []NamespaceListRecord `json:"records"`
Total int `json:"total"`
Type ResponseType `json:"type"`
Records []NamespaceListRecord `json:"records"`
Total int `json:"total"`
SentAnyMetricsData bool `json:"sentAnyMetricsData"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention"`
}
func (r *NamespaceListResponse) SortBy(orderBy *v3.OrderBy) {
@@ -300,9 +308,11 @@ type ClusterListRequest struct {
}
type ClusterListResponse struct {
Type ResponseType `json:"type"`
Records []ClusterListRecord `json:"records"`
Total int `json:"total"`
Type ResponseType `json:"type"`
Records []ClusterListRecord `json:"records"`
Total int `json:"total"`
SentAnyMetricsData bool `json:"sentAnyMetricsData"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention"`
}
func (r *ClusterListResponse) SortBy(orderBy *v3.OrderBy) {
@@ -354,9 +364,11 @@ type DeploymentListRequest struct {
}
type DeploymentListResponse struct {
Type ResponseType `json:"type"`
Records []DeploymentListRecord `json:"records"`
Total int `json:"total"`
Type ResponseType `json:"type"`
Records []DeploymentListRecord `json:"records"`
Total int `json:"total"`
SentAnyMetricsData bool `json:"sentAnyMetricsData"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention"`
}
func (r *DeploymentListResponse) SortBy(orderBy *v3.OrderBy) {
@@ -433,9 +445,11 @@ type DaemonSetListRequest struct {
}
type DaemonSetListResponse struct {
Type ResponseType `json:"type"`
Records []DaemonSetListRecord `json:"records"`
Total int `json:"total"`
Type ResponseType `json:"type"`
Records []DaemonSetListRecord `json:"records"`
Total int `json:"total"`
SentAnyMetricsData bool `json:"sentAnyMetricsData"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention"`
}
func (r *DaemonSetListResponse) SortBy(orderBy *v3.OrderBy) {
@@ -512,9 +526,11 @@ type StatefulSetListRequest struct {
}
type StatefulSetListResponse struct {
Type ResponseType `json:"type"`
Records []StatefulSetListRecord `json:"records"`
Total int `json:"total"`
Type ResponseType `json:"type"`
Records []StatefulSetListRecord `json:"records"`
Total int `json:"total"`
SentAnyMetricsData bool `json:"sentAnyMetricsData"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention"`
}
func (r *StatefulSetListResponse) SortBy(orderBy *v3.OrderBy) {
@@ -591,9 +607,11 @@ type JobListRequest struct {
}
type JobListResponse struct {
Type ResponseType `json:"type"`
Records []JobListRecord `json:"records"`
Total int `json:"total"`
Type ResponseType `json:"type"`
Records []JobListRecord `json:"records"`
Total int `json:"total"`
SentAnyMetricsData bool `json:"sentAnyMetricsData"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention"`
}
func (r *JobListResponse) SortBy(orderBy *v3.OrderBy) {
@@ -680,9 +698,11 @@ type VolumeListRequest struct {
}
type VolumeListResponse struct {
Type ResponseType `json:"type"`
Records []VolumeListRecord `json:"records"`
Total int `json:"total"`
Type ResponseType `json:"type"`
Records []VolumeListRecord `json:"records"`
Total int `json:"total"`
SentAnyMetricsData bool `json:"sentAnyMetricsData"`
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention"`
}
func (r *VolumeListResponse) SortBy(orderBy *v3.OrderBy) {