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
22 changed files with 1752 additions and 1596 deletions

View File

@@ -1,12 +1,11 @@
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 { K8sBaseFilters } from '../Base/K8sBaseList';
import { UnderscoreToDotMap } from '../utils';
export interface K8sNamespacesListPayload {
filters: TagFilter;
@@ -60,7 +59,7 @@ export function mapNamespacesMeta(
}
export const getK8sNamespacesList = async (
props: K8sBaseFilters,
props: K8sNamespacesListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
dotMetricsEnabled = false,
@@ -72,7 +71,7 @@ export const getK8sNamespacesList = 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;

View File

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

View File

@@ -0,0 +1,17 @@
.infra-monitoring-container {
.namespaces-list-table {
.expanded-table-container {
padding-left: 80px;
}
.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;
}
}
}

View File

@@ -1,116 +1,716 @@
import React, { useCallback } from 'react';
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 { K8sNamespacesListPayload } from 'api/infraMonitoring/getK8sNamespacesList';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { useGetK8sNamespacesList } from 'hooks/infraMonitoring/useGetK8sNamespacesList';
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 { getK8sNamespacesList, K8sNamespacesData } from './api';
import {
getNamespaceMetricsQueryPayload,
k8sNamespaceDetailsMetadataConfig,
k8sNamespaceGetEntityName,
k8sNamespaceGetSelectedItemFilters,
k8sNamespaceInitialEventsFilter,
k8sNamespaceInitialFilters,
k8sNamespaceInitialLogTracesFilter,
namespaceWidgetInfo,
} from './constants';
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
K8sCategory,
} from '../constants';
import {
k8sNamespacesColumns,
k8sNamespacesColumnsConfig,
k8sNamespacesRenderRowData,
} from './table';
useInfraMonitoringCurrentPage,
useInfraMonitoringEventsFilters,
useInfraMonitoringGroupBy,
useInfraMonitoringLogFilters,
useInfraMonitoringNamespaceUID,
useInfraMonitoringOrderBy,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from '../hooks';
import K8sHeader from '../K8sHeader';
import LoadingContainer from '../LoadingContainer';
import { usePageSize } from '../utils';
import NamespaceDetails from './NamespaceDetails';
import {
defaultAddedColumns,
formatDataForTable,
getK8sNamespacesListColumns,
getK8sNamespacesListQuery,
K8sNamespacesRowData,
} from './utils';
import '../InfraMonitoringK8s.styles.scss';
import './K8sNamespacesList.styles.scss';
function K8sNamespacesList({
controlListPrefix,
isFiltersVisible,
handleFilterVisibilityChange,
quickFiltersLastUpdated,
}: {
controlListPrefix?: React.ReactNode;
isFiltersVisible: boolean;
handleFilterVisibilityChange: () => void;
quickFiltersLastUpdated: number;
}): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
const [filtersInitialised, setFiltersInitialised] = useState(false);
const [orderBy, setOrderBy] = useInfraMonitoringOrderBy();
const [
selectedNamespaceUID,
setselectedNamespaceUID,
] = useInfraMonitoringNamespaceUID();
const { pageSize, setPageSize } = usePageSize(K8sCategory.NAMESPACES);
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
const [, setView] = useInfraMonitoringView();
const [, setTracesFilters] = useInfraMonitoringTracesFilters();
const [, setEventsFilters] = useInfraMonitoringEventsFilters();
const [, setLogFilters] = useInfraMonitoringLogFilters();
const [
selectedRowData,
setSelectedRowData,
] = useState<K8sNamespacesRowData | 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 fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
const createFiltersForSelectedRowData = (
selectedRowData: K8sNamespacesRowData,
groupBy: IBuilderQuery['groupBy'],
): IBuilderQuery['filters'] => {
const baseFilters: IBuilderQuery['filters'] = {
items: [...queryFilters.items],
op: 'and',
};
const response = await getK8sNamespacesList(
filters,
signal,
undefined,
dotMetricsEnabled,
);
if (!selectedRowData) {
return baseFilters;
}
return {
data: response.payload?.data.records || [],
total: response.payload?.data.total || 0,
error: response.error,
};
},
[dotMetricsEnabled],
);
const { groupedByMeta } = selectedRowData;
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{ data: K8sNamespacesData | null; error?: string | null }> => {
const response = await getK8sNamespacesList(
{
filters: filters.filters,
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
for (const key of groupBy) {
baseFilters.items.push({
key: {
key: key.key,
type: null,
},
signal,
undefined,
dotMetricsEnabled,
);
op: '=',
value: groupedByMeta[key.key],
id: key.key,
});
}
const records = response.payload?.data.records || [];
return baseFilters;
};
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
const fetchGroupedByRowDataQuery = useMemo(() => {
if (!selectedRowData) {
return null;
}
const baseQuery = getK8sNamespacesListQuery();
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 (selectedNamespaceUID) {
return [
'namespaceList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
selectedRowDataKey,
];
}
return [
'namespaceList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
selectedRowDataKey,
String(minTime),
String(maxTime),
];
}, [
queryFilters,
orderBy,
selectedNamespaceUID,
minTime,
maxTime,
selectedRowData,
]);
const {
data: groupedByRowData,
isFetching: isFetchingGroupedByRowData,
isLoading: isLoadingGroupedByRowData,
isError: isErrorGroupedByRowData,
refetch: fetchGroupedByRowData,
} = useGetK8sNamespacesList(
fetchGroupedByRowDataQuery as K8sNamespacesListPayload,
{
queryKey: groupedByRowDataQueryKey,
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
},
[dotMetricsEnabled],
undefined,
dotMetricsEnabled,
);
const {
data: groupByFiltersData,
isLoading: isLoadingGroupByFilters,
} = useGetAggregateKeys(
{
dataSource: currentQuery.builder.queryData[0].dataSource,
aggregateAttribute: GetK8sEntityToAggregateAttribute(
K8sCategory.NAMESPACES,
dotMetricsEnabled,
),
aggregateOperator: 'noop',
searchText: '',
tagType: '',
},
{
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
},
true,
K8sCategory.NODES,
);
const query = useMemo(() => {
const baseQuery = getK8sNamespacesListQuery();
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 formattedGroupedByNamespacesData = useMemo(
() =>
formatDataForTable(groupedByRowData?.payload?.data?.records || [], groupBy),
[groupedByRowData, groupBy],
);
const queryKey = useMemo(() => {
if (selectedNamespaceUID) {
return [
'namespaceList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
];
}
return [
'namespaceList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
String(minTime),
String(maxTime),
];
}, [
selectedNamespaceUID,
pageSize,
currentPage,
queryFilters,
orderBy,
groupBy,
minTime,
maxTime,
]);
const { data, isFetching, isLoading, isError } = useGetK8sNamespacesList(
query as K8sNamespacesListPayload,
{
queryKey,
enabled: !!query,
keepPreviousData: true,
},
undefined,
dotMetricsEnabled,
);
const namespacesData = useMemo(() => data?.payload?.data?.records || [], [
data,
]);
const totalCount = data?.payload?.data?.total || 0;
const formattedNamespacesData = useMemo(
() => formatDataForTable(namespacesData, groupBy),
[namespacesData, groupBy],
);
const nestedNamespacesData = useMemo(() => {
if (!selectedRowData || !groupedByRowData?.payload?.data.records) {
return [];
}
return groupedByRowData?.payload?.data?.records || [];
}, [groupedByRowData, selectedRowData]);
const columns = useMemo(() => getK8sNamespacesListColumns(groupBy), [groupBy]);
const handleGroupByRowClick = (record: K8sNamespacesRowData): 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<K8sNamespacesRowData>['onChange'] = useCallback(
(
pagination: TablePaginationConfig,
_filters: Record<string, (string | number | boolean)[] | null>,
sorter:
| SorterResult<K8sNamespacesRowData>
| SorterResult<K8sNamespacesRowData>[],
): void => {
if (pagination.current) {
setCurrentPage(pagination.current);
logEvent(InfraMonitoringEvents.PageNumberChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: InfraMonitoringEvents.Namespace,
});
}
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.Namespace,
});
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
useEffect(() => {
logEvent(InfraMonitoringEvents.PageVisited, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: InfraMonitoringEvents.Namespace,
total: data?.payload?.data?.total,
});
}, [data?.payload?.data?.total]);
const selectedNamespaceData = useMemo(() => {
if (!selectedNamespaceUID) {
return null;
}
if (groupBy.length > 0) {
// If grouped by, return the namespace from the formatted grouped by namespaces data
return (
nestedNamespacesData.find(
(namespace) => namespace.namespaceName === selectedNamespaceUID,
) || null
);
}
// If not grouped by, return the node from the nodes data
return (
namespacesData.find(
(namespace) => namespace.namespaceName === selectedNamespaceUID,
) || null
);
}, [
selectedNamespaceUID,
groupBy.length,
namespacesData,
nestedNamespacesData,
]);
const openNamespaceInNewTab = (record: K8sNamespacesRowData): void => {
const newParams = new URLSearchParams(document.location.search);
newParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.NAMESPACE_UID,
record.namespaceUID,
);
openInNewTab(
buildAbsolutePath({
relativePath: '',
urlQueryString: newParams.toString(),
}),
);
};
const handleRowClick = (
record: K8sNamespacesRowData,
event: React.MouseEvent,
): void => {
if (event && isModifierKeyPressed(event)) {
openNamespaceInNewTab(record);
return;
}
if (groupBy.length === 0) {
setSelectedRowData(null);
setselectedNamespaceUID(record.namespaceUID);
} else {
handleGroupByRowClick(record);
}
logEvent(InfraMonitoringEvents.ItemClicked, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: InfraMonitoringEvents.Namespace,
});
};
const nestedColumns = useMemo(() => getK8sNamespacesListColumns([]), []);
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<K8sNamespacesRowData>[]}
dataSource={formattedGroupedByNamespacesData}
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 (isModifierKeyPressed(event)) {
openNamespaceInNewTab(record);
return;
}
setselectedNamespaceUID(record.namespaceUID);
},
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: K8sNamespacesRowData,
e: React.MouseEvent<HTMLButtonElement>,
) => void;
record: K8sNamespacesRowData;
}): 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 handleCloseNamespaceDetail = (): void => {
setselectedNamespaceUID(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);
}
}
// Reset pagination on switching to groupBy
setCurrentPage(1);
setGroupBy(groupBy);
setExpandedRowKeys([]);
logEvent(InfraMonitoringEvents.GroupByChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: InfraMonitoringEvents.Namespace,
});
},
[groupByFiltersData, setCurrentPage, setGroupBy],
);
useEffect(() => {
if (groupByFiltersData?.payload) {
setGroupByOptions(
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
value: filter.key,
label: filter.key,
})) || [],
);
}
}, [groupByFiltersData]);
const onPaginationChange = (page: number, pageSize: number): void => {
setCurrentPage(page);
setPageSize(pageSize);
logEvent(InfraMonitoringEvents.PageNumberChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: InfraMonitoringEvents.Namespace,
});
};
const showTableLoadingState =
(isFetching || isLoading) && formattedNamespacesData.length === 0;
return (
<>
<K8sBaseList<K8sNamespacesData>
controlListPrefix={controlListPrefix}
entity={K8sCategory.NAMESPACES}
tableColumnsDefinitions={k8sNamespacesColumns}
tableColumns={k8sNamespacesColumnsConfig}
fetchListData={fetchListData}
renderRowData={k8sNamespacesRenderRowData}
eventCategory={InfraMonitoringEvents.Namespace}
<div className="k8s-list">
<K8sHeader
isFiltersVisible={isFiltersVisible}
handleFilterVisibilityChange={handleFilterVisibilityChange}
defaultAddedColumns={defaultAddedColumns}
handleFiltersChange={handleFiltersChange}
groupByOptions={groupByOptions}
isLoadingGroupByFilters={isLoadingGroupByFilters}
handleGroupByChange={handleGroupByChange}
selectedGroupBy={groupBy}
entity={K8sCategory.NODES}
showAutoRefresh={!selectedNamespaceData}
/>
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
<K8sBaseDetails<K8sNamespacesData>
category={K8sCategory.NAMESPACES}
eventCategory={InfraMonitoringEvents.Namespace}
getSelectedItemFilters={k8sNamespaceGetSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={k8sNamespaceGetEntityName}
getInitialLogTracesFilters={k8sNamespaceInitialLogTracesFilter}
getInitialEventsFilters={k8sNamespaceInitialEventsFilter}
primaryFilterKeys={k8sNamespaceInitialFilters}
metadataConfig={k8sNamespaceDetailsMetadataConfig}
entityWidgetInfo={namespaceWidgetInfo}
getEntityQueryPayload={getNamespaceMetricsQueryPayload}
queryKeyPrefix="namespace"
<Table
className="k8s-list-table namespaces-list-table"
dataSource={showTableLoadingState ? [] : formattedNamespacesData}
columns={columns}
pagination={{
current: currentPage,
pageSize,
total: totalCount,
showSizeChanger: true,
hideOnSinglePage: false,
onChange: onPaginationChange,
}}
scroll={{ x: true }}
loading={{
spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}}
locale={{
emptyText: showTableLoadingState ? null : (
<div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-content">
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
<Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again!
</Typography.Text>
</div>
</div>
),
}}
tableLayout="fixed"
onChange={handleTableChange}
onRow={(
record,
): { onClick: (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,
}}
/>
</>
<NamespaceDetails
namespace={selectedNamespaceData}
isModalTimeSelection
onClose={handleCloseNamespaceDetail}
/>
</div>
);
}

View File

@@ -0,0 +1,7 @@
import { K8sNamespacesData } from 'api/infraMonitoring/getK8sNamespacesList';
export type NamespaceDetailsProps = {
namespace: K8sNamespacesData | null;
isModalTimeSelection: boolean;
onClose: () => void;
};

View File

@@ -0,0 +1,249 @@
.namespace-detail-drawer {
border-left: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
.ant-drawer-header {
padding: 8px 16px;
border-bottom: none;
align-items: stretch;
border-bottom: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
}
.ant-drawer-close {
margin-inline-end: 0px;
}
.ant-drawer-body {
display: flex;
flex-direction: column;
padding: 16px;
}
.title {
color: var(--text-vanilla-400);
font-family: 'Geist Mono';
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.radio-button {
display: flex;
align-items: center;
justify-content: center;
padding-top: var(--padding-1);
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
.namespace-detail-drawer__namespace {
.namespace-details-grid {
.labels-row,
.values-row {
display: grid;
grid-template-columns: 1fr 1.5fr 1.5fr 1.5fr;
gap: 30px;
align-items: center;
}
.labels-row {
margin-bottom: 8px;
}
.namespace-details-metadata-label {
color: var(--text-vanilla-400);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
}
.namespace-details-metadata-value {
color: var(--text-vanilla-400);
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.status-tag {
margin: 0;
&.active {
color: var(--success-500);
background: var(--success-100);
border-color: var(--success-500);
}
&.inactive {
color: var(--error-500);
background: var(--error-100);
border-color: var(--error-500);
}
}
.progress-container {
width: 158px;
.ant-progress {
margin: 0;
.ant-progress-text {
font-weight: 600;
}
}
}
.ant-card {
&.ant-card-bordered {
border: 1px solid var(--bg-slate-500) !important;
}
}
}
}
.tabs-and-search {
display: flex;
justify-content: space-between;
align-items: center;
margin: 16px 0;
.action-btn {
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
}
.views-tabs-container {
margin-top: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
.views-tabs {
color: var(--text-vanilla-400);
.view-title {
display: flex;
gap: var(--margin-2);
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
font-style: normal;
font-weight: var(--font-weight-normal);
}
.tab {
border: 1px solid var(--bg-slate-400);
width: 114px;
}
.tab::before {
background: var(--bg-slate-400);
}
.selected_view {
background: var(--bg-slate-300);
color: var(--text-vanilla-100);
border: 1px solid var(--bg-slate-400);
}
.selected_view::before {
background: var(--bg-slate-400);
}
}
.compass-button {
width: 30px;
height: 30px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
.ant-drawer-close {
padding: 0px;
}
}
.lightMode {
.ant-drawer-header {
border-bottom: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
}
.namespace-detail-drawer {
.title {
color: var(--text-ink-300);
}
.namespace-detail-drawer__namespace {
.ant-typography {
color: var(--text-ink-300);
background: transparent;
}
}
.radio-button {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
.views-tabs {
.tab {
background: var(--bg-vanilla-100);
}
.selected_view {
background: var(--bg-vanilla-300);
border: 1px solid var(--bg-slate-300);
color: var(--text-ink-400);
}
.selected_view::before {
background: var(--bg-vanilla-300);
border-left: 1px solid var(--bg-slate-300);
}
}
.compass-button {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
.tabs-and-search {
.action-btn {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
color: var(--text-ink-300);
}
}
}
}

View File

@@ -6,6 +6,7 @@ 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 { K8sNamespacesData } from 'api/infraMonitoring/getK8sNamespacesList';
import { VIEW_TYPES, VIEWS } from 'components/HostMetricsDetail/constants';
import { InfraMonitoringEvents } from 'constants/events';
import { QueryParams } from 'constants/query';
@@ -16,10 +17,6 @@ import {
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,
@@ -55,19 +52,23 @@ import {
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuidv4 } from 'uuid';
import NamespaceEvents from '../../EntityDetailsUtils/EntityEvents';
import NamespaceLogs from '../../EntityDetailsUtils/EntityLogs';
import NamespaceMetrics from '../../EntityDetailsUtils/EntityMetrics';
import NamespaceTraces from '../../EntityDetailsUtils/EntityTraces';
import {
getStatefulSetMetricsQueryPayload,
statefulSetWidgetInfo,
getNamespaceMetricsQueryPayload,
namespaceWidgetInfo,
} from './constants';
import { StatefulSetDetailsProps } from './StatefulSetDetails.interfaces';
import { NamespaceDetailsProps } from './NamespaceDetails.interfaces';
import '../../EntityDetailsUtils/entityDetails.styles.scss';
import './NamespaceDetails.styles.scss';
function StatefulSetDetails({
statefulSet,
function NamespaceDetails({
namespace,
onClose,
isModalTimeSelection,
}: StatefulSetDetailsProps): JSX.Element {
}: NamespaceDetailsProps): JSX.Element {
const { maxTime, minTime, selectedTime } = useSelector<
AppState,
GlobalReducer
@@ -116,17 +117,6 @@ function StatefulSetDetails({
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: {
@@ -136,13 +126,12 @@ function StatefulSetDetails({
id: 'k8s_namespace_name--string--resource--false',
},
op: '=',
value: statefulSet?.meta.k8s_namespace_name || '',
value: namespace?.namespaceName || '',
},
],
};
}, [
statefulSet?.meta.k8s_statefulset_name,
statefulSet?.meta.k8s_namespace_name,
namespace?.namespaceName,
selectedView,
logFiltersParam,
tracesFiltersParam,
@@ -164,7 +153,7 @@ function StatefulSetDetails({
id: 'k8s.object.kind--string--resource--false',
},
op: '=',
value: 'StatefulSet',
value: 'Namespace',
},
{
id: uuidv4(),
@@ -175,11 +164,11 @@ function StatefulSetDetails({
id: 'k8s.object.name--string--resource--false',
},
op: '=',
value: statefulSet?.meta.k8s_statefulset_name || '',
value: namespace?.namespaceName || '',
},
],
};
}, [statefulSet?.meta.k8s_statefulset_name, eventsFiltersParam]);
}, [namespace?.namespaceName, eventsFiltersParam]);
const [logAndTracesFilters, setLogAndTracesFilters] = useState<
IBuilderQuery['filters']
@@ -190,14 +179,14 @@ function StatefulSetDetails({
);
useEffect(() => {
if (statefulSet) {
if (namespace) {
logEvent(InfraMonitoringEvents.PageVisited, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: InfraMonitoringEvents.StatefulSet,
category: InfraMonitoringEvents.Namespace,
});
}
}, [statefulSet]);
}, [namespace]);
useEffect(() => {
setLogAndTracesFilters(initialFilters);
@@ -226,7 +215,7 @@ function StatefulSetDetails({
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: InfraMonitoringEvents.StatefulSet,
category: InfraMonitoringEvents.Namespace,
view: e.target.value,
});
};
@@ -253,7 +242,7 @@ function StatefulSetDetails({
logEvent(InfraMonitoringEvents.TimeUpdated, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: InfraMonitoringEvents.StatefulSet,
category: InfraMonitoringEvents.Namespace,
interval,
view: selectedView,
});
@@ -266,7 +255,7 @@ function StatefulSetDetails({
(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(
[QUERY_KEYS.K8S_NAMESPACE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME].includes(
item.key?.key ?? '',
),
);
@@ -275,16 +264,15 @@ function StatefulSetDetails({
);
const newFilters = value?.items?.filter(
(item) =>
item.key?.key !== 'id' &&
item.key?.key !== QUERY_KEYS.K8S_STATEFUL_SET_NAME,
item.key?.key !== 'id' && item.key?.key !== QUERY_KEYS.K8S_NAMESPACE_NAME,
);
if (newFilters && newFilters?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: InfraMonitoringEvents.StatefulSet,
view: 'logs',
category: InfraMonitoringEvents.Namespace,
view: InfraMonitoringEvents.LogsView,
});
}
@@ -313,7 +301,7 @@ function StatefulSetDetails({
(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(
[QUERY_KEYS.K8S_NAMESPACE_NAME, QUERY_KEYS.K8S_CLUSTER_NAME].includes(
item.key?.key ?? '',
),
);
@@ -322,8 +310,8 @@ function StatefulSetDetails({
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: InfraMonitoringEvents.StatefulSet,
view: 'traces',
category: InfraMonitoringEvents.Namespace,
view: InfraMonitoringEvents.TracesView,
});
}
@@ -333,7 +321,7 @@ function StatefulSetDetails({
[
...(primaryFilters || []),
...(value?.items?.filter(
(item) => item.key?.key !== QUERY_KEYS.K8S_STATEFUL_SET_NAME,
(item) => item.key?.key !== QUERY_KEYS.K8S_NAMESPACE_NAME,
) || []),
].filter((item): item is TagFilterItem => item !== undefined),
),
@@ -352,10 +340,10 @@ function StatefulSetDetails({
const handleChangeEventsFilters = useCallback(
(value: IBuilderQuery['filters'], view: VIEWS) => {
setEventsFilters((prevFilters) => {
const statefulSetKindFilter = prevFilters?.items?.find(
const namespaceKindFilter = prevFilters?.items?.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_KIND,
);
const statefulSetNameFilter = prevFilters?.items?.find(
const namespaceNameFilter = prevFilters?.items?.find(
(item) => item.key?.key === QUERY_KEYS.K8S_OBJECT_NAME,
);
@@ -363,16 +351,16 @@ function StatefulSetDetails({
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: InfraMonitoringEvents.StatefulSet,
view: 'logs',
category: InfraMonitoringEvents.Namespace,
view: InfraMonitoringEvents.EventsView,
});
}
const updatedFilters = {
op: 'AND',
items: [
statefulSetKindFilter,
statefulSetNameFilter,
namespaceKindFilter,
namespaceNameFilter,
...(value?.items?.filter(
(item) =>
item.key?.key !== QUERY_KEYS.K8S_OBJECT_KIND &&
@@ -403,7 +391,7 @@ function StatefulSetDetails({
logEvent(InfraMonitoringEvents.ExploreClicked, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: InfraMonitoringEvents.StatefulSet,
category: InfraMonitoringEvents.Namespace,
view: selectedView,
});
@@ -483,13 +471,13 @@ function StatefulSetDetails({
<>
<Divider type="vertical" />
<Typography.Text className="title">
{statefulSet?.meta.k8s_statefulset_name}
{namespace?.namespaceName}
</Typography.Text>
</>
}
placement="right"
onClose={handleClose}
open={!!statefulSet}
open={!!namespace}
style={{
overscrollBehavior: 'contain',
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
@@ -498,7 +486,7 @@ function StatefulSetDetails({
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
>
{statefulSet && (
{namespace && (
<>
<div className="entity-detail-drawer__entity">
<div className="entity-details-grid">
@@ -507,24 +495,24 @@ function StatefulSetDetails({
type="secondary"
className="entity-details-metadata-label"
>
Statefulset Name
Namespace Name
</Typography.Text>
<Typography.Text
type="secondary"
className="entity-details-metadata-label"
>
Namespace Name
Cluster 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 title={namespace.namespaceName}>
{namespace.namespaceName}
</Tooltip>
</Typography.Text>
<Typography.Text className="entity-details-metadata-value">
<Tooltip title={statefulSet.meta.k8s_namespace_name}>
{statefulSet.meta.k8s_namespace_name}
<Tooltip title="Cluster name">
{namespace.meta.k8s_cluster_name}
</Tooltip>
</Typography.Text>
</div>
@@ -593,60 +581,54 @@ function StatefulSetDetails({
)}
</div>
{selectedView === VIEW_TYPES.METRICS && (
<EntityMetrics
<NamespaceMetrics<K8sNamespacesData>
timeRange={modalTimeRange}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
entity={statefulSet}
entityWidgetInfo={statefulSetWidgetInfo}
getEntityQueryPayload={getStatefulSetMetricsQueryPayload}
category={K8sCategory.STATEFULSETS}
queryKey="statefulsetMetrics"
entity={namespace}
entityWidgetInfo={namespaceWidgetInfo}
getEntityQueryPayload={getNamespaceMetricsQueryPayload}
category={K8sCategory.NAMESPACES}
queryKey="namespaceMetrics"
/>
)}
{selectedView === VIEW_TYPES.LOGS && (
<EntityLogs
<NamespaceLogs
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,
]}
queryKey="namespaceLogs"
category={K8sCategory.NAMESPACES}
queryKeyFilters={[QUERY_KEYS.K8S_NAMESPACE_NAME]}
/>
)}
{selectedView === VIEW_TYPES.TRACES && (
<EntityTraces
<NamespaceTraces
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,
]}
queryKey="namespaceTraces"
category={InfraMonitoringEvents.Namespace}
queryKeyFilters={[QUERY_KEYS.K8S_NAMESPACE_NAME]}
/>
)}
{selectedView === VIEW_TYPES.EVENTS && (
<EntityEvents
<NamespaceEvents
timeRange={modalTimeRange}
handleChangeEventFilters={handleChangeEventsFilters}
filters={eventsFilters}
isModalTimeSelection={isModalTimeSelection}
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
category={K8sCategory.STATEFULSETS}
queryKey="statefulsetEvents"
category={K8sCategory.NAMESPACES}
queryKey="namespaceEvents"
/>
)}
</>
@@ -655,4 +637,4 @@ function StatefulSetDetails({
);
}
export default StatefulSetDetails;
export default NamespaceDetails;

View File

@@ -1,67 +1,11 @@
import { K8sNamespacesData } from 'api/infraMonitoring/getK8sNamespacesList';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
TagFilter,
TagFilterItem,
} 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 { K8sNamespacesData } from './api';
export const k8sNamespaceGetSelectedItemFilters = (
selectedItemId: string,
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_namespace_name',
key: {
key: 'k8s_namespace_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
export const k8sNamespaceDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sNamespacesData>[] = [
{ label: 'Namespace Name', getValue: (p): string => p.namespaceName },
{
label: 'Cluster Name',
getValue: (p): string => p.meta.k8s_cluster_name,
},
];
export const k8sNamespaceInitialFilters = [
QUERY_KEYS.K8S_NAMESPACE_NAME,
QUERY_KEYS.K8S_CLUSTER_NAME,
];
export const k8sNamespaceInitialEventsFilter = (
item: K8sNamespacesData,
): TagFilterItem[] => [
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Namespace'),
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.namespaceName),
];
export const k8sNamespaceInitialLogTracesFilter = (
item: K8sNamespacesData,
): TagFilterItem[] => [
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.namespaceName),
];
export const k8sNamespaceGetEntityName = (item: K8sNamespacesData): string =>
item.namespaceName;
export const namespaceWidgetInfo = [
{
title: 'CPU Usage (cores)',

View File

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

View File

@@ -1,155 +0,0 @@
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 { K8sNamespacesData, K8sNamespacesListPayload } from './api';
import styles from './table.module.scss';
export interface K8sNamespacesRowData {
key: string;
itemKey: string;
namespaceUID: string;
namespaceName: React.ReactNode;
clusterName: string;
cpu: React.ReactNode;
memory: React.ReactNode;
groupedByMeta?: Record<string, string>;
}
export const k8sNamespacesColumns: IEntityColumn[] = [
{
label: 'Namespace Group',
value: 'namespaceGroup',
id: 'namespaceGroup',
canBeHidden: false,
defaultVisibility: true,
behavior: 'hidden-on-collapse',
},
{
label: 'Namespace Name',
value: 'namespaceName',
id: 'namespaceName',
canBeHidden: false,
defaultVisibility: true,
behavior: 'hidden-on-expand',
},
{
label: 'Cluster Name',
value: 'clusterName',
id: 'clusterName',
canBeHidden: false,
defaultVisibility: true,
behavior: 'always-visible',
},
{
label: 'CPU Usage (cores)',
value: 'cpu',
id: 'cpu',
canBeHidden: false,
defaultVisibility: true,
behavior: 'always-visible',
},
{
label: 'Memory Usage (WSS)',
value: 'memory',
id: 'memory',
canBeHidden: false,
defaultVisibility: true,
behavior: 'always-visible',
},
];
export const getK8sNamespacesListQuery = (): K8sNamespacesListPayload => ({
filters: {
items: [],
op: 'and',
},
orderBy: { columnName: 'cpu', order: 'desc' },
});
export const k8sNamespacesColumnsConfig: ColumnType<K8sRenderedRowData>[] = [
{
title: (
<div className={styles.entityGroupHeader}>
<Group size={14} /> NAMESPACE GROUP
</div>
),
dataIndex: 'namespaceGroup',
key: 'namespaceGroup',
ellipsis: true,
width: 150,
align: 'left',
sorter: false,
},
{
title: <div>Namespace Name</div>,
dataIndex: 'namespaceName',
key: 'namespaceName',
ellipsis: true,
width: 120,
sorter: false,
align: 'left',
},
{
title: <div>Cluster Name</div>,
dataIndex: 'clusterName',
key: 'clusterName',
ellipsis: true,
width: 120,
sorter: false,
align: 'left',
},
{
title: <div>CPU Usage (cores)</div>,
dataIndex: 'cpu',
key: 'cpu',
width: 100,
sorter: true,
align: 'left',
},
{
title: <div>Mem Usage (WSS)</div>,
dataIndex: 'memory',
key: 'memory',
width: 120,
sorter: true,
align: 'left',
},
];
export const k8sNamespacesRenderRowData = (
namespace: K8sNamespacesData,
groupBy: BaseAutocompleteData[],
): K8sRenderedRowData => ({
key: getRowKey(
namespace,
() => namespace.namespaceName || namespace.meta.k8s_namespace_name,
groupBy,
),
itemKey: namespace.meta.k8s_namespace_name,
namespaceUID: namespace.namespaceName,
namespaceName: (
<Tooltip title={namespace.namespaceName}>
{namespace.namespaceName || ''}
</Tooltip>
),
clusterName: namespace.meta.k8s_cluster_name,
cpu: (
<ValidateColumnValueWrapper value={namespace.cpuUsage}>
{namespace.cpuUsage}
</ValidateColumnValueWrapper>
),
memory: (
<ValidateColumnValueWrapper value={namespace.memoryUsage}>
{formatBytes(namespace.memoryUsage)}
</ValidateColumnValueWrapper>
),
namespaceGroup: getGroupByEl(namespace, groupBy),
...namespace.meta,
groupedByMeta: getGroupedByMeta(namespace, groupBy),
});

View File

@@ -0,0 +1,179 @@
import { Color } from '@signozhq/design-tokens';
import { TableColumnType as ColumnType, Tag } from 'antd';
import {
K8sNamespacesData,
K8sNamespacesListPayload,
} from 'api/infraMonitoring/getK8sNamespacesList';
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: 'Namespace Name',
value: 'namespaceName',
id: 'namespaceName',
canRemove: false,
},
{
label: 'Cluster Name',
value: 'clusterName',
id: 'clusterName',
canRemove: false,
},
{
label: 'CPU Utilization (cores)',
value: 'cpu',
id: 'cpu',
canRemove: false,
},
{
label: 'Memory Utilization (bytes)',
value: 'memory',
id: 'memory',
canRemove: false,
},
];
export interface K8sNamespacesRowData {
key: string;
namespaceUID: string;
namespaceName: string;
clusterName: string;
cpu: React.ReactNode;
memory: React.ReactNode;
groupedByMeta?: any;
}
const namespaceGroupColumnConfig = {
title: (
<div className="column-header entity-group-header">
<Group size={14} /> NAMESPACE GROUP
</div>
),
dataIndex: 'namespaceGroup',
key: 'namespaceGroup',
ellipsis: true,
width: 150,
align: 'left',
sorter: false,
className: 'column entity-group-header',
};
export const getK8sNamespacesListQuery = (): K8sNamespacesListPayload => ({
filters: {
items: [],
op: 'and',
},
orderBy: { columnName: 'cpu', order: 'desc' },
});
const columnsConfig = [
{
title: <div className="column-header-left">Namespace Name</div>,
dataIndex: 'namespaceName',
key: 'namespaceName',
ellipsis: true,
width: 120,
sorter: false,
align: 'left',
},
{
title: <div className="column-header-left">Cluster Name</div>,
dataIndex: 'clusterName',
key: 'clusterName',
ellipsis: true,
width: 120,
sorter: false,
align: 'left',
},
{
title: <div className="column-header-left">CPU Usage (cores)</div>,
dataIndex: 'cpu',
key: 'cpu',
width: 100,
sorter: true,
align: 'left',
},
{
title: <div className="column-header-left">Mem Usage (WSS)</div>,
dataIndex: 'memory',
key: 'memory',
width: 120,
sorter: true,
align: 'left',
},
];
export const getK8sNamespacesListColumns = (
groupBy: IBuilderQuery['groupBy'],
): ColumnType<K8sNamespacesRowData>[] => {
if (groupBy.length > 0) {
const filteredColumns = [...columnsConfig].filter(
(column) => column.key !== 'namespaceName',
);
filteredColumns.unshift(namespaceGroupColumnConfig);
return filteredColumns as ColumnType<K8sNamespacesRowData>[];
}
return columnsConfig as ColumnType<K8sNamespacesRowData>[];
};
const dotToUnder: Record<string, keyof K8sNamespacesData['meta']> = {
'k8s.namespace.name': 'k8s_namespace_name',
'k8s.cluster.name': 'k8s_cluster_name',
};
const getGroupByEle = (
namespace: K8sNamespacesData,
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 namespace.meta;
const value = namespace.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: K8sNamespacesData[],
groupBy: IBuilderQuery['groupBy'],
): K8sNamespacesRowData[] =>
data.map((namespace, index) => ({
key: index.toString(),
namespaceUID: namespace.namespaceName,
namespaceName: namespace.namespaceName,
clusterName: namespace.meta.k8s_cluster_name,
cpu: (
<ValidateColumnValueWrapper value={namespace.cpuUsage}>
{namespace.cpuUsage}
</ValidateColumnValueWrapper>
),
memory: (
<ValidateColumnValueWrapper value={namespace.memoryUsage}>
{formatBytes(namespace.memoryUsage)}
</ValidateColumnValueWrapper>
),
namespaceGroup: getGroupByEle(namespace, groupBy),
meta: namespace.meta,
...namespace.meta,
groupedByMeta: namespace.meta,
}));

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,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

@@ -1,6 +1,11 @@
.entityGroupHeader {
padding-left: var(--spacing-5);
gap: var(--spacing-5);
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

@@ -4,21 +4,22 @@ 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 { render, screen } from '@testing-library/react';
import StatefulSetDetails from 'container/InfraMonitoringK8s/StatefulSets/StatefulSetDetails/StatefulSetDetails';
import { fireEvent, render, screen } from '@testing-library/react';
import NamespaceDetails from 'container/InfraMonitoringK8s/Namespaces/NamespaceDetails/NamespaceDetails';
import { withNuqsTestingAdapter } from 'nuqs/adapters/testing';
import { userEvent } from 'tests/test-utils';
import store from 'store';
const queryClient = new QueryClient();
const Wrapper = withNuqsTestingAdapter({ searchParams: {} });
describe('StatefulSetDetails', () => {
const mockStatefulSet = {
describe('NamespaceDetails', () => {
const mockNamespace = {
namespaceName: 'test-namespace',
meta: {
k8s_namespace_name: 'test-namespace',
k8s_statefulset_name: 'test-stateful-set',
k8s_cluster_name: 'test-cluster',
},
} as any;
const mockOnClose = jest.fn();
@@ -27,37 +28,41 @@ describe('StatefulSetDetails', () => {
render(
<Wrapper>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<StatefulSetDetails
statefulSet={mockStatefulSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
<Provider store={store}>
<MemoryRouter>
<NamespaceDetails
namespace={mockNamespace}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</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();
const clusterNameElements = screen.getAllByText('test-cluster');
expect(clusterNameElements.length).toBeGreaterThan(0);
expect(clusterNameElements[0]).toBeInTheDocument();
});
it('should render modal with 4 tabs', () => {
render(
<Wrapper>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<StatefulSetDetails
statefulSet={mockStatefulSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
<Provider store={store}>
<MemoryRouter>
<NamespaceDetails
namespace={mockNamespace}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
@@ -79,13 +84,15 @@ describe('StatefulSetDetails', () => {
render(
<Wrapper>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<StatefulSetDetails
statefulSet={mockStatefulSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
<Provider store={store}>
<MemoryRouter>
<NamespaceDetails
namespace={mockNamespace}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
@@ -94,44 +101,48 @@ describe('StatefulSetDetails', () => {
expect(metricsTab).toBeChecked();
});
it('should switch to events tab when events tab is clicked', async () => {
it('should switch to events tab when events tab is clicked', () => {
render(
<Wrapper>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<StatefulSetDetails
statefulSet={mockStatefulSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
<Provider store={store}>
<MemoryRouter>
<NamespaceDetails
namespace={mockNamespace}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const eventsTab = screen.getByRole('radio', { name: 'Events' });
expect(eventsTab).not.toBeChecked();
await userEvent.click(eventsTab, { pointerEventsCheck: 0 });
fireEvent.click(eventsTab);
expect(eventsTab).toBeChecked();
});
it('should close modal when close button is clicked', async () => {
it('should close modal when close button is clicked', () => {
render(
<Wrapper>
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<StatefulSetDetails
statefulSet={mockStatefulSet}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
<Provider store={store}>
<MemoryRouter>
<NamespaceDetails
namespace={mockNamespace}
isModalTimeSelection
onClose={mockOnClose}
/>
</MemoryRouter>
</Provider>
</QueryClientProvider>
</Wrapper>,
);
const closeButton = screen.getByRole('button', { name: 'Close' });
await userEvent.click(closeButton);
fireEvent.click(closeButton);
expect(mockOnClose).toHaveBeenCalled();
});
});

View File

@@ -1,33 +1,29 @@
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import {
getK8sStatefulSetsList,
K8sStatefulSetsListPayload,
K8sStatefulSetsListResponse,
} from 'api/infraMonitoring/getsK8sStatefulSetsList';
getK8sNamespacesList,
K8sNamespacesListPayload,
K8sNamespacesListResponse,
} from 'api/infraMonitoring/getK8sNamespacesList';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseGetK8sStatefulSetsList = (
requestData: K8sStatefulSetsListPayload,
type UseGetK8sNamespacesList = (
requestData: K8sNamespacesListPayload,
options?: UseQueryOptions<
SuccessResponse<K8sStatefulSetsListResponse> | ErrorResponse,
SuccessResponse<K8sNamespacesListResponse> | ErrorResponse,
Error
>,
headers?: Record<string, string>,
dotMetricsEnabled?: boolean,
) => UseQueryResult<
SuccessResponse<K8sStatefulSetsListResponse> | ErrorResponse,
SuccessResponse<K8sNamespacesListResponse> | ErrorResponse,
Error
>;
export const useGetK8sStatefulSetsList: UseGetK8sStatefulSetsList = (
export const useGetK8sNamespacesList: UseGetK8sNamespacesList = (
requestData,
options,
headers,
dotMetricsEnabled,
) => {
@@ -40,18 +36,16 @@ export const useGetK8sStatefulSetsList: UseGetK8sStatefulSetsList = (
return options.queryKey;
}
return [REACT_QUERY_KEY.GET_STATEFULSET_LIST, requestData];
return [REACT_QUERY_KEY.GET_NAMESPACE_LIST, requestData];
}, [options?.queryKey, requestData]);
return useQuery<
SuccessResponse<K8sStatefulSetsListResponse> | ErrorResponse,
SuccessResponse<K8sNamespacesListResponse> | ErrorResponse,
Error
>({
queryFn: ({ signal }) =>
getK8sStatefulSetsList(requestData, signal, headers, dotMetricsEnabled),
getK8sNamespacesList(requestData, signal, headers, dotMetricsEnabled),
...options,
queryKey,
});
};