Compare commits

...

1 Commits

Author SHA1 Message Date
Vinícius Lourenço
d1cbb46e5c feat(infra-monitoring): migrate statefulsets to shared component 2026-04-06 14:22:01 -03:00
13 changed files with 471 additions and 1995 deletions

View File

@@ -411,11 +411,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
)}
{selectedCategory === K8sCategories.STATEFULSETS && (
<K8sStatefulSetsList
isFiltersVisible={showFilters}
handleFilterVisibilityChange={handleFilterVisibilityChange}
quickFiltersLastUpdated={quickFiltersLastUpdated}
/>
<K8sStatefulSetsList controlListPrefix={showFiltersComp} />
)}
{selectedCategory === K8sCategories.JOBS && (

View File

@@ -1,62 +0,0 @@
.infra-monitoring-container {
.statefulSets-list-table {
.expanded-table-container {
padding-left: 20px;
}
.ant-table-cell.column-progress-bar {
min-width: 180px;
max-width: 180px;
}
.ant-table-row-expand-icon-cell {
min-width: 30px !important;
max-width: 30px !important;
}
.progress-container {
display: flex;
justify-content: start;
}
}
}
.ant-table-cell {
&:has(.statefulset-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(.med-col) {
min-width: 200px !important;
max-width: 200px !important;
}
}
.ant-table-cell {
&:has(.small-col) {
min-width: 120px !important;
max-width: 120px !important;
}
}
.expanded-statefulsets-list-table {
.ant-table-cell {
min-width: 180px !important;
max-width: 180px !important;
}
.ant-table-row-expand-icon-cell {
min-width: 30px !important;
max-width: 30px !important;
}
}

View File

@@ -1,719 +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 logEvent from 'api/common/logEvent';
import { K8sStatefulSetsListPayload } from 'api/infraMonitoring/getsK8sStatefulSetsList';
import classNames from 'classnames';
import React, { useCallback } from 'react';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useGetK8sStatefulSetsList } from 'hooks/infraMonitoring/useGetK8sStatefulSetsList';
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 { getK8sStatefulSetsList, K8sStatefulSetsData } from './api';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from '../constants';
getStatefulSetMetricsQueryPayload,
k8sStatefulSetDetailsMetadataConfig,
k8sStatefulSetGetEntityName,
k8sStatefulSetGetSelectedItemFilters,
k8sStatefulSetInitialEventsFilter,
k8sStatefulSetInitialFilters,
k8sStatefulSetInitialLogTracesFilter,
statefulSetWidgetInfo,
} from './constants';
import {
useInfraMonitoringCurrentPage,
useInfraMonitoringEventsFilters,
useInfraMonitoringGroupBy,
useInfraMonitoringLogFilters,
useInfraMonitoringOrderBy,
useInfraMonitoringStatefulSetUID,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from '../hooks';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import { usePageSize } from '../utils';
import StatefulSetDetails from './StatefulSetDetails';
import {
defaultAddedColumns,
formatDataForTable,
getK8sStatefulSetsListColumns,
getK8sStatefulSetsListQuery,
K8sStatefulSetsRowData,
} from './utils';
import '../InfraMonitoringK8s.styles.scss';
import './K8sStatefulSetsList.styles.scss';
k8sStatefulSetsColumns,
k8sStatefulSetsColumnsConfig,
k8sStatefulSetsRenderRowData,
} from './table';
function K8sStatefulSetsList({
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 [
selectedStatefulSetUID,
setselectedStatefulSetUID,
] = useInfraMonitoringStatefulSetUID();
const { pageSize, setPageSize } = usePageSize(K8sCategory.STATEFULSETS);
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
const [, setView] = useInfraMonitoringView();
const [, setTracesFilters] = useInfraMonitoringTracesFilters();
const [, setEventsFilters] = useInfraMonitoringEventsFilters();
const [, setLogFilters] = useInfraMonitoringLogFilters();
const [
selectedRowData,
setSelectedRowData,
] = useState<K8sStatefulSetsRowData | 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: K8sStatefulSetsRowData,
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: 'cpu',
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 = getK8sStatefulSetsListQuery();
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(() => {
// 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 (selectedStatefulSetUID) {
return [
'statefulSetList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
selectedRowDataKey,
];
}
return [
'statefulSetList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
selectedRowDataKey,
String(minTime),
String(maxTime),
];
}, [
queryFilters,
orderBy,
selectedStatefulSetUID,
minTime,
maxTime,
selectedRowData,
]);
const {
data: groupedByRowData,
isFetching: isFetchingGroupedByRowData,
isLoading: isLoadingGroupedByRowData,
isError: isErrorGroupedByRowData,
refetch: fetchGroupedByRowData,
} = useGetK8sStatefulSetsList(
fetchGroupedByRowDataQuery as K8sStatefulSetsListPayload,
{
queryKey: groupedByRowDataQueryKey,
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
},
undefined,
dotMetricsEnabled,
);
const {
data: groupByFiltersData,
isLoading: isLoadingGroupByFilters,
} = useGetAggregateKeys(
{
dataSource: currentQuery.builder.queryData[0].dataSource,
aggregateAttribute: GetK8sEntityToAggregateAttribute(
K8sCategory.STATEFULSETS,
const response = await getK8sStatefulSetsList(
filters,
signal,
undefined,
dotMetricsEnabled,
),
aggregateOperator: 'noop',
searchText: '',
tagType: '',
},
{
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
},
true,
K8sCategory.STATEFULSETS,
);
const query = useMemo(() => {
const baseQuery = getK8sStatefulSetsListQuery();
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 formattedGroupedByStatefulSetsData = useMemo(
() =>
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
[groupedByRowData, groupBy],
);
const nestedStatefulSetsData = useMemo(() => {
if (!selectedRowData || !groupedByRowData?.payload?.data.records) {
return [];
}
return groupedByRowData?.payload?.data?.records || [];
}, [groupedByRowData, selectedRowData]);
const queryKey = useMemo(() => {
if (selectedStatefulSetUID) {
return [
'statefulSetList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
];
}
return [
'statefulSetList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
String(minTime),
String(maxTime),
];
}, [
selectedStatefulSetUID,
pageSize,
currentPage,
queryFilters,
orderBy,
groupBy,
minTime,
maxTime,
]);
const { data, isFetching, isLoading, isError } = useGetK8sStatefulSetsList(
query as K8sStatefulSetsListPayload,
{
queryKey,
enabled: !!query,
keepPreviousData: true,
},
undefined,
dotMetricsEnabled,
);
const statefulSetsData = useMemo(() => data?.payload?.data?.records || [], [
data,
]);
const totalCount = data?.payload?.data?.total || 0;
const formattedStatefulSetsData = useMemo(
() => formatDataForTable(statefulSetsData, groupBy),
[statefulSetsData, groupBy],
);
const columns = useMemo(() => getK8sStatefulSetsListColumns(groupBy), [
groupBy,
]);
const handleGroupByRowClick = (record: K8sStatefulSetsRowData): 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<K8sStatefulSetsRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
_filters: Record<string, (string | number | boolean)[] | null>,
sorter:
| SorterResult<K8sStatefulSetsRowData>
| SorterResult<K8sStatefulSetsRowData>[],
): void => {
if (pagination.current) {
setCurrentPage(pagination.current);
logEvent(InfraMonitoringEvents.PageNumberChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: InfraMonitoringEvents.StatefulSet,
});
}
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.StatefulSet,
});
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
useEffect(() => {
logEvent(InfraMonitoringEvents.PageVisited, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: InfraMonitoringEvents.StatefulSet,
total: data?.payload?.data?.total,
});
}, [data?.payload?.data?.total]);
const selectedStatefulSetData = useMemo(() => {
if (!selectedStatefulSetUID) {
return null;
}
if (groupBy.length > 0) {
return (
nestedStatefulSetsData.find(
(statefulSet) => statefulSet.statefulSetName === selectedStatefulSetUID,
) || null
);
}
return (
statefulSetsData.find(
(statefulSet) => statefulSet.statefulSetName === selectedStatefulSetUID,
) || null
);
}, [
selectedStatefulSetUID,
groupBy.length,
statefulSetsData,
nestedStatefulSetsData,
]);
const openStatefulSetInNewTab = (record: K8sStatefulSetsRowData): void => {
const newParams = new URLSearchParams(document.location.search);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.STATEFULSET_UID,
record.statefulsetUID,
);
openInNewTab(
buildAbsolutePath({
relativePath: '',
urlQueryString: newParams.toString(),
}),
);
};
const handleRowClick = (
record: K8sStatefulSetsRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
openStatefulSetInNewTab(record);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedStatefulSetUID(record.statefulsetUID);
} else {
handleGroupByRowClick(record);
}
logEvent(InfraMonitoringEvents.ItemClicked, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: InfraMonitoringEvents.StatefulSet,
});
};
const nestedColumns = useMemo(() => getK8sStatefulSetsListColumns([]), []);
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<K8sStatefulSetsRowData>[]}
dataSource={formattedGroupedByStatefulSetsData}
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)) {
openStatefulSetInNewTab(record);
return;
}
setselectedStatefulSetUID(record.statefulsetUID);
},
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: K8sStatefulSetsRowData,
e: React.MouseEvent<HTMLButtonElement>,
) => void;
record: K8sStatefulSetsRowData;
}): 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 handleCloseStatefulSetDetail = (): void => {
setselectedStatefulSetUID(null);
setView(null);
setTracesFilters(null);
setEventsFilters(null);
setLogFilters(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.StatefulSet,
});
return {
data: response.payload?.data.records || [],
total: response.payload?.data.total || 0,
error: response.error,
};
},
[groupByFiltersData, 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: K8sStatefulSetsData | null; error?: string | null }> => {
const response = await getK8sStatefulSetsList(
{
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.StatefulSet,
});
};
const records = response.payload?.data.records || [];
const showTableLoadingState =
(isFetching || isLoading) && formattedStatefulSetsData.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<K8sStatefulSetsData>
controlListPrefix={controlListPrefix}
entity={K8sCategory.STATEFULSETS}
showAutoRefresh={!selectedStatefulSetData}
/>
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
<Table
className={classNames('k8s-list-table', 'statefulSets-list-table', {
'expanded-statefulsets-list-table': isGroupedByAttribute,
})}
dataSource={showTableLoadingState ? [] : formattedStatefulSetsData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{
spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText: showTableLoadingState ? null : (
<div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
),
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(
record,
): { onClick: (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={k8sStatefulSetsColumns}
tableColumns={k8sStatefulSetsColumnsConfig}
fetchListData={fetchListData}
renderRowData={k8sStatefulSetsRenderRowData}
eventCategory={InfraMonitoringEvents.StatefulSet}
/>
<StatefulSetDetails
statefulSet={selectedStatefulSetData}
isModalTimeSelection
onClose={handleCloseStatefulSetDetail}
<K8sBaseDetails<K8sStatefulSetsData>
category={K8sCategory.STATEFULSETS}
eventCategory={InfraMonitoringEvents.StatefulSet}
getSelectedItemFilters={k8sStatefulSetGetSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={k8sStatefulSetGetEntityName}
getInitialLogTracesFilters={k8sStatefulSetInitialLogTracesFilter}
getInitialEventsFilters={k8sStatefulSetInitialEventsFilter}
primaryFilterKeys={k8sStatefulSetInitialFilters}
metadataConfig={k8sStatefulSetDetailsMetadataConfig}
entityWidgetInfo={statefulSetWidgetInfo}
getEntityQueryPayload={getStatefulSetMetricsQueryPayload}
queryKeyPrefix="statefulSet"
/>
</div>
</>
);
}

View File

@@ -1,7 +0,0 @@
import { K8sStatefulSetsData } from 'api/infraMonitoring/getsK8sStatefulSetsList';
export type StatefulSetDetailsProps = {
statefulSet: K8sStatefulSetsData | null;
isModalTimeSelection: boolean;
onClose: () => void;
};

View File

@@ -1,658 +0,0 @@
/* eslint-disable sonarjs/no-identical-functions */
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 { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import type { RadioChangeEvent } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { VIEW_TYPES, VIEWS } from 'components/HostMetricsDetail/constants';
import { InfraMonitoringEvents } from 'constants/events';
import { QueryParams } from 'constants/query';
import {
initialQueryBuilderFormValuesMap,
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { filterDuplicateFilters } from 'container/InfraMonitoringK8s/commonUtils';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import EntityEvents from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityEvents';
import EntityLogs from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityLogs';
import EntityMetrics from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityMetrics';
import EntityTraces from 'container/InfraMonitoringK8s/EntityDetailsUtils/EntityTraces';
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 GetMinMax from 'lib/getMinMax';
import {
BarChart2,
ChevronsLeftRight,
Compass,
DraftingCompass,
ScrollText,
X,
} from 'lucide-react';
import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
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 {
getStatefulSetMetricsQueryPayload,
statefulSetWidgetInfo,
} from './constants';
import { StatefulSetDetailsProps } from './StatefulSetDetails.interfaces';
import '../../EntityDetailsUtils/entityDetails.styles.scss';
function StatefulSetDetails({
statefulSet,
onClose,
isModalTimeSelection,
}: StatefulSetDetailsProps): 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 urlQuery = useUrlQuery();
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 [selectedView, setSelectedView] = useInfraMonitoringView();
const [logFiltersParam, setLogFiltersParam] = useInfraMonitoringLogFilters();
const [
tracesFiltersParam,
setTracesFiltersParam,
] = useInfraMonitoringTracesFilters();
const [
eventsFiltersParam,
setEventsFiltersParam,
] = useInfraMonitoringEventsFilters();
const isDarkMode = useIsDarkMode();
const initialFilters = useMemo(() => {
const filters =
selectedView === VIEW_TYPES.LOGS ? logFiltersParam : tracesFiltersParam;
if (filters) {
return filters;
}
return {
op: 'AND',
items: [
{
id: uuidv4(),
key: {
key: QUERY_KEYS.K8S_STATEFUL_SET_NAME,
dataType: DataTypes.String,
type: 'resource',
id: 'k8s_statefulset_name--string--resource--false',
},
op: '=',
value: statefulSet?.meta.k8s_statefulset_name || '',
},
{
id: uuidv4(),
key: {
key: QUERY_KEYS.K8S_NAMESPACE_NAME,
dataType: DataTypes.String,
type: 'resource',
id: 'k8s_namespace_name--string--resource--false',
},
op: '=',
value: statefulSet?.meta.k8s_namespace_name || '',
},
],
};
}, [
statefulSet?.meta.k8s_statefulset_name,
statefulSet?.meta.k8s_namespace_name,
selectedView,
logFiltersParam,
tracesFiltersParam,
]);
const initialEventsFilters = useMemo(() => {
if (eventsFiltersParam) {
return eventsFiltersParam;
}
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: 'StatefulSet',
},
{
id: uuidv4(),
key: {
key: QUERY_KEYS.K8S_OBJECT_NAME,
dataType: DataTypes.String,
type: 'resource',
id: 'k8s.object.name--string--resource--false',
},
op: '=',
value: statefulSet?.meta.k8s_statefulset_name || '',
},
],
};
}, [statefulSet?.meta.k8s_statefulset_name, eventsFiltersParam]);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
>(initialFilters);
const [eventsFilters, setEventsFilters] = useState<IBuilderQuery['filters']>(
initialEventsFilters,
);
useEffect(() => {
if (statefulSet) {
logEvent(InfraMonitoringEvents.PageVisited, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: InfraMonitoringEvents.StatefulSet,
});
}
}, [statefulSet]);
useEffect(() => {
setLogAndTracesFilters(initialFilters);
setEventsFilters(initialEventsFilters);
}, [initialFilters, initialEventsFilters]);
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 handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setLogFiltersParam(null);
setTracesFiltersParam(null);
setEventsFiltersParam(null);
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: InfraMonitoringEvents.StatefulSet,
view: e.target.value,
});
};
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.StatefulSet,
interval,
view: selectedView,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const handleChangeLogFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters?.items?.filter((item) =>
[QUERY_KEYS.K8S_STATEFUL_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].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_STATEFUL_SET_NAME,
);
if (newFilters && newFilters?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: InfraMonitoringEvents.StatefulSet,
view: 'logs',
});
}
const updatedFilters = {
op: 'AND',
items: filterDuplicateFilters(
[
...(primaryFilters || []),
...(newFilters || []),
...(paginationFilter ? [paginationFilter] : []),
].filter((item): item is TagFilterItem => item !== undefined),
),
};
setLogFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const handleChangeTracesFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setLogAndTracesFilters((prevFilters) => {
const primaryFilters = prevFilters?.items?.filter((item) =>
[QUERY_KEYS.K8S_STATEFUL_SET_NAME, QUERY_KEYS.K8S_NAMESPACE_NAME].includes(
item.key?.key ?? '',
),
);
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: InfraMonitoringEvents.StatefulSet,
view: 'traces',
});
}
const updatedFilters = {
op: 'AND',
items: filterDuplicateFilters(
[
...(primaryFilters || []),
...(value?.items?.filter(
(item) => item.key?.key !== QUERY_KEYS.K8S_STATEFUL_SET_NAME,
) || []),
].filter((item): item is TagFilterItem => item !== undefined),
),
};
setTracesFiltersParam(updatedFilters);
setSelectedView(view);
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const handleChangeEventsFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setEventsFilters((prevFilters) => {
const statefulSetKindFilter = prevFilters?.items?.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
);
const statefulSetNameFilter = prevFilters?.items?.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
);
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: InfraMonitoringEvents.StatefulSet,
view: 'logs',
});
}
const updatedFilters = {
op: 'AND',
items: [
statefulSetKindFilter,
statefulSetNameFilter,
...(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);
setSelectedView(view);
return updatedFilters;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const handleExplorePagesRedirect = (): void => {
if (selectedInterval !== 'custom') {
urlQuery.set(QueryParams.relativeTime, selectedInterval);
} else {
urlQuery.delete(QueryParams.relativeTime);
urlQuery.set(QueryParams.startTime, modalTimeRange.startTime.toString());
urlQuery.set(QueryParams.endTime, modalTimeRange.endTime.toString());
}
logEvent(InfraMonitoringEvents.ExploreClicked, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: InfraMonitoringEvents.StatefulSet,
view: selectedView,
});
if (selectedView === VIEW_TYPES.LOGS) {
const filtersWithoutPagination = {
...logAndTracesFilters,
items:
logAndTracesFilters?.items?.filter((item) => item.key?.key !== 'id') || [],
};
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.logs,
aggregateOperator: LogsAggregatorOperator.NOOP,
filters: filtersWithoutPagination,
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
filters: logAndTracesFilters,
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
}
};
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),
});
}
setSelectedView(VIEW_TYPES.METRICS);
onClose();
};
return (
<Drawer
width="70%"
title={
<>
<Divider type="vertical" />
<Typography.Text className="title">
{statefulSet?.meta.k8s_statefulset_name}
</Typography.Text>
</>
}
placement="right"
onClose={handleClose}
open={!!statefulSet}
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 }} />}
>
{statefulSet && (
<>
<div className="entity-detail-drawer__entity">
<div className="entity-details-grid">
<div className="labels-row">
<Typography.Text
type="secondary"
className="entity-details-metadata-label"
>
Statefulset 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={statefulSet.meta.k8s_statefulset_name}>
{statefulSet.meta.k8s_statefulset_name}
</Tooltip>
</Typography.Text>
<Typography.Text className="entity-details-metadata-value">
<Tooltip title={statefulSet.meta.k8s_namespace_name}>
{statefulSet.meta.k8s_namespace_name}
</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}
>
<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.METRICS && (
<EntityMetrics
timeRange={modalTimeRange}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
entity={statefulSet}
entityWidgetInfo={statefulSetWidgetInfo}
getEntityQueryPayload={getStatefulSetMetricsQueryPayload}
category={K8sCategory.STATEFULSETS}
queryKey="statefulsetMetrics"
/>
)}
{selectedView === VIEW_TYPES.LOGS && (
<EntityLogs
timeRange={modalTimeRange}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeLogFilters={handleChangeLogFilters}
logFilters={logAndTracesFilters}
selectedInterval={selectedInterval}
queryKey="statefulsetLogs"
category={K8sCategory.STATEFULSETS}
queryKeyFilters={[
QUERY_KEYS.K8S_STATEFUL_SET_NAME,
QUERY_KEYS.K8S_NAMESPACE_NAME,
]}
/>
)}
{selectedView === VIEW_TYPES.TRACES && (
<EntityTraces
timeRange={modalTimeRange}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
handleChangeTracesFilters={handleChangeTracesFilters}
tracesFilters={logAndTracesFilters}
selectedInterval={selectedInterval}
queryKey="statefulsetTraces"
category={InfraMonitoringEvents.StatefulSet}
queryKeyFilters={[
QUERY_KEYS.K8S_STATEFUL_SET_NAME,
QUERY_KEYS.K8S_NAMESPACE_NAME,
]}
/>
)}
{selectedView === VIEW_TYPES.EVENTS && (
<EntityEvents
timeRange={modalTimeRange}
handleChangeEventFilters={handleChangeEventsFilters}
filters={eventsFilters}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
category={K8sCategory.STATEFULSETS}
queryKey="statefulsetEvents"
/>
)}
</>
)}
</Drawer>
);
}
export default StatefulSetDetails;

View File

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

View File

@@ -1,11 +1,12 @@
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 K8sStatefulSetsListPayload {
filters: TagFilter;
@@ -66,39 +67,35 @@ export function mapStatefulSetsMeta(
}
export const getK8sStatefulSetsList = async (
props: K8sStatefulSetsListPayload,
props: K8sBaseFilters,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
): Promise<SuccessResponse<K8sStatefulSetsListResponse> | ErrorResponse> => {
try {
// Prepare filters
const requestProps =
dotMetricsEnabled && Array.isArray(props.filters?.items)
dotMetricsEnabled && props.filters && Array.isArray(props.filters.items)
? {
...props,
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);
}
items: props.filters.items.reduce<TagFilter['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 TagFilter['items']),
},
}
: props;
@@ -109,7 +106,6 @@ export const getK8sStatefulSetsList = async (
});
const payload: K8sStatefulSetsListResponse = response.data;
// apply our helper
payload.data.records = payload.data.records.map((record) => ({
...record,
meta: mapStatefulSetsMeta(record.meta as Record<string, unknown>),

View File

@@ -1,11 +1,72 @@
import { K8sStatefulSetsData } from 'api/infraMonitoring/getsK8sStatefulSetsList';
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 { K8sStatefulSetsData } from './api';
export const k8sStatefulSetGetSelectedItemFilters = (
selectedItemId: string,
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_statefulset_name',
key: {
key: 'k8s_statefulset_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
export const k8sStatefulSetDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sStatefulSetsData>[] = [
{
label: 'Statefulset Name',
getValue: (p): string => p.meta.k8s_statefulset_name,
},
{
label: 'Namespace Name',
getValue: (p): string => p.meta.k8s_namespace_name,
},
];
export const k8sStatefulSetInitialFilters = [
QUERY_KEYS.K8S_STATEFUL_SET_NAME,
QUERY_KEYS.K8S_NAMESPACE_NAME,
];
export const k8sStatefulSetInitialEventsFilter = (
item: K8sStatefulSetsData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'StatefulSet'),
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_statefulset_name),
];
export const k8sStatefulSetInitialLogTracesFilter = (
item: K8sStatefulSetsData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(
QUERY_KEYS.K8S_STATEFUL_SET_NAME,
item.meta.k8s_statefulset_name,
),
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.k8s_namespace_name),
];
export const k8sStatefulSetGetEntityName = (
item: K8sStatefulSetsData,
): string => item.meta.k8s_statefulset_name;
export const statefulSetWidgetInfo = [
{
title: 'CPU usage, request, limits',

View File

@@ -0,0 +1,11 @@
.entityGroupHeader {
display: flex;
align-items: center;
padding-left: var(--spacing-5);
gap: var(--spacing-5);
}
.progressBar {
display: flex;
justify-content: start;
}

View File

@@ -0,0 +1,295 @@
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 { K8sStatefulSetsData } from './api';
import styles from './table.module.scss';
export const k8sStatefulSetsColumns: IEntityColumn[] = [
{
label: 'StatefulSet Group',
value: 'statefulSetGroup',
id: 'statefulSetGroup',
canBeHidden: false,
defaultVisibility: true,
behavior: 'hidden-on-collapse',
},
{
label: 'StatefulSet Name',
value: 'statefulsetName',
id: 'statefulsetName',
canBeHidden: false,
defaultVisibility: true,
behavior: 'hidden-on-expand',
},
{
label: 'Namespace Name',
value: 'namespaceName',
id: 'namespaceName',
canBeHidden: false,
defaultVisibility: true,
behavior: 'always-visible',
},
{
label: 'Available',
value: 'available_pods',
id: 'available_pods',
canBeHidden: false,
defaultVisibility: true,
behavior: 'always-visible',
},
{
label: 'Desired',
value: 'desired_pods',
id: 'desired_pods',
canBeHidden: false,
defaultVisibility: true,
behavior: 'always-visible',
},
{
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',
},
];
export const k8sStatefulSetsColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
{
title: (
<div className={styles.entityGroupHeader}>
<Group size={14} /> STATEFULSET GROUP
</div>
),
dataIndex: 'statefulSetGroup',
key: 'statefulSetGroup',
ellipsis: true,
width: 150,
align: 'left',
sorter: false,
},
{
title: <div>StatefulSet Name</div>,
dataIndex: 'statefulsetName',
key: 'statefulsetName',
ellipsis: true,
width: 80,
sorter: false,
align: 'left',
},
{
title: <div>Namespace Name</div>,
dataIndex: 'namespaceName',
key: 'namespaceName',
ellipsis: true,
width: 80,
sorter: false,
align: 'left',
},
{
title: <div>Available</div>,
dataIndex: 'available_pods',
key: 'available_pods',
width: 80,
sorter: true,
align: 'left',
},
{
title: <div>Desired</div>,
dataIndex: 'desired_pods',
key: 'desired_pods',
width: 80,
sorter: true,
align: 'left',
},
{
title: <div>CPU Req Usage (%)</div>,
dataIndex: 'cpu_request',
key: 'cpu_request',
width: 120,
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: 80,
sorter: true,
align: 'left',
},
];
export const k8sStatefulSetsRenderRowData = (
statefulSet: K8sStatefulSetsData,
groupBy: BaseAutocompleteData[],
): K8sRenderedRowData => ({
key: getRowKey(
statefulSet,
() =>
statefulSet.statefulSetName || statefulSet.meta.k8s_statefulset_name || '',
groupBy,
),
itemKey: statefulSet.meta.k8s_statefulset_name,
statefulsetName: (
<Tooltip title={statefulSet.meta.k8s_statefulset_name}>
{statefulSet.meta.k8s_statefulset_name || ''}
</Tooltip>
),
namespaceName: (
<Tooltip title={statefulSet.meta.k8s_namespace_name}>
{statefulSet.meta.k8s_namespace_name || ''}
</Tooltip>
),
cpu_request: (
<ValidateColumnValueWrapper
value={statefulSet.cpuRequest}
entity={K8sCategory.STATEFULSETS}
attribute="CPU Request"
>
<div className={styles.progressBar}>
<EntityProgressBar value={statefulSet.cpuRequest} type="request" />
</div>
</ValidateColumnValueWrapper>
),
cpu_limit: (
<ValidateColumnValueWrapper
value={statefulSet.cpuLimit}
entity={K8sCategory.STATEFULSETS}
attribute="CPU Limit"
>
<div className={styles.progressBar}>
<EntityProgressBar value={statefulSet.cpuLimit} type="limit" />
</div>
</ValidateColumnValueWrapper>
),
cpu: (
<ValidateColumnValueWrapper value={statefulSet.cpuUsage}>
{statefulSet.cpuUsage}
</ValidateColumnValueWrapper>
),
memory_request: (
<ValidateColumnValueWrapper
value={statefulSet.memoryRequest}
entity={K8sCategory.STATEFULSETS}
attribute="Memory Request"
>
<div className={styles.progressBar}>
<EntityProgressBar value={statefulSet.memoryRequest} type="request" />
</div>
</ValidateColumnValueWrapper>
),
memory_limit: (
<ValidateColumnValueWrapper
value={statefulSet.memoryLimit}
entity={K8sCategory.STATEFULSETS}
attribute="Memory Limit"
>
<div className={styles.progressBar}>
<EntityProgressBar value={statefulSet.memoryLimit} type="limit" />
</div>
</ValidateColumnValueWrapper>
),
memory: (
<ValidateColumnValueWrapper value={statefulSet.memoryUsage}>
{formatBytes(statefulSet.memoryUsage)}
</ValidateColumnValueWrapper>
),
available_pods: (
<ValidateColumnValueWrapper value={statefulSet.availablePods}>
{statefulSet.availablePods}
</ValidateColumnValueWrapper>
),
desired_pods: (
<ValidateColumnValueWrapper value={statefulSet.desiredPods}>
{statefulSet.desiredPods}
</ValidateColumnValueWrapper>
),
statefulSetGroup: getGroupByEl(statefulSet, groupBy),
...statefulSet.meta,
groupedByMeta: getGroupedByMeta(statefulSet, groupBy),
});

View File

@@ -1,356 +0,0 @@
import { Color } from '@signozhq/design-tokens';
import { TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
import {
K8sStatefulSetsData,
K8sStatefulSetsListPayload,
} from 'api/infraMonitoring/getsK8sStatefulSetsList';
import { Group } from 'lucide-react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
EntityProgressBar,
formatBytes,
ValidateColumnValueWrapper,
} from '../commonUtils';
import { K8sCategory } from '../constants';
import { IEntityColumn } from '../utils';
export const defaultAddedColumns: IEntityColumn[] = [
{
label: 'StatefulSet Name',
value: 'statefulsetName',
id: 'statefulsetName',
canRemove: false,
},
{
label: 'Namespace Name',
value: 'namespaceName',
id: 'namespaceName',
canRemove: false,
},
{
label: 'Available',
value: 'available_pods',
id: 'available_pods',
canRemove: false,
},
{
label: 'Desired',
value: 'desired_pods',
id: 'desired_pods',
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,
},
];
export interface K8sStatefulSetsRowData {
key: string;
statefulsetUID: string;
statefulsetName: React.ReactNode;
cpu_request: React.ReactNode;
cpu_limit: React.ReactNode;
cpu: React.ReactNode;
memory_request: React.ReactNode;
memory_limit: React.ReactNode;
memory: React.ReactNode;
desired_pods: React.ReactNode;
available_pods: React.ReactNode;
namespaceName: React.ReactNode;
groupedByMeta?: any;
}
const statefulSetGroupColumnConfig = {
title: (
<div className="column-header entity-group-header">
<Group size={14} /> STATEFULSET GROUP
</div>
),
dataIndex: 'statefulSetGroup',
key: 'statefulSetGroup',
ellipsis: true,
width: 150,
align: 'left',
sorter: false,
className: 'column entity-group-header',
};
export const getK8sStatefulSetsListQuery = (): K8sStatefulSetsListPayload => ({
filters: {
items: [],
op: 'and',
},
orderBy: { columnName: 'cpu', order: 'desc' },
});
const columnProgressBarClassName = 'column-progress-bar';
const columnsConfig = [
{
title: (
<div className="column-header-left statefulset-name-header">
StatefulSet Name
</div>
),
dataIndex: 'statefulsetName',
key: 'statefulsetName',
ellipsis: true,
width: 80,
sorter: false,
align: 'left',
},
{
title: (
<div className="column-header-left namespace-name-header">
Namespace Name
</div>
),
dataIndex: 'namespaceName',
key: 'namespaceName',
ellipsis: true,
width: 80,
sorter: false,
align: 'left',
},
{
title: <div className="column-header small-col">Available</div>,
dataIndex: 'available_pods',
key: 'available_pods',
ellipsis: true,
sorter: true,
align: 'left',
className: `column small-col`,
},
{
title: <div className="column-header small-col">Desired</div>,
dataIndex: 'desired_pods',
key: 'desired_pods',
sorter: true,
align: 'left',
className: `column small-col`,
},
{
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 small-col">CPU Usage (cores)</div>,
dataIndex: 'cpu',
key: 'cpu',
width: 80,
sorter: true,
align: 'left',
className: `column ${columnProgressBarClassName}`,
},
{
title: <div className="column-header 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}`,
},
];
export const getK8sStatefulSetsListColumns = (
groupBy: IBuilderQuery['groupBy'],
): ColumnType<K8sStatefulSetsRowData>[] => {
if (groupBy.length > 0) {
const filteredColumns = [...columnsConfig].filter(
(column) => column.key !== 'statefulsetName',
);
filteredColumns.unshift(statefulSetGroupColumnConfig);
return filteredColumns as ColumnType<K8sStatefulSetsRowData>[];
}
return columnsConfig as ColumnType<K8sStatefulSetsRowData>[];
};
const dotToUnder: Record<string, keyof K8sStatefulSetsData['meta']> = {
'k8s.namespace.name': 'k8s_namespace_name',
'k8s.statefulset.name': 'k8s_statefulset_name',
};
const getGroupByEle = (
statefulSet: K8sStatefulSetsData,
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 statefulSet.meta;
const value = statefulSet.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: K8sStatefulSetsData[],
groupBy: IBuilderQuery['groupBy'],
): K8sStatefulSetsRowData[] =>
data.map((statefulSet, index) => ({
key: index.toString(),
statefulsetUID: statefulSet.statefulSetName,
statefulsetName: (
<Tooltip title={statefulSet.meta.k8s_statefulset_name}>
{statefulSet.meta.k8s_statefulset_name || ''}
</Tooltip>
),
namespaceName: (
<Tooltip title={statefulSet.meta.k8s_namespace_name}>
{statefulSet.meta.k8s_namespace_name || ''}
</Tooltip>
),
cpu_request: (
<ValidateColumnValueWrapper
value={statefulSet.cpuRequest}
entity={K8sCategory.STATEFULSETS}
attribute="CPU Request"
>
<div className="progress-container">
<EntityProgressBar value={statefulSet.cpuRequest} type="request" />
</div>
</ValidateColumnValueWrapper>
),
cpu_limit: (
<ValidateColumnValueWrapper
value={statefulSet.cpuLimit}
entity={K8sCategory.STATEFULSETS}
attribute="CPU Limit"
>
<div className="progress-container">
<EntityProgressBar value={statefulSet.cpuLimit} type="limit" />
</div>
</ValidateColumnValueWrapper>
),
cpu: (
<ValidateColumnValueWrapper value={statefulSet.cpuUsage}>
{statefulSet.cpuUsage}
</ValidateColumnValueWrapper>
),
memory_request: (
<ValidateColumnValueWrapper
value={statefulSet.memoryRequest}
entity={K8sCategory.STATEFULSETS}
attribute="Memory Request"
>
<div className="progress-container">
<EntityProgressBar value={statefulSet.memoryRequest} type="request" />
</div>
</ValidateColumnValueWrapper>
),
memory_limit: (
<ValidateColumnValueWrapper
value={statefulSet.memoryLimit}
entity={K8sCategory.STATEFULSETS}
attribute="Memory Limit"
>
<div className="progress-container">
<EntityProgressBar value={statefulSet.memoryLimit} type="limit" />
</div>
</ValidateColumnValueWrapper>
),
memory: (
<ValidateColumnValueWrapper value={statefulSet.memoryUsage}>
{formatBytes(statefulSet.memoryUsage)}
</ValidateColumnValueWrapper>
),
available_pods: (
<ValidateColumnValueWrapper value={statefulSet.availablePods}>
{statefulSet.availablePods}
</ValidateColumnValueWrapper>
),
desired_pods: (
<ValidateColumnValueWrapper value={statefulSet.desiredPods}>
{statefulSet.desiredPods}
</ValidateColumnValueWrapper>
),
statefulSetGroup: getGroupByEle(statefulSet, groupBy),
meta: statefulSet.meta,
...statefulSet.meta,
groupedByMeta: statefulSet.meta,
}));

View File

@@ -1,137 +0,0 @@
import setupCommonMocks from '../../commonMocks';
setupCommonMocks();
import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { MemoryRouter } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import StatefulSetDetails from 'container/InfraMonitoringK8s/StatefulSets/StatefulSetDetails/StatefulSetDetails';
import { withNuqsTestingAdapter } from 'nuqs/adapters/testing';
import { userEvent } from 'tests/test-utils';
const queryClient = new QueryClient();
const Wrapper = withNuqsTestingAdapter({ searchParams: {} });
describe('StatefulSetDetails', () => {
const mockStatefulSet = {
meta: {
k8s_namespace_name: 'test-namespace',
k8s_statefulset_name: 'test-stateful-set',
},
} as any;
const mockOnClose = jest.fn();
it('should render modal with relevant metadata', () => {
render(
<Wrapper>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<StatefulSetDetails
statefulSet={mockStatefulSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</QueryClientProvider>
</Wrapper>,
);
const statefulSetNameElements = screen.getAllByText('test-stateful-set');
expect(statefulSetNameElements.length).toBeGreaterThan(0);
expect(statefulSetNameElements[0]).toBeInTheDocument();
const namespaceNameElements = screen.getAllByText('test-namespace');
expect(namespaceNameElements.length).toBeGreaterThan(0);
expect(namespaceNameElements[0]).toBeInTheDocument();
});
it('should render modal with 4 tabs', () => {
render(
<Wrapper>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<StatefulSetDetails
statefulSet={mockStatefulSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</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}>
<MemoryRouter>
<StatefulSetDetails
statefulSet={mockStatefulSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</QueryClientProvider>
</Wrapper>,
);
const metricsTab = screen.getByRole('radio', { name: 'Metrics' });
expect(metricsTab).toBeChecked();
});
it('should switch to events tab when events tab is clicked', async () => {
render(
<Wrapper>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<StatefulSetDetails
statefulSet={mockStatefulSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</QueryClientProvider>
</Wrapper>,
);
const eventsTab = screen.getByRole('radio', { name: 'Events' });
expect(eventsTab).not.toBeChecked();
await userEvent.click(eventsTab, { pointerEventsCheck: 0 });
expect(eventsTab).toBeChecked();
});
it('should close modal when close button is clicked', async () => {
render(
<Wrapper>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<StatefulSetDetails
statefulSet={mockStatefulSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</QueryClientProvider>
</Wrapper>,
);
const closeButton = screen.getByRole('button', { name: 'Close' });
await userEvent.click(closeButton);
expect(mockOnClose).toHaveBeenCalled();
});
});

View File

@@ -1,57 +0,0 @@
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import {
getK8sStatefulSetsList,
K8sStatefulSetsListPayload,
K8sStatefulSetsListResponse,
} from 'api/infraMonitoring/getsK8sStatefulSetsList';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseGetK8sStatefulSetsList = (
requestData: K8sStatefulSetsListPayload,
options?: UseQueryOptions<
SuccessResponse<K8sStatefulSetsListResponse> | ErrorResponse,
Error
>,
headers?: Record<string, string>,
dotMetricsEnabled?: boolean,
) => UseQueryResult<
SuccessResponse<K8sStatefulSetsListResponse> | ErrorResponse,
Error
>;
export const useGetK8sStatefulSetsList: UseGetK8sStatefulSetsList = (
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_STATEFULSET_LIST, requestData];
}, [options?.queryKey, requestData]);
return useQuery<
SuccessResponse<K8sStatefulSetsListResponse> | ErrorResponse,
Error
>({
queryFn: ({ signal }) =>
getK8sStatefulSetsList(requestData, signal, headers, dotMetricsEnabled),
...options,
queryKey,
});
};