mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-06 20:20:28 +01:00
Compare commits
34 Commits
main
...
feat/migra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfa237b71e | ||
|
|
aaaf17e379 | ||
|
|
09150e8b3c | ||
|
|
2edde14f1c | ||
|
|
3aead98c9c | ||
|
|
7a9063b64f | ||
|
|
2be4e49215 | ||
|
|
1f50a3bd8b | ||
|
|
46a16a77ad | ||
|
|
b4056956be | ||
|
|
8a8cc857f9 | ||
|
|
1ed468af21 | ||
|
|
dfb1dcd86a | ||
|
|
976d2d2f15 | ||
|
|
e8b5aef7c9 | ||
|
|
b0831ca7e2 | ||
|
|
4c12bebe00 | ||
|
|
b81217735e | ||
|
|
6a63fa5659 | ||
|
|
6fa1a4dd0d | ||
|
|
1d78dd9da0 | ||
|
|
f0193088d4 | ||
|
|
0e0febf9bb | ||
|
|
8247fcb4b9 | ||
|
|
d7f55f0950 | ||
|
|
8772b1808d | ||
|
|
db88491045 | ||
|
|
7ba97faf20 | ||
|
|
b5d46780d9 | ||
|
|
86e6e51c81 | ||
|
|
4ff7529856 | ||
|
|
eea9c0c586 | ||
|
|
d8273eab86 | ||
|
|
3a9c5daab8 |
@@ -54,7 +54,7 @@
|
||||
"@signozhq/checkbox": "0.0.2",
|
||||
"@signozhq/combobox": "0.0.2",
|
||||
"@signozhq/command": "0.0.0",
|
||||
"@signozhq/design-tokens": "2.1.1",
|
||||
"@signozhq/design-tokens": "2.1.4",
|
||||
"@signozhq/dialog": "^0.0.2",
|
||||
"@signozhq/drawer": "0.0.4",
|
||||
"@signozhq/icons": "0.1.0",
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
|
||||
import type { RadioChangeEvent } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { K8sPodsData } from 'api/infraMonitoring/getK8sPodsList';
|
||||
import { VIEW_TYPES, VIEWS } from 'components/HostMetricsDetail/constants';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -15,21 +13,13 @@ import {
|
||||
initialQueryState,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import { QUERY_KEYS } from 'container/InfraMonitoringK8s/EntityDetailsUtils/utils';
|
||||
import {
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from 'container/InfraMonitoringK8s/hooks';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import {
|
||||
BarChart2,
|
||||
@@ -39,48 +29,133 @@ import {
|
||||
ScrollText,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilter,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
LogsAggregatorOperator,
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import PodEvents from '../../EntityDetailsUtils/EntityEvents';
|
||||
import PodLogs from '../../EntityDetailsUtils/EntityLogs';
|
||||
import PodMetrics from '../../EntityDetailsUtils/EntityMetrics';
|
||||
import PodTraces from '../../EntityDetailsUtils/EntityTraces';
|
||||
import { getPodMetricsQueryPayload, podWidgetInfo } from './constants';
|
||||
import { PodDetailProps } from './PodDetail.interfaces';
|
||||
import {
|
||||
isCustomTimeRange,
|
||||
useGlobalTimeStore,
|
||||
} from '../../../store/globalTime';
|
||||
import {
|
||||
getAutoRefreshQueryKey,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from '../../../store/globalTime/utils';
|
||||
import { DEFAULT_TIME_RANGE } from '../../TopNav/DateTimeSelectionV2/constants';
|
||||
import { filterDuplicateFilters } from '../commonUtils';
|
||||
import { K8sCategory } from '../constants';
|
||||
import EntityEvents from '../EntityDetailsUtils/EntityEvents';
|
||||
import EntityLogs from '../EntityDetailsUtils/EntityLogs';
|
||||
import EntityMetrics from '../EntityDetailsUtils/EntityMetrics';
|
||||
import EntityTraces from '../EntityDetailsUtils/EntityTraces';
|
||||
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
|
||||
import {
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringSelectedItem,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from '../hooks';
|
||||
import LoadingContainer from '../LoadingContainer';
|
||||
|
||||
import '../../EntityDetailsUtils/entityDetails.styles.scss';
|
||||
import '../EntityDetailsUtils/entityDetails.styles.scss';
|
||||
|
||||
const TimeRangeOffset = 1000000000;
|
||||
|
||||
function PodDetails({
|
||||
pod,
|
||||
onClose,
|
||||
isModalTimeSelection,
|
||||
}: PodDetailProps): JSX.Element {
|
||||
const { maxTime, minTime, selectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
export interface K8sDetailsMetadataConfig<T> {
|
||||
label: string;
|
||||
getValue: (entity: T) => string;
|
||||
}
|
||||
|
||||
const startMs = useMemo(() => Math.floor(Number(minTime) / TimeRangeOffset), [
|
||||
minTime,
|
||||
]);
|
||||
const endMs = useMemo(() => Math.floor(Number(maxTime) / TimeRangeOffset), [
|
||||
maxTime,
|
||||
]);
|
||||
export interface K8sDetailsFilters {
|
||||
filters: TagFilter;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
export interface K8sBaseDetailsProps<T> {
|
||||
category: K8sCategory;
|
||||
eventCategory: string;
|
||||
// Data fetching configuration
|
||||
getSelectedItemFilters: (selectedItem: string) => TagFilter;
|
||||
fetchEntityData: (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<{ data: T | null; error?: string | null }>;
|
||||
// Entity configuration
|
||||
getEntityName: (entity: T) => string;
|
||||
getInitialLogTracesFilters: (entity: T) => TagFilterItem[];
|
||||
getInitialEventsFilters: (entity: T) => TagFilterItem[];
|
||||
primaryFilterKeys: string[];
|
||||
metadataConfig: K8sDetailsMetadataConfig<T>[];
|
||||
entityWidgetInfo: {
|
||||
title: string;
|
||||
yAxisUnit: string;
|
||||
}[];
|
||||
getEntityQueryPayload: (
|
||||
entity: T,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
) => GetQueryResultsProps[];
|
||||
queryKeyPrefix: string;
|
||||
/** When true, only metrics are shown and the Metrics/Logs/Traces/Events tab bar is hidden. */
|
||||
hideDetailViewTabs?: boolean;
|
||||
}
|
||||
|
||||
export function createFilterItem(
|
||||
key: string,
|
||||
value: string,
|
||||
dataType: DataTypes = DataTypes.String,
|
||||
): TagFilterItem {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key,
|
||||
dataType,
|
||||
type: 'resource',
|
||||
id: `${key}--string--resource--false`,
|
||||
},
|
||||
op: '=',
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function K8sBaseDetails<T>({
|
||||
category,
|
||||
eventCategory,
|
||||
getSelectedItemFilters,
|
||||
fetchEntityData,
|
||||
getEntityName,
|
||||
getInitialLogTracesFilters,
|
||||
getInitialEventsFilters,
|
||||
primaryFilterKeys,
|
||||
metadataConfig,
|
||||
entityWidgetInfo,
|
||||
getEntityQueryPayload,
|
||||
queryKeyPrefix,
|
||||
hideDetailViewTabs = false,
|
||||
}: K8sBaseDetailsProps<T>): JSX.Element {
|
||||
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
|
||||
const { startMs, endMs } = useMemo(() => {
|
||||
const { minTime: startNs, maxTime: endNs } = getMinMaxTime(selectedTime);
|
||||
|
||||
return {
|
||||
startMs: Math.floor(startNs / NANO_SECOND_MULTIPLIER),
|
||||
endMs: Math.floor(endNs / NANO_SECOND_MULTIPLIER),
|
||||
};
|
||||
}, [getMinMaxTime, selectedTime]);
|
||||
|
||||
const [modalTimeRange, setModalTimeRange] = useState(() => ({
|
||||
startTime: startMs,
|
||||
@@ -88,14 +163,17 @@ function PodDetails({
|
||||
}));
|
||||
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
: isCustomTimeRange(selectedTime)
|
||||
? DEFAULT_TIME_RANGE
|
||||
: selectedTime,
|
||||
);
|
||||
|
||||
const [selectedView, setSelectedView] = useInfraMonitoringView();
|
||||
const effectiveView = hideDetailViewTabs ? VIEW_TYPES.METRICS : selectedView;
|
||||
|
||||
const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters();
|
||||
const [
|
||||
tracesFiltersParam,
|
||||
@@ -107,79 +185,89 @@ function PodDetails({
|
||||
] = useInfraMonitoringEventsFilters();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const [selectedItem, setSelectedItem] = useInfraMonitoringSelectedItem();
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
hideDetailViewTabs &&
|
||||
selectedItem &&
|
||||
selectedView !== VIEW_TYPES.METRICS
|
||||
) {
|
||||
setSelectedView(VIEW_TYPES.METRICS);
|
||||
}
|
||||
}, [hideDetailViewTabs, selectedItem, selectedView, setSelectedView]);
|
||||
|
||||
const entityQueryKey = useMemo(
|
||||
() =>
|
||||
getAutoRefreshQueryKey(
|
||||
selectedTime,
|
||||
`${queryKeyPrefix}EntityDetails`,
|
||||
selectedItem,
|
||||
),
|
||||
[queryKeyPrefix, selectedItem, selectedTime],
|
||||
);
|
||||
|
||||
const {
|
||||
data: entityResponse,
|
||||
isLoading: isEntityLoading,
|
||||
isError: isEntityError,
|
||||
} = useQuery({
|
||||
queryKey: entityQueryKey,
|
||||
queryFn: ({ signal }) => {
|
||||
if (!selectedItem) {
|
||||
return { data: null };
|
||||
}
|
||||
const filters = getSelectedItemFilters(selectedItem);
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
|
||||
return fetchEntityData(
|
||||
{
|
||||
filters,
|
||||
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
|
||||
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
|
||||
},
|
||||
signal,
|
||||
);
|
||||
},
|
||||
enabled: !!selectedItem,
|
||||
});
|
||||
|
||||
const entity = entityResponse?.data ?? null;
|
||||
|
||||
const initialFilters = useMemo(() => {
|
||||
const filters =
|
||||
selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam;
|
||||
effectiveView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam;
|
||||
if (filters) {
|
||||
return filters;
|
||||
}
|
||||
if (!entity) {
|
||||
return { op: 'AND', items: [] };
|
||||
}
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_POD_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s_pod_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: pod?.meta.k8s_pod_name || '',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s_pod_name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: pod?.meta.k8s_namespace_name || '',
|
||||
},
|
||||
],
|
||||
items: getInitialLogTracesFilters(entity),
|
||||
};
|
||||
}, [
|
||||
pod?.meta.k8s_namespace_name,
|
||||
pod?.meta.k8s_pod_name,
|
||||
selectedView,
|
||||
entity,
|
||||
effectiveView,
|
||||
logFiltersParam,
|
||||
tracesFiltersParam,
|
||||
getInitialLogTracesFilters,
|
||||
]);
|
||||
|
||||
const initialEventsFilters = useMemo(() => {
|
||||
if (eventsFiltersParam) {
|
||||
return eventsFiltersParam;
|
||||
}
|
||||
if (!entity) {
|
||||
return { op: 'AND', items: [] };
|
||||
}
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s.object.kind--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: 'Pod',
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'k8s.object.name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: pod?.meta.k8s_pod_name || '',
|
||||
},
|
||||
],
|
||||
items: getInitialEventsFilters(entity),
|
||||
};
|
||||
}, [pod?.meta.k8s_pod_name, eventsFiltersParam]);
|
||||
}, [entity, eventsFiltersParam, getInitialEventsFilters]);
|
||||
|
||||
const [logsAndTracesFilters, setLogsAndTracesFilters] = useState<
|
||||
IBuilderQuery['filters']
|
||||
@@ -190,14 +278,14 @@ function PodDetails({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (pod) {
|
||||
if (entity) {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
category: eventCategory,
|
||||
});
|
||||
}
|
||||
}, [pod]);
|
||||
}, [entity, eventCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
setLogsAndTracesFilters(initialFilters);
|
||||
@@ -206,17 +294,16 @@ function PodDetails({
|
||||
|
||||
useEffect(() => {
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
if (!isCustomTimeRange(currentSelectedInterval)) {
|
||||
setSelectedInterval(currentSelectedInterval);
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / TimeRangeOffset),
|
||||
endTime: Math.floor(maxTime / TimeRangeOffset),
|
||||
});
|
||||
}
|
||||
}, [selectedTime, minTime, maxTime]);
|
||||
}, [getMinMaxTime, selectedTime]);
|
||||
|
||||
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||
setSelectedView(e.target.value);
|
||||
@@ -226,7 +313,7 @@ function PodDetails({
|
||||
logEvent(InfraMonitoringEvents.TabChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
category: eventCategory,
|
||||
view: e.target.value,
|
||||
});
|
||||
};
|
||||
@@ -253,38 +340,34 @@ function PodDetails({
|
||||
logEvent(InfraMonitoringEvents.TimeUpdated, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
category: eventCategory,
|
||||
interval,
|
||||
view: selectedView,
|
||||
view: effectiveView,
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
[eventCategory, effectiveView],
|
||||
);
|
||||
|
||||
const handleChangeLogFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogsAndTracesFilters((prevFilters) => {
|
||||
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||
[
|
||||
QUERY_KEYS.K8S_POD_NAME,
|
||||
QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
].includes(item.key?.key ?? ''),
|
||||
primaryFilterKeys.includes(item.key?.key ?? ''),
|
||||
);
|
||||
const paginationFilter = value?.items?.find(
|
||||
(item) => item.key?.key === 'id',
|
||||
);
|
||||
const newFilters = value?.items?.filter(
|
||||
(item) =>
|
||||
item.key?.key !== 'id' && item.key?.key !== QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||
item.key?.key !== 'id' &&
|
||||
!primaryFilterKeys.includes(item.key?.key ?? ''),
|
||||
);
|
||||
|
||||
if (newFilters && newFilters?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
category: eventCategory,
|
||||
view: selectedView,
|
||||
});
|
||||
}
|
||||
@@ -306,26 +389,27 @@ function PodDetails({
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
[
|
||||
setLogFiltersParam,
|
||||
setSelectedView,
|
||||
primaryFilterKeys,
|
||||
eventCategory,
|
||||
selectedView,
|
||||
],
|
||||
);
|
||||
|
||||
const handleChangeTracesFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogsAndTracesFilters((prevFilters) => {
|
||||
const primaryFilters = prevFilters?.items?.filter((item) =>
|
||||
[
|
||||
QUERY_KEYS.K8S_POD_NAME,
|
||||
QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
].includes(item.key?.key ?? ''),
|
||||
primaryFilterKeys.includes(item.key?.key ?? ''),
|
||||
);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
category: eventCategory,
|
||||
view: selectedView,
|
||||
});
|
||||
}
|
||||
@@ -336,7 +420,7 @@ function PodDetails({
|
||||
[
|
||||
...(primaryFilters || []),
|
||||
...(value?.items?.filter(
|
||||
(item) => item.key?.key !== QUERY_KEYS.K8S_POD_NAME,
|
||||
(item) => !primaryFilterKeys.includes(item.key?.key ?? ''),
|
||||
) || []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
@@ -348,17 +432,22 @@ function PodDetails({
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
[
|
||||
setTracesFiltersParam,
|
||||
setSelectedView,
|
||||
primaryFilterKeys,
|
||||
eventCategory,
|
||||
selectedView,
|
||||
],
|
||||
);
|
||||
|
||||
const handleChangeEventsFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setEventsFilters((prevFilters) => {
|
||||
const podKindFilter = prevFilters?.items?.find(
|
||||
const kindFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
|
||||
);
|
||||
const podNameFilter = prevFilters?.items?.find(
|
||||
const nameFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
);
|
||||
|
||||
@@ -366,22 +455,24 @@ function PodDetails({
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
category: eventCategory,
|
||||
view: selectedView,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: [
|
||||
podKindFilter,
|
||||
podNameFilter,
|
||||
...(value?.items?.filter(
|
||||
(item) =>
|
||||
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
|
||||
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
) || []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
items: filterDuplicateFilters(
|
||||
[
|
||||
kindFilter,
|
||||
nameFilter,
|
||||
...(value?.items?.filter(
|
||||
(item) =>
|
||||
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
|
||||
item.key?.key !== QUERY_KEYS.K8S_OBJECT_NAME,
|
||||
) || []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
),
|
||||
};
|
||||
|
||||
setEventsFiltersParam(updatedFilters);
|
||||
@@ -390,8 +481,7 @@ function PodDetails({
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
[eventCategory, selectedView, setEventsFiltersParam, setSelectedView],
|
||||
);
|
||||
|
||||
const handleExplorePagesRedirect = (): void => {
|
||||
@@ -406,7 +496,7 @@ function PodDetails({
|
||||
logEvent(InfraMonitoringEvents.ExploreClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
category: eventCategory,
|
||||
view: selectedView,
|
||||
});
|
||||
|
||||
@@ -476,24 +566,28 @@ function PodDetails({
|
||||
endTime: Math.floor(maxTime / TimeRangeOffset),
|
||||
});
|
||||
}
|
||||
setSelectedView(VIEW_TYPES.METRICS);
|
||||
onClose();
|
||||
|
||||
setSelectedItem(null);
|
||||
setSelectedView(null);
|
||||
setTracesFiltersParam(null);
|
||||
setEventsFiltersParam(null);
|
||||
setLogFiltersParam(null);
|
||||
};
|
||||
|
||||
const entityName = entity ? getEntityName(entity) : '';
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width="70%"
|
||||
title={
|
||||
<>
|
||||
<Divider type="vertical" />
|
||||
<Typography.Text className="title">
|
||||
{pod?.meta.k8s_pod_name}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="title">{entityName}</Typography.Text>
|
||||
</>
|
||||
}
|
||||
placement="right"
|
||||
onClose={handleClose}
|
||||
open={!!pod}
|
||||
open={!!selectedItem}
|
||||
style={{
|
||||
overscrollBehavior: 'contain',
|
||||
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||
@@ -502,173 +596,157 @@ function PodDetails({
|
||||
destroyOnClose
|
||||
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||
>
|
||||
{pod && (
|
||||
{isEntityLoading && <LoadingContainer />}
|
||||
{isEntityError && (
|
||||
<Typography.Text type="danger">
|
||||
{entityResponse?.error || 'Failed to load entity details'}
|
||||
</Typography.Text>
|
||||
)}
|
||||
{entity && !isEntityLoading && (
|
||||
<>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
NAMESPACE
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Cluster Name
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Node
|
||||
</Typography.Text>
|
||||
{metadataConfig.map((config) => (
|
||||
<Typography.Text
|
||||
key={config.label}
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
{config.label}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="values-row">
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={pod.meta.k8s_namespace_name}>
|
||||
{pod.meta.k8s_namespace_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={pod.meta.k8s_cluster_name}>
|
||||
{pod.meta.k8s_cluster_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={pod.meta.k8s_node_name}>
|
||||
{pod.meta.k8s_node_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
{metadataConfig.map((config) => {
|
||||
const value = config.getValue(entity);
|
||||
return (
|
||||
<Typography.Text
|
||||
key={config.label}
|
||||
className="entity-details-metadata-value"
|
||||
>
|
||||
<Tooltip title={value}>{value}</Tooltip>
|
||||
</Typography.Text>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="views-tabs-container">
|
||||
<Radio.Group
|
||||
className="views-tabs"
|
||||
onChange={handleTabChange}
|
||||
value={selectedView}
|
||||
>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.METRICS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.METRICS}
|
||||
{!hideDetailViewTabs && (
|
||||
<div className="views-tabs-container">
|
||||
<Radio.Group
|
||||
className="views-tabs"
|
||||
onChange={handleTabChange}
|
||||
value={selectedView}
|
||||
>
|
||||
<div className="view-title">
|
||||
<BarChart2 size={14} />
|
||||
Metrics
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.LOGS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.LOGS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<ScrollText size={14} />
|
||||
Logs
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.TRACES ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.TRACES}
|
||||
>
|
||||
<div className="view-title">
|
||||
<DraftingCompass size={14} />
|
||||
Traces
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.EVENTS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.EVENTS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<ChevronsLeftRight size={14} />
|
||||
Events
|
||||
</div>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.METRICS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.METRICS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<BarChart2 size={14} />
|
||||
Metrics
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.LOGS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.LOGS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<ScrollText size={14} />
|
||||
Logs
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.TRACES ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.TRACES}
|
||||
>
|
||||
<div className="view-title">
|
||||
<DraftingCompass size={14} />
|
||||
Traces
|
||||
</div>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
className={
|
||||
selectedView === VIEW_TYPES.EVENTS ? 'selected_view tab' : 'tab'
|
||||
}
|
||||
value={VIEW_TYPES.EVENTS}
|
||||
>
|
||||
<div className="view-title">
|
||||
<ChevronsLeftRight size={14} />
|
||||
Events
|
||||
</div>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
|
||||
{(selectedView === VIEW_TYPES.LOGS ||
|
||||
selectedView === VIEW_TYPES.TRACES) && (
|
||||
<Button
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{(selectedView === VIEW_TYPES.LOGS ||
|
||||
selectedView === VIEW_TYPES.TRACES) && (
|
||||
<Button
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedView === VIEW_TYPES.METRICS && (
|
||||
<PodMetrics<K8sPodsData>
|
||||
entity={pod}
|
||||
{effectiveView === VIEW_TYPES.METRICS && (
|
||||
<EntityMetrics<T>
|
||||
entity={entity}
|
||||
selectedInterval={selectedInterval}
|
||||
timeRange={modalTimeRange}
|
||||
handleTimeChange={handleTimeChange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
entityWidgetInfo={podWidgetInfo}
|
||||
getEntityQueryPayload={getPodMetricsQueryPayload}
|
||||
category={K8sCategory.PODS}
|
||||
queryKey="podMetrics"
|
||||
isModalTimeSelection
|
||||
entityWidgetInfo={entityWidgetInfo}
|
||||
getEntityQueryPayload={getEntityQueryPayload}
|
||||
category={category}
|
||||
queryKey={`${queryKeyPrefix}Metrics`}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<PodLogs
|
||||
{effectiveView === VIEW_TYPES.LOGS && (
|
||||
<EntityLogs
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
isModalTimeSelection
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeLogFilters={handleChangeLogFilters}
|
||||
logFilters={logsAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKeyFilters={[
|
||||
QUERY_KEYS.K8S_POD_NAME,
|
||||
QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
]}
|
||||
queryKey="podLogs"
|
||||
category={K8sCategory.PODS}
|
||||
queryKeyFilters={primaryFilterKeys}
|
||||
queryKey={`${queryKeyPrefix}Logs`}
|
||||
category={category}
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<PodTraces
|
||||
{effectiveView === VIEW_TYPES.TRACES && (
|
||||
<EntityTraces
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
isModalTimeSelection
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeTracesFilters={handleChangeTracesFilters}
|
||||
tracesFilters={logsAndTracesFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey="podTraces"
|
||||
category={InfraMonitoringEvents.Pod}
|
||||
queryKeyFilters={[
|
||||
QUERY_KEYS.K8S_POD_NAME,
|
||||
QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
]}
|
||||
queryKey={`${queryKeyPrefix}Traces`}
|
||||
category={eventCategory}
|
||||
queryKeyFilters={primaryFilterKeys}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedView === VIEW_TYPES.EVENTS && (
|
||||
<PodEvents
|
||||
{effectiveView === VIEW_TYPES.EVENTS && (
|
||||
<EntityEvents
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
isModalTimeSelection
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeEventFilters={handleChangeEventsFilters}
|
||||
filters={eventsFilters}
|
||||
selectedInterval={selectedInterval}
|
||||
category={K8sCategory.PODS}
|
||||
queryKey="podEvents"
|
||||
category={category}
|
||||
queryKey={`${queryKeyPrefix}Events`}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -677,4 +755,4 @@ function PodDetails({
|
||||
);
|
||||
}
|
||||
|
||||
export default PodDetails;
|
||||
export default K8sBaseDetails;
|
||||
@@ -0,0 +1,171 @@
|
||||
.clickableRow {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.expandedClickableRow {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.expandedTableContainer {
|
||||
border: 1px solid var(--l1-border);
|
||||
overflow-x: auto;
|
||||
padding-left: 48px;
|
||||
|
||||
:global(.ant-table-tbody > tr:hover > td) {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.expandedTableFooter {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
padding-left: 42px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.viewAllButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.k8sListTable {
|
||||
:global(.ant-table) {
|
||||
:global(.ant-table-thead > tr > th) {
|
||||
padding: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
|
||||
border-bottom: none;
|
||||
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.44px;
|
||||
text-transform: uppercase;
|
||||
background: var(--bg-ink-500) !important;
|
||||
|
||||
&::before {
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-table-cell) {
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: var(--l1-foreground);
|
||||
background: var(--bg-ink-500);
|
||||
border-bottom: none;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.hostname-column-value) {
|
||||
color: var(--l1-foreground);
|
||||
font-family: 'Geist Mono';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
:global(.progress-container) {
|
||||
:global(.ant-progress-bg) {
|
||||
height: 8px !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-table-tbody > tr:hover > td) {
|
||||
background: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
:global(.ant-table-tbody > tr:not(.ant-table-expanded-row):hover > td) {
|
||||
background: var(--l2-background);
|
||||
}
|
||||
|
||||
:global(.ant-table-tbody > tr > td) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:global(.ant-empty-normal) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
:global(.ant-table-cell) {
|
||||
min-width: 180px !important;
|
||||
max-width: 180px !important;
|
||||
}
|
||||
|
||||
:global(.ant-table-cell:first-of-type):not(:global(.ant-table-row-expand-icon-cell)) {
|
||||
min-width: 250px !important;
|
||||
max-width: 250px !important;
|
||||
}
|
||||
|
||||
:global(.ant-table-row-expand-icon-cell) {
|
||||
min-width: 40px !important;
|
||||
max-width: 40px !important;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-pagination) {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 16px;
|
||||
background-color: var(--bg-ink-500);
|
||||
margin: 0 !important;
|
||||
padding-right: 72px;
|
||||
|
||||
:global(.ant-pagination-item) {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:global(.ant-pagination-item-active) {
|
||||
background: var(--l2-background);
|
||||
border-color: var(--l2-border);
|
||||
|
||||
a {
|
||||
color: var(--l1-foreground) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.noFilteredHostsMessageContainer {
|
||||
height: 30vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.noFilteredHostsMessageContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
|
||||
width: fit-content;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.noFilteredHostsMessage {
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
.emptyStateSvg {
|
||||
width: 32px;
|
||||
max-width: 100%;
|
||||
}
|
||||
650
frontend/src/container/InfraMonitoringK8s/Base/K8sBaseList.tsx
Normal file
650
frontend/src/container/InfraMonitoringK8s/Base/K8sBaseList.tsx
Normal file
@@ -0,0 +1,650 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Spin,
|
||||
Table,
|
||||
TableColumnType as ColumnType,
|
||||
TablePaginationConfig,
|
||||
TableProps,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import type { SorterResult } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { ChevronDown, ChevronRight, CornerDownRight } from 'lucide-react';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { useGlobalTimeStore } from 'store/globalTime';
|
||||
import {
|
||||
getAutoRefreshQueryKey,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime/utils';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { K8sCategory } from '../constants';
|
||||
import {
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringFilters,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringQueryFilters,
|
||||
useInfraMonitoringSelectedItem,
|
||||
} from '../hooks';
|
||||
import LoadingContainer from '../LoadingContainer';
|
||||
import { OrderBySchemaType } from '../schemas';
|
||||
import { usePageSize } from '../utils';
|
||||
import K8sHeader from './K8sHeader';
|
||||
import {
|
||||
IEntityColumn,
|
||||
useInfraMonitoringTableColumnsForPage,
|
||||
useInfraMonitoringTableColumnsStore,
|
||||
} from './useInfraMonitoringTableColumnsStore';
|
||||
|
||||
import styles from './K8sBaseList.module.scss';
|
||||
|
||||
export type K8sBaseFilters = {
|
||||
filters?: TagFilter;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
start: number;
|
||||
end: number;
|
||||
orderBy?: OrderBySchemaType;
|
||||
};
|
||||
|
||||
export type K8sRenderedRowData = {
|
||||
/**
|
||||
* The unique ID for the row
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* The ID to the selectedItem
|
||||
*/
|
||||
itemKey: string;
|
||||
groupedByMeta: Record<string, string>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type K8sBaseListProps<T = unknown> = {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
entity: K8sCategory;
|
||||
tableColumnsDefinitions: IEntityColumn[];
|
||||
tableColumns: ColumnType<K8sRenderedRowData>[];
|
||||
fetchListData: (
|
||||
filters: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<{
|
||||
data: T[];
|
||||
total: number;
|
||||
error?: string | null;
|
||||
}>;
|
||||
renderRowData: (
|
||||
record: T,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
) => K8sRenderedRowData;
|
||||
eventCategory: InfraMonitoringEvents;
|
||||
};
|
||||
|
||||
export type K8sExpandedRowProps<T> = {
|
||||
record: K8sRenderedRowData;
|
||||
entity: K8sCategory;
|
||||
tableColumns: ColumnType<K8sRenderedRowData>[];
|
||||
fetchListData: K8sBaseListProps<T>['fetchListData'];
|
||||
renderRowData: K8sBaseListProps<T>['renderRowData'];
|
||||
};
|
||||
|
||||
function K8sExpandedRow<T>({
|
||||
record,
|
||||
entity,
|
||||
tableColumns,
|
||||
fetchListData,
|
||||
renderRowData,
|
||||
}: K8sExpandedRowProps<T>): JSX.Element {
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
const [, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [, setFilters] = useInfraMonitoringFilters();
|
||||
const [, setSelectedItem] = useInfraMonitoringSelectedItem();
|
||||
|
||||
const queryFilters = useInfraMonitoringQueryFilters();
|
||||
|
||||
const [
|
||||
columnsDefinitions,
|
||||
columnsHidden,
|
||||
] = useInfraMonitoringTableColumnsForPage(entity);
|
||||
|
||||
const hiddenColumnIdsForNested = useMemo(
|
||||
() =>
|
||||
columnsDefinitions
|
||||
.filter((col) => col.behavior === 'hidden-on-collapse')
|
||||
.map((col) => col.id),
|
||||
[columnsDefinitions],
|
||||
);
|
||||
|
||||
const nestedColumns = useMemo(
|
||||
() =>
|
||||
tableColumns.filter(
|
||||
(c) =>
|
||||
!columnsHidden.includes(c.key?.toString() || '') &&
|
||||
!hiddenColumnIdsForNested.includes(c.key?.toString() || ''),
|
||||
),
|
||||
[tableColumns, columnsHidden, hiddenColumnIdsForNested],
|
||||
);
|
||||
|
||||
const createFiltersForRecord = useCallback((): IBuilderQuery['filters'] => {
|
||||
const baseFilters: IBuilderQuery['filters'] = {
|
||||
items: [...queryFilters.items],
|
||||
op: 'and',
|
||||
};
|
||||
|
||||
const { groupedByMeta } = record;
|
||||
|
||||
for (const key of Object.keys(groupedByMeta)) {
|
||||
baseFilters.items.push({
|
||||
key: {
|
||||
key,
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: groupedByMeta[key],
|
||||
id: key,
|
||||
});
|
||||
}
|
||||
|
||||
return baseFilters;
|
||||
}, [queryFilters.items, record]);
|
||||
|
||||
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
|
||||
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
|
||||
const isRefreshEnabled = useGlobalTimeStore((s) => s.isRefreshEnabled);
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
return getAutoRefreshQueryKey(selectedTime, [
|
||||
'k8sExpandedRow',
|
||||
record.key,
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
]);
|
||||
}, [selectedTime, record.key, queryFilters, orderBy]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useQuery({
|
||||
queryKey,
|
||||
queryFn: ({ signal }) => {
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
|
||||
return fetchListData(
|
||||
{
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
filters: createFiltersForRecord(),
|
||||
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
|
||||
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
|
||||
orderBy: orderBy || undefined,
|
||||
groupBy: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
},
|
||||
refetchInterval: isRefreshEnabled ? refreshInterval : false,
|
||||
});
|
||||
|
||||
const formattedData = useMemo(() => {
|
||||
if (!data?.data) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const rows = data.data.map((item) => renderRowData(item, groupBy));
|
||||
|
||||
// Without handling duplicated keys, the table became unpredictable/unstable
|
||||
const keyCount = new Map<string, number>();
|
||||
return rows.map(
|
||||
(row): K8sRenderedRowData => {
|
||||
const count = keyCount.get(row.key) || 0;
|
||||
keyCount.set(row.key, count + 1);
|
||||
|
||||
if (count > 0) {
|
||||
return { ...row, key: `${row.key}-${count}` };
|
||||
}
|
||||
return row;
|
||||
},
|
||||
);
|
||||
}, [data?.data, renderRowData, groupBy]);
|
||||
|
||||
const openRecordInNewTab = (rowRecord: K8sRenderedRowData): void => {
|
||||
const newParams = new URLSearchParams(document.location.search);
|
||||
newParams.set('selectedItem', rowRecord.itemKey);
|
||||
openInNewTab(
|
||||
buildAbsolutePath({
|
||||
relativePath: '',
|
||||
urlQueryString: newParams.toString(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleViewAllClick = (): void => {
|
||||
const filters = createFiltersForRecord();
|
||||
setFilters(JSON.stringify(filters));
|
||||
setCurrentPage(1);
|
||||
setGroupBy([]);
|
||||
setOrderBy(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.expandedTableContainer}
|
||||
data-testid="expanded-table-container"
|
||||
>
|
||||
{isError && (
|
||||
<Typography>{data?.error?.toString() || 'Something went wrong'}</Typography>
|
||||
)}
|
||||
|
||||
{isFetching || isLoading ? (
|
||||
<LoadingContainer />
|
||||
) : (
|
||||
<div data-testid="expanded-table">
|
||||
<Table
|
||||
columns={nestedColumns}
|
||||
dataSource={formattedData}
|
||||
pagination={false}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
showHeader={false}
|
||||
loading={{
|
||||
spinning: isFetching || isLoading,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
onRow={(
|
||||
rowRecord: K8sRenderedRowData,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
openRecordInNewTab(rowRecord);
|
||||
return;
|
||||
}
|
||||
setSelectedItem(rowRecord.itemKey);
|
||||
},
|
||||
className: styles.expandedClickableRow,
|
||||
})}
|
||||
/>
|
||||
|
||||
{data?.total && data?.total > 10 && (
|
||||
<div className={styles.expandedTableFooter}>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
className={styles.viewAllButton}
|
||||
onClick={handleViewAllClick}
|
||||
>
|
||||
<CornerDownRight size={14} />
|
||||
View All
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function K8sBaseList<T>({
|
||||
controlListPrefix,
|
||||
entity,
|
||||
tableColumnsDefinitions,
|
||||
tableColumns,
|
||||
fetchListData,
|
||||
renderRowData,
|
||||
eventCategory,
|
||||
}: K8sBaseListProps<T>): JSX.Element {
|
||||
const queryFilters = useInfraMonitoringQueryFilters();
|
||||
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [groupBy] = useInfraMonitoringGroupBy();
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
const [initialOrderBy] = useState(orderBy);
|
||||
const [selectedItem, setSelectedItem] = useQueryState(
|
||||
'selectedItem',
|
||||
parseAsString,
|
||||
);
|
||||
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||
useEffect(() => {
|
||||
setExpandedRowKeys([]);
|
||||
}, [groupBy, currentPage]);
|
||||
const { pageSize, setPageSize } = usePageSize(entity);
|
||||
|
||||
const initializeTableColumns = useInfraMonitoringTableColumnsStore(
|
||||
(state) => state.initializePageColumns,
|
||||
);
|
||||
useEffect(() => {
|
||||
initializeTableColumns(entity, tableColumnsDefinitions);
|
||||
}, [initializeTableColumns, entity, tableColumnsDefinitions]);
|
||||
|
||||
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
|
||||
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
|
||||
const isRefreshEnabled = useGlobalTimeStore((s) => s.isRefreshEnabled);
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
return getAutoRefreshQueryKey(
|
||||
selectedTime,
|
||||
'k8sBaseList',
|
||||
entity,
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
);
|
||||
}, [
|
||||
selectedTime,
|
||||
entity,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
]);
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey,
|
||||
queryFn: ({ signal }) => {
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
|
||||
return fetchListData(
|
||||
{
|
||||
limit: pageSize,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
filters: queryFilters,
|
||||
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
|
||||
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
|
||||
orderBy: orderBy || undefined,
|
||||
groupBy: groupBy?.length > 0 ? groupBy : undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
},
|
||||
refetchInterval: isRefreshEnabled ? refreshInterval : false,
|
||||
});
|
||||
|
||||
const pageData = data?.data;
|
||||
const totalCount = data?.total || 0;
|
||||
|
||||
const formattedItemsData = useMemo(() => {
|
||||
if (!pageData) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const rows = pageData.map((item) => renderRowData(item, groupBy));
|
||||
|
||||
// Without handling duplicated keys, the table became unpredictable/unstable
|
||||
const keyCount = new Map<string, number>();
|
||||
return rows.map(
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
(row): K8sRenderedRowData => {
|
||||
const count = keyCount.get(row.key) || 0;
|
||||
keyCount.set(row.key, count + 1);
|
||||
|
||||
if (count > 0) {
|
||||
return { ...row, key: `${row.key}-${count}` };
|
||||
}
|
||||
return row;
|
||||
},
|
||||
);
|
||||
}, [pageData, renderRowData, groupBy]);
|
||||
|
||||
const handleTableChange: TableProps<K8sRenderedRowData>['onChange'] = useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter:
|
||||
| SorterResult<K8sRenderedRowData>
|
||||
| SorterResult<K8sRenderedRowData>[],
|
||||
): void => {
|
||||
if (pagination.current) {
|
||||
setCurrentPage(pagination.current);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
});
|
||||
}
|
||||
|
||||
if ('field' in sorter && sorter.order) {
|
||||
setOrderBy({
|
||||
columnName: sorter.field as string,
|
||||
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
|
||||
});
|
||||
} else {
|
||||
setOrderBy(null);
|
||||
}
|
||||
},
|
||||
[eventCategory, setCurrentPage, setOrderBy],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
total: totalCount,
|
||||
});
|
||||
}, [eventCategory, totalCount]);
|
||||
|
||||
const handleGroupByRowClick = (record: K8sRenderedRowData): void => {
|
||||
if (expandedRowKeys.includes(record.key)) {
|
||||
setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.key));
|
||||
} else {
|
||||
setExpandedRowKeys([record.key]);
|
||||
}
|
||||
};
|
||||
|
||||
const openItemInNewTab = (record: K8sRenderedRowData): void => {
|
||||
const newParams = new URLSearchParams(document.location.search);
|
||||
newParams.set('selectedItem', record.itemKey);
|
||||
openInNewTab(
|
||||
buildAbsolutePath({
|
||||
relativePath: '',
|
||||
urlQueryString: newParams.toString(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sRenderedRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openItemInNewTab(record);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedItem(record.itemKey);
|
||||
} else {
|
||||
handleGroupByRowClick(record);
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
});
|
||||
};
|
||||
|
||||
const [
|
||||
columnsDefinitions,
|
||||
columnsHidden,
|
||||
] = useInfraMonitoringTableColumnsForPage(entity);
|
||||
|
||||
const hiddenColumnIdsOnList = useMemo(
|
||||
() =>
|
||||
columnsDefinitions
|
||||
.filter(
|
||||
(col) =>
|
||||
(groupBy?.length > 0 && col.behavior === 'hidden-on-expand') ||
|
||||
(!groupBy?.length && col.behavior === 'hidden-on-collapse'),
|
||||
)
|
||||
.map((col) => col.id),
|
||||
[columnsDefinitions, groupBy?.length],
|
||||
);
|
||||
|
||||
const mapDefaultSort = useCallback(
|
||||
(
|
||||
tableColumn: ColumnType<K8sRenderedRowData>,
|
||||
): ColumnType<K8sRenderedRowData> => {
|
||||
if (tableColumn.key === initialOrderBy?.columnName) {
|
||||
return {
|
||||
...tableColumn,
|
||||
defaultSortOrder: initialOrderBy?.order === 'asc' ? 'ascend' : 'descend',
|
||||
};
|
||||
}
|
||||
|
||||
return tableColumn;
|
||||
},
|
||||
[initialOrderBy?.columnName, initialOrderBy?.order],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
tableColumns
|
||||
.filter(
|
||||
(c) =>
|
||||
!hiddenColumnIdsOnList.includes(c.key?.toString() || '') &&
|
||||
!columnsHidden.includes(c.key?.toString() || ''),
|
||||
)
|
||||
.map(mapDefaultSort),
|
||||
[columnsHidden, hiddenColumnIdsOnList, mapDefaultSort, tableColumns],
|
||||
);
|
||||
|
||||
const isGroupedByAttribute = groupBy.length > 0;
|
||||
|
||||
const expandedRowRender = (record: K8sRenderedRowData): JSX.Element => (
|
||||
<K8sExpandedRow<T>
|
||||
record={record}
|
||||
entity={entity}
|
||||
tableColumns={tableColumns}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={renderRowData}
|
||||
/>
|
||||
);
|
||||
|
||||
const expandRowIconRenderer = ({
|
||||
expanded,
|
||||
onExpand,
|
||||
record,
|
||||
}: {
|
||||
expanded: boolean;
|
||||
onExpand: (
|
||||
record: K8sRenderedRowData,
|
||||
e: React.MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
record: K8sRenderedRowData;
|
||||
}): JSX.Element | null => {
|
||||
if (!isGroupedByAttribute) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return expanded ? (
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
|
||||
onExpand(record, e)
|
||||
}
|
||||
role="button"
|
||||
>
|
||||
<ChevronDown size={14} />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
|
||||
onExpand(record, e)
|
||||
}
|
||||
role="button"
|
||||
>
|
||||
<ChevronRight size={14} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const onPaginationChange = (page: number, pageSize: number): void => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(pageSize);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
});
|
||||
};
|
||||
|
||||
const showTableLoadingState = isLoading;
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sHeader
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={entity}
|
||||
showAutoRefresh={!selectedItem}
|
||||
/>
|
||||
{isError && (
|
||||
<Typography>{data?.error?.toString() || 'Something went wrong'}</Typography>
|
||||
)}
|
||||
|
||||
<Table
|
||||
className={styles.k8sListTable}
|
||||
dataSource={showTableLoadingState ? [] : formattedItemsData}
|
||||
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={styles.noFilteredHostsMessageContainer}>
|
||||
<div className={styles.noFilteredHostsMessageContent}>
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className={styles.emptyStateSvg}
|
||||
/>
|
||||
|
||||
<Typography.Text className={styles.noFilteredHostsMessage}>
|
||||
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: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: styles.clickableRow,
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
expandIcon: expandRowIconRenderer,
|
||||
expandedRowKeys,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
.drawer {
|
||||
--dialog-description-padding: 0px 0px var(--spacing-8) 0px;
|
||||
}
|
||||
|
||||
.columnItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.columnsTitle {
|
||||
color: var(--l2-foreground);
|
||||
font-size: var(--periscope-font-size-small, 11px);
|
||||
font-weight: var(--periscope-font-weight-medium, 500);
|
||||
text-transform: uppercase;
|
||||
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border-bottom: 1px solid var(--l2-border);
|
||||
|
||||
&:not(:first-of-type) {
|
||||
border-top: 1px solid var(--l2-border);
|
||||
}
|
||||
}
|
||||
|
||||
.columnsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.columnItem {
|
||||
justify-content: flex-start !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.horizontalDivider {
|
||||
border-top: 1px solid var(--l1-border);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Button, DrawerWrapper } from '@signozhq/ui';
|
||||
|
||||
import { K8sCategory } from '../constants';
|
||||
import {
|
||||
useInfraMonitoringTableColumnsForPage,
|
||||
useInfraMonitoringTableColumnsStore,
|
||||
} from './useInfraMonitoringTableColumnsStore';
|
||||
|
||||
import styles from './K8sFiltersSidePanel.module.scss';
|
||||
|
||||
function K8sFiltersSidePanel({
|
||||
open,
|
||||
onClose,
|
||||
entity,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
entity: K8sCategory;
|
||||
}): JSX.Element {
|
||||
const addColumn = useInfraMonitoringTableColumnsStore(
|
||||
(state) => state.addColumn,
|
||||
);
|
||||
const removeColumn = useInfraMonitoringTableColumnsStore(
|
||||
(state) => state.removeColumn,
|
||||
);
|
||||
|
||||
const [columns, columnsHidden] = useInfraMonitoringTableColumnsForPage(entity);
|
||||
|
||||
const drawerContent = (
|
||||
<>
|
||||
<div className={styles.columnsTitle}>Added Columns (Click to remove)</div>
|
||||
|
||||
<div className={styles.columnsList}>
|
||||
{columns
|
||||
.filter(
|
||||
(column) =>
|
||||
!columnsHidden.includes(column.id) &&
|
||||
column.behavior !== 'hidden-on-collapse',
|
||||
)
|
||||
.map((column) => (
|
||||
<div className={styles.columnItem} key={column.value}>
|
||||
{/*<GripVertical size={16} /> TODO: Add support back when update the table component */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="none"
|
||||
className={styles.columnItem}
|
||||
disabled={!column.canBeHidden}
|
||||
data-testid={`remove-column-${column.id}`}
|
||||
onClick={(): void => removeColumn(entity, column.id)}
|
||||
>
|
||||
{column.label}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.horizontalDivider} />
|
||||
|
||||
<div className={styles.columnsTitle}>Other Columns (Click to add)</div>
|
||||
|
||||
<div className={styles.columnsList}>
|
||||
{columns
|
||||
.filter((column) => columnsHidden.includes(column.id))
|
||||
.map((column) => (
|
||||
<div className={styles.columnItem} key={column.value}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="none"
|
||||
className={styles.columnItem}
|
||||
data-can-be-added="true"
|
||||
data-testid={`add-column-${column.id}`}
|
||||
onClick={(): void => addColumn(entity, column.id)}
|
||||
tabIndex={0}
|
||||
>
|
||||
{column.label}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<DrawerWrapper
|
||||
open={open}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
title="Columns"
|
||||
direction="right"
|
||||
showCloseButton
|
||||
showOverlay={false}
|
||||
className={styles.drawer}
|
||||
>
|
||||
{drawerContent}
|
||||
</DrawerWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sFiltersSidePanel;
|
||||
@@ -0,0 +1,80 @@
|
||||
.k8sListControls {
|
||||
padding: var(--spacing-4);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
|
||||
:global(.ant-select-selector) {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border) !important;
|
||||
background-color: var(--l2-background) !important;
|
||||
|
||||
input {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:global(.ant-tag .ant-typography) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.k8sListControlsLeft {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-4);
|
||||
|
||||
.k8sQbSearchContainer {
|
||||
flex: 1;
|
||||
min-width: 240px;
|
||||
max-width: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
.k8sAttributeSearchContainer {
|
||||
flex: 1;
|
||||
min-width: 240px;
|
||||
max-width: 40%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.groupByLabel {
|
||||
min-width: max-content;
|
||||
font-size: var(--periscope-font-size-base, 13px);
|
||||
font-weight: var(--periscope-font-weight-regular, 400);
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
border-radius: 2px 0px 0px 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-right: none;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
|
||||
display: flex;
|
||||
height: 32px;
|
||||
padding: var(--spacing-3) var(--spacing-3) var(--spacing-3) var(--spacing-4);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.groupBySelect {
|
||||
:global(.ant-select-selector) {
|
||||
border-left: none;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.k8sListControlsRight {
|
||||
min-width: 240px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
226
frontend/src/container/InfraMonitoringK8s/Base/K8sHeader.tsx
Normal file
226
frontend/src/container/InfraMonitoringK8s/Base/K8sHeader.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Select } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { SlidersHorizontal } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { GetK8sEntityToAggregateAttribute, K8sCategory } from '../constants';
|
||||
import {
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringFiltersK8s,
|
||||
useInfraMonitoringGroupBy,
|
||||
} from '../hooks';
|
||||
import K8sFiltersSidePanel from './K8sFiltersSidePanel';
|
||||
|
||||
import styles from './K8sHeader.module.scss';
|
||||
|
||||
interface K8sHeaderProps {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
entity: K8sCategory;
|
||||
showAutoRefresh: boolean;
|
||||
}
|
||||
|
||||
function K8sHeader({
|
||||
controlListPrefix,
|
||||
entity,
|
||||
showAutoRefresh,
|
||||
}: K8sHeaderProps): JSX.Element {
|
||||
const [isFiltersSidePanelOpen, setIsFiltersSidePanelOpen] = useState(false);
|
||||
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
|
||||
|
||||
const currentQuery = initialQueriesMap[DataSource.METRICS];
|
||||
|
||||
const updatedCurrentQuery = useMemo(() => {
|
||||
let { filters } = currentQuery.builder.queryData[0];
|
||||
if (urlFilters) {
|
||||
filters = urlFilters;
|
||||
}
|
||||
return {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...currentQuery.builder.queryData[0],
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||
},
|
||||
filters,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}, [currentQuery, urlFilters]);
|
||||
|
||||
const query = useMemo(
|
||||
() => updatedCurrentQuery?.builder?.queryData[0] || null,
|
||||
[updatedCurrentQuery],
|
||||
);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const [, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const handleChangeTagFilters = useCallback(
|
||||
(value: IBuilderQuery['filters']) => {
|
||||
setUrlFilters(value || null);
|
||||
handleChangeQueryData('filters', value);
|
||||
setCurrentPage(1);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
});
|
||||
}
|
||||
},
|
||||
[handleChangeQueryData, setCurrentPage, setUrlFilters],
|
||||
);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const {
|
||||
data: groupByFiltersData,
|
||||
isLoading: isLoadingGroupByFilters,
|
||||
} = useGetAggregateKeys(
|
||||
{
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateAttribute: GetK8sEntityToAggregateAttribute(
|
||||
entity,
|
||||
dotMetricsEnabled,
|
||||
),
|
||||
aggregateOperator: 'noop',
|
||||
searchText: '',
|
||||
tagType: '',
|
||||
},
|
||||
{
|
||||
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
|
||||
},
|
||||
true,
|
||||
entity,
|
||||
);
|
||||
|
||||
const groupByOptions = useMemo(
|
||||
() =>
|
||||
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
||||
value: filter.key,
|
||||
label: filter.key,
|
||||
})) || [],
|
||||
[groupByFiltersData],
|
||||
);
|
||||
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
|
||||
const handleGroupByChange = useCallback(
|
||||
(value: IBuilderQuery['groupBy']) => {
|
||||
const newGroupBy = [];
|
||||
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
const element = (value[index] as unknown) as string;
|
||||
|
||||
const key = groupByFiltersData?.payload?.attributeKeys?.find(
|
||||
(k) => k.key === element,
|
||||
);
|
||||
|
||||
if (key) {
|
||||
newGroupBy.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset pagination on switching to groupBy
|
||||
setCurrentPage(1);
|
||||
setGroupBy(newGroupBy);
|
||||
|
||||
logEvent(InfraMonitoringEvents.GroupByChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
});
|
||||
},
|
||||
[groupByFiltersData, setCurrentPage, setGroupBy],
|
||||
);
|
||||
|
||||
const onClickOutside = useCallback(() => {
|
||||
setIsFiltersSidePanelOpen(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.k8sListControls}>
|
||||
<div className={styles.k8sListControlsLeft}>
|
||||
{controlListPrefix}
|
||||
|
||||
<div className={styles.k8sQbSearchContainer}>
|
||||
<QueryBuilderSearch
|
||||
query={query as IBuilderQuery}
|
||||
onChange={handleChangeTagFilters}
|
||||
isInfraMonitoring
|
||||
disableNavigationShortcuts
|
||||
entity={entity}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.k8sAttributeSearchContainer}>
|
||||
<div className={styles.groupByLabel}> Group by </div>
|
||||
<Select
|
||||
className={styles.groupBySelect}
|
||||
loading={isLoadingGroupByFilters}
|
||||
mode="multiple"
|
||||
value={groupBy}
|
||||
allowClear
|
||||
maxTagCount="responsive"
|
||||
placeholder="Search for attribute"
|
||||
style={{ width: '100%' }}
|
||||
options={groupByOptions}
|
||||
onChange={handleGroupByChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.k8sListControlsRight}>
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh={showAutoRefresh}
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="none"
|
||||
disabled={groupBy?.length > 0}
|
||||
data-testid="k8s-list-filters-button"
|
||||
onClick={(): void => setIsFiltersSidePanelOpen(true)}
|
||||
>
|
||||
<SlidersHorizontal size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<K8sFiltersSidePanel
|
||||
open={isFiltersSidePanelOpen}
|
||||
entity={entity}
|
||||
onClose={onClickOutside}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sHeader;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,113 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
export interface IEntityColumn {
|
||||
label: string;
|
||||
value: string;
|
||||
id: string;
|
||||
defaultVisibility: boolean;
|
||||
canBeHidden: boolean;
|
||||
behavior: 'hidden-on-expand' | 'hidden-on-collapse' | 'always-visible';
|
||||
}
|
||||
|
||||
export interface IInfraMonitoringTableColumnsStore {
|
||||
columns: Record<string, IEntityColumn[]>;
|
||||
columnsHidden: Record<string, string[]>;
|
||||
addColumn: (page: string, columnId: string) => void;
|
||||
removeColumn: (page: string, columnId: string) => void;
|
||||
initializePageColumns: (page: string, columns: IEntityColumn[]) => void;
|
||||
}
|
||||
|
||||
export const useInfraMonitoringTableColumnsStore = create<IInfraMonitoringTableColumnsStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
columns: {},
|
||||
columnsHidden: {},
|
||||
addColumn: (page, columnId): void => {
|
||||
const state = get();
|
||||
const columnDefinition = state.columns[page]?.find(
|
||||
(c) => c.id === columnId,
|
||||
);
|
||||
|
||||
if (!columnDefinition) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!columnDefinition.canBeHidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
const columnsHidden = state.columnsHidden[page];
|
||||
|
||||
if (columnsHidden.includes(columnId)) {
|
||||
set({
|
||||
columnsHidden: {
|
||||
...state.columnsHidden,
|
||||
[page]: columnsHidden.filter((id) => id !== columnId),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
removeColumn: (page, columnId): void => {
|
||||
const state = get();
|
||||
const columnDefinition = state.columns[page]?.find(
|
||||
(c) => c.id === columnId,
|
||||
);
|
||||
|
||||
if (!columnDefinition) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!columnDefinition.canBeHidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
const columnsHidden = state.columnsHidden[page];
|
||||
|
||||
if (!columnsHidden.includes(columnId)) {
|
||||
set({
|
||||
columnsHidden: {
|
||||
...state.columnsHidden,
|
||||
[page]: [...columnsHidden, columnId],
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
initializePageColumns: (page, columns): void => {
|
||||
const state = get();
|
||||
|
||||
set({
|
||||
columns: {
|
||||
...state.columns,
|
||||
[page]: columns,
|
||||
},
|
||||
});
|
||||
|
||||
if (state.columnsHidden[page] === undefined) {
|
||||
set({
|
||||
columnsHidden: {
|
||||
...state.columnsHidden,
|
||||
[page]: columns
|
||||
.filter((c) => c.defaultVisibility === false)
|
||||
.map((c) => c.id),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: '@signoz/infra-monitoring-columns',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export const useInfraMonitoringTableColumnsForPage = (
|
||||
page: string,
|
||||
): [columns: IEntityColumn[], columnsHidden: string[]] => {
|
||||
const state = useInfraMonitoringTableColumnsStore((s) => s.columns);
|
||||
const columnsHidden = useInfraMonitoringTableColumnsStore(
|
||||
(s) => s.columnsHidden,
|
||||
);
|
||||
|
||||
return [state[page] ?? [], columnsHidden[page] ?? []];
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
.itemDataGroup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.itemDataGroupTagItem {
|
||||
display: block !important;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
93
frontend/src/container/InfraMonitoringK8s/Base/utils.tsx
Normal file
93
frontend/src/container/InfraMonitoringK8s/Base/utils.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Badge } from '@signozhq/ui';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import styles from './utils.module.scss';
|
||||
|
||||
const dotToUnder: Record<string, string> = {
|
||||
'k8s.node.name': 'k8s_node_name',
|
||||
'k8s.cluster.name': 'k8s_cluster_name',
|
||||
'k8s.node.uid': 'k8s_node_uid',
|
||||
'k8s.cronjob.name': 'k8s_cronjob_name',
|
||||
'k8s.daemonset.name': 'k8s_daemonset_name',
|
||||
'k8s.deployment.name': 'k8s_deployment_name',
|
||||
'k8s.job.name': 'k8s_job_name',
|
||||
'k8s.namespace.name': 'k8s_namespace_name',
|
||||
'k8s.pod.name': 'k8s_pod_name',
|
||||
'k8s.pod.uid': 'k8s_pod_uid',
|
||||
'k8s.statefulset.name': 'k8s_statefulset_name',
|
||||
'k8s.persistentvolumeclaim.name': 'k8s_persistentvolumeclaim_name',
|
||||
};
|
||||
|
||||
export function getGroupedByMeta<T extends { meta: Record<string, string> }>(
|
||||
itemData: T,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
|
||||
groupBy.forEach((group) => {
|
||||
const rawKey = group.key as string;
|
||||
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof itemData.meta;
|
||||
result[rawKey] = (itemData.meta[metaKey] || itemData.meta[rawKey]) ?? '';
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getRowKey<T extends { meta: Record<string, string> }>(
|
||||
itemData: T,
|
||||
getItemIdentifier: () => string,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): string {
|
||||
const nodeIdentifier = getItemIdentifier();
|
||||
|
||||
if (groupBy.length === 0) {
|
||||
return nodeIdentifier || JSON.stringify(itemData.meta);
|
||||
}
|
||||
|
||||
const groupedMeta = getGroupedByMeta(itemData, groupBy);
|
||||
const groupKey = Object.values(groupedMeta).join('-');
|
||||
|
||||
if (groupKey && nodeIdentifier) {
|
||||
return `${groupKey}-${nodeIdentifier}`;
|
||||
}
|
||||
if (groupKey) {
|
||||
return groupKey;
|
||||
}
|
||||
if (nodeIdentifier) {
|
||||
return nodeIdentifier;
|
||||
}
|
||||
|
||||
return JSON.stringify(itemData.meta);
|
||||
}
|
||||
|
||||
export function getGroupByEl<T extends { meta: Record<string, string> }>(
|
||||
itemData: T,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): React.ReactNode {
|
||||
const groupByValues: string[] = [];
|
||||
|
||||
groupBy.forEach((group) => {
|
||||
const rawKey = group.key as string;
|
||||
|
||||
// Choose mapped key if present, otherwise use rawKey
|
||||
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof itemData.meta;
|
||||
const value = itemData.meta[metaKey] || itemData.meta[rawKey] || '<no-value>';
|
||||
|
||||
groupByValues.push(value);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.itemDataGroup}>
|
||||
{groupByValues.map((value) => (
|
||||
<Badge
|
||||
key={value}
|
||||
color="secondary"
|
||||
className={styles.itemDataGroupTagItem}
|
||||
>
|
||||
{value === '' ? '<no-value>' : value}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -29,6 +29,7 @@ export const QUERY_KEYS = {
|
||||
K8S_STATEFUL_SET_NAME: 'k8s.statefulset.name',
|
||||
K8S_JOB_NAME: 'k8s.job.name',
|
||||
K8S_DAEMON_SET_NAME: 'k8s.daemonset.name',
|
||||
K8S_PERSISTENT_VOLUME_CLAIM_NAME: 'k8s.persistentvolumeclaim.name',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { VerticalAlignTopOutlined } from '@ant-design/icons';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import type { CollapseProps } from 'antd';
|
||||
import { Button, CollapseProps } from 'antd';
|
||||
import { Collapse, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Computer,
|
||||
Container,
|
||||
FilePenLine,
|
||||
Filter,
|
||||
Group,
|
||||
HardDrive,
|
||||
Workflow,
|
||||
@@ -67,9 +68,9 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const handleFilterVisibilityChange = (): void => {
|
||||
setShowFilters(!showFilters);
|
||||
};
|
||||
const handleFilterVisibilityChange = useCallback((): void => {
|
||||
setShowFilters((show) => !show);
|
||||
}, []);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
@@ -321,6 +322,25 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
const showFiltersComp = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
{!showFilters && (
|
||||
<div className="quick-filters-toggle-container">
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={handleFilterVisibilityChange}
|
||||
>
|
||||
<Filter size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [handleFilterVisibilityChange, showFilters]);
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className="infra-monitoring-container">
|
||||
@@ -355,11 +375,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
}`}
|
||||
>
|
||||
{selectedCategory === K8sCategories.PODS && (
|
||||
<K8sPodLists
|
||||
isFiltersVisible={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
quickFiltersLastUpdated={quickFiltersLastUpdated}
|
||||
/>
|
||||
<K8sPodLists controlListPrefix={showFiltersComp} />
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.NODES && (
|
||||
@@ -419,11 +435,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
)}
|
||||
|
||||
{selectedCategory === K8sCategories.VOLUMES && (
|
||||
<K8sVolumesList
|
||||
isFiltersVisible={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
quickFiltersLastUpdated={quickFiltersLastUpdated}
|
||||
/>
|
||||
<K8sVolumesList controlListPrefix={showFiltersComp} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,753 +1,116 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Spin,
|
||||
Table,
|
||||
TableColumnType as ColumnType,
|
||||
TablePaginationConfig,
|
||||
TableProps,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import type { SorterResult } from 'antd/es/table/interface';
|
||||
import get from 'api/browser/localstorage/get';
|
||||
import set from 'api/browser/localstorage/set';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { K8sPodsListPayload } from 'api/infraMonitoring/getK8sPodsList';
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback } from 'react';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useGetK8sPodsList } from 'hooks/infraMonitoring/useGetK8sPodsList';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { ChevronDown, ChevronRight, CornerDownRight } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseFilters, K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sCategory } from '../constants';
|
||||
import { getK8sPodsList, K8sPodsData } from './api';
|
||||
import {
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
K8sCategory,
|
||||
} from '../constants';
|
||||
getPodMetricsQueryPayload,
|
||||
k8sPodDetailsMetadataConfig,
|
||||
k8sPodGetEntityName,
|
||||
k8sPodGetSelectedItemFilters,
|
||||
k8sPodInitialEventsFilter,
|
||||
k8sPodInitialFilters,
|
||||
k8sPodInitialLogTracesFilter,
|
||||
podWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringPodUID,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from '../hooks';
|
||||
import K8sHeader from '../K8sHeader';
|
||||
import LoadingContainer from '../LoadingContainer';
|
||||
import {
|
||||
defaultAddedColumns,
|
||||
defaultAvailableColumns,
|
||||
formatDataForTable,
|
||||
getK8sPodsListColumns,
|
||||
getK8sPodsListQuery,
|
||||
IEntityColumn,
|
||||
K8sPodsRowData,
|
||||
usePageSize,
|
||||
} from '../utils';
|
||||
import PodDetails from './PodDetails/PodDetails';
|
||||
|
||||
import '../InfraMonitoringK8s.styles.scss';
|
||||
k8sPodColumns,
|
||||
k8sPodColumnsConfig,
|
||||
k8sPodRenderRowData,
|
||||
} from './table';
|
||||
|
||||
function K8sPodsList({
|
||||
isFiltersVisible,
|
||||
handleFilterVisibilityChange,
|
||||
quickFiltersLastUpdated,
|
||||
controlListPrefix,
|
||||
}: {
|
||||
isFiltersVisible: boolean;
|
||||
handleFilterVisibilityChange: () => void;
|
||||
quickFiltersLastUpdated: number;
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
const [defaultOrderBy] = useState(orderBy);
|
||||
const [selectedPodUID, setSelectedPodUID] = useInfraMonitoringPodUID();
|
||||
const [, setView] = useInfraMonitoringView();
|
||||
const [, setTracesFilters] = useInfraMonitoringTracesFilters();
|
||||
const [, setEventsFilters] = useInfraMonitoringEventsFilters();
|
||||
const [, setLogFilters] = useInfraMonitoringLogFilters();
|
||||
|
||||
const [filtersInitialised, setFiltersInitialised] = useState(false);
|
||||
|
||||
const [addedColumns, setAddedColumns] = useState<IEntityColumn[]>([]);
|
||||
|
||||
const [availableColumns, setAvailableColumns] = useState<IEntityColumn[]>(
|
||||
defaultAvailableColumns,
|
||||
);
|
||||
|
||||
const [selectedRowData, setSelectedRowData] = useState<K8sPodsRowData | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||
|
||||
const [groupByOptions, setGroupByOptions] = useState<
|
||||
{ value: string; label: string }[]
|
||||
>([]);
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const queryFilters = useMemo(
|
||||
() =>
|
||||
currentQuery?.builder?.queryData[0]?.filters || {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
[currentQuery?.builder?.queryData],
|
||||
);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const {
|
||||
data: groupByFiltersData,
|
||||
isLoading: isLoadingGroupByFilters,
|
||||
} = useGetAggregateKeys(
|
||||
{
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateAttribute: GetK8sEntityToAggregateAttribute(
|
||||
K8sCategory.PODS,
|
||||
const fetchListData = useCallback(
|
||||
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
|
||||
filters.orderBy ||= {
|
||||
columnName: 'cpu',
|
||||
order: 'desc',
|
||||
};
|
||||
|
||||
const response = await getK8sPodsList(
|
||||
filters,
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
),
|
||||
aggregateOperator: 'noop',
|
||||
searchText: '',
|
||||
tagType: '',
|
||||
},
|
||||
{
|
||||
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
|
||||
},
|
||||
true, // isInfraMonitoring
|
||||
K8sCategory.PODS, // infraMonitoringEntity
|
||||
);
|
||||
|
||||
// Reset pagination every time quick filters are changed
|
||||
useEffect(() => {
|
||||
if (quickFiltersLastUpdated !== -1) {
|
||||
setCurrentPage(1);
|
||||
}
|
||||
}, [quickFiltersLastUpdated, setCurrentPage]);
|
||||
|
||||
useEffect(() => {
|
||||
const addedColumns = JSON.parse(get('k8sPodsAddedColumns') ?? '[]');
|
||||
|
||||
if (addedColumns && addedColumns.length > 0) {
|
||||
const availableColumns = defaultAvailableColumns.filter(
|
||||
(column) => !addedColumns.includes(column.id),
|
||||
);
|
||||
|
||||
const newAddedColumns = defaultAvailableColumns.filter((column) =>
|
||||
addedColumns.includes(column.id),
|
||||
);
|
||||
|
||||
setAvailableColumns(availableColumns);
|
||||
setAddedColumns(newAddedColumns);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { pageSize, setPageSize } = usePageSize(K8sCategory.PODS);
|
||||
|
||||
const query = useMemo(() => {
|
||||
const baseQuery = getK8sPodsListQuery();
|
||||
|
||||
const queryPayload = {
|
||||
...baseQuery,
|
||||
limit: pageSize,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
filters: queryFilters,
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
orderBy: orderBy || baseQuery.orderBy,
|
||||
};
|
||||
|
||||
if (groupBy.length > 0) {
|
||||
queryPayload.groupBy = groupBy;
|
||||
}
|
||||
|
||||
return queryPayload;
|
||||
}, [pageSize, currentPage, queryFilters, minTime, maxTime, orderBy, groupBy]);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedPodUID) {
|
||||
return [
|
||||
'podList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'podList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
selectedPodUID,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sPodsList(
|
||||
query as K8sPodsListPayload,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
return {
|
||||
data: response.payload?.data.records || [],
|
||||
total: response.payload?.data.total || 0,
|
||||
error: response.error,
|
||||
};
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
[dotMetricsEnabled],
|
||||
);
|
||||
|
||||
const createFiltersForSelectedRowData = (
|
||||
selectedRowData: K8sPodsRowData,
|
||||
): IBuilderQuery['filters'] => {
|
||||
const baseFilters: IBuilderQuery['filters'] = {
|
||||
items: [...queryFilters.items],
|
||||
op: 'and',
|
||||
};
|
||||
|
||||
if (!selectedRowData) {
|
||||
return baseFilters;
|
||||
}
|
||||
|
||||
const { groupedByMeta } = selectedRowData;
|
||||
|
||||
for (const key of Object.keys(groupedByMeta)) {
|
||||
baseFilters.items.push({
|
||||
key: {
|
||||
key,
|
||||
type: null,
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ data: K8sPodsData | null; error?: string | null }> => {
|
||||
const response = await getK8sPodsList(
|
||||
{
|
||||
filters: filters.filters,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
op: '=',
|
||||
value: groupedByMeta[key],
|
||||
id: key,
|
||||
});
|
||||
}
|
||||
|
||||
return baseFilters;
|
||||
};
|
||||
|
||||
const fetchGroupedByRowDataQuery = useMemo(() => {
|
||||
if (!selectedRowData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseQuery = getK8sPodsListQuery();
|
||||
|
||||
const filters = createFiltersForSelectedRowData(selectedRowData);
|
||||
|
||||
return {
|
||||
...baseQuery,
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
filters,
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
orderBy: orderBy || baseQuery.orderBy,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [minTime, maxTime, orderBy, selectedRowData]);
|
||||
|
||||
const groupedByRowDataQueryKey = useMemo(() => {
|
||||
// be careful with what you serialize from selectedRowData
|
||||
// since it's react node, it could contain circular references
|
||||
const selectedRowDataKey = JSON.stringify(selectedRowData?.groupedByMeta);
|
||||
if (selectedPodUID) {
|
||||
return [
|
||||
'podList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
selectedRowDataKey,
|
||||
];
|
||||
}
|
||||
return [
|
||||
'podList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
selectedRowDataKey,
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [queryFilters, orderBy, selectedPodUID, minTime, maxTime, selectedRowData]);
|
||||
|
||||
const {
|
||||
data: groupedByRowData,
|
||||
isFetching: isFetchingGroupedByRowData,
|
||||
isLoading: isLoadingGroupedByRowData,
|
||||
isError: isErrorGroupedByRowData,
|
||||
refetch: fetchGroupedByRowData,
|
||||
} = useGetK8sPodsList(
|
||||
fetchGroupedByRowDataQuery as K8sPodsListPayload,
|
||||
{
|
||||
queryKey: groupedByRowDataQueryKey,
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const podsData = useMemo(() => data?.payload?.data?.records || [], [data]);
|
||||
const totalCount = data?.payload?.data?.total || 0;
|
||||
|
||||
const nestedPodsData = useMemo(() => {
|
||||
if (!selectedRowData || !groupedByRowData?.payload?.data.records) {
|
||||
return [];
|
||||
}
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const formattedPodsData = useMemo(
|
||||
() => formatDataForTable(podsData, groupBy),
|
||||
[podsData, groupBy],
|
||||
);
|
||||
|
||||
const formattedGroupedByPodsData = useMemo(
|
||||
() =>
|
||||
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => getK8sPodsListColumns(addedColumns, groupBy, defaultOrderBy),
|
||||
[addedColumns, groupBy, defaultOrderBy],
|
||||
);
|
||||
|
||||
const handleTableChange: TableProps<K8sPodsRowData>['onChange'] = useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter: SorterResult<K8sPodsRowData> | SorterResult<K8sPodsRowData>[],
|
||||
): void => {
|
||||
if (pagination.current) {
|
||||
setCurrentPage(pagination.current);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
});
|
||||
}
|
||||
|
||||
if ('field' in sorter && sorter.order) {
|
||||
setOrderBy({
|
||||
columnName: sorter.field as string,
|
||||
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
|
||||
});
|
||||
} else {
|
||||
setOrderBy(null);
|
||||
}
|
||||
},
|
||||
[setCurrentPage, setOrderBy],
|
||||
);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const handleFiltersChange = useCallback(
|
||||
(value: IBuilderQuery['filters']): void => {
|
||||
handleChangeQueryData('filters', value);
|
||||
if (filtersInitialised) {
|
||||
setCurrentPage(1);
|
||||
} else {
|
||||
setFiltersInitialised(true);
|
||||
}
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
});
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const handleGroupByChange = useCallback(
|
||||
(value: IBuilderQuery['groupBy']) => {
|
||||
const newGroupBy = [];
|
||||
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
const element = (value[index] as unknown) as string;
|
||||
|
||||
const key = groupByFiltersData?.payload?.attributeKeys?.find(
|
||||
(k) => k.key === element,
|
||||
);
|
||||
|
||||
if (key) {
|
||||
newGroupBy.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset pagination on switching to groupBy
|
||||
setCurrentPage(1);
|
||||
setGroupBy(newGroupBy);
|
||||
setExpandedRowKeys([]);
|
||||
|
||||
logEvent(InfraMonitoringEvents.GroupByChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
});
|
||||
},
|
||||
[groupByFiltersData, setCurrentPage, setGroupBy],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
total: data?.payload?.data?.total,
|
||||
});
|
||||
}, [data?.payload?.data?.total]);
|
||||
|
||||
const selectedPodData = useMemo(() => {
|
||||
if (!selectedPodUID) {
|
||||
return null;
|
||||
}
|
||||
if (groupBy.length > 0) {
|
||||
// If grouped by, return the pod from the formatted grouped by pods data
|
||||
return nestedPodsData.find((pod) => pod.podUID === selectedPodUID) || null;
|
||||
}
|
||||
// If not grouped by, return the node from the nodes data
|
||||
return podsData.find((pod) => pod.podUID === selectedPodUID) || null;
|
||||
}, [selectedPodUID, groupBy.length, podsData, nestedPodsData]);
|
||||
|
||||
const handleGroupByRowClick = (record: K8sPodsRowData): void => {
|
||||
setSelectedRowData(record);
|
||||
|
||||
if (expandedRowKeys.includes(record.key)) {
|
||||
setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.key));
|
||||
} else {
|
||||
setExpandedRowKeys([record.key]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRowData) {
|
||||
fetchGroupedByRowData();
|
||||
}
|
||||
}, [selectedRowData, fetchGroupedByRowData]);
|
||||
|
||||
const openPodInNewTab = (record: K8sPodsRowData): void => {
|
||||
const newParams = new URLSearchParams(document.location.search);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.POD_UID, record.podUID);
|
||||
openInNewTab(
|
||||
buildAbsolutePath({
|
||||
relativePath: '',
|
||||
urlQueryString: newParams.toString(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sPodsRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openPodInNewTab(record);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedPodUID(record.podUID);
|
||||
setSelectedRowData(null);
|
||||
} else {
|
||||
handleGroupByRowClick(record);
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
});
|
||||
};
|
||||
|
||||
const handleClosePodDetail = (): void => {
|
||||
setSelectedPodUID(null);
|
||||
setView(null);
|
||||
setTracesFilters(null);
|
||||
setEventsFilters(null);
|
||||
setLogFilters(null);
|
||||
};
|
||||
|
||||
const handleAddColumn = useCallback(
|
||||
(column: IEntityColumn): void => {
|
||||
setAddedColumns((prev) => [...prev, column]);
|
||||
|
||||
setAvailableColumns((prev) => prev.filter((c) => c.value !== column.value));
|
||||
},
|
||||
[setAddedColumns, setAvailableColumns],
|
||||
);
|
||||
|
||||
// Update local storage when added columns updated
|
||||
useEffect(() => {
|
||||
const addedColumnIDs = addedColumns.map((column) => column.id);
|
||||
|
||||
set('k8sPodsAddedColumns', JSON.stringify(addedColumnIDs));
|
||||
}, [addedColumns]);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupByFiltersData?.payload) {
|
||||
setGroupByOptions(
|
||||
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
||||
value: filter.key,
|
||||
label: filter.key,
|
||||
})) || [],
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
}
|
||||
}, [groupByFiltersData]);
|
||||
|
||||
const handleRemoveColumn = useCallback(
|
||||
(column: IEntityColumn): void => {
|
||||
setAddedColumns((prev) => prev.filter((c) => c.value !== column.value));
|
||||
const records = response.payload?.data.records || [];
|
||||
|
||||
setAvailableColumns((prev) => [...prev, column]);
|
||||
return {
|
||||
data: records.length > 0 ? records[0] : null,
|
||||
error: response.error,
|
||||
};
|
||||
},
|
||||
[setAddedColumns, setAvailableColumns],
|
||||
[dotMetricsEnabled],
|
||||
);
|
||||
|
||||
const nestedColumns = useMemo(
|
||||
() => getK8sPodsListColumns(addedColumns, [], defaultOrderBy),
|
||||
[addedColumns, defaultOrderBy],
|
||||
);
|
||||
|
||||
const isGroupedByAttribute = groupBy.length > 0;
|
||||
|
||||
const handleExpandedRowViewAllClick = (): void => {
|
||||
if (!selectedRowData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filters = createFiltersForSelectedRowData(selectedRowData);
|
||||
|
||||
handleFiltersChange(filters);
|
||||
|
||||
setCurrentPage(1);
|
||||
setSelectedRowData(null);
|
||||
setGroupBy([]);
|
||||
setOrderBy(null);
|
||||
};
|
||||
|
||||
const expandedRowRender = (): JSX.Element => (
|
||||
<div className="expanded-table-container">
|
||||
{isErrorGroupedByRowData && (
|
||||
<Typography>{groupedByRowData?.error || 'Something went wrong'}</Typography>
|
||||
)}
|
||||
|
||||
{isFetchingGroupedByRowData || isLoadingGroupedByRowData ? (
|
||||
<LoadingContainer />
|
||||
) : (
|
||||
<div className="expanded-table">
|
||||
<Table
|
||||
columns={nestedColumns as ColumnType<K8sPodsRowData>[]}
|
||||
dataSource={formattedGroupedByPodsData}
|
||||
pagination={false}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
showHeader={false}
|
||||
loading={{
|
||||
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
onRow={(
|
||||
record: K8sPodsRowData,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
openPodInNewTab(record);
|
||||
return;
|
||||
}
|
||||
setSelectedPodUID(record.podUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
{groupedByRowData?.payload?.data?.total &&
|
||||
groupedByRowData?.payload?.data?.total > 10 && (
|
||||
<div className="expanded-table-footer">
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
className="periscope-btn secondary"
|
||||
onClick={handleExpandedRowViewAllClick}
|
||||
>
|
||||
<CornerDownRight size={14} />
|
||||
View All
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const expandRowIconRenderer = ({
|
||||
expanded,
|
||||
onExpand,
|
||||
record,
|
||||
}: {
|
||||
expanded: boolean;
|
||||
onExpand: (
|
||||
record: K8sPodsRowData,
|
||||
e: React.MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
record: K8sPodsRowData;
|
||||
}): JSX.Element | null => {
|
||||
if (!isGroupedByAttribute) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return expanded ? (
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
|
||||
onExpand(record, e)
|
||||
}
|
||||
role="button"
|
||||
>
|
||||
<ChevronDown size={14} />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
|
||||
onExpand(record, e)
|
||||
}
|
||||
role="button"
|
||||
>
|
||||
<ChevronRight size={14} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const onPaginationChange = (page: number, pageSize: number): void => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(pageSize);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
});
|
||||
};
|
||||
|
||||
const showTableLoadingState =
|
||||
(isFetching || isLoading) && formattedPodsData.length === 0;
|
||||
|
||||
return (
|
||||
<div className="k8s-list">
|
||||
<K8sHeader
|
||||
selectedGroupBy={groupBy}
|
||||
groupByOptions={groupByOptions}
|
||||
isLoadingGroupByFilters={isLoadingGroupByFilters}
|
||||
isFiltersVisible={isFiltersVisible}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
defaultAddedColumns={defaultAddedColumns}
|
||||
addedColumns={addedColumns}
|
||||
availableColumns={availableColumns}
|
||||
handleFiltersChange={handleFiltersChange}
|
||||
handleGroupByChange={handleGroupByChange}
|
||||
onAddColumn={handleAddColumn}
|
||||
onRemoveColumn={handleRemoveColumn}
|
||||
<>
|
||||
<K8sBaseList<K8sPodsData>
|
||||
controlListPrefix={controlListPrefix}
|
||||
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: K8sPodsRowData,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
expandIcon: expandRowIconRenderer,
|
||||
expandedRowKeys,
|
||||
}}
|
||||
tableColumnsDefinitions={k8sPodColumns}
|
||||
tableColumns={k8sPodColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={k8sPodRenderRowData}
|
||||
eventCategory={InfraMonitoringEvents.Pod}
|
||||
/>
|
||||
|
||||
{selectedPodData && (
|
||||
<PodDetails
|
||||
pod={selectedPodData}
|
||||
isModalTimeSelection
|
||||
onClose={handleClosePodDetail}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<K8sBaseDetails<K8sPodsData>
|
||||
category={K8sCategory.PODS}
|
||||
eventCategory={InfraMonitoringEvents.Pod}
|
||||
getSelectedItemFilters={k8sPodGetSelectedItemFilters}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sPodGetEntityName}
|
||||
getInitialLogTracesFilters={k8sPodInitialLogTracesFilter}
|
||||
getInitialEventsFilters={k8sPodInitialEventsFilter}
|
||||
primaryFilterKeys={k8sPodInitialFilters}
|
||||
metadataConfig={k8sPodDetailsMetadataConfig}
|
||||
entityWidgetInfo={podWidgetInfo}
|
||||
getEntityQueryPayload={getPodMetricsQueryPayload}
|
||||
queryKeyPrefix="pod"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { K8sPodsData } from 'api/infraMonitoring/getK8sPodsList';
|
||||
|
||||
export type PodDetailProps = {
|
||||
pod: K8sPodsData | null;
|
||||
isModalTimeSelection: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
import PodDetails from './PodDetails';
|
||||
|
||||
export default PodDetails;
|
||||
@@ -1,21 +1,35 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { UnderscoreToDotMap } from 'api/utils';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { UnderscoreToDotMap } from '../utils';
|
||||
import { K8sBaseFilters } from '../Base/K8sBaseList';
|
||||
|
||||
export interface K8sPodsListPayload {
|
||||
filters: TagFilter;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
export const podsMetaMap = [
|
||||
{ dot: 'k8s.cronjob.name', under: 'k8s_cronjob_name' },
|
||||
{ dot: 'k8s.daemonset.name', under: 'k8s_daemonset_name' },
|
||||
{ dot: 'k8s.deployment.name', under: 'k8s_deployment_name' },
|
||||
{ dot: 'k8s.job.name', under: 'k8s_job_name' },
|
||||
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
|
||||
{ dot: 'k8s.node.name', under: 'k8s_node_name' },
|
||||
{ dot: 'k8s.pod.name', under: 'k8s_pod_name' },
|
||||
{ dot: 'k8s.pod.uid', under: 'k8s_pod_uid' },
|
||||
{ dot: 'k8s.statefulset.name', under: 'k8s_statefulset_name' },
|
||||
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
|
||||
] as const;
|
||||
|
||||
export function mapPodsMeta(raw: Record<string, unknown>): K8sPodsData['meta'] {
|
||||
// clone everything
|
||||
const out: Record<string, unknown> = { ...raw };
|
||||
// overlay only the dot→under mappings
|
||||
podsMetaMap.forEach(({ dot, under }) => {
|
||||
if (dot in raw) {
|
||||
const v = raw[dot];
|
||||
out[under] = typeof v === 'string' ? v : raw[under];
|
||||
}
|
||||
});
|
||||
return out as K8sPodsData['meta'];
|
||||
}
|
||||
|
||||
export interface TimeSeriesValue {
|
||||
@@ -71,35 +85,9 @@ export interface K8sPodsListResponse {
|
||||
};
|
||||
}
|
||||
|
||||
export const podsMetaMap = [
|
||||
{ dot: 'k8s.cronjob.name', under: 'k8s_cronjob_name' },
|
||||
{ dot: 'k8s.daemonset.name', under: 'k8s_daemonset_name' },
|
||||
{ dot: 'k8s.deployment.name', under: 'k8s_deployment_name' },
|
||||
{ dot: 'k8s.job.name', under: 'k8s_job_name' },
|
||||
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
|
||||
{ dot: 'k8s.node.name', under: 'k8s_node_name' },
|
||||
{ dot: 'k8s.pod.name', under: 'k8s_pod_name' },
|
||||
{ dot: 'k8s.pod.uid', under: 'k8s_pod_uid' },
|
||||
{ dot: 'k8s.statefulset.name', under: 'k8s_statefulset_name' },
|
||||
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
|
||||
] as const;
|
||||
|
||||
export function mapPodsMeta(raw: Record<string, unknown>): K8sPodsData['meta'] {
|
||||
// clone everything
|
||||
const out: Record<string, unknown> = { ...raw };
|
||||
// overlay only the dot→under mappings
|
||||
podsMetaMap.forEach(({ dot, under }) => {
|
||||
if (dot in raw) {
|
||||
const v = raw[dot];
|
||||
out[under] = typeof v === 'string' ? v : raw[under];
|
||||
}
|
||||
});
|
||||
return out as K8sPodsData['meta'];
|
||||
}
|
||||
|
||||
// getK8sPodsList
|
||||
// TODO: Remove this method once we move this to OpenAPI
|
||||
export const getK8sPodsList = async (
|
||||
props: K8sPodsListPayload,
|
||||
props: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
@@ -111,7 +99,7 @@ export const getK8sPodsList = async (
|
||||
...props,
|
||||
filters: {
|
||||
...props.filters,
|
||||
items: props.filters.items.reduce<typeof props.filters.items>(
|
||||
items: props.filters?.items.reduce<typeof props.filters.items>(
|
||||
(acc, item) => {
|
||||
if (item.value === undefined) {
|
||||
return acc;
|
||||
@@ -1,11 +1,69 @@
|
||||
import { K8sPodsData } from 'api/infraMonitoring/getK8sPodsList';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
createFilterItem,
|
||||
K8sDetailsMetadataConfig,
|
||||
} from '../Base/K8sBaseDetails';
|
||||
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
|
||||
import { K8sPodsData } from './api';
|
||||
|
||||
export const k8sPodGetSelectedItemFilters = (
|
||||
selectedItemId: string,
|
||||
): TagFilter => {
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: 'k8s_pod_uid',
|
||||
key: {
|
||||
key: 'k8s_pod_uid',
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: selectedItemId,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const k8sPodDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sPodsData>[] = [
|
||||
{ label: 'NAMESPACE', getValue: (p): string => p.meta.k8s_namespace_name },
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
getValue: (p): string => p.meta.k8s_cluster_name,
|
||||
},
|
||||
{ label: 'Node', getValue: (p): string => p.meta.k8s_node_name },
|
||||
];
|
||||
|
||||
export const k8sPodInitialFilters = [
|
||||
QUERY_KEYS.K8S_POD_NAME,
|
||||
QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
];
|
||||
|
||||
export const k8sPodInitialEventsFilter = (
|
||||
pod: K8sPodsData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Pod'),
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, pod.meta.k8s_pod_name),
|
||||
];
|
||||
|
||||
export const k8sPodInitialLogTracesFilter = (
|
||||
pod: K8sPodsData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_POD_NAME, pod.meta.k8s_pod_name),
|
||||
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, pod.meta.k8s_namespace_name),
|
||||
];
|
||||
|
||||
export const k8sPodGetEntityName = (pod: K8sPodsData): string =>
|
||||
pod.meta.k8s_pod_name;
|
||||
|
||||
export const podWidgetInfo = [
|
||||
{
|
||||
title: 'CPU Usage (cores)',
|
||||
11
frontend/src/container/InfraMonitoringK8s/Pods/table.module.scss
generated
Normal file
11
frontend/src/container/InfraMonitoringK8s/Pods/table.module.scss
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
.entityGroupHeader {
|
||||
padding-left: var(--spacing-5);
|
||||
gap: var(--spacing-5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
}
|
||||
328
frontend/src/container/InfraMonitoringK8s/Pods/table.tsx
generated
Normal file
328
frontend/src/container/InfraMonitoringK8s/Pods/table.tsx
generated
Normal file
@@ -0,0 +1,328 @@
|
||||
import React from 'react';
|
||||
import { TableColumnType as ColumnType, Tooltip } from 'antd';
|
||||
import { Group } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { K8sRenderedRowData } from '../Base/K8sBaseList';
|
||||
import { IEntityColumn } from '../Base/useInfraMonitoringTableColumnsStore';
|
||||
import { getGroupByEl, getGroupedByMeta, getRowKey } from '../Base/utils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
formatBytes,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../commonUtils';
|
||||
import { K8sCategory } from '../constants';
|
||||
import { K8sPodsData } from './api';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
|
||||
export interface K8sPodsRowData {
|
||||
key: string;
|
||||
podName: React.ReactNode;
|
||||
podUID: string;
|
||||
cpu_request: React.ReactNode;
|
||||
cpu_limit: React.ReactNode;
|
||||
cpu: React.ReactNode;
|
||||
memory_request: React.ReactNode;
|
||||
memory_limit: React.ReactNode;
|
||||
memory: React.ReactNode;
|
||||
restarts: React.ReactNode;
|
||||
groupedByMeta?: any;
|
||||
}
|
||||
|
||||
export const k8sPodColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'Pod Group',
|
||||
value: 'podGroup',
|
||||
id: 'podGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
},
|
||||
{
|
||||
label: 'Pod name',
|
||||
value: 'podName',
|
||||
id: 'podName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'CPU Req Usage (%)',
|
||||
value: 'cpu_request',
|
||||
id: 'cpu_request',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Limit Usage (%)',
|
||||
value: 'cpu_limit',
|
||||
id: 'cpu_limit',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Req Usage (%)',
|
||||
value: 'memory_request',
|
||||
id: 'memory_request',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Limit Usage (%)',
|
||||
value: 'memory_limit',
|
||||
id: 'memory_limit',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Mem Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Namespace name',
|
||||
value: 'namespace',
|
||||
id: 'namespace',
|
||||
canBeHidden: true,
|
||||
defaultVisibility: false,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Node name',
|
||||
value: 'node',
|
||||
id: 'node',
|
||||
canBeHidden: true,
|
||||
defaultVisibility: false,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Cluster name',
|
||||
value: 'cluster',
|
||||
id: 'cluster',
|
||||
canBeHidden: true,
|
||||
defaultVisibility: false,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
// TODO - Re-enable the column once backend issue is fixed
|
||||
// {
|
||||
// label: 'Restarts',
|
||||
// value: 'restarts',
|
||||
// id: 'restarts',
|
||||
// canRemove: false,
|
||||
// },
|
||||
];
|
||||
|
||||
export const k8sPodColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> POD GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'podGroup',
|
||||
key: 'podGroup',
|
||||
ellipsis: true,
|
||||
width: 180,
|
||||
sorter: false,
|
||||
},
|
||||
{
|
||||
title: <div>Pod Name</div>,
|
||||
dataIndex: 'podName',
|
||||
key: 'podName',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
sorter: false,
|
||||
},
|
||||
{
|
||||
title: <div>CPU Req Usage (%)</div>,
|
||||
dataIndex: 'cpu_request',
|
||||
key: 'cpu_request',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Limit Usage (%)</div>,
|
||||
dataIndex: 'cpu_limit',
|
||||
key: 'cpu_limit',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Req Usage (%)</div>,
|
||||
dataIndex: 'memory_request',
|
||||
key: 'memory_request',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Limit Usage (%)</div>,
|
||||
dataIndex: 'memory_limit',
|
||||
key: 'memory_limit',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Mem Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Namespace</div>,
|
||||
dataIndex: 'namespace',
|
||||
key: 'namespace',
|
||||
width: 100,
|
||||
sorter: false,
|
||||
ellipsis: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Node</div>,
|
||||
dataIndex: 'node',
|
||||
key: 'node',
|
||||
width: 100,
|
||||
sorter: false,
|
||||
ellipsis: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Cluster</div>,
|
||||
dataIndex: 'cluster',
|
||||
key: 'cluster',
|
||||
width: 100,
|
||||
sorter: false,
|
||||
ellipsis: true,
|
||||
align: 'left',
|
||||
},
|
||||
// TODO - Re-enable the column once backend issue is fixed
|
||||
// {
|
||||
// title: (
|
||||
// <div className="column-header">
|
||||
// <Tooltip title="Container Restarts">Restarts</Tooltip>
|
||||
// </div>
|
||||
// ),
|
||||
// dataIndex: 'restarts',
|
||||
// key: 'restarts',
|
||||
// width: 40,
|
||||
// ellipsis: true,
|
||||
// sorter: true,
|
||||
// align: 'left',
|
||||
// className: `column ${columnProgressBarClassName}`,
|
||||
// },
|
||||
];
|
||||
|
||||
export const k8sPodRenderRowData = (
|
||||
pod: K8sPodsData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => ({
|
||||
key: getRowKey(
|
||||
pod,
|
||||
() => pod.podUID || pod.meta.k8s_pod_uid || pod.meta.k8s_pod_name,
|
||||
groupBy,
|
||||
),
|
||||
itemKey: pod.podUID,
|
||||
podName: (
|
||||
<Tooltip title={pod.meta.k8s_pod_name || ''}>
|
||||
{pod.meta.k8s_pod_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
podUID: pod.podUID || '',
|
||||
cpu_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={pod.podCPURequest}
|
||||
entity={K8sCategory.PODS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={pod.podCPURequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={pod.podCPULimit}
|
||||
entity={K8sCategory.PODS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={pod.podCPULimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={pod.podCPU}>
|
||||
{pod.podCPU}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={pod.podMemoryRequest}
|
||||
entity={K8sCategory.PODS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={pod.podMemoryRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={pod.podMemoryLimit}
|
||||
entity={K8sCategory.PODS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<div className={styles.progressBar}>
|
||||
<EntityProgressBar value={pod.podMemoryLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={pod.podMemory}>
|
||||
{formatBytes(pod.podMemory)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
restarts: (
|
||||
<ValidateColumnValueWrapper value={pod.restartCount}>
|
||||
{pod.restartCount}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
namespace: pod.meta.k8s_namespace_name,
|
||||
node: pod.meta.k8s_node_name,
|
||||
cluster: pod.meta.k8s_cluster_name,
|
||||
meta: pod.meta,
|
||||
podGroup: getGroupByEl(pod, groupBy),
|
||||
...pod.meta,
|
||||
groupedByMeta: getGroupedByMeta(pod, groupBy),
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
.infra-monitoring-container {
|
||||
.volumes-list-table {
|
||||
.expanded-table-container {
|
||||
padding-left: 60px;
|
||||
}
|
||||
|
||||
.ant-table-row-expand-icon-cell {
|
||||
min-width: 30px !important;
|
||||
max-width: 30px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.pvc-name-header) {
|
||||
min-width: 250px !important;
|
||||
max-width: 250px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.namespace-name-header) {
|
||||
min-width: 220px !important;
|
||||
max-width: 220px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
&:has(.small-col) {
|
||||
min-width: 120px !important;
|
||||
max-width: 120px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.expanded-volumes-list-table {
|
||||
.ant-table-cell {
|
||||
min-width: 223px !important;
|
||||
max-width: 223px !important;
|
||||
}
|
||||
|
||||
.ant-table-row-expand-icon-cell {
|
||||
min-width: 30px !important;
|
||||
max-width: 30px !important;
|
||||
}
|
||||
}
|
||||
@@ -1,674 +1,117 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Spin,
|
||||
Table,
|
||||
TableColumnType as ColumnType,
|
||||
TablePaginationConfig,
|
||||
TableProps,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import type { SorterResult } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { K8sVolumesListPayload } from 'api/infraMonitoring/getK8sVolumesList';
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback } from 'react';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useGetK8sVolumesList } from 'hooks/infraMonitoring/useGetK8sVolumesList';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseFilters, K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sCategory } from '../constants';
|
||||
import { getK8sVolumesList, K8sVolumesData } from './api';
|
||||
import {
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
K8sCategory,
|
||||
} from '../constants';
|
||||
getVolumeMetricsQueryPayload,
|
||||
k8sVolumeDetailsMetadataConfig,
|
||||
k8sVolumeGetEntityName,
|
||||
k8sVolumeGetSelectedItemFilters,
|
||||
k8sVolumeInitialEventsFilter,
|
||||
k8sVolumeInitialFilters,
|
||||
k8sVolumeInitialLogTracesFilter,
|
||||
volumeWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringVolumeUID,
|
||||
} from '../hooks';
|
||||
import K8sHeader from '../K8sHeader';
|
||||
import LoadingContainer from '../LoadingContainer';
|
||||
import { usePageSize } from '../utils';
|
||||
import {
|
||||
defaultAddedColumns,
|
||||
formatDataForTable,
|
||||
getK8sVolumesListColumns,
|
||||
getK8sVolumesListQuery,
|
||||
getVolumeListGroupedByRowDataQueryKey,
|
||||
getVolumesListQueryKey,
|
||||
K8sVolumesRowData,
|
||||
} from './utils';
|
||||
import VolumeDetails from './VolumeDetails';
|
||||
|
||||
import '../InfraMonitoringK8s.styles.scss';
|
||||
import './K8sVolumesList.styles.scss';
|
||||
k8sVolumesColumns,
|
||||
k8sVolumesColumnsConfig,
|
||||
k8sVolumesRenderRowData,
|
||||
} from './table';
|
||||
|
||||
function K8sVolumesList({
|
||||
isFiltersVisible,
|
||||
handleFilterVisibilityChange,
|
||||
quickFiltersLastUpdated,
|
||||
controlListPrefix,
|
||||
}: {
|
||||
isFiltersVisible: boolean;
|
||||
handleFilterVisibilityChange: () => void;
|
||||
quickFiltersLastUpdated: number;
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [filtersInitialised, setFiltersInitialised] = useState(false);
|
||||
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
|
||||
const [
|
||||
selectedVolumeUID,
|
||||
setselectedVolumeUID,
|
||||
] = useInfraMonitoringVolumeUID();
|
||||
|
||||
const { pageSize, setPageSize } = usePageSize(K8sCategory.VOLUMES);
|
||||
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
|
||||
const [
|
||||
selectedRowData,
|
||||
setSelectedRowData,
|
||||
] = useState<K8sVolumesRowData | null>(null);
|
||||
|
||||
const [groupByOptions, setGroupByOptions] = useState<
|
||||
{ value: string; label: string }[]
|
||||
>([]);
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const queryFilters = useMemo(
|
||||
() =>
|
||||
currentQuery?.builder?.queryData[0]?.filters || {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
[currentQuery?.builder?.queryData],
|
||||
);
|
||||
|
||||
// Reset pagination every time quick filters are changed
|
||||
useEffect(() => {
|
||||
if (quickFiltersLastUpdated !== -1) {
|
||||
setCurrentPage(1);
|
||||
}
|
||||
}, [quickFiltersLastUpdated, setCurrentPage]);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const createFiltersForSelectedRowData = (
|
||||
selectedRowData: K8sVolumesRowData,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): IBuilderQuery['filters'] => {
|
||||
const baseFilters: IBuilderQuery['filters'] = {
|
||||
items: [...queryFilters.items],
|
||||
op: 'and',
|
||||
};
|
||||
const fetchListData = useCallback(
|
||||
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
|
||||
filters.orderBy ||= {
|
||||
columnName: 'usage',
|
||||
order: 'desc',
|
||||
};
|
||||
|
||||
if (!selectedRowData) {
|
||||
return baseFilters;
|
||||
}
|
||||
|
||||
const { groupedByMeta } = selectedRowData;
|
||||
|
||||
for (const key of groupBy) {
|
||||
baseFilters.items.push({
|
||||
key: {
|
||||
key: key.key,
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: groupedByMeta[key.key],
|
||||
id: key.key,
|
||||
});
|
||||
}
|
||||
|
||||
return baseFilters;
|
||||
};
|
||||
|
||||
const fetchGroupedByRowDataQuery = useMemo(() => {
|
||||
if (!selectedRowData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseQuery = getK8sVolumesListQuery();
|
||||
|
||||
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
|
||||
|
||||
return {
|
||||
...baseQuery,
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
filters,
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
orderBy: orderBy || baseQuery.orderBy,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
|
||||
|
||||
const groupedByRowDataQueryKey = useMemo(
|
||||
() =>
|
||||
getVolumeListGroupedByRowDataQueryKey(
|
||||
selectedRowData?.groupedByMeta,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
),
|
||||
[
|
||||
selectedRowData?.groupedByMeta,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
],
|
||||
);
|
||||
|
||||
const {
|
||||
data: groupedByRowData,
|
||||
isFetching: isFetchingGroupedByRowData,
|
||||
isLoading: isLoadingGroupedByRowData,
|
||||
isError: isErrorGroupedByRowData,
|
||||
refetch: fetchGroupedByRowData,
|
||||
} = useGetK8sVolumesList(
|
||||
fetchGroupedByRowDataQuery as K8sVolumesListPayload,
|
||||
{
|
||||
queryKey: groupedByRowDataQueryKey,
|
||||
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const {
|
||||
data: groupByFiltersData,
|
||||
isLoading: isLoadingGroupByFilters,
|
||||
} = useGetAggregateKeys(
|
||||
{
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateAttribute: GetK8sEntityToAggregateAttribute(
|
||||
K8sCategory.VOLUMES,
|
||||
const response = await getK8sVolumesList(
|
||||
filters,
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
),
|
||||
aggregateOperator: 'noop',
|
||||
searchText: '',
|
||||
tagType: '',
|
||||
},
|
||||
{
|
||||
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
|
||||
},
|
||||
true,
|
||||
K8sCategory.VOLUMES,
|
||||
);
|
||||
|
||||
const query = useMemo(() => {
|
||||
const baseQuery = getK8sVolumesListQuery();
|
||||
const queryPayload = {
|
||||
...baseQuery,
|
||||
limit: pageSize,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
filters: queryFilters,
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
orderBy: orderBy || baseQuery.orderBy,
|
||||
};
|
||||
if (groupBy.length > 0) {
|
||||
queryPayload.groupBy = groupBy;
|
||||
}
|
||||
return queryPayload;
|
||||
}, [pageSize, currentPage, queryFilters, minTime, maxTime, orderBy, groupBy]);
|
||||
|
||||
const volumesListQueryKey = useMemo(() => {
|
||||
return getVolumesListQueryKey(
|
||||
selectedVolumeUID,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
orderBy,
|
||||
groupBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
);
|
||||
}, [
|
||||
selectedVolumeUID,
|
||||
pageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
groupBy,
|
||||
orderBy,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const formattedGroupedByVolumesData = useMemo(
|
||||
() =>
|
||||
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
|
||||
[groupedByRowData, groupBy],
|
||||
);
|
||||
|
||||
const nestedVolumesData = useMemo(() => {
|
||||
if (!selectedRowData || !groupedByRowData?.payload?.data.records) {
|
||||
return [];
|
||||
}
|
||||
return groupedByRowData?.payload?.data?.records || [];
|
||||
}, [groupedByRowData, selectedRowData]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetK8sVolumesList(
|
||||
query as K8sVolumesListPayload,
|
||||
{
|
||||
queryKey: volumesListQueryKey,
|
||||
enabled: !!query,
|
||||
},
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const volumesData = useMemo(() => data?.payload?.data?.records || [], [data]);
|
||||
const totalCount = data?.payload?.data?.total || 0;
|
||||
|
||||
const formattedVolumesData = useMemo(
|
||||
() => formatDataForTable(volumesData, groupBy),
|
||||
[volumesData, groupBy],
|
||||
);
|
||||
|
||||
const columns = useMemo(() => getK8sVolumesListColumns(groupBy), [groupBy]);
|
||||
|
||||
const handleGroupByRowClick = (record: K8sVolumesRowData): void => {
|
||||
setSelectedRowData(record);
|
||||
|
||||
if (expandedRowKeys.includes(record.key)) {
|
||||
setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.key));
|
||||
} else {
|
||||
setExpandedRowKeys([record.key]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRowData) {
|
||||
fetchGroupedByRowData();
|
||||
}
|
||||
}, [selectedRowData, fetchGroupedByRowData]);
|
||||
|
||||
const handleTableChange: TableProps<K8sVolumesRowData>['onChange'] = useCallback(
|
||||
(
|
||||
pagination: TablePaginationConfig,
|
||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||
sorter: SorterResult<K8sVolumesRowData> | SorterResult<K8sVolumesRowData>[],
|
||||
): void => {
|
||||
if (pagination.current) {
|
||||
setCurrentPage(pagination.current);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Volumes,
|
||||
});
|
||||
}
|
||||
|
||||
if ('field' in sorter && sorter.order) {
|
||||
setOrderBy({
|
||||
columnName: sorter.field as string,
|
||||
order: (sorter.order === 'ascend' ? 'asc' : 'desc') as 'asc' | 'desc',
|
||||
});
|
||||
} else {
|
||||
setOrderBy(null);
|
||||
}
|
||||
},
|
||||
[setCurrentPage, setOrderBy],
|
||||
);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const handleFiltersChange = useCallback(
|
||||
(value: IBuilderQuery['filters']): void => {
|
||||
handleChangeQueryData('filters', value);
|
||||
if (filtersInitialised) {
|
||||
setCurrentPage(1);
|
||||
} else {
|
||||
setFiltersInitialised(true);
|
||||
}
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Volumes,
|
||||
});
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Volumes,
|
||||
total: data?.payload?.data?.total,
|
||||
});
|
||||
}, [data?.payload?.data?.total]);
|
||||
|
||||
const selectedVolumeData = useMemo(() => {
|
||||
if (!selectedVolumeUID) {
|
||||
return null;
|
||||
}
|
||||
if (groupBy.length > 0) {
|
||||
return (
|
||||
nestedVolumesData.find(
|
||||
(volume) => volume.persistentVolumeClaimName === selectedVolumeUID,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
return (
|
||||
volumesData.find(
|
||||
(volume) => volume.persistentVolumeClaimName === selectedVolumeUID,
|
||||
) || null
|
||||
);
|
||||
}, [selectedVolumeUID, volumesData, groupBy.length, nestedVolumesData]);
|
||||
|
||||
const openVolumeInNewTab = (record: K8sVolumesRowData): void => {
|
||||
const newParams = new URLSearchParams(document.location.search);
|
||||
newParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID, record.volumeUID);
|
||||
openInNewTab(
|
||||
buildAbsolutePath({
|
||||
relativePath: '',
|
||||
urlQueryString: newParams.toString(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRowClick = (
|
||||
record: K8sVolumesRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openVolumeInNewTab(record);
|
||||
return;
|
||||
}
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedRowData(null);
|
||||
setselectedVolumeUID(record.volumeUID);
|
||||
} else {
|
||||
handleGroupByRowClick(record);
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Volumes,
|
||||
});
|
||||
};
|
||||
|
||||
const nestedColumns = useMemo(() => getK8sVolumesListColumns([]), []);
|
||||
|
||||
const isGroupedByAttribute = groupBy.length > 0;
|
||||
|
||||
const handleExpandedRowViewAllClick = (): void => {
|
||||
if (!selectedRowData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filters = createFiltersForSelectedRowData(selectedRowData, groupBy);
|
||||
|
||||
handleFiltersChange(filters);
|
||||
|
||||
setCurrentPage(1);
|
||||
setSelectedRowData(null);
|
||||
setGroupBy([]);
|
||||
setOrderBy(null);
|
||||
};
|
||||
|
||||
const expandedRowRender = (): JSX.Element => (
|
||||
<div className="expanded-table-container">
|
||||
{isErrorGroupedByRowData && (
|
||||
<Typography>{groupedByRowData?.error || 'Something went wrong'}</Typography>
|
||||
)}
|
||||
{isFetchingGroupedByRowData || isLoadingGroupedByRowData ? (
|
||||
<LoadingContainer />
|
||||
) : (
|
||||
<div className="expanded-table">
|
||||
<Table
|
||||
columns={nestedColumns as ColumnType<K8sVolumesRowData>[]}
|
||||
dataSource={formattedGroupedByVolumesData}
|
||||
pagination={false}
|
||||
scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
size="small"
|
||||
loading={{
|
||||
spinning: isFetchingGroupedByRowData || isLoadingGroupedByRowData,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
}}
|
||||
showHeader={false}
|
||||
onRow={(
|
||||
record,
|
||||
): { onClick: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
if (event && isModifierKeyPressed(event)) {
|
||||
openVolumeInNewTab(record);
|
||||
return;
|
||||
}
|
||||
setselectedVolumeUID(record.volumeUID);
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
{groupedByRowData?.payload?.data?.total &&
|
||||
groupedByRowData?.payload?.data?.total > 10 ? (
|
||||
<div className="expanded-table-footer">
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
className="periscope-btn secondary"
|
||||
onClick={handleExpandedRowViewAllClick}
|
||||
>
|
||||
View All
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const expandRowIconRenderer = ({
|
||||
expanded,
|
||||
onExpand,
|
||||
record,
|
||||
}: {
|
||||
expanded: boolean;
|
||||
onExpand: (
|
||||
record: K8sVolumesRowData,
|
||||
e: React.MouseEvent<HTMLButtonElement>,
|
||||
) => void;
|
||||
record: K8sVolumesRowData;
|
||||
}): JSX.Element | null => {
|
||||
if (!isGroupedByAttribute) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return expanded ? (
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
|
||||
onExpand(record, e)
|
||||
}
|
||||
role="button"
|
||||
>
|
||||
<ChevronDown size={14} />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>): void =>
|
||||
onExpand(record, e)
|
||||
}
|
||||
role="button"
|
||||
>
|
||||
<ChevronRight size={14} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const handleCloseVolumeDetail = (): void => {
|
||||
setselectedVolumeUID(null);
|
||||
};
|
||||
|
||||
const handleGroupByChange = useCallback(
|
||||
(value: IBuilderQuery['groupBy']) => {
|
||||
const groupBy = [];
|
||||
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
const element = (value[index] as unknown) as string;
|
||||
|
||||
const key = groupByFiltersData?.payload?.attributeKeys?.find(
|
||||
(key) => key.key === element,
|
||||
);
|
||||
|
||||
if (key) {
|
||||
groupBy.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentPage(1);
|
||||
setGroupBy(groupBy);
|
||||
setExpandedRowKeys([]);
|
||||
|
||||
logEvent(InfraMonitoringEvents.GroupByChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Volumes,
|
||||
});
|
||||
return {
|
||||
data: response.payload?.data.records || [],
|
||||
total: response.payload?.data.total || 0,
|
||||
error: response.error,
|
||||
};
|
||||
},
|
||||
[groupByFiltersData?.payload?.attributeKeys, setCurrentPage, setGroupBy],
|
||||
[dotMetricsEnabled],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupByFiltersData?.payload) {
|
||||
setGroupByOptions(
|
||||
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
||||
value: filter.key,
|
||||
label: filter.key,
|
||||
})) || [],
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ data: K8sVolumesData | null; error?: string | null }> => {
|
||||
const response = await getK8sVolumesList(
|
||||
{
|
||||
filters: filters.filters,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
}
|
||||
}, [groupByFiltersData]);
|
||||
|
||||
const onPaginationChange = (page: number, pageSize: number): void => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(pageSize);
|
||||
logEvent(InfraMonitoringEvents.PageNumberChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Volumes,
|
||||
});
|
||||
};
|
||||
const records = response.payload?.data.records || [];
|
||||
|
||||
const showTableLoadingState =
|
||||
(isFetching || isLoading) && formattedVolumesData.length === 0;
|
||||
return {
|
||||
data: records.length > 0 ? records[0] : null,
|
||||
error: response.error,
|
||||
};
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="k8s-list">
|
||||
<K8sHeader
|
||||
isFiltersVisible={isFiltersVisible}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
defaultAddedColumns={defaultAddedColumns}
|
||||
handleFiltersChange={handleFiltersChange}
|
||||
groupByOptions={groupByOptions}
|
||||
isLoadingGroupByFilters={isLoadingGroupByFilters}
|
||||
handleGroupByChange={handleGroupByChange}
|
||||
selectedGroupBy={groupBy}
|
||||
<>
|
||||
<K8sBaseList<K8sVolumesData>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={K8sCategory.VOLUMES}
|
||||
showAutoRefresh={!selectedVolumeData}
|
||||
/>
|
||||
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
|
||||
|
||||
<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: (event: React.MouseEvent) => void; className: string } => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
expandIcon: expandRowIconRenderer,
|
||||
expandedRowKeys,
|
||||
}}
|
||||
tableColumnsDefinitions={k8sVolumesColumns}
|
||||
tableColumns={k8sVolumesColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
renderRowData={k8sVolumesRenderRowData}
|
||||
eventCategory={InfraMonitoringEvents.Volumes}
|
||||
/>
|
||||
|
||||
<VolumeDetails
|
||||
volume={selectedVolumeData}
|
||||
isModalTimeSelection
|
||||
onClose={handleCloseVolumeDetail}
|
||||
<K8sBaseDetails<K8sVolumesData>
|
||||
category={K8sCategory.VOLUMES}
|
||||
eventCategory={InfraMonitoringEvents.Volume}
|
||||
getSelectedItemFilters={k8sVolumeGetSelectedItemFilters}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sVolumeGetEntityName}
|
||||
getInitialLogTracesFilters={k8sVolumeInitialLogTracesFilter}
|
||||
getInitialEventsFilters={k8sVolumeInitialEventsFilter}
|
||||
primaryFilterKeys={k8sVolumeInitialFilters}
|
||||
metadataConfig={k8sVolumeDetailsMetadataConfig}
|
||||
entityWidgetInfo={volumeWidgetInfo}
|
||||
getEntityQueryPayload={getVolumeMetricsQueryPayload}
|
||||
queryKeyPrefix="volume"
|
||||
hideDetailViewTabs
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { Divider, Drawer, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import { X } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import VolumeMetrics from '../../EntityDetailsUtils/EntityMetrics';
|
||||
import { getVolumeQueryPayload, volumeWidgetInfo } from './constants';
|
||||
import { VolumeDetailsProps } from './VoumeDetails.interfaces';
|
||||
|
||||
import '../../EntityDetailsUtils/entityDetails.styles.scss';
|
||||
|
||||
function VolumeDetails({
|
||||
volume,
|
||||
onClose,
|
||||
isModalTimeSelection,
|
||||
}: VolumeDetailsProps): JSX.Element {
|
||||
const { maxTime, minTime, selectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const startMs = useMemo(() => Math.floor(Number(minTime) / 1000000000), [
|
||||
minTime,
|
||||
]);
|
||||
const endMs = useMemo(() => Math.floor(Number(maxTime) / 1000000000), [
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const [modalTimeRange, setModalTimeRange] = useState(() => ({
|
||||
startTime: startMs,
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: (selectedTime as Time),
|
||||
);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
useEffect(() => {
|
||||
if (volume) {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Volume,
|
||||
});
|
||||
}
|
||||
}, [volume]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
setSelectedInterval(currentSelectedInterval as Time);
|
||||
|
||||
if (currentSelectedInterval !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
endTime: Math.floor(maxTime / 1000000000),
|
||||
});
|
||||
}
|
||||
}, [selectedTime, minTime, maxTime]);
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
setSelectedInterval(interval as Time);
|
||||
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(dateTimeRange[0] / 1000),
|
||||
endTime: Math.floor(dateTimeRange[1] / 1000),
|
||||
});
|
||||
} else {
|
||||
const { maxTime, minTime } = GetMinMax(interval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
endTime: Math.floor(maxTime / 1000000000),
|
||||
});
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.TimeUpdated, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: InfraMonitoringEvents.Volume,
|
||||
interval,
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleClose = (): void => {
|
||||
lastSelectedInterval.current = null;
|
||||
setSelectedInterval(selectedTime as Time);
|
||||
|
||||
if (selectedTime !== 'custom') {
|
||||
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / 1000000000),
|
||||
endTime: Math.floor(maxTime / 1000000000),
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width="70%"
|
||||
title={
|
||||
<>
|
||||
<Divider type="vertical" />
|
||||
<Typography.Text className="title">
|
||||
{volume?.persistentVolumeClaimName}
|
||||
</Typography.Text>
|
||||
</>
|
||||
}
|
||||
placement="right"
|
||||
onClose={handleClose}
|
||||
open={!!volume}
|
||||
style={{
|
||||
overscrollBehavior: 'contain',
|
||||
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||
}}
|
||||
className="entity-detail-drawer"
|
||||
destroyOnClose
|
||||
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||
>
|
||||
{volume && (
|
||||
<>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
PVC Name
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Cluster Name
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
Namespace Name
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="values-row">
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={volume.persistentVolumeClaimName}>
|
||||
{volume.persistentVolumeClaimName}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={volume.meta.k8s_cluster_name}>
|
||||
{volume.meta.k8s_cluster_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text className="entity-details-metadata-value">
|
||||
<Tooltip title={volume.meta.k8s_namespace_name}>
|
||||
{volume.meta.k8s_namespace_name}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VolumeMetrics
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
entity={volume}
|
||||
entityWidgetInfo={volumeWidgetInfo}
|
||||
getEntityQueryPayload={getVolumeQueryPayload}
|
||||
queryKey="volumeMetrics"
|
||||
category={K8sCategory.VOLUMES}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default VolumeDetails;
|
||||
@@ -1,7 +0,0 @@
|
||||
import { K8sVolumesData } from 'api/infraMonitoring/getK8sVolumesList';
|
||||
|
||||
export type VolumeDetailsProps = {
|
||||
volume: K8sVolumesData | null;
|
||||
isModalTimeSelection: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
import VolumeDetails from './VolumeDetails';
|
||||
|
||||
export default VolumeDetails;
|
||||
@@ -1,22 +1,10 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { UnderscoreToDotMap } from 'api/utils';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { UnderscoreToDotMap } from '../utils';
|
||||
|
||||
export interface K8sVolumesListPayload {
|
||||
filters: TagFilter;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
}
|
||||
import { K8sBaseFilters } from '../Base/K8sBaseList';
|
||||
|
||||
export interface K8sVolumesData {
|
||||
persistentVolumeClaimName: string;
|
||||
@@ -68,10 +56,8 @@ export const volumesMetaMap: Array<{
|
||||
export function mapVolumesMeta(
|
||||
rawMeta: Record<string, unknown>,
|
||||
): K8sVolumesData['meta'] {
|
||||
// start with everything that was already there
|
||||
const out: Record<string, unknown> = { ...rawMeta };
|
||||
|
||||
// for each dot→under rule, if the raw has the dot, overwrite the underscore
|
||||
volumesMetaMap.forEach(({ dot, under }) => {
|
||||
if (dot in rawMeta) {
|
||||
const val = rawMeta[dot];
|
||||
@@ -83,42 +69,47 @@ export function mapVolumesMeta(
|
||||
}
|
||||
|
||||
export const getK8sVolumesList = async (
|
||||
props: K8sVolumesListPayload,
|
||||
props: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
): Promise<SuccessResponse<K8sVolumesListResponse> | ErrorResponse> => {
|
||||
try {
|
||||
// Prepare filters
|
||||
const { orderBy, ...rest } = props;
|
||||
const basePayload = {
|
||||
...rest,
|
||||
filters: props.filters ?? { items: [], op: 'and' },
|
||||
...(orderBy != null ? { orderBy } : {}),
|
||||
};
|
||||
|
||||
const requestProps =
|
||||
dotMetricsEnabled && Array.isArray(props.filters?.items)
|
||||
dotMetricsEnabled && Array.isArray(basePayload.filters?.items)
|
||||
? {
|
||||
...props,
|
||||
...basePayload,
|
||||
filters: {
|
||||
...props.filters,
|
||||
items: props.filters.items.reduce<typeof props.filters.items>(
|
||||
(acc, item) => {
|
||||
if (item.value === undefined) {
|
||||
return acc;
|
||||
}
|
||||
if (
|
||||
item.key &&
|
||||
typeof item.key === 'object' &&
|
||||
'key' in item.key &&
|
||||
typeof item.key.key === 'string'
|
||||
) {
|
||||
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
|
||||
acc.push({ ...item, key: { ...item.key, key: mappedKey } });
|
||||
} else {
|
||||
acc.push(item);
|
||||
}
|
||||
...basePayload.filters,
|
||||
items: basePayload.filters.items.reduce<
|
||||
typeof basePayload.filters.items
|
||||
>((acc, item) => {
|
||||
if (item.value === undefined) {
|
||||
return acc;
|
||||
},
|
||||
[] as typeof props.filters.items,
|
||||
),
|
||||
}
|
||||
if (
|
||||
item.key &&
|
||||
typeof item.key === 'object' &&
|
||||
'key' in item.key &&
|
||||
typeof item.key.key === 'string'
|
||||
) {
|
||||
const mappedKey = UnderscoreToDotMap[item.key.key] ?? item.key.key;
|
||||
acc.push({ ...item, key: { ...item.key, key: mappedKey } });
|
||||
} else {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
}, [] as typeof basePayload.filters.items),
|
||||
},
|
||||
}
|
||||
: props;
|
||||
: basePayload;
|
||||
|
||||
const response = await axios.post('/pvcs/list', requestProps, {
|
||||
signal,
|
||||
@@ -1,11 +1,75 @@
|
||||
import { K8sVolumesData } from 'api/infraMonitoring/getK8sVolumesList';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
createFilterItem,
|
||||
K8sDetailsMetadataConfig,
|
||||
} from '../Base/K8sBaseDetails';
|
||||
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
|
||||
import { K8sVolumesData } from './api';
|
||||
|
||||
export const k8sVolumeGetSelectedItemFilters = (
|
||||
selectedItemId: string,
|
||||
): TagFilter => ({
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: 'k8s_persistentvolumeclaim_name',
|
||||
key: {
|
||||
key: 'k8s_persistentvolumeclaim_name',
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: selectedItemId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const k8sVolumeDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sVolumesData>[] = [
|
||||
{
|
||||
label: 'PVC Name',
|
||||
getValue: (p): string => p.persistentVolumeClaimName,
|
||||
},
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
getValue: (p): string => p.meta.k8s_cluster_name,
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
getValue: (p): string => p.meta.k8s_namespace_name,
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sVolumeInitialFilters = [
|
||||
QUERY_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME,
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
];
|
||||
|
||||
export const k8sVolumeInitialEventsFilter = (
|
||||
item: K8sVolumesData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'PersistentVolumeClaim'),
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.persistentVolumeClaimName),
|
||||
];
|
||||
|
||||
export const k8sVolumeInitialLogTracesFilter = (
|
||||
item: K8sVolumesData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(
|
||||
QUERY_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME,
|
||||
item.meta.k8s_persistentvolumeclaim_name,
|
||||
),
|
||||
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.k8s_namespace_name),
|
||||
];
|
||||
|
||||
export const k8sVolumeGetEntityName = (item: K8sVolumesData): string =>
|
||||
item.persistentVolumeClaimName;
|
||||
|
||||
export const volumeWidgetInfo = [
|
||||
{
|
||||
title: 'Volume available',
|
||||
@@ -29,7 +93,7 @@ export const volumeWidgetInfo = [
|
||||
},
|
||||
];
|
||||
|
||||
export const getVolumeQueryPayload = (
|
||||
export const getVolumeMetricsQueryPayload = (
|
||||
volume: K8sVolumesData,
|
||||
start: number,
|
||||
end: number,
|
||||
@@ -0,0 +1,6 @@
|
||||
.entityGroupHeader {
|
||||
padding-left: var(--spacing-5);
|
||||
gap: var(--spacing-5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
164
frontend/src/container/InfraMonitoringK8s/Volumes/table.tsx
Normal file
164
frontend/src/container/InfraMonitoringK8s/Volumes/table.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import { TableColumnType as ColumnType, Tooltip } from 'antd';
|
||||
import { Group } from 'lucide-react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { K8sRenderedRowData } from '../Base/K8sBaseList';
|
||||
import { IEntityColumn } from '../Base/useInfraMonitoringTableColumnsStore';
|
||||
import { getGroupByEl, getGroupedByMeta, getRowKey } from '../Base/utils';
|
||||
import { formatBytes, ValidateColumnValueWrapper } from '../commonUtils';
|
||||
import { K8sVolumesData } from './api';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
|
||||
export const k8sVolumesColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'Volume Group',
|
||||
value: 'volumeGroup',
|
||||
id: 'volumeGroup',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-collapse',
|
||||
},
|
||||
{
|
||||
label: 'PVC Name',
|
||||
value: 'pvcName',
|
||||
id: 'pvcName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'hidden-on-expand',
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
value: 'namespaceName',
|
||||
id: 'namespaceName',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Volume Capacity',
|
||||
value: 'capacity',
|
||||
id: 'capacity',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Volume Utilization',
|
||||
value: 'usage',
|
||||
id: 'usage',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
{
|
||||
label: 'Volume Available',
|
||||
value: 'available',
|
||||
id: 'available',
|
||||
canBeHidden: false,
|
||||
defaultVisibility: true,
|
||||
behavior: 'always-visible',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sVolumesColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
|
||||
{
|
||||
title: (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
<Group size={14} /> VOLUME GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'volumeGroup',
|
||||
key: 'volumeGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
},
|
||||
{
|
||||
title: <div>PVC Name</div>,
|
||||
dataIndex: 'pvcName',
|
||||
key: 'pvcName',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Namespace Name</div>,
|
||||
dataIndex: 'namespaceName',
|
||||
key: 'namespaceName',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Volume Capacity</div>,
|
||||
dataIndex: 'capacity',
|
||||
key: 'capacity',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Volume Utilization</div>,
|
||||
dataIndex: 'usage',
|
||||
key: 'usage',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div>Volume Available</div>,
|
||||
dataIndex: 'available',
|
||||
key: 'available',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sVolumesRenderRowData = (
|
||||
volume: K8sVolumesData,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): K8sRenderedRowData => ({
|
||||
key: getRowKey(
|
||||
volume,
|
||||
() =>
|
||||
volume.persistentVolumeClaimName ||
|
||||
volume.meta.k8s_persistentvolumeclaim_name ||
|
||||
'',
|
||||
groupBy,
|
||||
),
|
||||
itemKey: volume.persistentVolumeClaimName,
|
||||
pvcName: (
|
||||
<Tooltip title={volume.persistentVolumeClaimName}>
|
||||
{volume.persistentVolumeClaimName || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
namespaceName: (
|
||||
<Tooltip title={volume.meta.k8s_namespace_name}>
|
||||
{volume.meta.k8s_namespace_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
available: (
|
||||
<ValidateColumnValueWrapper value={volume.volumeAvailable}>
|
||||
{formatBytes(volume.volumeAvailable)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
capacity: (
|
||||
<ValidateColumnValueWrapper value={volume.volumeCapacity}>
|
||||
{formatBytes(volume.volumeCapacity)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
usage: (
|
||||
<ValidateColumnValueWrapper value={volume.volumeUsage}>
|
||||
{formatBytes(volume.volumeUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
volumeGroup: getGroupByEl(volume, groupBy),
|
||||
...volume.meta,
|
||||
groupedByMeta: getGroupedByMeta(volume, groupBy),
|
||||
});
|
||||
@@ -1,285 +0,0 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
|
||||
import {
|
||||
K8sVolumesData,
|
||||
K8sVolumesListPayload,
|
||||
} from 'api/infraMonitoring/getK8sVolumesList';
|
||||
import { Group } from 'lucide-react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { formatBytes, ValidateColumnValueWrapper } from '../commonUtils';
|
||||
import { IEntityColumn } from '../utils';
|
||||
|
||||
export const defaultAddedColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'PVC Name',
|
||||
value: 'pvcName',
|
||||
id: 'pvcName',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
value: 'namespaceName',
|
||||
id: 'namespaceName',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Volume Capacity',
|
||||
value: 'capacity',
|
||||
id: 'capacity',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Volume Utilization',
|
||||
value: 'usage',
|
||||
id: 'usage',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Volume Available',
|
||||
value: 'available',
|
||||
id: 'available',
|
||||
canRemove: false,
|
||||
},
|
||||
];
|
||||
|
||||
export interface K8sVolumesRowData {
|
||||
key: string;
|
||||
volumeUID: string;
|
||||
pvcName: React.ReactNode;
|
||||
namespaceName: React.ReactNode;
|
||||
capacity: React.ReactNode;
|
||||
usage: React.ReactNode;
|
||||
available: React.ReactNode;
|
||||
groupedByMeta?: any;
|
||||
}
|
||||
|
||||
const volumeGroupColumnConfig = {
|
||||
title: (
|
||||
<div className="column-header entity-group-header">
|
||||
<Group size={14} /> VOLUME GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'volumeGroup',
|
||||
key: 'volumeGroup',
|
||||
ellipsis: true,
|
||||
width: 150,
|
||||
align: 'left',
|
||||
sorter: false,
|
||||
className: 'column entity-group-header',
|
||||
};
|
||||
|
||||
export const getK8sVolumesListQuery = (): K8sVolumesListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
orderBy: { columnName: 'usage', order: 'desc' },
|
||||
});
|
||||
|
||||
export const getVolumeListGroupedByRowDataQueryKey = (
|
||||
groupedByMeta: K8sVolumesData['meta'] | undefined,
|
||||
queryFilters: IBuilderQuery['filters'],
|
||||
orderBy: { columnName: string; order: 'asc' | 'desc' } | null,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
minTime: number,
|
||||
maxTime: number,
|
||||
): (string | undefined)[] => {
|
||||
// When we have grouped by metadata defined
|
||||
// We need to leave out the min/max time
|
||||
// Otherwise it will cause a loop
|
||||
const groupedByMetaStr = JSON.stringify(groupedByMeta || undefined) ?? '';
|
||||
if (groupedByMetaStr) {
|
||||
return [
|
||||
'volumeList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
groupedByMetaStr,
|
||||
];
|
||||
}
|
||||
return [
|
||||
'volumeList',
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
groupedByMetaStr,
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
};
|
||||
|
||||
export const getVolumesListQueryKey = (
|
||||
selectedVolumeUID: string | null,
|
||||
pageSize: number,
|
||||
currentPage: number,
|
||||
queryFilters: IBuilderQuery['filters'],
|
||||
orderBy: { columnName: string; order: 'asc' | 'desc' } | null,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
minTime: number,
|
||||
maxTime: number,
|
||||
): (string | undefined)[] => {
|
||||
// When selected volume is defined
|
||||
// We need to leave out the min/max time
|
||||
// Otherwise it will cause a loop
|
||||
if (selectedVolumeUID) {
|
||||
return [
|
||||
'volumeList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'volumeList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
};
|
||||
|
||||
const columnsConfig = [
|
||||
{
|
||||
title: <div className="column-header-left pvc-name-header">PVC Name</div>,
|
||||
dataIndex: 'pvcName',
|
||||
key: 'pvcName',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<div className="column-header-left namespace-name-header">
|
||||
Namespace Name
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'namespaceName',
|
||||
key: 'namespaceName',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left small-col">Volume Capacity</div>,
|
||||
dataIndex: 'capacity',
|
||||
key: 'capacity',
|
||||
ellipsis: true,
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left small-col">Volume Utilization</div>,
|
||||
dataIndex: 'usage',
|
||||
key: 'usage',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header-left small-col">Volume Available</div>,
|
||||
dataIndex: 'available',
|
||||
key: 'available',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
|
||||
export const getK8sVolumesListColumns = (
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): ColumnType<K8sVolumesRowData>[] => {
|
||||
if (groupBy.length > 0) {
|
||||
const filteredColumns = [...columnsConfig].filter(
|
||||
(column) => column.key !== 'pvcName',
|
||||
);
|
||||
filteredColumns.unshift(volumeGroupColumnConfig);
|
||||
return filteredColumns as ColumnType<K8sVolumesRowData>[];
|
||||
}
|
||||
|
||||
return columnsConfig as ColumnType<K8sVolumesRowData>[];
|
||||
};
|
||||
|
||||
const dotToUnder: Record<string, keyof K8sVolumesData['meta']> = {
|
||||
'k8s.namespace.name': 'k8s_namespace_name',
|
||||
'k8s.node.name': 'k8s_node_name',
|
||||
'k8s.pod.name': 'k8s_pod_name',
|
||||
'k8s.pod.uid': 'k8s_pod_uid',
|
||||
'k8s.statefulset.name': 'k8s_statefulset_name',
|
||||
'k8s.cluster.name': 'k8s_cluster_name',
|
||||
'k8s.persistentvolumeclaim.name': 'k8s_persistentvolumeclaim_name',
|
||||
};
|
||||
|
||||
const getGroupByEle = (
|
||||
volume: K8sVolumesData,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): React.ReactNode => {
|
||||
const groupByValues: string[] = [];
|
||||
|
||||
groupBy.forEach((group) => {
|
||||
const rawKey = group.key as string;
|
||||
|
||||
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof volume.meta;
|
||||
|
||||
const value = volume.meta[metaKey];
|
||||
|
||||
groupByValues.push(value);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="pod-group">
|
||||
{groupByValues.map((value) => (
|
||||
<Tag key={value} color={Color.BG_SLATE_400} className="pod-group-tag-item">
|
||||
{value === '' ? '<no-value>' : value}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const formatDataForTable = (
|
||||
data: K8sVolumesData[],
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): K8sVolumesRowData[] =>
|
||||
data.map((volume, index) => ({
|
||||
key: index.toString(),
|
||||
volumeUID: volume.persistentVolumeClaimName,
|
||||
pvcName: (
|
||||
<Tooltip title={volume.persistentVolumeClaimName}>
|
||||
{volume.persistentVolumeClaimName || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
namespaceName: (
|
||||
<Tooltip title={volume.meta.k8s_namespace_name}>
|
||||
{volume.meta.k8s_namespace_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
available: (
|
||||
<ValidateColumnValueWrapper value={volume.volumeAvailable}>
|
||||
{formatBytes(volume.volumeAvailable)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
capacity: (
|
||||
<ValidateColumnValueWrapper value={volume.volumeCapacity}>
|
||||
{formatBytes(volume.volumeCapacity)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
usage: (
|
||||
<ValidateColumnValueWrapper value={volume.volumeUsage}>
|
||||
{formatBytes(volume.volumeUsage)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
volumeGroup: getGroupByEle(volume, groupBy),
|
||||
meta: volume.meta,
|
||||
...volume.meta,
|
||||
groupedByMeta: volume.meta,
|
||||
}));
|
||||
@@ -1,155 +0,0 @@
|
||||
/**
|
||||
* Tests for URL parameter parsing in K8s Infra Monitoring components.
|
||||
*
|
||||
* These tests verify the fix for the double URL decoding bug where
|
||||
* components were calling decodeURIComponent() on values already
|
||||
* decoded by URLSearchParams.get(), causing crashes on K8s parameters
|
||||
* with special characters.
|
||||
*/
|
||||
|
||||
import { getFiltersFromParams } from '../../commonUtils';
|
||||
|
||||
describe('K8sPodsList URL Parameter Parsing', () => {
|
||||
describe('getFiltersFromParams', () => {
|
||||
it('should return null when no filters in params', () => {
|
||||
const searchParams = new URLSearchParams();
|
||||
const result = getFiltersFromParams(searchParams, 'filters');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should parse filters from URL params', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { key: 'k8s_namespace_name' },
|
||||
op: '=',
|
||||
value: 'default',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('filters', JSON.stringify(filters));
|
||||
|
||||
const result = getFiltersFromParams(searchParams, 'filters');
|
||||
expect(result).toEqual(filters);
|
||||
});
|
||||
|
||||
it('should handle URL-encoded filters (auto-decoded by URLSearchParams)', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { key: 'k8s_pod_name' },
|
||||
op: 'contains',
|
||||
value: 'api-server',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
const encodedValue = encodeURIComponent(JSON.stringify(filters));
|
||||
const searchParams = new URLSearchParams(`filters=${encodedValue}`);
|
||||
|
||||
const result = getFiltersFromParams(searchParams, 'filters');
|
||||
expect(result).toEqual(filters);
|
||||
});
|
||||
|
||||
it('should return null on malformed JSON instead of crashing', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('filters', '{invalid-json}');
|
||||
|
||||
const result = getFiltersFromParams(searchParams, 'filters');
|
||||
expect(result).toBeNull();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should handle filters with K8s container image names', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { key: 'k8s_container_name' },
|
||||
op: '=',
|
||||
value: 'registry.k8s.io/coredns/coredns:v1.10.1',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
const encodedValue = encodeURIComponent(JSON.stringify(filters));
|
||||
const searchParams = new URLSearchParams(`filters=${encodedValue}`);
|
||||
|
||||
const result = getFiltersFromParams(searchParams, 'filters');
|
||||
expect(result).toEqual(filters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('regression: double decoding issue', () => {
|
||||
it('should not crash when URL params are already decoded by URLSearchParams', () => {
|
||||
// The key bug: URLSearchParams.get() auto-decodes, so encoding once in URL
|
||||
// means .get() returns decoded value. Old code called decodeURIComponent()
|
||||
// again which could crash on certain characters.
|
||||
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { key: 'k8s_namespace_name' },
|
||||
op: '=',
|
||||
value: 'kube-system',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const encodedValue = encodeURIComponent(JSON.stringify(filters));
|
||||
const searchParams = new URLSearchParams(`filters=${encodedValue}`);
|
||||
|
||||
// This should work without crashing
|
||||
const result = getFiltersFromParams(searchParams, 'filters');
|
||||
expect(result).toEqual(filters);
|
||||
});
|
||||
|
||||
it('should handle values with percent signs in labels', () => {
|
||||
// K8s labels might contain literal "%" characters like "cpu-usage-50%"
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { key: 'k8s_label' },
|
||||
op: '=',
|
||||
value: 'cpu-50%',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const encodedValue = encodeURIComponent(JSON.stringify(filters));
|
||||
const searchParams = new URLSearchParams(`filters=${encodedValue}`);
|
||||
|
||||
const result = getFiltersFromParams(searchParams, 'filters');
|
||||
expect(result).toEqual(filters);
|
||||
});
|
||||
|
||||
it('should handle complex K8s deployment names with special chars', () => {
|
||||
const filters = {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { key: 'k8s_deployment_name' },
|
||||
op: '=',
|
||||
value: 'nginx-ingress-controller',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
const encodedValue = encodeURIComponent(JSON.stringify(filters));
|
||||
const searchParams = new URLSearchParams(`filters=${encodedValue}`);
|
||||
|
||||
const result = getFiltersFromParams(searchParams, 'filters');
|
||||
expect(result).toEqual(filters);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,134 +0,0 @@
|
||||
import setupCommonMocks from '../../commonMocks';
|
||||
|
||||
setupCommonMocks();
|
||||
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import PodDetails from 'container/InfraMonitoringK8s/Pods/PodDetails/PodDetails';
|
||||
import { withNuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import store from 'store';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const Wrapper = withNuqsTestingAdapter({ searchParams: {} });
|
||||
|
||||
describe('PodDetails', () => {
|
||||
const mockPod = {
|
||||
podName: 'test-pod',
|
||||
meta: {
|
||||
k8s_cluster_name: 'test-cluster',
|
||||
k8s_namespace_name: 'test-namespace',
|
||||
k8s_node_name: 'test-node',
|
||||
},
|
||||
} as any;
|
||||
const mockOnClose = jest.fn();
|
||||
|
||||
it('should render modal with relevant metadata', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
<PodDetails pod={mockPod} isModalTimeSelection onClose={mockOnClose} />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const clusterNameElements = screen.getAllByText('test-cluster');
|
||||
expect(clusterNameElements.length).toBeGreaterThan(0);
|
||||
expect(clusterNameElements[0]).toBeInTheDocument();
|
||||
|
||||
const namespaceNameElements = screen.getAllByText('test-namespace');
|
||||
expect(namespaceNameElements.length).toBeGreaterThan(0);
|
||||
expect(namespaceNameElements[0]).toBeInTheDocument();
|
||||
|
||||
const nodeNameElements = screen.getAllByText('test-node');
|
||||
expect(nodeNameElements.length).toBeGreaterThan(0);
|
||||
expect(nodeNameElements[0]).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render modal with 4 tabs', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
<PodDetails pod={mockPod} isModalTimeSelection onClose={mockOnClose} />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const metricsTab = screen.getByText('Metrics');
|
||||
expect(metricsTab).toBeInTheDocument();
|
||||
|
||||
const eventsTab = screen.getByText('Events');
|
||||
expect(eventsTab).toBeInTheDocument();
|
||||
|
||||
const logsTab = screen.getByText('Logs');
|
||||
expect(logsTab).toBeInTheDocument();
|
||||
|
||||
const tracesTab = screen.getByText('Traces');
|
||||
expect(tracesTab).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('default tab should be metrics', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
<PodDetails pod={mockPod} isModalTimeSelection onClose={mockOnClose} />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const metricsTab = screen.getByRole('radio', { name: 'Metrics' });
|
||||
expect(metricsTab).toBeChecked();
|
||||
});
|
||||
|
||||
it('should switch to events tab when events tab is clicked', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
<PodDetails pod={mockPod} isModalTimeSelection onClose={mockOnClose} />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const eventsTab = screen.getByRole('radio', { name: 'Events' });
|
||||
expect(eventsTab).not.toBeChecked();
|
||||
fireEvent.click(eventsTab);
|
||||
expect(eventsTab).toBeChecked();
|
||||
});
|
||||
|
||||
it('should close modal when close button is clicked', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
<PodDetails pod={mockPod} isModalTimeSelection onClose={mockOnClose} />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const closeButton = screen.getByRole('button', { name: 'Close' });
|
||||
fireEvent.click(closeButton);
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,390 +0,0 @@
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { MemoryRouter } from 'react-router-dom-v5-compat';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import K8sVolumesList from 'container/InfraMonitoringK8s/Volumes/K8sVolumesList';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { IAppContext, IUser } from 'providers/App/types';
|
||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { applyMiddleware, legacy_createStore as createStore } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import reducers from 'store/reducers';
|
||||
import { act, render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
||||
import { LicenseResModel } from 'types/api/licensesV3/getActive';
|
||||
|
||||
import { INFRA_MONITORING_K8S_PARAMS_KEYS } from '../../constants';
|
||||
|
||||
const SERVER_URL = 'http://localhost/api';
|
||||
|
||||
// jsdom does not implement IntersectionObserver — provide a no-op stub
|
||||
const mockObserver = {
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
};
|
||||
global.IntersectionObserver = jest
|
||||
.fn()
|
||||
.mockImplementation(() => mockObserver) as any;
|
||||
|
||||
const mockVolume = {
|
||||
persistentVolumeClaimName: 'test-pvc',
|
||||
volumeAvailable: 1000000,
|
||||
volumeCapacity: 5000000,
|
||||
volumeInodes: 100,
|
||||
volumeInodesFree: 50,
|
||||
volumeInodesUsed: 50,
|
||||
volumeUsage: 4000000,
|
||||
meta: {
|
||||
k8s_cluster_name: 'test-cluster',
|
||||
k8s_namespace_name: 'test-namespace',
|
||||
k8s_node_name: 'test-node',
|
||||
k8s_persistentvolumeclaim_name: 'test-pvc',
|
||||
k8s_pod_name: 'test-pod',
|
||||
k8s_pod_uid: 'test-pod-uid',
|
||||
k8s_statefulset_name: '',
|
||||
},
|
||||
};
|
||||
|
||||
const mockVolumesResponse = {
|
||||
status: 'success',
|
||||
data: {
|
||||
type: '',
|
||||
records: [mockVolume],
|
||||
groups: null,
|
||||
total: 1,
|
||||
sentAnyHostMetricsData: false,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
},
|
||||
};
|
||||
|
||||
/** Renders K8sVolumesList with a real Redux store so dispatched actions affect state. */
|
||||
function renderWithRealStore(
|
||||
initialEntries?: Record<string, any>,
|
||||
): { testStore: ReturnType<typeof createStore> } {
|
||||
const testStore = createStore(reducers, applyMiddleware(thunk as any));
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
render(
|
||||
<NuqsTestingAdapter searchParams={initialEntries}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<QueryBuilderProvider>
|
||||
<MemoryRouter>
|
||||
<K8sVolumesList
|
||||
isFiltersVisible={false}
|
||||
handleFilterVisibilityChange={jest.fn()}
|
||||
quickFiltersLastUpdated={-1}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</QueryBuilderProvider>
|
||||
</QueryClientProvider>
|
||||
</NuqsTestingAdapter>,
|
||||
);
|
||||
|
||||
return { testStore };
|
||||
}
|
||||
|
||||
describe('K8sVolumesList - useGetAggregateKeys Category Regression', () => {
|
||||
let requestsMade: Array<{
|
||||
url: string;
|
||||
params: URLSearchParams;
|
||||
body?: any;
|
||||
}> = [];
|
||||
|
||||
beforeEach(() => {
|
||||
requestsMade = [];
|
||||
|
||||
server.use(
|
||||
rest.get(`${SERVER_URL}/v3/autocomplete/attribute_keys`, (req, res, ctx) => {
|
||||
const url = req.url.toString();
|
||||
const params = req.url.searchParams;
|
||||
|
||||
requestsMade.push({
|
||||
url,
|
||||
params,
|
||||
});
|
||||
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
attributeKeys: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}),
|
||||
rest.post(`${SERVER_URL}/v1/pvcs/list`, (req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
type: 'list',
|
||||
records: [],
|
||||
groups: null,
|
||||
total: 0,
|
||||
sentAnyHostMetricsData: false,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('should call aggregate keys API with k8s_volume_capacity', async () => {
|
||||
renderWithRealStore();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(requestsMade.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Find the attribute_keys request
|
||||
const attributeKeysRequest = requestsMade.find((req) =>
|
||||
req.url.includes('/autocomplete/attribute_keys'),
|
||||
);
|
||||
|
||||
expect(attributeKeysRequest).toBeDefined();
|
||||
|
||||
const aggregateAttribute = attributeKeysRequest?.params.get(
|
||||
'aggregateAttribute',
|
||||
);
|
||||
|
||||
expect(aggregateAttribute).toBe('k8s_volume_capacity');
|
||||
});
|
||||
|
||||
it('should call aggregate keys API with k8s.volume.capacity when dotMetrics enabled', async () => {
|
||||
jest
|
||||
.spyOn(await import('providers/App/App'), 'useAppContext')
|
||||
.mockReturnValue({
|
||||
featureFlags: [
|
||||
{
|
||||
name: FeatureKeys.DOT_METRICS_ENABLED,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: 0,
|
||||
route: '',
|
||||
},
|
||||
],
|
||||
user: { role: 'ADMIN' } as IUser,
|
||||
activeLicense: (null as unknown) as LicenseResModel,
|
||||
} as IAppContext);
|
||||
|
||||
renderWithRealStore();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(requestsMade.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const attributeKeysRequest = requestsMade.find((req) =>
|
||||
req.url.includes('/autocomplete/attribute_keys'),
|
||||
);
|
||||
|
||||
expect(attributeKeysRequest).toBeDefined();
|
||||
|
||||
const aggregateAttribute = attributeKeysRequest?.params.get(
|
||||
'aggregateAttribute',
|
||||
);
|
||||
|
||||
expect(aggregateAttribute).toBe('k8s.volume.capacity');
|
||||
});
|
||||
});
|
||||
|
||||
describe('K8sVolumesList', () => {
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.post('http://localhost/api/v1/pvcs/list', (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockVolumesResponse)),
|
||||
),
|
||||
rest.get(
|
||||
'http://localhost/api/v3/autocomplete/attribute_keys',
|
||||
(_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ data: { attributeKeys: [] } })),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('renders volume rows from API response', async () => {
|
||||
renderWithRealStore();
|
||||
|
||||
await waitFor(async () => {
|
||||
const elements = await screen.findAllByText('test-pvc');
|
||||
|
||||
expect(elements.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('opens VolumeDetails when a volume row is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithRealStore();
|
||||
|
||||
const pvcCells = await screen.findAllByText('test-pvc');
|
||||
expect(pvcCells.length).toBeGreaterThan(0);
|
||||
|
||||
const row = pvcCells[0].closest('tr');
|
||||
expect(row).not.toBeNull();
|
||||
await user.click(row!);
|
||||
|
||||
await waitFor(async () => {
|
||||
const cells = await screen.findAllByText('test-pvc');
|
||||
expect(cells.length).toBeGreaterThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('closes VolumeDetails when the close button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithRealStore();
|
||||
|
||||
const pvcCells = await screen.findAllByText('test-pvc');
|
||||
expect(pvcCells.length).toBeGreaterThan(0);
|
||||
|
||||
const row = pvcCells[0].closest('tr');
|
||||
await user.click(row!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Close' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'Close' }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not re-fetch the volumes list when time range changes after selecting a volume', async () => {
|
||||
const user = userEvent.setup();
|
||||
let apiCallCount = 0;
|
||||
server.use(
|
||||
rest.post('http://localhost/api/v1/pvcs/list', async (_req, res, ctx) => {
|
||||
apiCallCount += 1;
|
||||
return res(ctx.status(200), ctx.json(mockVolumesResponse));
|
||||
}),
|
||||
);
|
||||
|
||||
const { testStore } = renderWithRealStore();
|
||||
|
||||
await waitFor(() => expect(apiCallCount).toBe(1));
|
||||
|
||||
const pvcCells = await screen.findAllByText('test-pvc');
|
||||
const row = pvcCells[0].closest('tr');
|
||||
await user.click(row!);
|
||||
await waitFor(async () => {
|
||||
const cells = await screen.findAllByText('test-pvc');
|
||||
expect(cells.length).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
// Wait for nuqs URL state to fully propagate to the component
|
||||
// The selectedVolumeUID is managed via nuqs (async URL state),
|
||||
// so we need to ensure the state has settled before dispatching time changes
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
});
|
||||
|
||||
const countAfterClick = apiCallCount;
|
||||
|
||||
// There's a specific component causing the min/max time to be updated
|
||||
// After the volume loads, it triggers the change again
|
||||
// And then the query to fetch data for the selected volume enters in a loop
|
||||
act(() => {
|
||||
testStore.dispatch({
|
||||
type: UPDATE_TIME_INTERVAL,
|
||||
payload: {
|
||||
minTime: Date.now() * 1000000 - 30 * 60 * 1000 * 1000000,
|
||||
maxTime: Date.now() * 1000000,
|
||||
selectedTime: '30m',
|
||||
},
|
||||
} as any);
|
||||
});
|
||||
|
||||
// Allow any potential re-fetch to settle
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
|
||||
expect(apiCallCount).toBe(countAfterClick);
|
||||
});
|
||||
|
||||
it('does not re-fetch groupedByRowData when time range changes after expanding a volume row with groupBy', async () => {
|
||||
const user = userEvent.setup();
|
||||
const groupByValue = [{ key: 'k8s_namespace_name' }];
|
||||
|
||||
let groupedByRowDataCallCount = 0;
|
||||
server.use(
|
||||
rest.post('http://localhost/api/v1/pvcs/list', async (req, res, ctx) => {
|
||||
const body = await req.json();
|
||||
// Check for both underscore and dot notation keys since dotMetricsEnabled
|
||||
// may be true or false depending on test order
|
||||
const isGroupedByRowDataRequest = body.filters?.items?.some(
|
||||
(item: { key?: { key?: string }; value?: string }) =>
|
||||
(item.key?.key === 'k8s_namespace_name' ||
|
||||
item.key?.key === 'k8s.namespace.name') &&
|
||||
item.value === 'test-namespace',
|
||||
);
|
||||
if (isGroupedByRowDataRequest) {
|
||||
groupedByRowDataCallCount += 1;
|
||||
}
|
||||
return res(ctx.status(200), ctx.json(mockVolumesResponse));
|
||||
}),
|
||||
rest.get(
|
||||
'http://localhost/api/v3/autocomplete/attribute_keys',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: {
|
||||
attributeKeys: [{ key: 'k8s_namespace_name', dataType: 'string' }],
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const { testStore } = renderWithRealStore({
|
||||
[INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY]: JSON.stringify(groupByValue),
|
||||
});
|
||||
|
||||
await waitFor(async () => {
|
||||
const elements = await screen.findAllByText('test-namespace');
|
||||
return expect(elements.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const row = (await screen.findAllByText('test-namespace'))[0].closest('tr');
|
||||
expect(row).not.toBeNull();
|
||||
user.click(row as HTMLElement);
|
||||
await waitFor(() => expect(groupedByRowDataCallCount).toBe(1));
|
||||
|
||||
const countAfterExpand = groupedByRowDataCallCount;
|
||||
|
||||
act(() => {
|
||||
testStore.dispatch({
|
||||
type: UPDATE_TIME_INTERVAL,
|
||||
payload: {
|
||||
minTime: Date.now() * 1000000 - 30 * 60 * 1000 * 1000000,
|
||||
maxTime: Date.now() * 1000000,
|
||||
selectedTime: '30m',
|
||||
},
|
||||
} as any);
|
||||
});
|
||||
|
||||
// Allow any potential re-fetch to settle
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
|
||||
expect(groupedByRowDataCallCount).toBe(countAfterExpand);
|
||||
});
|
||||
});
|
||||
@@ -1,72 +0,0 @@
|
||||
import setupCommonMocks from '../../commonMocks';
|
||||
|
||||
setupCommonMocks();
|
||||
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import VolumeDetails from 'container/InfraMonitoringK8s/Volumes/VolumeDetails/VolumeDetails';
|
||||
import store from 'store';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
describe('VolumeDetails', () => {
|
||||
const mockVolume = {
|
||||
persistentVolumeClaimName: 'test-volume',
|
||||
meta: {
|
||||
k8s_cluster_name: 'test-cluster',
|
||||
k8s_namespace_name: 'test-namespace',
|
||||
},
|
||||
} as any;
|
||||
const mockOnClose = jest.fn();
|
||||
|
||||
it('should render modal with relevant metadata', () => {
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
<VolumeDetails
|
||||
volume={mockVolume}
|
||||
isModalTimeSelection
|
||||
onClose={mockOnClose}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
const volumeNameElements = screen.getAllByText('test-volume');
|
||||
expect(volumeNameElements.length).toBeGreaterThan(0);
|
||||
expect(volumeNameElements[0]).toBeInTheDocument();
|
||||
|
||||
const clusterNameElements = screen.getAllByText('test-cluster');
|
||||
expect(clusterNameElements.length).toBeGreaterThan(0);
|
||||
expect(clusterNameElements[0]).toBeInTheDocument();
|
||||
|
||||
const namespaceNameElements = screen.getAllByText('test-namespace');
|
||||
expect(namespaceNameElements.length).toBeGreaterThan(0);
|
||||
expect(namespaceNameElements[0]).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should close modal when close button is clicked', () => {
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
<VolumeDetails
|
||||
volume={mockVolume}
|
||||
isModalTimeSelection
|
||||
onClose={mockOnClose}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
const closeButton = screen.getByRole('button', { name: 'Close' });
|
||||
fireEvent.click(closeButton);
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -791,4 +791,5 @@ export const INFRA_MONITORING_K8S_PARAMS_KEYS = {
|
||||
EVENTS_FILTERS: 'eventsFilters',
|
||||
HOSTS_FILTERS: 'hostsFilters',
|
||||
CURRENT_PAGE: 'currentPage',
|
||||
SELECTED_ITEM: 'selectedItem',
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { VIEWS } from 'components/HostMetricsDetail/constants';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import {
|
||||
Options,
|
||||
parseAsInteger,
|
||||
@@ -222,3 +224,26 @@ export const useInfraMonitoringVolumeUID = (): UseQueryStateReturn<
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.VOLUME_UID,
|
||||
parseAsString.withOptions(defaultNuqsOptions),
|
||||
);
|
||||
|
||||
export const useInfraMonitoringQueryFilters = (): TagFilter => {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
currentQuery?.builder?.queryData[0]?.filters || {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
[currentQuery?.builder?.queryData],
|
||||
);
|
||||
};
|
||||
|
||||
export const useInfraMonitoringSelectedItem = (): UseQueryStateReturn<
|
||||
string,
|
||||
string | undefined
|
||||
> => {
|
||||
return useQueryState(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM,
|
||||
parseAsString,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,22 +1,8 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
|
||||
import get from 'api/browser/localstorage/get';
|
||||
import set from 'api/browser/localstorage/set';
|
||||
import {
|
||||
K8sPodsData,
|
||||
K8sPodsListPayload,
|
||||
} from 'api/infraMonitoring/getK8sPodsList';
|
||||
import { Group } from 'lucide-react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
EntityProgressBar,
|
||||
formatBytes,
|
||||
ValidateColumnValueWrapper,
|
||||
} from './commonUtils';
|
||||
import { DEFAULT_PAGE_SIZE, K8sCategory } from './constants';
|
||||
import { OrderBySchemaType } from './schemas';
|
||||
import { DEFAULT_PAGE_SIZE } from './constants';
|
||||
|
||||
import './InfraMonitoringK8s.styles.scss';
|
||||
|
||||
@@ -26,404 +12,6 @@ export interface IEntityColumn {
|
||||
id: string;
|
||||
canRemove: boolean;
|
||||
}
|
||||
|
||||
const columnProgressBarClassName = 'column-progress-bar';
|
||||
|
||||
export const defaultAddedColumns: IEntityColumn[] = [
|
||||
{
|
||||
label: 'Pod name',
|
||||
value: 'podName',
|
||||
id: 'podName',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Req Usage (%)',
|
||||
value: 'cpu_request',
|
||||
id: 'cpu_request',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Limit Usage (%)',
|
||||
value: 'cpu_limit',
|
||||
id: 'cpu_limit',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'CPU Usage (cores)',
|
||||
value: 'cpu',
|
||||
id: 'cpu',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Mem Req Usage (%)',
|
||||
value: 'memory_request',
|
||||
id: 'memory_request',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Mem Limit Usage (%)',
|
||||
value: 'memory_limit',
|
||||
id: 'memory_limit',
|
||||
canRemove: false,
|
||||
},
|
||||
{
|
||||
label: 'Mem Usage (WSS)',
|
||||
value: 'memory',
|
||||
id: 'memory',
|
||||
canRemove: false,
|
||||
},
|
||||
// TODO - Re-enable the column once backend issue is fixed
|
||||
// {
|
||||
// label: 'Restarts',
|
||||
// value: 'restarts',
|
||||
// id: 'restarts',
|
||||
// canRemove: false,
|
||||
// },
|
||||
];
|
||||
|
||||
export const defaultAvailableColumns = [
|
||||
{
|
||||
label: 'Namespace name',
|
||||
value: 'namespace',
|
||||
id: 'namespace',
|
||||
canRemove: true,
|
||||
},
|
||||
{
|
||||
label: 'Node name',
|
||||
value: 'node',
|
||||
id: 'node',
|
||||
canRemove: true,
|
||||
},
|
||||
{
|
||||
label: 'Cluster name',
|
||||
value: 'cluster',
|
||||
id: 'cluster',
|
||||
canRemove: true,
|
||||
},
|
||||
];
|
||||
|
||||
export interface K8sPodsRowData {
|
||||
key: string;
|
||||
podName: React.ReactNode;
|
||||
podUID: string;
|
||||
cpu_request: React.ReactNode;
|
||||
cpu_limit: React.ReactNode;
|
||||
cpu: React.ReactNode;
|
||||
memory_request: React.ReactNode;
|
||||
memory_limit: React.ReactNode;
|
||||
memory: React.ReactNode;
|
||||
restarts: React.ReactNode;
|
||||
groupedByMeta?: any;
|
||||
}
|
||||
|
||||
export const getK8sPodsListQuery = (): K8sPodsListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
const podGroupColumnConfig = {
|
||||
title: (
|
||||
<div className="column-header entity-group-header">
|
||||
<Group size={14} /> POD GROUP
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'podGroup',
|
||||
key: 'podGroup',
|
||||
ellipsis: true,
|
||||
width: 180,
|
||||
sorter: false,
|
||||
className: 'column entity-group-header',
|
||||
};
|
||||
|
||||
export const dummyColumnConfig = {
|
||||
title: <div className="column-header dummy-column"> </div>,
|
||||
dataIndex: 'dummy',
|
||||
key: 'dummy',
|
||||
width: 40,
|
||||
sorter: false,
|
||||
align: 'left',
|
||||
className: 'column column-dummy',
|
||||
};
|
||||
|
||||
const columnsConfig: ColumnType<K8sPodsRowData>[] = [
|
||||
{
|
||||
title: <div className="column-header pod-name-header">Pod Name</div>,
|
||||
dataIndex: 'podName',
|
||||
key: 'podName',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
sorter: false,
|
||||
className: 'column column-pod-name',
|
||||
},
|
||||
{
|
||||
title: <div className="column-header med-col">CPU Req Usage (%)</div>,
|
||||
dataIndex: 'cpu_request',
|
||||
key: 'cpu_request',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header med-col">CPU Limit Usage (%)</div>,
|
||||
dataIndex: 'cpu_limit',
|
||||
key: 'cpu_limit',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header">CPU Usage (cores)</div>,
|
||||
dataIndex: 'cpu',
|
||||
key: 'cpu',
|
||||
width: 80,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-heade med-col">Mem Req Usage (%)</div>,
|
||||
dataIndex: 'memory_request',
|
||||
key: 'memory_request',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header med-col">Mem Limit Usage (%)</div>,
|
||||
dataIndex: 'memory_limit',
|
||||
key: 'memory_limit',
|
||||
width: 120,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
{
|
||||
title: <div className="column-header med-col">Mem Usage (WSS)</div>,
|
||||
dataIndex: 'memory',
|
||||
key: 'memory',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
sorter: true,
|
||||
align: 'left',
|
||||
className: `column ${columnProgressBarClassName}`,
|
||||
},
|
||||
// TODO - Re-enable the column once backend issue is fixed
|
||||
// {
|
||||
// title: (
|
||||
// <div className="column-header">
|
||||
// <Tooltip title="Container Restarts">Restarts</Tooltip>
|
||||
// </div>
|
||||
// ),
|
||||
// dataIndex: 'restarts',
|
||||
// key: 'restarts',
|
||||
// width: 40,
|
||||
// ellipsis: true,
|
||||
// sorter: true,
|
||||
// align: 'left',
|
||||
// className: `column ${columnProgressBarClassName}`,
|
||||
// },
|
||||
];
|
||||
|
||||
export const namespaceColumnConfig: ColumnType<K8sPodsRowData> = {
|
||||
title: <div className="column-header">Namespace</div>,
|
||||
dataIndex: 'namespace',
|
||||
key: 'namespace',
|
||||
width: 100,
|
||||
sorter: false,
|
||||
ellipsis: true,
|
||||
align: 'left',
|
||||
className: 'column column-namespace',
|
||||
};
|
||||
|
||||
export const nodeColumnConfig: ColumnType<K8sPodsRowData> = {
|
||||
title: <div className="column-header">Node</div>,
|
||||
dataIndex: 'node',
|
||||
key: 'node',
|
||||
width: 100,
|
||||
sorter: false,
|
||||
ellipsis: true,
|
||||
align: 'left',
|
||||
className: 'column column-node',
|
||||
};
|
||||
|
||||
export const clusterColumnConfig: ColumnType<K8sPodsRowData> = {
|
||||
title: <div className="column-header">Cluster</div>,
|
||||
dataIndex: 'cluster',
|
||||
key: 'cluster',
|
||||
width: 100,
|
||||
sorter: false,
|
||||
ellipsis: true,
|
||||
align: 'left',
|
||||
className: 'column column-cluster',
|
||||
};
|
||||
|
||||
export const columnConfigMap: Record<string, ColumnType<K8sPodsRowData>> = {
|
||||
namespace: namespaceColumnConfig,
|
||||
node: nodeColumnConfig,
|
||||
cluster: clusterColumnConfig,
|
||||
};
|
||||
|
||||
export const getK8sPodsListColumns = (
|
||||
addedColumns: IEntityColumn[],
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
defaultOrderBy: OrderBySchemaType,
|
||||
): ColumnType<K8sPodsRowData>[] => {
|
||||
const updatedColumnsConfig: ColumnType<K8sPodsRowData>[] = [...columnsConfig];
|
||||
|
||||
for (const column of addedColumns) {
|
||||
const config = columnConfigMap[column.id as keyof typeof columnConfigMap];
|
||||
if (config) {
|
||||
updatedColumnsConfig.push(config);
|
||||
}
|
||||
}
|
||||
|
||||
if (groupBy.length > 0) {
|
||||
const filteredColumns = [...updatedColumnsConfig].filter(
|
||||
(column) => column.key !== 'podName',
|
||||
);
|
||||
|
||||
filteredColumns.unshift(podGroupColumnConfig);
|
||||
|
||||
return filteredColumns as ColumnType<K8sPodsRowData>[];
|
||||
}
|
||||
|
||||
for (const column of updatedColumnsConfig) {
|
||||
if (column.sorter && column.key === defaultOrderBy?.columnName) {
|
||||
column.defaultSortOrder =
|
||||
defaultOrderBy?.order === 'asc' ? 'ascend' : 'descend';
|
||||
}
|
||||
}
|
||||
|
||||
return updatedColumnsConfig;
|
||||
};
|
||||
|
||||
const dotToUnder: Record<string, keyof K8sPodsData['meta']> = {
|
||||
'k8s.cronjob.name': 'k8s_cronjob_name',
|
||||
'k8s.daemonset.name': 'k8s_daemonset_name',
|
||||
'k8s.deployment.name': 'k8s_deployment_name',
|
||||
'k8s.job.name': 'k8s_job_name',
|
||||
'k8s.namespace.name': 'k8s_namespace_name',
|
||||
'k8s.node.name': 'k8s_node_name',
|
||||
'k8s.pod.name': 'k8s_pod_name',
|
||||
'k8s.pod.uid': 'k8s_pod_uid',
|
||||
'k8s.statefulset.name': 'k8s_statefulset_name',
|
||||
'k8s.cluster.name': 'k8s_cluster_name',
|
||||
};
|
||||
|
||||
const getGroupByEle = (
|
||||
pod: K8sPodsData,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): React.ReactNode => {
|
||||
const groupByValues: string[] = [];
|
||||
|
||||
groupBy.forEach((group) => {
|
||||
const rawKey = group.key as string;
|
||||
|
||||
// Choose mapped key if present, otherwise use rawKey
|
||||
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof pod.meta;
|
||||
const value = pod.meta[metaKey];
|
||||
|
||||
groupByValues.push(value);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="pod-group">
|
||||
{groupByValues.map((value) => (
|
||||
<Tag key={value} color={Color.BG_SLATE_400} className="pod-group-tag-item">
|
||||
{value === '' ? '<no-value>' : value}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const formatDataForTable = (
|
||||
data: K8sPodsData[],
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): K8sPodsRowData[] =>
|
||||
data.map((pod, index) => ({
|
||||
key: `${pod.podUID}-${index}`,
|
||||
podName: (
|
||||
<Tooltip title={pod.meta.k8s_pod_name || ''}>
|
||||
{pod.meta.k8s_pod_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
podUID: pod.podUID || '',
|
||||
cpu_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={pod.podCPURequest}
|
||||
entity={K8sCategory.PODS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={pod.podCPURequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={pod.podCPULimit}
|
||||
entity={K8sCategory.PODS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={pod.podCPULimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
cpu: (
|
||||
<ValidateColumnValueWrapper value={pod.podCPU}>
|
||||
{pod.podCPU}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={pod.podMemoryRequest}
|
||||
entity={K8sCategory.PODS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={pod.podMemoryRequest} type="request" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory_limit: (
|
||||
<ValidateColumnValueWrapper
|
||||
value={pod.podMemoryLimit}
|
||||
entity={K8sCategory.PODS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<div className="progress-container">
|
||||
<EntityProgressBar value={pod.podMemoryLimit} type="limit" />
|
||||
</div>
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
memory: (
|
||||
<ValidateColumnValueWrapper value={pod.podMemory}>
|
||||
{formatBytes(pod.podMemory)}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
restarts: (
|
||||
<ValidateColumnValueWrapper value={pod.restartCount}>
|
||||
{pod.restartCount}
|
||||
</ValidateColumnValueWrapper>
|
||||
),
|
||||
namespace: pod.meta.k8s_namespace_name,
|
||||
node: pod.meta.k8s_node_name,
|
||||
cluster: pod.meta.k8s_job_name,
|
||||
meta: pod.meta,
|
||||
podGroup: getGroupByEle(pod, groupBy),
|
||||
...pod.meta,
|
||||
groupedByMeta: pod.meta,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Custom hook to manage the page size for a table.
|
||||
* The page size is stored in local storage and is retrieved on initialization.
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||
import {
|
||||
getK8sPodsList,
|
||||
K8sPodsListPayload,
|
||||
K8sPodsListResponse,
|
||||
} from 'api/infraMonitoring/getK8sPodsList';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
type UseGetK8sPodsList = (
|
||||
requestData: K8sPodsListPayload,
|
||||
options?: UseQueryOptions<
|
||||
SuccessResponse<K8sPodsListResponse> | ErrorResponse,
|
||||
Error
|
||||
>,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled?: boolean,
|
||||
) => UseQueryResult<
|
||||
SuccessResponse<K8sPodsListResponse> | ErrorResponse,
|
||||
Error
|
||||
>;
|
||||
|
||||
export const useGetK8sPodsList: UseGetK8sPodsList = (
|
||||
requestData,
|
||||
options,
|
||||
headers,
|
||||
dotMetricsEnabled,
|
||||
) => {
|
||||
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_POD_LIST, requestData];
|
||||
}, [options?.queryKey, requestData]);
|
||||
|
||||
return useQuery<SuccessResponse<K8sPodsListResponse> | ErrorResponse, Error>({
|
||||
queryFn: ({ signal }) =>
|
||||
getK8sPodsList(requestData, signal, headers, dotMetricsEnabled),
|
||||
...options,
|
||||
queryKey,
|
||||
});
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||
import {
|
||||
getK8sVolumesList,
|
||||
K8sVolumesListPayload,
|
||||
K8sVolumesListResponse,
|
||||
} from 'api/infraMonitoring/getK8sVolumesList';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
type UseGetK8sVolumesList = (
|
||||
requestData: K8sVolumesListPayload,
|
||||
options?: UseQueryOptions<
|
||||
SuccessResponse<K8sVolumesListResponse> | ErrorResponse,
|
||||
Error
|
||||
>,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled?: boolean,
|
||||
) => UseQueryResult<
|
||||
SuccessResponse<K8sVolumesListResponse> | ErrorResponse,
|
||||
Error
|
||||
>;
|
||||
|
||||
export const useGetK8sVolumesList: UseGetK8sVolumesList = (
|
||||
requestData,
|
||||
options,
|
||||
headers,
|
||||
dotMetricsEnabled,
|
||||
) => {
|
||||
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_VOLUME_LIST, requestData];
|
||||
}, [options?.queryKey, requestData]);
|
||||
|
||||
return useQuery<
|
||||
SuccessResponse<K8sVolumesListResponse> | ErrorResponse,
|
||||
Error
|
||||
>({
|
||||
queryFn: ({ signal }) =>
|
||||
getK8sVolumesList(requestData, signal, headers, dotMetricsEnabled),
|
||||
...options,
|
||||
queryKey,
|
||||
});
|
||||
};
|
||||
@@ -26,8 +26,7 @@ export const useGlobalTimeStore = create<IGlobalTimeStore>((set, get) => ({
|
||||
};
|
||||
});
|
||||
},
|
||||
getMinMaxTime: (): ParsedTimeRange => {
|
||||
const { selectedTime } = get();
|
||||
return parseSelectedTime(selectedTime);
|
||||
getMinMaxTime: (selectedTime): ParsedTimeRange => {
|
||||
return parseSelectedTime(selectedTime || get().selectedTime);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -48,5 +48,5 @@ export interface IGlobalTimeStoreActions {
|
||||
* For durations, computes fresh values based on Date.now().
|
||||
* For custom ranges, extracts the stored values.
|
||||
*/
|
||||
getMinMaxTime: () => ParsedTimeRange;
|
||||
getMinMaxTime: (selectedItem?: GlobalTimeSelectedTime) => ParsedTimeRange;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ export const CUSTOM_TIME_SEPARATOR: CustomTimeRangeSeparator = '||_||';
|
||||
/**
|
||||
* Check if selectedTime represents a custom time range
|
||||
*/
|
||||
export function isCustomTimeRange(selectedTime: string): boolean {
|
||||
export function isCustomTimeRange(
|
||||
selectedTime: string,
|
||||
): selectedTime is CustomTimeRange {
|
||||
return selectedTime.includes(CUSTOM_TIME_SEPARATOR);
|
||||
}
|
||||
|
||||
|
||||
@@ -5551,10 +5551,10 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/design-tokens@2.1.1":
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/design-tokens/-/design-tokens-2.1.1.tgz#9c36d433fd264410713cc0c5ebdd75ce0ebecba3"
|
||||
integrity sha512-SdziCHg5Lwj+6oY6IRUPplaKZ+kTHjbrlhNj//UoAJ8aQLnRdR2F/miPzfSi4vrYw88LtXxNA9J9iJyacCp37A==
|
||||
"@signozhq/design-tokens@2.1.4":
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/design-tokens/-/design-tokens-2.1.4.tgz#f209da6fbd2ac97ab4434b71472f741009306550"
|
||||
integrity sha512-Ny7/VA5YGFFmZx58jMh7ATFyu7VePaJ4ySmj/DopP1hilmfdxQsKWnpqKaZJWRXrbNkc0gmq3cR7q7Z8nnN7ZQ==
|
||||
|
||||
"@signozhq/dialog@^0.0.2":
|
||||
version "0.0.2"
|
||||
|
||||
Reference in New Issue
Block a user