mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-10 16:40:34 +01:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66f4a63978 | ||
|
|
7d29a22e32 | ||
|
|
1e257a5b26 |
@@ -3148,6 +3148,8 @@ components:
|
||||
type: string
|
||||
image:
|
||||
type: string
|
||||
legacy:
|
||||
type: boolean
|
||||
locked:
|
||||
type: boolean
|
||||
name:
|
||||
@@ -3180,6 +3182,7 @@ components:
|
||||
- name
|
||||
- tags
|
||||
- spec
|
||||
- legacy
|
||||
- pinned
|
||||
type: object
|
||||
DashboardtypesListedDashboardV2:
|
||||
@@ -3193,6 +3196,8 @@ components:
|
||||
type: string
|
||||
image:
|
||||
type: string
|
||||
legacy:
|
||||
type: boolean
|
||||
locked:
|
||||
type: boolean
|
||||
name:
|
||||
@@ -3223,6 +3228,7 @@ components:
|
||||
- name
|
||||
- tags
|
||||
- spec
|
||||
- legacy
|
||||
type: object
|
||||
DashboardtypesListedDashboardV2Spec:
|
||||
properties:
|
||||
@@ -4478,10 +4484,14 @@ components:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
misscheduledNodes:
|
||||
type: integer
|
||||
podCountsByPhase:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
|
||||
podCountsByStatus:
|
||||
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
|
||||
readyNodes:
|
||||
type: integer
|
||||
required:
|
||||
- daemonSetName
|
||||
- daemonSetCPU
|
||||
@@ -4492,6 +4502,8 @@ components:
|
||||
- daemonSetMemoryLimit
|
||||
- desiredNodes
|
||||
- currentNodes
|
||||
- readyNodes
|
||||
- misscheduledNodes
|
||||
- podCountsByPhase
|
||||
- podCountsByStatus
|
||||
- meta
|
||||
@@ -15993,21 +16005,24 @@ paths:
|
||||
daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row
|
||||
also reports the latest known node-level counters from kube-state-metrics:
|
||||
desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the
|
||||
daemonset wants to run on) and currentNodes (k8s.daemonset.current_scheduled_nodes,
|
||||
the number of nodes the daemonset currently runs on) — note these are node
|
||||
counts, not pod counts. It also reports per-group podCountsByPhase ({ pending,
|
||||
running, succeeded, failed, unknown } from each pod''s latest k8s.pod.phase
|
||||
value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name,
|
||||
k8s.cluster.name). The response type is ''list'' for the default k8s.daemonset.name
|
||||
grouping or ''grouped_list'' for custom groupBy keys; in both modes every
|
||||
row aggregates pods owned by daemonsets in the group. Supports filtering via
|
||||
a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit
|
||||
/ memory / memory_request / memory_limit / desired_nodes / current_nodes,
|
||||
daemonset wants to run on), currentNodes (k8s.daemonset.current_scheduled_nodes,
|
||||
the number of nodes the daemonset currently runs on), readyNodes (k8s.daemonset.ready_nodes,
|
||||
the number of nodes running at least one ready daemon pod) and misscheduledNodes
|
||||
(k8s.daemonset.misscheduled_nodes, the number of nodes running the daemon
|
||||
pod but not supposed to) — note these are node counts, not pod counts. It
|
||||
also reports per-group podCountsByPhase ({ pending, running, succeeded, failed,
|
||||
unknown } from each pod''s latest k8s.pod.phase value). Each daemonset includes
|
||||
metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name).
|
||||
The response type is ''list'' for the default k8s.daemonset.name grouping
|
||||
or ''grouped_list'' for custom groupBy keys; in both modes every row aggregates
|
||||
pods owned by daemonsets in the group. Supports filtering via a filter expression,
|
||||
custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request
|
||||
/ memory_limit / desired_nodes / current_nodes / ready_nodes / misscheduled_nodes,
|
||||
and pagination via offset/limit. Also reports whether the requested time range
|
||||
falls before the data retention boundary. Numeric metric fields (daemonSetCPU,
|
||||
daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest,
|
||||
daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel
|
||||
when no data is available for that field.'
|
||||
daemonSetMemoryLimit, desiredNodes, currentNodes, readyNodes, misscheduledNodes)
|
||||
return -1 as a sentinel when no data is available for that field.'
|
||||
operationId: ListDaemonSets
|
||||
requestBody:
|
||||
content:
|
||||
|
||||
@@ -219,7 +219,7 @@ export const useListClusters = <
|
||||
return useMutation(getListClustersMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on) and currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel when no data is available for that field.
|
||||
* Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on), currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on), readyNodes (k8s.daemonset.ready_nodes, the number of nodes running at least one ready daemon pod) and misscheduledNodes (k8s.daemonset.misscheduled_nodes, the number of nodes running the daemon pod but not supposed to) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes / ready_nodes / misscheduled_nodes, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes, readyNodes, misscheduledNodes) return -1 as a sentinel when no data is available for that field.
|
||||
* @summary List DaemonSets for Infra Monitoring
|
||||
*/
|
||||
export const listDaemonSets = (
|
||||
|
||||
@@ -5005,6 +5005,10 @@ export interface DashboardtypesListedDashboardForUserV2DTO {
|
||||
* @type string
|
||||
*/
|
||||
image?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
legacy: boolean;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
@@ -5080,6 +5084,10 @@ export interface DashboardtypesListedDashboardV2DTO {
|
||||
* @type string
|
||||
*/
|
||||
image?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
legacy: boolean;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
@@ -6082,8 +6090,16 @@ export interface InframonitoringtypesDaemonSetRecordDTO {
|
||||
* @type object,null
|
||||
*/
|
||||
meta: InframonitoringtypesDaemonSetRecordDTOMeta;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
misscheduledNodes: number;
|
||||
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
|
||||
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
readyNodes: number;
|
||||
}
|
||||
|
||||
export interface InframonitoringtypesDaemonSetsDTO {
|
||||
|
||||
@@ -22,6 +22,7 @@ export const getKeySuggestions = (
|
||||
fieldContext = '',
|
||||
fieldDataType = '',
|
||||
signalSource = '',
|
||||
metricNamespace = '',
|
||||
} = props;
|
||||
|
||||
const encodedSignal = encodeURIComponent(signal);
|
||||
@@ -30,8 +31,9 @@ export const getKeySuggestions = (
|
||||
const encodedFieldContext = encodeURIComponent(fieldContext);
|
||||
const encodedFieldDataType = encodeURIComponent(fieldDataType);
|
||||
const encodedSource = encodeURIComponent(signalSource);
|
||||
const encodedMetricNamespace = encodeURIComponent(metricNamespace);
|
||||
|
||||
return axios.get(
|
||||
`/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}&source=${encodedSource}`,
|
||||
`/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}&source=${encodedSource}&metricNamespace=${encodedMetricNamespace}`,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -89,6 +89,7 @@ interface QuerySearchProps {
|
||||
onChange: (value: string) => void;
|
||||
queryData: IBuilderQuery;
|
||||
dataSource: DataSource;
|
||||
metricNamespace?: string;
|
||||
signalSource?: string;
|
||||
hardcodedAttributeKeys?: QueryKeyDataSuggestionsProps[];
|
||||
onRun?: (query: string) => void;
|
||||
@@ -107,6 +108,7 @@ function QuerySearch({
|
||||
hardcodedAttributeKeys,
|
||||
showFilterSuggestionsWithoutMetric,
|
||||
initialExpression,
|
||||
metricNamespace,
|
||||
}: QuerySearchProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
|
||||
@@ -299,6 +301,7 @@ function QuerySearch({
|
||||
searchText: searchText || '',
|
||||
metricName: debouncedMetricName ?? undefined,
|
||||
signalSource: signalSource as 'meter' | '',
|
||||
metricNamespace,
|
||||
});
|
||||
|
||||
if (response.data.data) {
|
||||
@@ -331,6 +334,7 @@ function QuerySearch({
|
||||
signalSource,
|
||||
hardcodedAttributeKeys,
|
||||
showFilterSuggestionsWithoutMetric,
|
||||
metricNamespace,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -2,10 +2,22 @@
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
padding: var(--tanstack-cell-padding-top, 0.3rem)
|
||||
var(--tanstack-cell-padding-right, 0.3rem)
|
||||
var(--tanstack-cell-padding-bottom, 0.3rem)
|
||||
var(--tanstack-cell-padding-left, 0.3rem);
|
||||
padding: var(
|
||||
--tanstack-cell-header-padding-top-override,
|
||||
var(--tanstack-cell-padding-top, 0.3rem)
|
||||
)
|
||||
var(
|
||||
--tanstack-cell-header-padding-right-override,
|
||||
var(--tanstack-cell-padding-right, 0.3rem)
|
||||
)
|
||||
var(
|
||||
--tanstack-cell-header-padding-bottom-override,
|
||||
var(--tanstack-cell-padding-bottom, 0.3rem)
|
||||
)
|
||||
var(
|
||||
--tanstack-cell-header-padding-left-override,
|
||||
var(--tanstack-cell-padding-left, 0.3rem)
|
||||
);
|
||||
transform: translate3d(
|
||||
var(--tanstack-header-translate-x, 0px),
|
||||
var(--tanstack-header-translate-y, 0px),
|
||||
@@ -239,6 +251,7 @@
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
width: var(--tanstack-table-header-label-width);
|
||||
}
|
||||
|
||||
.tanstackSortIndicator {
|
||||
|
||||
@@ -1,33 +1,37 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { listHosts } from 'api/generated/services/inframonitoring';
|
||||
import {
|
||||
InframonitoringtypesHostRecordDTO,
|
||||
InframonitoringtypesHostStatusDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import K8sBaseDetails from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import K8sBaseDetails, {
|
||||
K8sDetailsFilters,
|
||||
} from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from 'container/InfraMonitoringK8sV2/Base/K8sBaseList';
|
||||
import StatusFilter from 'container/InfraMonitoringHostsV2/StatusFilter';
|
||||
import { K8sBaseFilters } from 'container/InfraMonitoringK8sV2/Base/types';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import {
|
||||
useInfraMonitoringFiltersK8s,
|
||||
useInfraMonitoringPageListing,
|
||||
} from 'container/InfraMonitoringK8sV2/hooks';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
fetchHostEntityData,
|
||||
fetchHostListData,
|
||||
getHostMetricsQueryPayload,
|
||||
hostDetailsMetadataConfig,
|
||||
hostGetEntityName,
|
||||
hostGetSelectedItemFilters,
|
||||
hostInitialEventsFilter,
|
||||
hostInitialLogTracesFilter,
|
||||
hostGetSelectedItemExpression,
|
||||
hostInitialEventsExpression,
|
||||
hostInitialLogTracesExpression,
|
||||
hostWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
@@ -42,85 +46,135 @@ import { ArrowUpToLine, Filter } from '@signozhq/icons';
|
||||
|
||||
function Hosts(): JSX.Element {
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
const [, setCurrentPage] = useInfraMonitoringPageListing();
|
||||
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
|
||||
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
const { redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
const isInitialized = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized.current) {
|
||||
return;
|
||||
}
|
||||
isInitialized.current = true;
|
||||
|
||||
if (!compositeQuery) {
|
||||
const defaultQuery = initialQueriesMap[DataSource.METRICS];
|
||||
redirectWithQueryBuilderData({
|
||||
...defaultQuery,
|
||||
builder: {
|
||||
...defaultQuery.builder,
|
||||
queryData: defaultQuery.builder.queryData.map((query) => ({
|
||||
...query,
|
||||
filter: { expression: '' },
|
||||
filters: { items: [], op: 'AND' as const },
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [compositeQuery, redirectWithQueryBuilderData]);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
// Track previous urlFilters to only sync when the value actually changes
|
||||
// (not when handleChangeQueryData changes due to query updates)
|
||||
const prevUrlFiltersRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const currentFiltersJson = urlFilters ? JSON.stringify(urlFilters) : null;
|
||||
|
||||
// Only sync if urlFilters value has actually changed
|
||||
if (prevUrlFiltersRef.current !== currentFiltersJson) {
|
||||
prevUrlFiltersRef.current = currentFiltersJson;
|
||||
// Sync filters to query builder, using empty filter when urlFilters is null
|
||||
handleChangeQueryData('filters', urlFilters || { items: [], op: 'and' });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [urlFilters]); // handleChangeQueryData intentionally omitted - we call the current version but don't re-run when it changes
|
||||
|
||||
const handleFilterVisibilityChange = (): void => {
|
||||
setShowFilters(!showFilters);
|
||||
};
|
||||
|
||||
const handleQuickFiltersChange = (query: Query): void => {
|
||||
const filters = query.builder.queryData[0].filters;
|
||||
// Nuqs batches these calls into a single URL update
|
||||
// The useEffect will sync filters to query builder
|
||||
setUrlFilters(filters || null);
|
||||
setCurrentPage(1);
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.HostEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
});
|
||||
};
|
||||
|
||||
const fetchListData = useCallback(
|
||||
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
|
||||
filters.orderBy ||= {
|
||||
columnName: 'cpu',
|
||||
order: 'desc',
|
||||
};
|
||||
try {
|
||||
const response = await listHosts(
|
||||
{
|
||||
filter: {
|
||||
expression: filters.filter.expression,
|
||||
filterByStatus: filters.filter.filterByStatus
|
||||
? (filters.filter.filterByStatus as InframonitoringtypesHostStatusDTO)
|
||||
: undefined,
|
||||
},
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return fetchHostListData(filters, signal);
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch hosts';
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesHostRecordDTO[],
|
||||
total: 0,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: Parameters<typeof fetchHostEntityData>[0],
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
) => fetchHostEntityData(filters, signal),
|
||||
): Promise<{
|
||||
data: InframonitoringtypesHostRecordDTO | null;
|
||||
error?: string | null;
|
||||
}> => {
|
||||
try {
|
||||
const response = await listHosts(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch host';
|
||||
return {
|
||||
data: null,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const getSelectedItemFilters = useCallback(
|
||||
(selectedItem: string) =>
|
||||
hostGetSelectedItemFilters(selectedItem, dotMetricsEnabled),
|
||||
const getInitialLogTracesExpression = useCallback(
|
||||
(host: InframonitoringtypesHostRecordDTO) =>
|
||||
hostInitialLogTracesExpression(host, dotMetricsEnabled),
|
||||
[dotMetricsEnabled],
|
||||
);
|
||||
|
||||
const getInitialLogTracesFilters = useCallback(
|
||||
(host: import('api/infraMonitoring/getHostLists').HostData) =>
|
||||
hostInitialLogTracesFilter(host, dotMetricsEnabled),
|
||||
[dotMetricsEnabled],
|
||||
);
|
||||
|
||||
const controlListPrefix = !showFilters ? (
|
||||
<div className={styles.quickFiltersToggleContainer}>
|
||||
<Button
|
||||
@@ -154,7 +208,6 @@ function Hosts(): JSX.Element {
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={getHostsQuickFiltersConfig(dotMetricsEnabled)}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
onFilterChange={handleQuickFiltersChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -163,8 +216,9 @@ function Hosts(): JSX.Element {
|
||||
showFilters ? ` ${styles.listContainerFiltersVisible}` : ''
|
||||
}`}
|
||||
>
|
||||
<K8sBaseList
|
||||
<K8sBaseList<InframonitoringtypesHostRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
leftFilters={<StatusFilter />}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
tableColumns={hostColumnsConfig}
|
||||
fetchListData={fetchListData}
|
||||
@@ -178,11 +232,11 @@ function Hosts(): JSX.Element {
|
||||
<K8sBaseDetails
|
||||
category={InfraMonitoringEntity.HOSTS}
|
||||
eventCategory={InfraMonitoringEvents.HostEntity}
|
||||
getSelectedItemFilters={getSelectedItemFilters}
|
||||
getSelectedItemExpression={hostGetSelectedItemExpression}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={hostGetEntityName}
|
||||
getInitialLogTracesFilters={getInitialLogTracesFilters}
|
||||
getInitialEventsFilters={hostInitialEventsFilter}
|
||||
getInitialLogTracesExpression={getInitialLogTracesExpression}
|
||||
getInitialEventsExpression={hostInitialEventsExpression}
|
||||
metadataConfig={hostDetailsMetadataConfig}
|
||||
entityWidgetInfo={hostWidgetInfo}
|
||||
getEntityQueryPayload={getHostMetricsQueryPayload}
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
height: 0.1rem;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
@@ -35,11 +35,12 @@
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--accent-primary);
|
||||
background: color-mix(in srgb, var(--l1-foreground) 20%, transparent);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
|
||||
background: color-mix(in srgb, var(--l1-foreground) 30%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +99,7 @@
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,32 +114,17 @@
|
||||
|
||||
.listContainer {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
min-width: 595px;
|
||||
|
||||
> * {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
> :global(.ant-table-wrapper) {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
:global(.ant-spin-container) {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:global(.ant-table),
|
||||
:global(.ant-table-container) {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listContainerFiltersVisible {
|
||||
@@ -146,7 +132,7 @@
|
||||
}
|
||||
|
||||
.quickFiltersToggleContainer {
|
||||
padding: 0 8px;
|
||||
grid-area: quickFilters;
|
||||
}
|
||||
|
||||
.infraMonitoringTags {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
.statusFilterContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
--toggle-group-item-size: 100%;
|
||||
--toggle-group-radius: 0px 2px 2px 0px;
|
||||
--toggle-group-item-font-size: var(--periscope-font-size-base);
|
||||
}
|
||||
|
||||
.statusLabel {
|
||||
min-width: max-content;
|
||||
font-size: var(--periscope-font-size-base);
|
||||
font-weight: var(--periscope-font-weight-regular);
|
||||
line-height: 18px;
|
||||
|
||||
border-radius: 2px 0px 0px 2px;
|
||||
border: 1px solid var(--l2-border);
|
||||
border-right: none;
|
||||
|
||||
display: flex;
|
||||
height: 32px;
|
||||
padding: var(--spacing-3) var(--spacing-3) var(--spacing-3) var(--spacing-4);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.statusToggleGroup {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.statusToggleItem {
|
||||
flex: unset;
|
||||
}
|
||||
|
||||
.statusDot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
min-height: 8px;
|
||||
min-width: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.allDot {
|
||||
border: 1px solid var(--text-muted);
|
||||
background-color: var(--bg-slate-100);
|
||||
}
|
||||
|
||||
.activeDot {
|
||||
background-color: var(--bg-forest-500);
|
||||
}
|
||||
|
||||
.inactiveDot {
|
||||
background-color: var(--bg-amber-500);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
NuqsTestingAdapter,
|
||||
OnUrlUpdateFunction,
|
||||
UrlUpdateEvent,
|
||||
} from 'nuqs/adapters/testing';
|
||||
import { AppContext } from 'providers/App/App';
|
||||
import TimezoneProvider from 'providers/Timezone';
|
||||
import store from 'store';
|
||||
import { getAppContextMock } from 'tests/test-utils';
|
||||
|
||||
import StatusFilter from './StatusFilter';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
function renderStatusFilter({
|
||||
searchParams = {},
|
||||
onUrlUpdate,
|
||||
}: {
|
||||
searchParams?: Record<string, string>;
|
||||
onUrlUpdate?: OnUrlUpdateFunction;
|
||||
}): ReturnType<typeof render> {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<TimezoneProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<AppContext.Provider value={getAppContextMock('ADMIN')}>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={searchParams}
|
||||
onUrlUpdate={onUrlUpdate}
|
||||
>
|
||||
<StatusFilter />
|
||||
</NuqsTestingAdapter>
|
||||
</AppContext.Provider>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</TimezoneProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('StatusFilter', () => {
|
||||
beforeEach(() => {
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
it('renders all status options', () => {
|
||||
renderStatusFilter({});
|
||||
|
||||
expect(screen.getByText('Status')).toBeInTheDocument();
|
||||
expect(screen.getByRole('radio', { name: 'All' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('radio', { name: 'Active' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('radio', { name: 'Inactive' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('selects "All" by default when no URL param', () => {
|
||||
renderStatusFilter({});
|
||||
|
||||
const allButton = screen.getByRole('radio', { name: 'All' });
|
||||
expect(allButton).toHaveAttribute('aria-checked', 'true');
|
||||
});
|
||||
|
||||
it('reads "active" from URL and shows Active selected', () => {
|
||||
renderStatusFilter({ searchParams: { statusFilter: 'active' } });
|
||||
|
||||
const activeButton = screen.getByRole('radio', { name: 'Active' });
|
||||
expect(activeButton).toHaveAttribute('aria-checked', 'true');
|
||||
});
|
||||
|
||||
it('reads "inactive" from URL and shows Inactive selected', () => {
|
||||
renderStatusFilter({ searchParams: { statusFilter: 'inactive' } });
|
||||
|
||||
const inactiveButton = screen.getByRole('radio', { name: 'Inactive' });
|
||||
expect(inactiveButton).toHaveAttribute('aria-checked', 'true');
|
||||
});
|
||||
|
||||
it('updates URL to "active" when Active clicked', async () => {
|
||||
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
|
||||
renderStatusFilter({ onUrlUpdate });
|
||||
|
||||
const activeButton = screen.getByRole('radio', { name: 'Active' });
|
||||
fireEvent.click(activeButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const statusFilterValue = onUrlUpdate.mock.calls
|
||||
.map((call) => call[0].searchParams.get('statusFilter'))
|
||||
.filter(Boolean)
|
||||
.pop();
|
||||
expect(statusFilterValue).toBe('active');
|
||||
});
|
||||
});
|
||||
|
||||
it('updates URL to "inactive" when Inactive clicked', async () => {
|
||||
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
|
||||
renderStatusFilter({ onUrlUpdate });
|
||||
|
||||
const inactiveButton = screen.getByRole('radio', { name: 'Inactive' });
|
||||
fireEvent.click(inactiveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const statusFilterValue = onUrlUpdate.mock.calls
|
||||
.map((call) => call[0].searchParams.get('statusFilter'))
|
||||
.filter(Boolean)
|
||||
.pop();
|
||||
expect(statusFilterValue).toBe('inactive');
|
||||
});
|
||||
});
|
||||
|
||||
it('removes statusFilter from URL when All clicked', async () => {
|
||||
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
|
||||
renderStatusFilter({
|
||||
searchParams: { statusFilter: 'active' },
|
||||
onUrlUpdate,
|
||||
});
|
||||
|
||||
const allButton = screen.getByRole('radio', { name: 'All' });
|
||||
fireEvent.click(allButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1];
|
||||
expect(lastCall[0].searchParams.get('statusFilter')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('resets page when filter changes', async () => {
|
||||
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
|
||||
renderStatusFilter({
|
||||
searchParams: { page: '3' },
|
||||
onUrlUpdate,
|
||||
});
|
||||
|
||||
const activeButton = screen.getByRole('radio', { name: 'Active' });
|
||||
fireEvent.click(activeButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1];
|
||||
const pageValue = lastCall[0].searchParams.get('page');
|
||||
// page=1 is default, so nuqs removes it from URL (null) or keeps as "1"
|
||||
expect(pageValue === null || pageValue === '1').toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
|
||||
import {
|
||||
StatusFilterValue,
|
||||
useInfraMonitoringPageListing,
|
||||
useInfraMonitoringStatusFilter,
|
||||
} from 'container/InfraMonitoringK8sV2/hooks';
|
||||
|
||||
import styles from './StatusFilter.module.scss';
|
||||
|
||||
const statusOptions: Array<{
|
||||
label: string;
|
||||
value: StatusFilterValue | 'all';
|
||||
}> = [
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: 'Active', value: 'active' },
|
||||
{ label: 'Inactive', value: 'inactive' },
|
||||
];
|
||||
|
||||
function StatusFilter(): JSX.Element {
|
||||
const [statusFilter, setStatusFilter] = useInfraMonitoringStatusFilter();
|
||||
const [, setCurrentPage] = useInfraMonitoringPageListing();
|
||||
|
||||
const handleChange = (value: string): void => {
|
||||
if (value !== undefined) {
|
||||
void setStatusFilter(value === 'all' ? '' : (value as StatusFilterValue));
|
||||
void setCurrentPage(1);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.statusFilterContainer}>
|
||||
<div className={styles.statusLabel}>Status</div>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={statusFilter === '' ? 'all' : statusFilter}
|
||||
onChange={handleChange}
|
||||
className={styles.statusToggleGroup}
|
||||
>
|
||||
{statusOptions.map((option) => (
|
||||
<ToggleGroupItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
aria-label={option.label}
|
||||
className={styles.statusToggleItem}
|
||||
>
|
||||
<span
|
||||
className={`${styles.statusDot} ${
|
||||
option.value === 'active'
|
||||
? styles.activeDot
|
||||
: option.value === 'inactive'
|
||||
? styles.inactiveDot
|
||||
: styles.allDot
|
||||
}`}
|
||||
/>
|
||||
{option.label}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default StatusFilter;
|
||||
@@ -4,26 +4,16 @@ import { Badge } from '@signozhq/ui/badge';
|
||||
import { Progress } from '@signozhq/ui/progress';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import {
|
||||
getHostLists,
|
||||
HostData,
|
||||
HostListPayload,
|
||||
} from 'api/infraMonitoring/getHostLists';
|
||||
import {
|
||||
createFilterItem,
|
||||
K8sDetailsFilters,
|
||||
K8sDetailsMetadataConfig,
|
||||
} from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
|
||||
import { K8sBaseFilters } from 'container/InfraMonitoringK8sV2/Base/types';
|
||||
InframonitoringtypesHostRecordDTO,
|
||||
InframonitoringtypesHostStatusDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { K8sDetailsMetadataConfig } from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import {
|
||||
getHostQueryPayload,
|
||||
hostWidgetInfo,
|
||||
} from 'container/LogDetailedView/InfraMetrics/constants';
|
||||
import {
|
||||
TagFilter,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { getHostListsQuery } from './utils';
|
||||
|
||||
import infraHostsStyles from './InfraMonitoringHosts.module.scss';
|
||||
|
||||
@@ -47,24 +37,32 @@ export function getMemoryProgressColor(percent: number): string {
|
||||
return Color.BG_FOREST_500;
|
||||
}
|
||||
|
||||
export const hostDetailsMetadataConfig: K8sDetailsMetadataConfig<HostData>[] = [
|
||||
export type HostDetailMetadataConfigType =
|
||||
K8sDetailsMetadataConfig<InframonitoringtypesHostRecordDTO>;
|
||||
export const hostDetailsMetadataConfig: HostDetailMetadataConfigType[] = [
|
||||
{
|
||||
label: 'STATUS',
|
||||
getValue: (h): string => (h.active ? 'ACTIVE' : 'INACTIVE'),
|
||||
render: (value, h): React.ReactNode => (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`${infraHostsStyles.infraMonitoringTags} ${
|
||||
h.active ? infraHostsStyles.tagsActive : infraHostsStyles.tagsInactive
|
||||
}`}
|
||||
>
|
||||
{value}
|
||||
</Badge>
|
||||
),
|
||||
getValue: (h): string =>
|
||||
h.status === InframonitoringtypesHostStatusDTO.active
|
||||
? 'ACTIVE'
|
||||
: 'INACTIVE',
|
||||
render: (value, h): React.ReactNode => {
|
||||
const isActive = h.status === InframonitoringtypesHostStatusDTO.active;
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`${infraHostsStyles.infraMonitoringTags} ${
|
||||
isActive ? infraHostsStyles.tagsActive : infraHostsStyles.tagsInactive
|
||||
}`}
|
||||
>
|
||||
{value}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'OPERATING SYSTEM',
|
||||
getValue: (h): string => h.os || '-',
|
||||
getValue: (h): string => h.meta?.['os.type'] || '-',
|
||||
render: (value): React.ReactNode =>
|
||||
value !== '-' ? (
|
||||
<Badge variant="outline" className={infraHostsStyles.infraMonitoringTags}>
|
||||
@@ -99,7 +97,7 @@ export const hostDetailsMetadataConfig: K8sDetailsMetadataConfig<HostData>[] = [
|
||||
];
|
||||
|
||||
export function getHostMetricsQueryPayload(
|
||||
host: HostData,
|
||||
host: InframonitoringtypesHostRecordDTO,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
@@ -109,83 +107,26 @@ export function getHostMetricsQueryPayload(
|
||||
|
||||
export { hostWidgetInfo };
|
||||
|
||||
export function hostGetSelectedItemFilters(
|
||||
selectedItem: string,
|
||||
export const hostGetSelectedItemExpression = (hostName: string): string =>
|
||||
`host.name = ${formatValueForExpression(hostName)}`;
|
||||
|
||||
export function hostInitialLogTracesExpression(
|
||||
host: InframonitoringtypesHostRecordDTO,
|
||||
dotMetricsEnabled: boolean,
|
||||
): TagFilter {
|
||||
const hostKey = dotMetricsEnabled ? 'host.name' : 'host_name';
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [createFilterItem(hostKey, selectedItem)],
|
||||
};
|
||||
): string {
|
||||
const hostKey = dotMetricsEnabled
|
||||
? INFRA_MONITORING_ATTR_KEYS.HOST_NAME
|
||||
: 'host_name';
|
||||
const hostName = formatValueForExpression(host.hostName || '');
|
||||
return `${hostKey} = ${hostName}`;
|
||||
}
|
||||
|
||||
export function hostInitialLogTracesFilter(
|
||||
host: HostData,
|
||||
dotMetricsEnabled: boolean,
|
||||
): TagFilterItem[] {
|
||||
const hostKey = dotMetricsEnabled ? 'host.name' : 'host_name';
|
||||
return [createFilterItem(hostKey, host.hostName || '')];
|
||||
export function hostInitialEventsExpression(
|
||||
_host: InframonitoringtypesHostRecordDTO,
|
||||
): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
export function hostInitialEventsFilter(_host: HostData): TagFilterItem[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
export const hostGetEntityName = (host: HostData): string => host.hostName;
|
||||
|
||||
export async function fetchHostListData(
|
||||
filters: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{
|
||||
data: HostData[];
|
||||
total: number;
|
||||
error?: string | null;
|
||||
rawData?: unknown;
|
||||
}> {
|
||||
const baseQuery = getHostListsQuery();
|
||||
const payload: HostListPayload = {
|
||||
...baseQuery,
|
||||
limit: filters.limit,
|
||||
offset: filters.offset,
|
||||
filters: filters.filters ?? { items: [], op: 'and' },
|
||||
orderBy: filters.orderBy,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
groupBy: filters.groupBy ?? [],
|
||||
};
|
||||
|
||||
const response = await getHostLists(payload, signal);
|
||||
|
||||
return {
|
||||
data: response.payload?.data?.records || [],
|
||||
total: response.payload?.data?.total || 0,
|
||||
error: response.error,
|
||||
rawData: response.payload?.data,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchHostEntityData(
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ data: HostData | null; error?: string | null }> {
|
||||
const response = await getHostLists(
|
||||
{
|
||||
...getHostListsQuery(),
|
||||
filters: filters.filters,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
groupBy: [],
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const records = response.payload?.data?.records || [];
|
||||
|
||||
return {
|
||||
data: records.length > 0 ? records[0] : null,
|
||||
error: response.error,
|
||||
};
|
||||
}
|
||||
export const hostGetEntityName = (
|
||||
host: InframonitoringtypesHostRecordDTO,
|
||||
): string => host.hostName;
|
||||
|
||||
@@ -1,55 +1,91 @@
|
||||
import React from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { HostData } from 'api/infraMonitoring/getHostLists';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Container } from '@signozhq/icons';
|
||||
import {
|
||||
InframonitoringtypesHostRecordDTO,
|
||||
InframonitoringtypesHostStatusDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
|
||||
import { getGroupByEl } from 'container/InfraMonitoringK8sV2/Base/utils';
|
||||
import {
|
||||
EntityProgressBar,
|
||||
ExpandButtonWrapper,
|
||||
GroupedStatusCounts,
|
||||
ValidateColumnValueWrapper,
|
||||
} from 'container/InfraMonitoringK8sV2/components';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import {
|
||||
INFRA_MONITORING_ATTR_KEYS,
|
||||
InfraMonitoringEntity,
|
||||
} from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { useInfraMonitoringGroupBy } from 'container/InfraMonitoringK8sV2/hooks';
|
||||
import ColumnHeader from 'container/InfraMonitoringK8sV2/Base/ColumnHeader';
|
||||
import EntityGroupHeader from 'container/InfraMonitoringK8sV2/Base/EntityGroupHeader';
|
||||
|
||||
import { HostnameCell } from './utils';
|
||||
|
||||
import styles from './table.module.scss';
|
||||
import { Container, Info } from '@signozhq/icons';
|
||||
import { Badge, BadgeColor } from '@signozhq/ui/badge';
|
||||
|
||||
function hostRowSource(host: HostData): { meta: Record<string, string> } {
|
||||
const statusMap: Record<
|
||||
InframonitoringtypesHostStatusDTO,
|
||||
{
|
||||
label: string;
|
||||
color: BadgeColor;
|
||||
}
|
||||
> = {
|
||||
[InframonitoringtypesHostStatusDTO.active]: {
|
||||
label: 'ACTIVE',
|
||||
color: 'forest',
|
||||
},
|
||||
[InframonitoringtypesHostStatusDTO.inactive]: {
|
||||
label: 'INACTIVE',
|
||||
color: 'amber',
|
||||
},
|
||||
['']: {
|
||||
label: 'UNKNOWN',
|
||||
color: 'secondary',
|
||||
},
|
||||
};
|
||||
|
||||
function hostRowSource(host: InframonitoringtypesHostRecordDTO): {
|
||||
meta: Record<string, string>;
|
||||
} {
|
||||
return {
|
||||
meta: {
|
||||
...(host.meta ?? {}),
|
||||
host_name: host.hostName ?? '',
|
||||
'host.name': host.hostName ?? '',
|
||||
os_type: host.os ?? '',
|
||||
'os.type': host.os ?? '',
|
||||
[INFRA_MONITORING_ATTR_KEYS.HOST_NAME]: host.hostName ?? '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getHostRowKey(host: HostData): string {
|
||||
export function getHostRowKey(host: InframonitoringtypesHostRecordDTO): string {
|
||||
return host.hostName || 'unknown';
|
||||
}
|
||||
|
||||
export function getHostItemKey(host: HostData): string {
|
||||
export function getHostItemKey(
|
||||
host: InframonitoringtypesHostRecordDTO,
|
||||
): string {
|
||||
return host.hostName ?? '';
|
||||
}
|
||||
|
||||
function HostGroupCell({ row }: { row: HostData }): JSX.Element {
|
||||
function HostGroupCell({
|
||||
row,
|
||||
}: {
|
||||
row: InframonitoringtypesHostRecordDTO;
|
||||
}): JSX.Element {
|
||||
const [groupBy] = useInfraMonitoringGroupBy();
|
||||
const synthetic = hostRowSource(row);
|
||||
return getGroupByEl(synthetic, groupBy) as JSX.Element;
|
||||
}
|
||||
|
||||
export const hostColumnsConfig: TableColumnDef<HostData>[] = [
|
||||
export type HostColumnConfigType =
|
||||
TableColumnDef<InframonitoringtypesHostRecordDTO>;
|
||||
export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
{
|
||||
id: 'hostGroup',
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="HOST GROUP" />,
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="Host Group" />,
|
||||
accessorFn: (row): string => row.hostName ?? '',
|
||||
width: { min: 300 },
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
@@ -64,7 +100,11 @@ export const hostColumnsConfig: TableColumnDef<HostData>[] = [
|
||||
{
|
||||
id: 'hostName',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader title="Hostname" icon={<Container size={14} />} />
|
||||
<EntityGroupHeader
|
||||
title="Hostname"
|
||||
icon={<Container size={14} />}
|
||||
docPath="/infrastructure-monitoring/host-monitoring#hostname"
|
||||
/>
|
||||
),
|
||||
accessorFn: (row): string => row.hostName ?? '',
|
||||
width: { min: 290 },
|
||||
@@ -78,27 +118,48 @@ export const hostColumnsConfig: TableColumnDef<HostData>[] = [
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'active',
|
||||
id: 'status',
|
||||
header: (): React.ReactNode => (
|
||||
<div className={styles.statusHeader}>
|
||||
<ColumnHeader
|
||||
tooltip="Sent system metrics in last 10 mins."
|
||||
docPath="/infrastructure-monitoring/host-monitoring#status"
|
||||
>
|
||||
Status
|
||||
<Tooltip title="Sent system metrics in last 10 mins">
|
||||
<Info size="md" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): boolean => row.active,
|
||||
width: { min: 150, default: 150 },
|
||||
accessorFn: (row): string => row.status,
|
||||
width: { min: 140 },
|
||||
enableSort: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const active = value as boolean;
|
||||
cell: ({ value, groupMeta, row }): React.ReactNode => {
|
||||
const status = value as InframonitoringtypesHostStatusDTO;
|
||||
|
||||
if (groupMeta) {
|
||||
return (
|
||||
<GroupedStatusCounts
|
||||
items={[
|
||||
{
|
||||
value: row.activeHostCount,
|
||||
label: 'Active',
|
||||
color: Color.BG_FOREST_500,
|
||||
},
|
||||
{
|
||||
value: row.inactiveHostCount,
|
||||
label: 'Inactive',
|
||||
color: Color.BG_AMBER_500,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const statusDetails = statusMap[status] || statusMap[''];
|
||||
return (
|
||||
<Badge
|
||||
className={`${styles.statusTag} ${
|
||||
active ? styles.statusTagActive : styles.statusTagInactive
|
||||
}`}
|
||||
variant="outline"
|
||||
color={statusDetails.color}
|
||||
className={`${styles.statusTag}`}
|
||||
>
|
||||
{active ? 'ACTIVE' : 'INACTIVE'}
|
||||
{statusDetails.label}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
@@ -106,7 +167,9 @@ export const hostColumnsConfig: TableColumnDef<HostData>[] = [
|
||||
{
|
||||
id: 'cpu',
|
||||
header: (): React.ReactNode => (
|
||||
<div className={styles.columnHeaderRight}>CPU Usage</div>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#cpu-usage">
|
||||
CPU Usage
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.cpu,
|
||||
width: { min: 220 },
|
||||
@@ -129,15 +192,16 @@ export const hostColumnsConfig: TableColumnDef<HostData>[] = [
|
||||
{
|
||||
id: 'memory',
|
||||
header: (): React.ReactNode => (
|
||||
<div className={`${styles.columnHeaderRight} ${styles.memoryUsageHeader}`}>
|
||||
Memory Usage
|
||||
<Tooltip title="Excluding cache memory">
|
||||
<Info size="md" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ColumnHeader
|
||||
tooltip="Excluding cache memory."
|
||||
docPath="/infrastructure-monitoring/host-monitoring#memory-usage"
|
||||
className={styles.memoryUsageHeader}
|
||||
>
|
||||
Memory Usage (WSS)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.memory,
|
||||
width: { min: 220 },
|
||||
width: { min: 240 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
@@ -157,10 +221,12 @@ export const hostColumnsConfig: TableColumnDef<HostData>[] = [
|
||||
{
|
||||
id: 'wait',
|
||||
header: (): React.ReactNode => (
|
||||
<div className={styles.columnHeaderRight}>IOWait</div>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#iowait">
|
||||
IOWait
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.wait,
|
||||
width: { min: 100, default: 100 },
|
||||
width: { min: 120 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const wait = value as number;
|
||||
@@ -171,7 +237,9 @@ export const hostColumnsConfig: TableColumnDef<HostData>[] = [
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
attribute="IOWait metric"
|
||||
>
|
||||
<TanStackTable.Text>{`${Number((wait * 100).toFixed(1))}%`}</TanStackTable.Text>
|
||||
<TanStackTable.Text>
|
||||
{`${Number((wait * 100).toFixed(1))}%`}
|
||||
</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
@@ -179,13 +247,15 @@ export const hostColumnsConfig: TableColumnDef<HostData>[] = [
|
||||
{
|
||||
id: 'load15',
|
||||
header: (): React.ReactNode => (
|
||||
<div className={styles.columnHeaderRight}>Load Avg</div>
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#load-avg">
|
||||
Load Avg (15min)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.load15,
|
||||
width: { min: 100, default: 100 },
|
||||
width: { min: 200 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const load15 = value as number;
|
||||
const load15 = Number(value);
|
||||
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
@@ -193,9 +263,35 @@ export const hostColumnsConfig: TableColumnDef<HostData>[] = [
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
attribute="load average metric"
|
||||
>
|
||||
<TanStackTable.Text>{load15}</TanStackTable.Text>
|
||||
<TanStackTable.Text>{load15.toFixed(2)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'diskUsage',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#disk-usage">
|
||||
Disk Usage
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.diskUsage,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const diskUsage = value as number;
|
||||
|
||||
return (
|
||||
<div className={styles.progressContainer}>
|
||||
<ValidateColumnValueWrapper
|
||||
value={diskUsage}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
attribute="disk usage metric"
|
||||
>
|
||||
<EntityProgressBar value={diskUsage} type="disk" />
|
||||
</ValidateColumnValueWrapper>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -9,13 +9,6 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.statusHeader {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.columnHeaderRight {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -2,14 +2,16 @@ import React from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
|
||||
import {
|
||||
FiltersType,
|
||||
IQuickFiltersConfig,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { TriangleAlert } from '@signozhq/icons';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { CellValueTooltip } from 'container/InfraMonitoringK8sV2/components';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
|
||||
const HOSTNAME_DOCS_URL =
|
||||
'https://signoz.io/docs/infrastructure-monitoring/hostmetrics/#host-name-is-blankempty';
|
||||
@@ -21,13 +23,15 @@ export function HostnameCell({
|
||||
}): React.ReactElement {
|
||||
const isEmpty = !hostName || !hostName.trim();
|
||||
if (!isEmpty) {
|
||||
return <div className="hostname-column-value">{hostName}</div>;
|
||||
return (
|
||||
<CellValueTooltip value={hostName}>
|
||||
<TanStackTable.Text>{hostName}</TanStackTable.Text>
|
||||
</CellValueTooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="hostname-cell-missing">
|
||||
<Typography.Text color="muted" className="hostname-cell-placeholder">
|
||||
-
|
||||
</Typography.Text>
|
||||
<>
|
||||
<Typography.Text color="muted">-</Typography.Text>
|
||||
<Tooltip
|
||||
title={
|
||||
<div>
|
||||
@@ -60,52 +64,16 @@ export function HostnameCell({
|
||||
<TriangleAlert size={14} color={Color.BG_CHERRY_500} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const getHostListsQuery = (): HostListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
groupBy: [],
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
export const HostsQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Host Name',
|
||||
attributeKey: {
|
||||
key: 'host_name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: 'system_cpu_load_average_15m',
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'OS Type',
|
||||
attributeKey: {
|
||||
key: 'os_type',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: 'system_cpu_load_average_15m',
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
];
|
||||
|
||||
export function getHostsQuickFiltersConfig(
|
||||
dotMetricsEnabled: boolean,
|
||||
): IQuickFiltersConfig[] {
|
||||
const hostNameKey = dotMetricsEnabled ? 'host.name' : 'host_name';
|
||||
const hostNameKey = dotMetricsEnabled
|
||||
? INFRA_MONITORING_ATTR_KEYS.HOST_NAME
|
||||
: 'host_name';
|
||||
const osTypeKey = dotMetricsEnabled ? 'os.type' : 'os_type';
|
||||
const metricName = dotMetricsEnabled
|
||||
? 'system.cpu.load_average.15m'
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
.columnHeader {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.infoIcon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.columnHeaderLabel {
|
||||
text-align: center;
|
||||
padding: var(--spacing-2) var(--spacing-2) var(--spacing-2) 0px;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Info } from '@signozhq/icons';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
|
||||
import styles from './ColumnHeader.module.scss';
|
||||
import cx from 'classnames';
|
||||
|
||||
const DOCS_BASE_URL = `${process.env.DOCS_BASE_URL}/docs`;
|
||||
|
||||
interface ColumnHeaderProps {
|
||||
children?: React.ReactNode;
|
||||
title?: string;
|
||||
docPath?: string;
|
||||
tooltip?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function ColumnHeader({
|
||||
children,
|
||||
title,
|
||||
docPath,
|
||||
tooltip,
|
||||
className,
|
||||
}: ColumnHeaderProps): JSX.Element {
|
||||
const renderContent = (): React.ReactNode => {
|
||||
if (children) {
|
||||
return children;
|
||||
}
|
||||
|
||||
if (title) {
|
||||
const parts = title.split('\n');
|
||||
return parts.map((part, index) => (
|
||||
<div key={`${part}-${index}`}>
|
||||
{part}
|
||||
{index < parts.length - 1 && <br />}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderInfoIcon = (): React.ReactNode => {
|
||||
if (docPath) {
|
||||
const tooltipTitle = tooltip || 'Not sure what this means?';
|
||||
return (
|
||||
<TooltipSimple
|
||||
arrow
|
||||
title={
|
||||
<>
|
||||
{tooltipTitle}{' '}
|
||||
<a
|
||||
href={`${DOCS_BASE_URL}${docPath}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e): void => e.stopPropagation()}
|
||||
>
|
||||
Learn more.
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className={styles.infoIcon}>
|
||||
<Info size="md" />
|
||||
</div>
|
||||
</TooltipSimple>
|
||||
);
|
||||
}
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<TooltipSimple title={tooltip}>
|
||||
<div className={styles.infoIcon}>
|
||||
<Info size="md" />
|
||||
</div>
|
||||
</TooltipSimple>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cx(styles.columnHeader, className)} data-slot="column-header">
|
||||
<div className={styles.columnHeaderLabel}>{renderContent()}</div>
|
||||
{renderInfoIcon()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ColumnHeader;
|
||||
@@ -2,4 +2,16 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-5);
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.infoIcon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--text-slate-secondary);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-slate-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,70 @@
|
||||
import { Group } from '@signozhq/icons';
|
||||
import { Group, Info } from '@signozhq/icons';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
|
||||
import styles from './EntityGroupHeader.module.scss';
|
||||
|
||||
const DOCS_BASE_URL = `${process.env.DOCS_BASE_URL}/docs`;
|
||||
|
||||
interface EntityGroupHeaderProps {
|
||||
title: string;
|
||||
icon?: React.ReactNode;
|
||||
docPath?: string;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
function EntityGroupHeader({
|
||||
title,
|
||||
icon,
|
||||
docPath,
|
||||
tooltip,
|
||||
}: EntityGroupHeaderProps): JSX.Element {
|
||||
const renderInfoIcon = (): React.ReactNode => {
|
||||
if (docPath) {
|
||||
const tooltipTitle = tooltip || 'Not sure what this means?';
|
||||
return (
|
||||
<TooltipSimple
|
||||
arrow
|
||||
title={
|
||||
<>
|
||||
{tooltipTitle}{' '}
|
||||
<a
|
||||
href={`${DOCS_BASE_URL}${docPath}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e): void => e.stopPropagation()}
|
||||
>
|
||||
Learn more.
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<span className={styles.infoIcon}>
|
||||
<Info size="md" />
|
||||
</span>
|
||||
</TooltipSimple>
|
||||
);
|
||||
}
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<TooltipSimple title={tooltip}>
|
||||
<span className={styles.infoIcon}>
|
||||
<Info size="md" />
|
||||
</span>
|
||||
</TooltipSimple>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.entityGroupHeader}>
|
||||
{icon || <Group size={14} data-hide-expanded="true" />} {title}
|
||||
<div className={styles.entityGroupHeader} data-slot="entity-group-header">
|
||||
<span data-slot="icon">
|
||||
{icon || <Group size={14} data-hide-expanded="true" />}
|
||||
</span>{' '}
|
||||
{title}
|
||||
{renderInfoIcon()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import React, {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { Button, Drawer, Tooltip } from 'antd';
|
||||
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
|
||||
@@ -14,7 +13,6 @@ import { Divider } from '@signozhq/ui/divider';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
|
||||
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
@@ -40,17 +38,11 @@ import {
|
||||
} from '@signozhq/icons';
|
||||
import { isCustomTimeRange, useGlobalTimeStore } from 'store/globalTime';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
TagFilter,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
LogsAggregatorOperator,
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { InfraMonitoringEntity, VIEW_TYPES } from '../constants';
|
||||
import EntityEvents from '../EntityDetailsUtils/EntityEvents';
|
||||
@@ -80,7 +72,7 @@ export interface K8sDetailsMetadataConfig<T> {
|
||||
}
|
||||
|
||||
export interface K8sDetailsFilters {
|
||||
filters: TagFilter;
|
||||
filter: { expression: string };
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
@@ -89,15 +81,15 @@ export interface K8sBaseDetailsProps<T> {
|
||||
category: InfraMonitoringEntity;
|
||||
eventCategory: string;
|
||||
// Data fetching configuration
|
||||
getSelectedItemFilters: (selectedItem: string) => TagFilter;
|
||||
getSelectedItemExpression: (selectedItem: string) => string;
|
||||
fetchEntityData: (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<{ data: T | null; error?: string | null }>;
|
||||
// Entity configuration
|
||||
getEntityName: (entity: T) => string;
|
||||
getInitialLogTracesFilters: (entity: T) => TagFilterItem[];
|
||||
getInitialEventsFilters: (entity: T) => TagFilterItem[];
|
||||
getInitialLogTracesExpression: (entity: T) => string;
|
||||
getInitialEventsExpression: (entity: T) => string;
|
||||
metadataConfig: K8sDetailsMetadataConfig<T>[];
|
||||
entityWidgetInfo: {
|
||||
title: string;
|
||||
@@ -134,33 +126,15 @@ export interface K8sBaseDetailsProps<T> {
|
||||
}>;
|
||||
}
|
||||
|
||||
export function createFilterItem(
|
||||
key: string,
|
||||
value: string,
|
||||
dataType: DataTypes = DataTypes.String,
|
||||
): TagFilterItem {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key,
|
||||
dataType,
|
||||
type: 'resource',
|
||||
id: `${key}--string--resource--false`,
|
||||
},
|
||||
op: '=',
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export default function K8sBaseDetails<T>({
|
||||
category,
|
||||
eventCategory,
|
||||
getSelectedItemFilters,
|
||||
getSelectedItemExpression,
|
||||
fetchEntityData,
|
||||
getEntityName,
|
||||
getInitialLogTracesFilters,
|
||||
getInitialEventsFilters,
|
||||
getInitialLogTracesExpression,
|
||||
getInitialEventsExpression,
|
||||
metadataConfig,
|
||||
entityWidgetInfo,
|
||||
getEntityQueryPayload,
|
||||
@@ -201,17 +175,12 @@ export default function K8sBaseDetails<T>({
|
||||
if (!selectedItem) {
|
||||
return { data: null };
|
||||
}
|
||||
const filters = getSelectedItemFilters(selectedItem);
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
const start = Math.floor(minTime / NANO_SECOND_MULTIPLIER);
|
||||
const end = Math.floor(maxTime / NANO_SECOND_MULTIPLIER);
|
||||
const expression = getSelectedItemExpression(selectedItem);
|
||||
|
||||
return fetchEntityData(
|
||||
{
|
||||
filters,
|
||||
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
|
||||
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
|
||||
},
|
||||
signal,
|
||||
);
|
||||
return fetchEntityData({ filter: { expression }, start, end }, signal);
|
||||
},
|
||||
enabled: !!selectedItem,
|
||||
});
|
||||
@@ -223,23 +192,15 @@ export default function K8sBaseDetails<T>({
|
||||
if (!entity) {
|
||||
return '';
|
||||
}
|
||||
const primaryFiltersOnly = {
|
||||
op: 'AND' as const,
|
||||
items: getInitialLogTracesFilters(entity),
|
||||
};
|
||||
return convertFiltersToExpression(primaryFiltersOnly).expression;
|
||||
}, [entity, getInitialLogTracesFilters]);
|
||||
return getInitialLogTracesExpression(entity);
|
||||
}, [entity, getInitialLogTracesExpression]);
|
||||
|
||||
const eventsInitialExpression = useMemo(() => {
|
||||
if (!entity) {
|
||||
return '';
|
||||
}
|
||||
const primaryFiltersOnly = {
|
||||
op: 'AND' as const,
|
||||
items: getInitialEventsFilters(entity),
|
||||
};
|
||||
return convertFiltersToExpression(primaryFiltersOnly).expression;
|
||||
}, [entity, getInitialEventsFilters]);
|
||||
return getInitialEventsExpression(entity);
|
||||
}, [entity, getInitialEventsExpression]);
|
||||
|
||||
const handleClose = useCallback((): void => {
|
||||
setSelectedItem(null);
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
.tableContainer {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.emptyStateContainer {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@@ -7,7 +14,6 @@
|
||||
}
|
||||
|
||||
.k8SListTable {
|
||||
padding-left: var(--spacing-2);
|
||||
--tanstack-table-header-cell-bg: var(--l2-background);
|
||||
--tanstack-table-header-cell-color: var(--l2-foreground);
|
||||
--tanstack-table-cell-bg: var(--l2-background);
|
||||
@@ -18,9 +24,12 @@
|
||||
--tanstack-table-resize-handle-hover-bg: var(--l2-border);
|
||||
--tanstack-table-row-height: 42px;
|
||||
|
||||
--tanstack-table-header-label-width: 100%;
|
||||
|
||||
--tanstack-cell-padding-top-override: 5px;
|
||||
--tanstack-cell-padding-bottom-override: 5px;
|
||||
--tanstack-cell-padding-left-override: 5px;
|
||||
--tanstack-cell-header-padding-left-override: 5px;
|
||||
--tanstack-cell-padding-left-override: 26px;
|
||||
--tanstack-cell-padding-right-override: 5px;
|
||||
|
||||
--tanstack-expansion-first-col-padding-left: 30px;
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useQuery, useQueryClient } from 'react-query';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import TanStackTable, {
|
||||
TableColumnDef,
|
||||
useCalculatedPageSize,
|
||||
useHiddenColumnIds,
|
||||
useTableParams,
|
||||
} from 'components/TanStackTableView';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { useGlobalTimeStore } from 'store/globalTime';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
import { Querybuildertypesv5QueryWarnDataDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import {
|
||||
@@ -17,15 +21,15 @@ import {
|
||||
InfraMonitoringEntity,
|
||||
} from '../constants';
|
||||
import {
|
||||
useInfraMonitoringFiltersK8s,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringPageListing,
|
||||
useInfraMonitoringPageSizeListing,
|
||||
useInfraMonitoringStatusFilter,
|
||||
} from '../hooks';
|
||||
import { useInfraMonitoringLineClamp } from '../components';
|
||||
import { K8sEmptyState } from './K8sEmptyState';
|
||||
import { K8sExpandedRow } from './K8sExpandedRow';
|
||||
import K8sHeader from './K8sHeader';
|
||||
import { K8sPaginationWarning } from './K8sPaginationWarning';
|
||||
import { K8sBaseFilters } from './types';
|
||||
import { getGroupedByMeta } from './utils';
|
||||
|
||||
@@ -38,24 +42,30 @@ export type K8sBaseListEmptyStateContext = {
|
||||
totalCount: number;
|
||||
hasFilters: boolean;
|
||||
isLoading: boolean;
|
||||
endTimeBeforeRetention?: boolean;
|
||||
rawData?: unknown;
|
||||
};
|
||||
|
||||
/** Base type constraint for K8s entity data */
|
||||
export type K8sEntityData = { meta?: Record<string, string> };
|
||||
export type K8sEntityData = { meta?: Record<string, string> | null };
|
||||
|
||||
export type K8sBaseListProps<T extends K8sEntityData> = {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
leftFilters?: React.ReactNode;
|
||||
entity: InfraMonitoringEntity;
|
||||
tableColumns: TableColumnDef<T>[];
|
||||
fetchListData: (
|
||||
filters: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<{
|
||||
data: T[];
|
||||
type?: 'list' | 'grouped_list';
|
||||
records?: T[];
|
||||
data?: T[];
|
||||
total: number;
|
||||
error?: string | null;
|
||||
rawData?: unknown;
|
||||
endTimeBeforeRetention?: boolean;
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO | null;
|
||||
}>;
|
||||
/** Function to get the unique key for a row. */
|
||||
getRowKey?: (record: T) => string;
|
||||
@@ -65,10 +75,12 @@ export type K8sBaseListProps<T extends K8sEntityData> = {
|
||||
renderEmptyState?: (
|
||||
context: K8sBaseListEmptyStateContext,
|
||||
) => React.ReactNode | null;
|
||||
extraQueryKeyParts?: string[];
|
||||
};
|
||||
|
||||
export function K8sBaseList<T extends K8sEntityData>({
|
||||
controlListPrefix,
|
||||
leftFilters,
|
||||
entity,
|
||||
tableColumns,
|
||||
fetchListData,
|
||||
@@ -76,12 +88,14 @@ export function K8sBaseList<T extends K8sEntityData>({
|
||||
getItemKey,
|
||||
eventCategory,
|
||||
renderEmptyState,
|
||||
extraQueryKeyParts = [],
|
||||
}: K8sBaseListProps<T>): JSX.Element {
|
||||
const [queryFilters] = useInfraMonitoringFiltersK8s();
|
||||
const [currentPage] = useInfraMonitoringPageListing();
|
||||
const [currentPageSize] = useInfraMonitoringPageSizeListing();
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const expression = currentQuery.builder.queryData[0]?.filter?.expression || '';
|
||||
const lineClamp = useInfraMonitoringLineClamp();
|
||||
const [groupBy] = useInfraMonitoringGroupBy();
|
||||
const [orderBy] = useInfraMonitoringOrderBy();
|
||||
const [statusFilter] = useInfraMonitoringStatusFilter();
|
||||
const [selectedItem, setSelectedItem] = useQueryState(
|
||||
'selectedItem',
|
||||
parseAsString,
|
||||
@@ -90,6 +104,27 @@ export function K8sBaseList<T extends K8sEntityData>({
|
||||
const columnStorageKey = `k8s-${entity}-columns`;
|
||||
const hiddenColumnIds = useHiddenColumnIds(columnStorageKey);
|
||||
|
||||
const { containerRef, calculatedPageSize } = useCalculatedPageSize({
|
||||
rowHeight: 42,
|
||||
});
|
||||
|
||||
const {
|
||||
page: currentPage,
|
||||
limit: currentPageSize,
|
||||
setLimit,
|
||||
} = useTableParams(
|
||||
{
|
||||
page: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE,
|
||||
limit: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE_SIZE,
|
||||
},
|
||||
{
|
||||
page: 1,
|
||||
limit: 10,
|
||||
storageKey: `k8s-${entity}`,
|
||||
calculatedPageSize,
|
||||
},
|
||||
);
|
||||
|
||||
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
|
||||
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
|
||||
const isRefreshEnabled = useGlobalTimeStore((s) => s.isRefreshEnabled);
|
||||
@@ -105,9 +140,11 @@ export function K8sBaseList<T extends K8sEntityData>({
|
||||
entity,
|
||||
String(currentPageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(queryFilters),
|
||||
expression || '',
|
||||
JSON.stringify(orderBy),
|
||||
JSON.stringify(groupBy),
|
||||
statusFilter,
|
||||
...extraQueryKeyParts,
|
||||
);
|
||||
}, [
|
||||
getAutoRefreshQueryKey,
|
||||
@@ -115,35 +152,64 @@ export function K8sBaseList<T extends K8sEntityData>({
|
||||
entity,
|
||||
currentPageSize,
|
||||
currentPage,
|
||||
queryFilters,
|
||||
expression,
|
||||
orderBy,
|
||||
groupBy,
|
||||
statusFilter,
|
||||
extraQueryKeyParts,
|
||||
]);
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey,
|
||||
queryFn: ({ signal }) => {
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return fetchListData(
|
||||
const { data, isLoading, isFetching, isError } = useQuery({
|
||||
queryKey,
|
||||
queryFn: async ({ signal }) => {
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
const start = Math.floor(minTime / NANO_SECOND_MULTIPLIER);
|
||||
const end = Math.floor(maxTime / NANO_SECOND_MULTIPLIER);
|
||||
|
||||
const response = await fetchListData(
|
||||
{
|
||||
limit: currentPageSize,
|
||||
filter: {
|
||||
expression: expression || '',
|
||||
filterByStatus:
|
||||
statusFilter === 'active' || statusFilter === 'inactive'
|
||||
? statusFilter
|
||||
: undefined,
|
||||
},
|
||||
groupBy:
|
||||
groupBy && groupBy.length > 0
|
||||
? groupBy.map((g) => ({ name: g }))
|
||||
: undefined,
|
||||
offset: (currentPage - 1) * currentPageSize,
|
||||
filters: queryFilters || { items: [], op: 'AND' },
|
||||
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
|
||||
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
|
||||
orderBy: orderBy || undefined,
|
||||
groupBy: groupBy?.length > 0 ? groupBy : undefined,
|
||||
limit: currentPageSize,
|
||||
start,
|
||||
end,
|
||||
orderBy: orderBy
|
||||
? { key: { name: orderBy.columnName }, direction: orderBy.order }
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
return {
|
||||
data: response.records || response.data || [],
|
||||
total: response.total,
|
||||
error: response.error,
|
||||
endTimeBeforeRetention: response.endTimeBeforeRetention,
|
||||
rawData: response.rawData ?? response,
|
||||
warning: response.warning ?? null,
|
||||
};
|
||||
},
|
||||
refetchInterval: isRefreshEnabled ? refreshInterval : false,
|
||||
});
|
||||
|
||||
const cancelQuery = useCallback((): void => {
|
||||
void queryClient.cancelQueries({ queryKey });
|
||||
}, [queryClient, queryKey]);
|
||||
|
||||
const pageData = data?.data ?? [];
|
||||
const totalCount = data?.total || 0;
|
||||
const hasFilters = (queryFilters?.items?.length ?? 0) > 0;
|
||||
const hasFilters = !!expression?.trim();
|
||||
|
||||
const getGroupKeyFn = useCallback(
|
||||
(item: T) => getGroupedByMeta(item, groupBy),
|
||||
@@ -151,7 +217,7 @@ export function K8sBaseList<T extends K8sEntityData>({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
void logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
@@ -162,10 +228,10 @@ export function K8sBaseList<T extends K8sEntityData>({
|
||||
const handleRowClick = useCallback(
|
||||
(_record: T, itemKey: string): void => {
|
||||
if (groupBy.length === 0) {
|
||||
setSelectedItem(itemKey);
|
||||
void setSelectedItem(itemKey);
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
void logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
@@ -185,7 +251,7 @@ export function K8sBaseList<T extends K8sEntityData>({
|
||||
url.searchParams.set('selectedItem', itemKey);
|
||||
openInNewTab(url.pathname + url.search);
|
||||
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
void logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
@@ -214,11 +280,19 @@ export function K8sBaseList<T extends K8sEntityData>({
|
||||
entity={entity}
|
||||
tableColumns={expandedRowColumns}
|
||||
fetchListData={fetchListData}
|
||||
extraQueryKeyParts={extraQueryKeyParts}
|
||||
getRowKey={getRowKey}
|
||||
getItemKey={getItemKey}
|
||||
/>
|
||||
),
|
||||
[entity, fetchListData, getRowKey, getItemKey, expandedRowColumns],
|
||||
[
|
||||
entity,
|
||||
fetchListData,
|
||||
getRowKey,
|
||||
getItemKey,
|
||||
expandedRowColumns,
|
||||
extraQueryKeyParts,
|
||||
],
|
||||
);
|
||||
|
||||
const getRowCanExpand = useCallback(
|
||||
@@ -234,64 +308,78 @@ export function K8sBaseList<T extends K8sEntityData>({
|
||||
totalCount,
|
||||
hasFilters,
|
||||
isLoading: showTableLoadingState,
|
||||
endTimeBeforeRetention: data?.endTimeBeforeRetention,
|
||||
rawData: data?.rawData,
|
||||
}) || (
|
||||
<K8sEmptyState
|
||||
isError={isError}
|
||||
error={data?.error}
|
||||
isLoading={showTableLoadingState}
|
||||
rawData={data?.rawData}
|
||||
endTimeBeforeRetention={data?.endTimeBeforeRetention}
|
||||
/>
|
||||
);
|
||||
|
||||
const showEmptyState = !showTableLoadingState && pageData.length === 0;
|
||||
|
||||
const paginationWarningContent = data?.warning ? (
|
||||
<K8sPaginationWarning warning={data.warning} />
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sHeader
|
||||
controlListPrefix={controlListPrefix}
|
||||
leftFilters={leftFilters}
|
||||
entity={entity}
|
||||
showAutoRefresh={!selectedItem}
|
||||
columns={tableColumns}
|
||||
columnStorageKey={columnStorageKey}
|
||||
isFetching={isFetching}
|
||||
cancelQuery={cancelQuery}
|
||||
/>
|
||||
{isError && (
|
||||
<Typography>{data?.error?.toString() || 'Something went wrong'}</Typography>
|
||||
)}
|
||||
<div ref={containerRef} className={styles.tableContainer}>
|
||||
{isError && (
|
||||
<Typography>
|
||||
{data?.error?.toString() || 'Something went wrong'}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{showEmptyState ? (
|
||||
<div className={styles.emptyStateContainer}>{emptyTableMessage}</div>
|
||||
) : (
|
||||
<TanStackTable<T>
|
||||
data={pageData}
|
||||
columns={tableColumns}
|
||||
columnStorageKey={columnStorageKey}
|
||||
isLoading={showTableLoadingState}
|
||||
getRowKey={getRowKey}
|
||||
getItemKey={getItemKey}
|
||||
groupBy={groupBy}
|
||||
getGroupKey={getGroupKeyFn}
|
||||
onRowClick={handleRowClick}
|
||||
onRowClickNewTab={handleRowClickNewTab}
|
||||
renderExpandedRow={isGroupedByAttribute ? renderExpandedRow : undefined}
|
||||
getRowCanExpand={isGroupedByAttribute ? getRowCanExpand : undefined}
|
||||
className={cx(styles.k8SListTable, expandedRowColumns)}
|
||||
enableQueryParams={{
|
||||
page: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE,
|
||||
limit: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE_SIZE,
|
||||
orderBy: INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
|
||||
expanded: INFRA_MONITORING_K8S_PARAMS_KEYS.EXPANDED,
|
||||
}}
|
||||
pagination={{
|
||||
total: totalCount,
|
||||
defaultLimit: 10,
|
||||
defaultPage: 1,
|
||||
showTotalCount: true,
|
||||
totalCountLabel: entity.charAt(0).toUpperCase() + entity.slice(1),
|
||||
}}
|
||||
paginationClassname={styles.paginationContainer}
|
||||
/>
|
||||
)}
|
||||
{showEmptyState ? (
|
||||
<div className={styles.emptyStateContainer}>{emptyTableMessage}</div>
|
||||
) : (
|
||||
<TanStackTable<T>
|
||||
data={pageData}
|
||||
columns={tableColumns}
|
||||
columnStorageKey={columnStorageKey}
|
||||
isLoading={showTableLoadingState}
|
||||
getRowKey={getRowKey}
|
||||
getItemKey={getItemKey}
|
||||
groupBy={groupBy.map((g) => ({ key: g }))}
|
||||
getGroupKey={getGroupKeyFn}
|
||||
onRowClick={handleRowClick}
|
||||
onRowClickNewTab={handleRowClickNewTab}
|
||||
renderExpandedRow={isGroupedByAttribute ? renderExpandedRow : undefined}
|
||||
getRowCanExpand={isGroupedByAttribute ? getRowCanExpand : undefined}
|
||||
className={cx(styles.k8SListTable, expandedRowColumns)}
|
||||
enableQueryParams={{
|
||||
page: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE,
|
||||
limit: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE_SIZE,
|
||||
orderBy: INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
|
||||
expanded: INFRA_MONITORING_K8S_PARAMS_KEYS.EXPANDED,
|
||||
}}
|
||||
pagination={{
|
||||
total: totalCount,
|
||||
showTotalCount: true,
|
||||
totalCountLabel: entity.charAt(0).toUpperCase() + entity.slice(1),
|
||||
calculatedPageSize,
|
||||
onLimitChange: setLimit,
|
||||
}}
|
||||
plainTextCellLineClamp={lineClamp}
|
||||
prefixPaginationContent={paginationWarningContent}
|
||||
paginationClassname={styles.paginationContainer}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,12 +11,6 @@ import type { K8sBaseListEmptyStateContext } from './K8sBaseList';
|
||||
|
||||
import styles from './K8sEmptyState.module.scss';
|
||||
|
||||
export interface K8sListResponseMetadata {
|
||||
sentAnyHostMetricsData?: boolean;
|
||||
isSendingK8SAgentMetrics?: boolean;
|
||||
endTimeBeforeRetention?: boolean;
|
||||
}
|
||||
|
||||
type K8sEmptyStateProps = Partial<K8sBaseListEmptyStateContext>;
|
||||
|
||||
const handleContactSupport = (isCloudUser: boolean): void => {
|
||||
@@ -31,7 +25,7 @@ export function K8sEmptyState({
|
||||
isError,
|
||||
error,
|
||||
isLoading,
|
||||
rawData,
|
||||
endTimeBeforeRetention,
|
||||
}: K8sEmptyStateProps): JSX.Element | null {
|
||||
const { isCloudUser } = useGetTenantLicense();
|
||||
|
||||
@@ -70,47 +64,7 @@ export function K8sEmptyState({
|
||||
);
|
||||
}
|
||||
|
||||
const metadata = rawData as K8sListResponseMetadata | undefined;
|
||||
|
||||
if (metadata?.sentAnyHostMetricsData === false) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<img className={styles.eyesEmoji} src={eyesEmojiUrl} alt="eyes emoji" />
|
||||
<div className={styles.noDataMessage}>
|
||||
<h5 className={styles.title}>No host metrics data received yet</h5>
|
||||
<span className={styles.message}>
|
||||
Please refer to{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/infrastructure-monitoring/hostmetrics/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
our documentation
|
||||
</a>{' '}
|
||||
to learn how to send host metrics.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (metadata?.isSendingK8SAgentMetrics) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<img className={styles.eyesEmoji} src={eyesEmojiUrl} alt="eyes emoji" />
|
||||
<span className={styles.message}>
|
||||
To see K8s metrics, upgrade to the latest version of SigNoz k8s-infra
|
||||
chart. Please contact support if you need help.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (metadata?.endTimeBeforeRetention) {
|
||||
if (endTimeBeforeRetention) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Querybuildertypesv5QueryWarnDataDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import TanStackTable, {
|
||||
@@ -7,16 +9,19 @@ import TanStackTable, {
|
||||
TableColumnDef,
|
||||
TanStackTableStateProvider,
|
||||
} from 'components/TanStackTableView';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { CornerDownRight } from '@signozhq/icons';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { useGlobalTimeStore } from 'store/globalTime';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
|
||||
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import {
|
||||
useInfraMonitoringFiltersK8s,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringPageListing,
|
||||
@@ -25,6 +30,7 @@ import {
|
||||
import { K8sBaseFilters } from './types';
|
||||
|
||||
import styles from './K8sExpandedRow.module.scss';
|
||||
import { buildExpressionFromGroupMeta } from './utils';
|
||||
|
||||
const EXPANDED_ROW_LIMIT = 10;
|
||||
|
||||
@@ -35,15 +41,21 @@ export type K8sExpandedRowProps<T> = {
|
||||
groupMeta?: Record<string, string>;
|
||||
entity: InfraMonitoringEntity;
|
||||
tableColumns: TableColumnDef<T>[];
|
||||
fetchListData: (
|
||||
/** API fetch function for expanded row data */
|
||||
fetchListData?: (
|
||||
filters: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<{
|
||||
data: T[];
|
||||
type?: 'list' | 'grouped_list';
|
||||
records?: T[];
|
||||
data?: T[];
|
||||
total: number;
|
||||
error?: string | null;
|
||||
rawData?: unknown;
|
||||
warning?: Querybuildertypesv5QueryWarnDataDTO | null;
|
||||
}>;
|
||||
/** Extra parts to include in the react-query cache key (e.g., status filter). */
|
||||
extraQueryKeyParts?: string[];
|
||||
/** Function to get the unique key for a row. */
|
||||
getRowKey?: (record: T) => string;
|
||||
/** Function to get the item key used for selection. Defaults to getRowKey if not provided. */
|
||||
@@ -56,14 +68,20 @@ export function K8sExpandedRow<T>({
|
||||
entity,
|
||||
tableColumns,
|
||||
fetchListData,
|
||||
extraQueryKeyParts = [],
|
||||
getRowKey,
|
||||
getItemKey,
|
||||
}: K8sExpandedRowProps<T>): JSX.Element {
|
||||
const [, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
const [, setCurrentPage] = useInfraMonitoringPageListing();
|
||||
const [queryFilters, setFilters] = useInfraMonitoringFiltersK8s();
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const parentExpression =
|
||||
currentQuery.builder.queryData[0]?.filter?.expression || '';
|
||||
const [, setSelectedItem] = useInfraMonitoringSelectedItem();
|
||||
const [, setMainOrderBy] = useInfraMonitoringOrderBy();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const urlQuery = useUrlQuery();
|
||||
const location = useLocation();
|
||||
|
||||
const orderByParamKey = useMemo(
|
||||
() => `orderBy_${rowKey.replace(/[^a-zA-Z0-9]/g, '_')}`,
|
||||
@@ -85,35 +103,10 @@ export function K8sExpandedRow<T>({
|
||||
|
||||
const storageKey = `k8s-${entity}-columns-expanded`;
|
||||
|
||||
const createFiltersForRecord = useCallback((): NonNullable<
|
||||
IBuilderQuery['filters']
|
||||
> => {
|
||||
const baseFilters: IBuilderQuery['filters'] = {
|
||||
items: [...(queryFilters?.items || [])],
|
||||
op: 'and',
|
||||
};
|
||||
|
||||
const metaKeys = groupMeta ?? {};
|
||||
|
||||
for (const key of Object.keys(metaKeys)) {
|
||||
const value = metaKeys[key];
|
||||
// Skip empty values to avoid creating invalid filters
|
||||
if (value === '' || value === undefined || value === null) {
|
||||
continue;
|
||||
}
|
||||
baseFilters.items.push({
|
||||
key: {
|
||||
key,
|
||||
type: 'resource',
|
||||
},
|
||||
op: '=',
|
||||
value,
|
||||
id: key,
|
||||
});
|
||||
}
|
||||
|
||||
return baseFilters;
|
||||
}, [queryFilters?.items, groupMeta]);
|
||||
const expressionForRecord = useMemo(
|
||||
() => buildExpressionFromGroupMeta(parentExpression || '', groupMeta),
|
||||
[parentExpression, groupMeta],
|
||||
);
|
||||
|
||||
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
|
||||
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
|
||||
@@ -130,8 +123,9 @@ export function K8sExpandedRow<T>({
|
||||
'k8sExpandedRow',
|
||||
JSON.stringify(groupMeta),
|
||||
rowKey,
|
||||
JSON.stringify(queryFilters),
|
||||
expressionForRecord,
|
||||
JSON.stringify(orderBy),
|
||||
...extraQueryKeyParts,
|
||||
);
|
||||
}, [
|
||||
getAutoRefreshQueryKey,
|
||||
@@ -139,49 +133,83 @@ export function K8sExpandedRow<T>({
|
||||
entity,
|
||||
groupMeta,
|
||||
rowKey,
|
||||
queryFilters,
|
||||
expressionForRecord,
|
||||
orderBy,
|
||||
extraQueryKeyParts,
|
||||
]);
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey,
|
||||
queryFn: async ({ signal }) => {
|
||||
if (!fetchListData) {
|
||||
return { data: [] as T[], total: 0 };
|
||||
}
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
|
||||
return await fetchListData(
|
||||
const response = await fetchListData(
|
||||
{
|
||||
filter: { expression: expressionForRecord },
|
||||
limit: EXPANDED_ROW_LIMIT,
|
||||
offset: 0,
|
||||
filters: createFiltersForRecord(),
|
||||
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
|
||||
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
|
||||
orderBy: orderBy || undefined,
|
||||
orderBy: orderBy
|
||||
? { key: { name: orderBy.columnName }, direction: orderBy.order }
|
||||
: undefined,
|
||||
groupBy: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.records || response.data || [],
|
||||
total: response.total,
|
||||
error: response.error,
|
||||
};
|
||||
},
|
||||
staleTime: 1000 * 60 * 30,
|
||||
refetchInterval: isRefreshEnabled ? refreshInterval : false,
|
||||
enabled: !!fetchListData,
|
||||
});
|
||||
|
||||
const expandedData = data?.data ?? [];
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(_row: T, itemKey: string): void => {
|
||||
setSelectedItem(itemKey);
|
||||
void setSelectedItem(itemKey);
|
||||
},
|
||||
[setSelectedItem],
|
||||
);
|
||||
|
||||
const handleViewAllClick = (): void => {
|
||||
const filters = createFiltersForRecord();
|
||||
setGroupBy([]);
|
||||
setCurrentPage(1);
|
||||
setFilters(filters);
|
||||
void setGroupBy([]);
|
||||
void setCurrentPage(1);
|
||||
if (orderBy) {
|
||||
setMainOrderBy(orderBy);
|
||||
void setMainOrderBy(orderBy);
|
||||
}
|
||||
|
||||
const updatedQuery = {
|
||||
...currentQuery,
|
||||
id: uuid(),
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...(currentQuery.builder.queryData[0] || {}),
|
||||
filter: { expression: expressionForRecord },
|
||||
filters: { items: [], op: 'AND' as const },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const newUrlQuery = new URLSearchParams(urlQuery.toString());
|
||||
newUrlQuery.set(
|
||||
QueryParams.compositeQuery,
|
||||
encodeURIComponent(JSON.stringify(updatedQuery)),
|
||||
);
|
||||
|
||||
safeNavigate(`${location.pathname}?${newUrlQuery.toString()}`);
|
||||
};
|
||||
|
||||
const total = data?.total ?? 0;
|
||||
|
||||
@@ -24,11 +24,37 @@
|
||||
.columnsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
[data-slot='icon'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-slot='entity-group-header'] {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
[data-slot='column-header'] {
|
||||
display: flex;
|
||||
width: auto;
|
||||
|
||||
span {
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
br {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.columnItem {
|
||||
justify-content: flex-start !important;
|
||||
width: 100%;
|
||||
|
||||
> span {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
.horizontalDivider {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DrawerWrapper } from '@signozhq/ui/drawer';
|
||||
import {
|
||||
@@ -12,7 +12,7 @@ import styles from './K8sFiltersSidePanel.module.scss';
|
||||
|
||||
type ColumnPickerItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
label: ReactNode;
|
||||
canBeHidden: boolean;
|
||||
visibilityBehavior:
|
||||
| 'hidden-on-expand'
|
||||
@@ -20,15 +20,16 @@ type ColumnPickerItem = {
|
||||
| 'always-visible';
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts TableColumnDef to column picker item format
|
||||
*/
|
||||
function renderHeader(header: string | (() => ReactNode)): ReactNode {
|
||||
return typeof header === 'function' ? header() : header;
|
||||
}
|
||||
|
||||
function toColumnPickerItems<T>(
|
||||
columns: TableColumnDef<T>[],
|
||||
): ColumnPickerItem[] {
|
||||
return columns.map((col) => ({
|
||||
id: col.id,
|
||||
label: typeof col.header === 'string' ? col.header : col.id,
|
||||
label: renderHeader(col.header),
|
||||
canBeHidden: col.canBeHidden !== false && col.enableRemove !== false,
|
||||
visibilityBehavior: col.visibilityBehavior ?? 'always-visible',
|
||||
}));
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getGroupByEl } from './utils';
|
||||
import { useInfraMonitoringGroupBy } from '../hooks';
|
||||
|
||||
interface K8sEntityWithMeta {
|
||||
meta?: Record<string, string>;
|
||||
meta?: Record<string, string> | null;
|
||||
}
|
||||
|
||||
function K8sGroupCell<T extends K8sEntityWithMeta>({
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
.k8SListControls {
|
||||
padding: var(--spacing-4);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-4);
|
||||
width: 100%;
|
||||
|
||||
:global(.ant-select-selector) {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--border) !important;
|
||||
border: 1px solid var(--l2-border) !important;
|
||||
background-color: var(--l2-background) !important;
|
||||
|
||||
input {
|
||||
@@ -20,28 +21,81 @@
|
||||
}
|
||||
}
|
||||
|
||||
.k8SListControlsLeft {
|
||||
flex: 1;
|
||||
.k8SListControlsRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-4);
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.k8SQbSearchContainer {
|
||||
flex: 1;
|
||||
min-width: 240px;
|
||||
max-width: 60%;
|
||||
.k8SQbSearchContainer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.k8SFiltersGroupByRow {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-4);
|
||||
min-width: 0;
|
||||
|
||||
@media (max-width: 1600px) {
|
||||
flex: 1 1 100%;
|
||||
order: 9;
|
||||
}
|
||||
}
|
||||
|
||||
.k8SAttributeSearchContainer {
|
||||
flex: 1;
|
||||
min-width: 240px;
|
||||
max-width: 40%;
|
||||
flex: 1 1 240px;
|
||||
max-width: 400px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.k8SRunButton {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.k8SFiltersButton {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.k8SDateTimeSelection {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
--date-time-selector-border-color: var(--l2-border);
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
flex: 1 1 100%;
|
||||
order: 10;
|
||||
}
|
||||
|
||||
:global(.timeSelection-input) {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
:global(.ant-input-affix-wrapper) {
|
||||
border-color: var(--l2-border);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l2-border) !important;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.refresh-actions .ant-form-item-control-input) {
|
||||
&,
|
||||
:global(.ant-btn) {
|
||||
height: 30px;
|
||||
min-height: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.groupByLabel {
|
||||
min-width: max-content;
|
||||
font-size: var(--periscope-font-size-base, 13px);
|
||||
@@ -50,7 +104,7 @@
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
border-radius: 2px 0px 0px 2px;
|
||||
border: 1px solid var(--border);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-right: none;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
@@ -64,6 +118,8 @@
|
||||
}
|
||||
|
||||
.groupBySelect {
|
||||
width: 100%;
|
||||
|
||||
:global(.ant-select-selector) {
|
||||
border-left: none;
|
||||
border-top-left-radius: 0px;
|
||||
|
||||
@@ -1,167 +1,215 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Select } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useGetFieldsKeys } from 'api/generated/services/fields';
|
||||
import {
|
||||
TelemetrytypesFieldContextDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { SlidersHorizontal } from '@signozhq/icons';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { SlidersHorizontal } from '@signozhq/icons';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import {
|
||||
useGlobalTimeQueryInvalidate,
|
||||
useGlobalTimeStore,
|
||||
} from 'store/globalTime';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
GetK8sEntityToAggregateAttribute,
|
||||
ENTITY_FILTER_PLACEHOLDERS,
|
||||
InfraMonitoringEntity,
|
||||
METRIC_NAMESPACE_BY_ENTITY,
|
||||
} from '../constants';
|
||||
import {
|
||||
useInfraMonitoringFiltersK8s,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringPageListing,
|
||||
} from '../hooks';
|
||||
import K8sFiltersSidePanel from './K8sFiltersSidePanel';
|
||||
|
||||
import styles from './K8sHeader.module.scss';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
|
||||
interface K8sHeaderProps<TData> {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
leftFilters?: React.ReactNode;
|
||||
entity: InfraMonitoringEntity;
|
||||
showAutoRefresh: boolean;
|
||||
columns: TableColumnDef<TData>[];
|
||||
columnStorageKey: string;
|
||||
isFetching?: boolean;
|
||||
cancelQuery: () => void;
|
||||
}
|
||||
|
||||
function K8sHeader<TData>({
|
||||
controlListPrefix,
|
||||
leftFilters,
|
||||
entity,
|
||||
showAutoRefresh,
|
||||
columns,
|
||||
columnStorageKey,
|
||||
isFetching = false,
|
||||
cancelQuery,
|
||||
}: K8sHeaderProps<TData>): JSX.Element {
|
||||
const [isFiltersSidePanelOpen, setIsFiltersSidePanelOpen] = useState(false);
|
||||
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
|
||||
// null = user never touched the search box; fall back to the current
|
||||
// query expression on Run so an untouched search doesn't wipe filters
|
||||
const stagedExpressionRef = useRef<string | null>(null);
|
||||
|
||||
const currentQuery = initialQueriesMap[DataSource.METRICS];
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const location = useLocation();
|
||||
const invalidateQueries = useGlobalTimeQueryInvalidate();
|
||||
|
||||
const updatedCurrentQuery = useMemo(() => {
|
||||
let { filters } = currentQuery.builder.queryData[0];
|
||||
if (urlFilters) {
|
||||
filters = urlFilters;
|
||||
}
|
||||
return {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...currentQuery.builder.queryData[0],
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||
},
|
||||
filters,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}, [currentQuery, urlFilters]);
|
||||
|
||||
const query = useMemo(
|
||||
() => updatedCurrentQuery?.builder?.queryData[0] || null,
|
||||
[updatedCurrentQuery],
|
||||
const queryData = useMemo(
|
||||
(): IBuilderQuery => ({
|
||||
...currentQuery.builder.queryData[0],
|
||||
aggregateOperator: 'noop',
|
||||
}),
|
||||
[currentQuery],
|
||||
);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const [, setCurrentPage] = useInfraMonitoringPageListing();
|
||||
const handleChangeTagFilters = useCallback(
|
||||
(value: IBuilderQuery['filters']) => {
|
||||
setUrlFilters(value || null);
|
||||
handleChangeQueryData('filters', value);
|
||||
setCurrentPage(1);
|
||||
|
||||
if (value?.items && value?.items?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
const handleRunQuery = useCallback(
|
||||
(newExpression?: string): void => {
|
||||
const currentExpression =
|
||||
currentQuery.builder.queryData[0]?.filter?.expression || '';
|
||||
const finalExpression = newExpression ?? currentExpression;
|
||||
void setCurrentPage(1);
|
||||
|
||||
const updatedQuery = {
|
||||
...currentQuery,
|
||||
id: uuid(),
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((query, idx) =>
|
||||
idx === 0
|
||||
? {
|
||||
...query,
|
||||
filter: { expression: finalExpression || '' },
|
||||
filters: { items: [], op: 'AND' as const },
|
||||
}
|
||||
: query,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
// Use window.location.search to get fresh URL params (avoids stale hook state)
|
||||
const newUrlQuery = new URLSearchParams(window.location.search);
|
||||
newUrlQuery.set(
|
||||
QueryParams.compositeQuery,
|
||||
encodeURIComponent(JSON.stringify(updatedQuery)),
|
||||
);
|
||||
|
||||
safeNavigate(`${location.pathname}?${newUrlQuery.toString()}`);
|
||||
void invalidateQueries();
|
||||
|
||||
if (finalExpression?.trim()) {
|
||||
void logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
category: entity,
|
||||
});
|
||||
}
|
||||
},
|
||||
[handleChangeQueryData, setCurrentPage, setUrlFilters],
|
||||
[
|
||||
currentQuery,
|
||||
safeNavigate,
|
||||
location.pathname,
|
||||
setCurrentPage,
|
||||
entity,
|
||||
invalidateQueries,
|
||||
],
|
||||
);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
const handleStageRunQuery = useCallback((): void => {
|
||||
handleRunQuery(stagedExpressionRef.current ?? undefined);
|
||||
}, [handleRunQuery]);
|
||||
|
||||
const handleExpressionChange = useCallback((value: string): void => {
|
||||
stagedExpressionRef.current = value;
|
||||
}, []);
|
||||
|
||||
const handleCancelQuery = useCallback((): void => {
|
||||
cancelQuery();
|
||||
}, [cancelQuery]);
|
||||
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
const startUnixMilli = Math.floor(minTime / NANO_SECOND_MULTIPLIER);
|
||||
const endUnixMilli = Math.floor(maxTime / NANO_SECOND_MULTIPLIER);
|
||||
|
||||
const { data: groupByFiltersData, isLoading: isLoadingGroupByFilters } =
|
||||
useGetAggregateKeys(
|
||||
useGetFieldsKeys(
|
||||
{
|
||||
dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
aggregateAttribute: GetK8sEntityToAggregateAttribute(
|
||||
entity,
|
||||
dotMetricsEnabled,
|
||||
),
|
||||
aggregateOperator: 'noop',
|
||||
searchText: '',
|
||||
tagType: '',
|
||||
signal: TelemetrytypesSignalDTO.metrics,
|
||||
metricNamespace: METRIC_NAMESPACE_BY_ENTITY[entity],
|
||||
limit: 100,
|
||||
startUnixMilli,
|
||||
endUnixMilli,
|
||||
fieldContext: TelemetrytypesFieldContextDTO.resource,
|
||||
// the search text is intentionally not included
|
||||
// searchText: expression,
|
||||
},
|
||||
{
|
||||
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
|
||||
query: {
|
||||
queryKey: ['getFieldsKeys', entity, startUnixMilli, endUnixMilli],
|
||||
},
|
||||
},
|
||||
true,
|
||||
entity,
|
||||
);
|
||||
|
||||
const flatFieldKeys = useMemo(() => {
|
||||
const keys = groupByFiltersData?.data?.keys;
|
||||
if (!keys) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allKeys = Object.values(keys).flat();
|
||||
const seen = new Set<string>();
|
||||
|
||||
return allKeys.filter((field) => {
|
||||
if (seen.has(field.name)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(field.name);
|
||||
return true;
|
||||
});
|
||||
}, [groupByFiltersData]);
|
||||
|
||||
const groupByOptions = useMemo(
|
||||
() =>
|
||||
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
||||
value: filter.key,
|
||||
label: filter.key,
|
||||
})) || [],
|
||||
[groupByFiltersData],
|
||||
flatFieldKeys.map((field) => ({
|
||||
value: field.name,
|
||||
label: field.name,
|
||||
})),
|
||||
[flatFieldKeys],
|
||||
);
|
||||
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
|
||||
const handleGroupByChange = useCallback(
|
||||
(value: IBuilderQuery['groupBy']) => {
|
||||
const newGroupBy = [];
|
||||
(value: string[]) => {
|
||||
void setCurrentPage(1);
|
||||
void setGroupBy(value);
|
||||
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
const element = value[index] as unknown as string;
|
||||
|
||||
const key = groupByFiltersData?.payload?.attributeKeys?.find(
|
||||
(k) => k.key === element,
|
||||
);
|
||||
|
||||
if (key) {
|
||||
newGroupBy.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset pagination on switching to groupBy
|
||||
setCurrentPage(1);
|
||||
setGroupBy(newGroupBy);
|
||||
|
||||
logEvent(InfraMonitoringEvents.GroupByChanged, {
|
||||
void logEvent(InfraMonitoringEvents.GroupByChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
});
|
||||
},
|
||||
[groupByFiltersData, setCurrentPage, setGroupBy],
|
||||
[setCurrentPage, setGroupBy],
|
||||
);
|
||||
|
||||
const onClickOutside = useCallback(() => {
|
||||
@@ -170,53 +218,69 @@ function K8sHeader<TData>({
|
||||
|
||||
return (
|
||||
<div className={styles.k8SListControls}>
|
||||
<div className={styles.k8SListControlsLeft}>
|
||||
<div className={styles.k8SListControlsRow}>
|
||||
{controlListPrefix}
|
||||
|
||||
<div className={styles.k8SQbSearchContainer}>
|
||||
<QueryBuilderSearch
|
||||
query={query as IBuilderQuery}
|
||||
onChange={handleChangeTagFilters}
|
||||
isInfraMonitoring
|
||||
disableNavigationShortcuts
|
||||
entity={entity}
|
||||
<div className={styles.k8SFiltersGroupByRow}>
|
||||
{leftFilters}
|
||||
|
||||
<div className={styles.k8SAttributeSearchContainer}>
|
||||
<div className={styles.groupByLabel}>Group by</div>
|
||||
<Select
|
||||
className={styles.groupBySelect}
|
||||
loading={isLoadingGroupByFilters}
|
||||
mode="multiple"
|
||||
value={groupBy}
|
||||
allowClear
|
||||
maxTagCount="responsive"
|
||||
placeholder="Search for attribute"
|
||||
options={groupByOptions}
|
||||
onChange={handleGroupByChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.k8SDateTimeSelection}>
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh={showAutoRefresh}
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.k8SAttributeSearchContainer}>
|
||||
<div className={styles.groupByLabel}> Group by </div>
|
||||
<Select
|
||||
className={styles.groupBySelect}
|
||||
loading={isLoadingGroupByFilters}
|
||||
mode="multiple"
|
||||
value={groupBy}
|
||||
allowClear
|
||||
maxTagCount="responsive"
|
||||
placeholder="Search for attribute"
|
||||
style={{ width: '100%' }}
|
||||
options={groupByOptions}
|
||||
onChange={handleGroupByChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.k8SListControlsRight}>
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh={showAutoRefresh}
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
<RunQueryBtn
|
||||
isLoadingQueries={isFetching}
|
||||
onStageRunQuery={handleStageRunQuery}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
className={styles.k8SRunButton}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="none"
|
||||
data-testid="k8s-list-filters-button"
|
||||
onClick={(): void => setIsFiltersSidePanelOpen(true)}
|
||||
>
|
||||
<SlidersHorizontal size={14} />
|
||||
</Button>
|
||||
<TooltipSimple title="Click to add more columns to this table">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
data-testid="k8s-list-filters-button"
|
||||
onClick={(): void => setIsFiltersSidePanelOpen(true)}
|
||||
className={styles.k8SFiltersButton}
|
||||
>
|
||||
<SlidersHorizontal size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
|
||||
<div className={styles.k8SQbSearchContainer}>
|
||||
<QuerySearch
|
||||
queryData={queryData}
|
||||
dataSource={DataSource.METRICS}
|
||||
onChange={handleExpressionChange}
|
||||
onRun={handleRunQuery}
|
||||
signalSource=""
|
||||
showFilterSuggestionsWithoutMetric
|
||||
placeholder={ENTITY_FILTER_PLACEHOLDERS[entity]}
|
||||
metricNamespace={METRIC_NAMESPACE_BY_ENTITY[entity]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<K8sFiltersSidePanel
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.paginationWarning {
|
||||
border: 1px solid var(--callout-warning-border);
|
||||
background-color: var(--callout-warning-background);
|
||||
|
||||
padding: var(--spacing-2) var(--spacing-4);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.warningIcon {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
import { Querybuildertypesv5QueryWarnDataDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import styles from './K8sPaginationWarning.module.scss';
|
||||
import TriangleAlert from '@signozhq/icons/TriangleAlert';
|
||||
|
||||
export type K8sPaginationWarningProps = {
|
||||
warning: Querybuildertypesv5QueryWarnDataDTO;
|
||||
};
|
||||
|
||||
export function K8sPaginationWarning({
|
||||
warning,
|
||||
}: K8sPaginationWarningProps): JSX.Element {
|
||||
return (
|
||||
<span data-testid="k8s-list-warning-popover">
|
||||
<WarningPopover
|
||||
warningData={{
|
||||
code: 'WARNING',
|
||||
message: warning.message ?? '',
|
||||
url: warning.url ?? '',
|
||||
warnings:
|
||||
warning.warnings?.map((w) => ({ message: w.message ?? '' })) ?? [],
|
||||
}}
|
||||
>
|
||||
<div className={styles.paginationWarning}>
|
||||
Your data contains some warnings
|
||||
<TriangleAlert
|
||||
size={16}
|
||||
className={styles.warningIcon}
|
||||
color={Color.BG_AMBER_500}
|
||||
/>
|
||||
</div>
|
||||
</WarningPopover>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,15 @@ import { openInNewTab } from 'utils/navigation';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
|
||||
import { InfraMonitoringEntity } from '../../constants';
|
||||
|
||||
window.ResizeObserver =
|
||||
window.ResizeObserver ||
|
||||
jest.fn().mockImplementation(() => ({
|
||||
disconnect: jest.fn(),
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
}));
|
||||
|
||||
import { K8sBaseList, K8sBaseListProps, K8sEntityData } from '../K8sBaseList';
|
||||
|
||||
jest.mock('utils/navigation', () => ({
|
||||
@@ -206,8 +215,10 @@ describe('K8sBaseList', () => {
|
||||
const itemId2 = Math.random().toString(36).slice(7);
|
||||
const onUrlUpdateMock = jest.fn<void, [UrlUpdateEvent]>();
|
||||
const fetchListDataMock = jest.fn<
|
||||
ReturnType<K8sBaseListProps<TestItemWithTitle>['fetchListData']>,
|
||||
Parameters<K8sBaseListProps<TestItemWithTitle>['fetchListData']>
|
||||
ReturnType<
|
||||
NonNullable<K8sBaseListProps<TestItemWithTitle>['fetchListData']>
|
||||
>,
|
||||
Parameters<NonNullable<K8sBaseListProps<TestItemWithTitle>['fetchListData']>>
|
||||
>();
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -259,7 +270,10 @@ describe('K8sBaseList', () => {
|
||||
const [filters] = fetchListDataMock.mock.calls[0];
|
||||
expect(filters.limit).toBe(10);
|
||||
expect(filters.offset).toBe(0);
|
||||
expect(filters.filters).toStrictEqual({ items: [], op: 'AND' });
|
||||
expect(filters.filter).toStrictEqual({
|
||||
expression: '',
|
||||
filterByStatus: undefined,
|
||||
});
|
||||
expect(filters.groupBy).toBeUndefined();
|
||||
expect(filters.orderBy).toBeUndefined();
|
||||
});
|
||||
@@ -419,11 +433,11 @@ describe('K8sBaseList', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('with URL params (orderBy, groupBy, pagination)', () => {
|
||||
describe('with URL params (orderBy, groupBy old format, pagination)', () => {
|
||||
const onUrlUpdateMock = jest.fn<void, [UrlUpdateEvent]>();
|
||||
const fetchListDataMock = jest.fn<
|
||||
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
|
||||
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
|
||||
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
|
||||
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
|
||||
>();
|
||||
const groupByValue = [
|
||||
{ key: 'k8s.namespace.name', dataType: 'string', type: 'resource' },
|
||||
@@ -462,8 +476,11 @@ describe('K8sBaseList', () => {
|
||||
});
|
||||
|
||||
const [filters] = fetchListDataMock.mock.calls[0];
|
||||
expect(filters.orderBy).toStrictEqual({ columnName: 'cpu', order: 'desc' });
|
||||
expect(filters.groupBy).toStrictEqual(groupByValue);
|
||||
expect(filters.orderBy).toStrictEqual({
|
||||
key: { name: 'cpu' },
|
||||
direction: 'desc',
|
||||
});
|
||||
expect(filters.groupBy).toStrictEqual([{ name: 'k8s.namespace.name' }]);
|
||||
expect(filters.offset).toBe(20); // (3 - 1) * 10 = 20
|
||||
expect(filters.limit).toBe(10);
|
||||
});
|
||||
@@ -487,14 +504,84 @@ describe('K8sBaseList', () => {
|
||||
(c) => c[0].groupBy && c[0].groupBy.length > 0,
|
||||
);
|
||||
expect(callWithGroupBy).toBeDefined();
|
||||
expect(callWithGroupBy?.[0].groupBy).toStrictEqual(groupByValue);
|
||||
expect(callWithGroupBy?.[0].groupBy).toStrictEqual([
|
||||
{ name: 'k8s.namespace.name' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with URL params (groupBy new format - string array)', () => {
|
||||
const onUrlUpdateMock = jest.fn<void, [UrlUpdateEvent]>();
|
||||
const fetchListDataMock = jest.fn<
|
||||
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
|
||||
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
|
||||
>();
|
||||
const groupByValue = ['k8s.namespace.name'];
|
||||
|
||||
beforeEach(() => {
|
||||
onUrlUpdateMock.mockClear();
|
||||
fetchListDataMock.mockClear();
|
||||
fetchListDataMock.mockResolvedValue({
|
||||
data: [
|
||||
{ id: 'namespace-default', meta: { 'k8s.namespace.name': 'default' } },
|
||||
],
|
||||
total: 50,
|
||||
error: null,
|
||||
});
|
||||
|
||||
renderComponent<TestItem>({
|
||||
onUrlUpdate: onUrlUpdateMock,
|
||||
entity: InfraMonitoringEntity.PODS,
|
||||
eventCategory: InfraMonitoringEvents.Pod,
|
||||
fetchListData: fetchListDataMock,
|
||||
queryParams: {
|
||||
orderBy: JSON.stringify({ columnName: 'cpu', order: 'desc' }),
|
||||
groupBy: JSON.stringify(groupByValue),
|
||||
page: '3',
|
||||
},
|
||||
tableColumns: createTestColumns(),
|
||||
getRowKey: (row): string => row.id,
|
||||
getItemKey: (row): string => row.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call fetchListData with groupBy from new format URL', async () => {
|
||||
await waitFor(() => {
|
||||
expect(fetchListDataMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const [filters] = fetchListDataMock.mock.calls[0];
|
||||
expect(filters.groupBy).toStrictEqual([{ name: 'k8s.namespace.name' }]);
|
||||
});
|
||||
|
||||
it('should render expand icons when groupBy is set with new format', async () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('namespace-default')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const expandButtons = screen.getAllByRole('button');
|
||||
expect(expandButtons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render data with new groupBy format', async () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('namespace-default')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const callWithGroupBy = fetchListDataMock.mock.calls.find(
|
||||
(c) => c[0].groupBy && c[0].groupBy.length > 0,
|
||||
);
|
||||
expect(callWithGroupBy).toBeDefined();
|
||||
expect(callWithGroupBy?.[0].groupBy).toStrictEqual([
|
||||
{ name: 'k8s.namespace.name' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with empty data', () => {
|
||||
const fetchListDataMock = jest.fn<
|
||||
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
|
||||
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
|
||||
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
|
||||
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
|
||||
>();
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -503,10 +590,6 @@ describe('K8sBaseList', () => {
|
||||
data: [],
|
||||
total: 0,
|
||||
error: null,
|
||||
rawData: {
|
||||
sentAnyHostMetricsData: true,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
},
|
||||
});
|
||||
|
||||
renderComponent<TestItem>({
|
||||
@@ -534,8 +617,8 @@ describe('K8sBaseList', () => {
|
||||
|
||||
describe('with error response', () => {
|
||||
const fetchListDataMock = jest.fn<
|
||||
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
|
||||
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
|
||||
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
|
||||
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
|
||||
>();
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -569,95 +652,10 @@ describe('K8sBaseList', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('with no metrics data (sentAnyHostMetricsData=false)', () => {
|
||||
const fetchListDataMock = jest.fn<
|
||||
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
|
||||
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
|
||||
>();
|
||||
|
||||
beforeEach(() => {
|
||||
fetchListDataMock.mockClear();
|
||||
fetchListDataMock.mockResolvedValue({
|
||||
data: [],
|
||||
total: 0,
|
||||
error: null,
|
||||
rawData: {
|
||||
sentAnyHostMetricsData: false,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
},
|
||||
});
|
||||
|
||||
renderComponent<TestItem>({
|
||||
entity: InfraMonitoringEntity.PODS,
|
||||
eventCategory: InfraMonitoringEvents.Pod,
|
||||
fetchListData: fetchListDataMock,
|
||||
tableColumns: createTestColumns(),
|
||||
getRowKey: (row): string => row.id,
|
||||
getItemKey: (row): string => row.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should display no metrics data message', async () => {
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/No host metrics data received yet/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display link to documentation', async () => {
|
||||
await waitFor(() => {
|
||||
const link = screen.getByRole('link', { name: /our documentation/i });
|
||||
expect(link).toBeInTheDocument();
|
||||
expect(link).toHaveAttribute(
|
||||
'href',
|
||||
'https://signoz.io/docs/infrastructure-monitoring/hostmetrics/',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with incorrect K8s agent metrics (isSendingK8SAgentMetrics=true)', () => {
|
||||
const fetchListDataMock = jest.fn<
|
||||
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
|
||||
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
|
||||
>();
|
||||
|
||||
beforeEach(() => {
|
||||
fetchListDataMock.mockClear();
|
||||
fetchListDataMock.mockResolvedValue({
|
||||
data: [],
|
||||
total: 0,
|
||||
error: null,
|
||||
rawData: {
|
||||
sentAnyHostMetricsData: true,
|
||||
isSendingK8SAgentMetrics: true,
|
||||
},
|
||||
});
|
||||
|
||||
renderComponent<TestItem>({
|
||||
entity: InfraMonitoringEntity.PODS,
|
||||
eventCategory: InfraMonitoringEvents.Pod,
|
||||
fetchListData: fetchListDataMock,
|
||||
tableColumns: createTestColumns(),
|
||||
getRowKey: (row): string => row.id,
|
||||
getItemKey: (row): string => row.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should display upgrade message', async () => {
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/upgrade to the latest version of SigNoz k8s-infra/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with end time before retention (endTimeBeforeRetention=true)', () => {
|
||||
const fetchListDataMock = jest.fn<
|
||||
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
|
||||
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
|
||||
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
|
||||
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
|
||||
>();
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -666,11 +664,7 @@ describe('K8sBaseList', () => {
|
||||
data: [],
|
||||
total: 0,
|
||||
error: null,
|
||||
rawData: {
|
||||
sentAnyHostMetricsData: true,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
endTimeBeforeRetention: true,
|
||||
},
|
||||
endTimeBeforeRetention: true,
|
||||
});
|
||||
|
||||
renderComponent<TestItem>({
|
||||
@@ -697,8 +691,8 @@ describe('K8sBaseList', () => {
|
||||
|
||||
describe('column visibility based on TanStack columns', () => {
|
||||
const fetchListDataMock = jest.fn<
|
||||
ReturnType<K8sBaseListProps<TestItemWithName>['fetchListData']>,
|
||||
Parameters<K8sBaseListProps<TestItemWithName>['fetchListData']>
|
||||
ReturnType<NonNullable<K8sBaseListProps<TestItemWithName>['fetchListData']>>,
|
||||
Parameters<NonNullable<K8sBaseListProps<TestItemWithName>['fetchListData']>>
|
||||
>();
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -736,8 +730,10 @@ describe('K8sBaseList', () => {
|
||||
|
||||
describe('column behavior with groupBy (expanded/collapsed)', () => {
|
||||
const fetchListDataMock = jest.fn<
|
||||
ReturnType<K8sBaseListProps<TestItemWithGroup>['fetchListData']>,
|
||||
Parameters<K8sBaseListProps<TestItemWithGroup>['fetchListData']>
|
||||
ReturnType<
|
||||
NonNullable<K8sBaseListProps<TestItemWithGroup>['fetchListData']>
|
||||
>,
|
||||
Parameters<NonNullable<K8sBaseListProps<TestItemWithGroup>['fetchListData']>>
|
||||
>();
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -807,8 +803,8 @@ describe('K8sBaseList', () => {
|
||||
|
||||
describe('column visibility in expanded row (nested table)', () => {
|
||||
const fetchListDataMock = jest.fn<
|
||||
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
|
||||
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
|
||||
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
|
||||
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
|
||||
>();
|
||||
const groupByValue = [
|
||||
{ key: 'k8s.namespace.name', dataType: 'string', type: 'resource' },
|
||||
@@ -852,8 +848,8 @@ describe('K8sBaseList', () => {
|
||||
|
||||
describe('TanStack table column rendering', () => {
|
||||
const fetchListDataMock = jest.fn<
|
||||
ReturnType<K8sBaseListProps<TestItemWithName>['fetchListData']>,
|
||||
Parameters<K8sBaseListProps<TestItemWithName>['fetchListData']>
|
||||
ReturnType<NonNullable<K8sBaseListProps<TestItemWithName>['fetchListData']>>,
|
||||
Parameters<NonNullable<K8sBaseListProps<TestItemWithName>['fetchListData']>>
|
||||
>();
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -909,4 +905,40 @@ describe('K8sBaseList', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with warnings from API', () => {
|
||||
const fetchListDataMock = jest.fn<
|
||||
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
|
||||
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
|
||||
>();
|
||||
|
||||
beforeEach(() => {
|
||||
fetchListDataMock.mockClear();
|
||||
fetchListDataMock.mockResolvedValue({
|
||||
data: [{ id: 'item-1' }],
|
||||
total: 1,
|
||||
error: null,
|
||||
warning: {
|
||||
message: 'Some data may be incomplete',
|
||||
url: 'https://docs.example.com/partial-data',
|
||||
warnings: [{ message: 'Node xyz did not report metrics' }],
|
||||
},
|
||||
});
|
||||
|
||||
renderComponent<TestItem>({
|
||||
entity: InfraMonitoringEntity.PODS,
|
||||
eventCategory: InfraMonitoringEvents.Pod,
|
||||
fetchListData: fetchListDataMock,
|
||||
tableColumns: createTestColumns(),
|
||||
getRowKey: (row): string => row.id,
|
||||
getItemKey: (row): string => row.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render warning popover in pagination area', async () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('k8s-list-warning-popover')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { OrderBySchemaType } from '../schemas';
|
||||
|
||||
export type K8sBaseFilters = {
|
||||
filters: TagFilter;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
filter: {
|
||||
expression: string;
|
||||
filterByStatus?: 'active' | 'inactive' | '';
|
||||
};
|
||||
groupBy?: Array<{ name: string }>;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
start: number;
|
||||
end: number;
|
||||
orderBy?: OrderBySchemaType;
|
||||
orderBy?: {
|
||||
key: { name: string };
|
||||
direction: 'asc' | 'desc';
|
||||
};
|
||||
};
|
||||
|
||||
export type K8sListResponse<T> = {
|
||||
type: 'list' | 'grouped_list';
|
||||
records: T[];
|
||||
total: number;
|
||||
endTimeBeforeRetention?: boolean;
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import styles from './utils.module.scss';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
|
||||
|
||||
const dotToUnder: Record<string, string> = {
|
||||
'os.type': 'os_type',
|
||||
@@ -22,15 +22,13 @@ const dotToUnder: Record<string, string> = {
|
||||
'k8s.persistentvolumeclaim.name': 'k8s_persistentvolumeclaim_name',
|
||||
};
|
||||
|
||||
export function getGroupedByMeta<T extends { meta?: Record<string, string> }>(
|
||||
itemData: T,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): Record<string, string> {
|
||||
export function getGroupedByMeta<
|
||||
T extends { meta?: Record<string, string> | null },
|
||||
>(itemData: T, groupBy: string[]): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
const meta = itemData.meta ?? {};
|
||||
|
||||
groupBy.forEach((group) => {
|
||||
const rawKey = group.key as string;
|
||||
groupBy.forEach((rawKey) => {
|
||||
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof meta;
|
||||
result[rawKey] = (meta[metaKey] || meta[rawKey]) ?? '';
|
||||
});
|
||||
@@ -38,45 +36,13 @@ export function getGroupedByMeta<T extends { meta?: Record<string, string> }>(
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getRowKey<T extends { meta?: Record<string, string> }>(
|
||||
itemData: T,
|
||||
getItemIdentifier: () => string,
|
||||
groupBy: BaseAutocompleteData[],
|
||||
): string {
|
||||
const nodeIdentifier = getItemIdentifier();
|
||||
const meta = itemData.meta ?? {};
|
||||
|
||||
if (groupBy.length === 0) {
|
||||
return nodeIdentifier || JSON.stringify(meta);
|
||||
}
|
||||
|
||||
const groupedMeta = getGroupedByMeta(itemData, groupBy);
|
||||
const groupKey = Object.values(groupedMeta).join('-');
|
||||
|
||||
if (groupKey && nodeIdentifier) {
|
||||
return `${groupKey}-${nodeIdentifier}`;
|
||||
}
|
||||
if (groupKey) {
|
||||
return groupKey;
|
||||
}
|
||||
if (nodeIdentifier) {
|
||||
return nodeIdentifier;
|
||||
}
|
||||
|
||||
return JSON.stringify(meta);
|
||||
}
|
||||
|
||||
export function getGroupByEl<T extends { meta?: Record<string, string> }>(
|
||||
itemData: T,
|
||||
groupBy: IBuilderQuery['groupBy'],
|
||||
): React.ReactNode {
|
||||
export function getGroupByEl<
|
||||
T extends { meta?: Record<string, string> | null },
|
||||
>(itemData: T, groupBy: string[]): React.ReactNode {
|
||||
const groupByValues: string[] = [];
|
||||
const meta = itemData.meta ?? {};
|
||||
|
||||
groupBy.forEach((group) => {
|
||||
const rawKey = group.key as string;
|
||||
|
||||
// Choose mapped key if present, otherwise use rawKey
|
||||
groupBy.forEach((rawKey) => {
|
||||
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof meta;
|
||||
const value = meta[metaKey] || meta[rawKey] || '<no-value>';
|
||||
|
||||
@@ -98,3 +64,28 @@ export function getGroupByEl<T extends { meta?: Record<string, string> }>(
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function buildExpressionFromGroupMeta(
|
||||
parentExpression: string,
|
||||
groupMeta: Record<string, string> | undefined,
|
||||
): string {
|
||||
const items: TagFilterItem[] = Object.entries(groupMeta ?? {})
|
||||
.filter(([, value]) => value !== '' && value !== undefined && value !== null)
|
||||
.map(([key, value]) => ({
|
||||
key: { key, type: 'resource' },
|
||||
op: '=',
|
||||
value,
|
||||
id: key,
|
||||
}));
|
||||
|
||||
const metaExpression = convertFiltersToExpression({
|
||||
items,
|
||||
op: 'AND',
|
||||
}).expression;
|
||||
|
||||
const parent = parentExpression?.trim();
|
||||
if (parent && metaExpression) {
|
||||
return `${parent} AND ${metaExpression}`;
|
||||
}
|
||||
return parent || metaExpression;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { listClusters } from 'api/generated/services/inframonitoring';
|
||||
import {
|
||||
InframonitoringtypesClusterRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { getK8sClustersList, K8sClusterData } from './api';
|
||||
import {
|
||||
clusterWidgetInfo,
|
||||
getClusterMetricsQueryPayload,
|
||||
k8sClusterDetailsMetadataConfig,
|
||||
k8sClusterGetEntityName,
|
||||
k8sClusterGetSelectedItemFilters,
|
||||
k8sClusterInitialEventsFilter,
|
||||
k8sClusterInitialLogTracesFilter,
|
||||
k8sClusterGetSelectedItemExpression,
|
||||
k8sClusterInitialEventsExpression,
|
||||
k8sClusterInitialLogTracesExpression,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sClusterItemKey,
|
||||
@@ -28,66 +31,93 @@ function K8sClustersList({
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
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',
|
||||
};
|
||||
try {
|
||||
const response = await listClusters(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const response = await getK8sClustersList(
|
||||
filters,
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.payload?.data.records || [],
|
||||
total: response.payload?.data.total || 0,
|
||||
error: response.error,
|
||||
rawData: response.payload?.data,
|
||||
};
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch clusters';
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesClusterRecordDTO[],
|
||||
total: 0,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ data: K8sClusterData | null; error?: string | null }> => {
|
||||
const response = await getK8sClustersList(
|
||||
{
|
||||
filters: filters.filters,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
): Promise<{
|
||||
data: InframonitoringtypesClusterRecordDTO | null;
|
||||
error?: string | null;
|
||||
}> => {
|
||||
try {
|
||||
const response = await listClusters(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const records = response.payload?.data.records || [];
|
||||
|
||||
return {
|
||||
data: records.length > 0 ? records[0] : null,
|
||||
error: response.error,
|
||||
};
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch cluster';
|
||||
return {
|
||||
data: null,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<K8sClusterData>
|
||||
<K8sBaseList<InframonitoringtypesClusterRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.CLUSTERS}
|
||||
tableColumns={k8sClustersColumnsConfig}
|
||||
@@ -97,14 +127,14 @@ function K8sClustersList({
|
||||
eventCategory={InfraMonitoringEvents.Cluster}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails<K8sClusterData>
|
||||
<K8sBaseDetails<InframonitoringtypesClusterRecordDTO>
|
||||
category={InfraMonitoringEntity.CLUSTERS}
|
||||
eventCategory={InfraMonitoringEvents.Cluster}
|
||||
getSelectedItemFilters={k8sClusterGetSelectedItemFilters}
|
||||
getSelectedItemExpression={k8sClusterGetSelectedItemExpression}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sClusterGetEntityName}
|
||||
getInitialLogTracesFilters={k8sClusterInitialLogTracesFilter}
|
||||
getInitialEventsFilters={k8sClusterInitialEventsFilter}
|
||||
getInitialLogTracesExpression={k8sClusterInitialLogTracesExpression}
|
||||
getInitialEventsExpression={k8sClusterInitialEventsExpression}
|
||||
metadataConfig={k8sClusterDetailsMetadataConfig}
|
||||
entityWidgetInfo={clusterWidgetInfo}
|
||||
getEntityQueryPayload={getClusterMetricsQueryPayload}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
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/types';
|
||||
|
||||
export interface K8sClustersListPayload {
|
||||
filters: TagFilter;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sClusterData {
|
||||
clusterUID: string;
|
||||
cpuUsage: number;
|
||||
cpuAllocatable: number;
|
||||
memoryUsage: number;
|
||||
memoryAllocatable: number;
|
||||
meta: {
|
||||
k8s_cluster_name: string;
|
||||
k8s_cluster_uid: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sClustersListResponse {
|
||||
status: string;
|
||||
data: {
|
||||
type: string;
|
||||
records: K8sClusterData[];
|
||||
groups: null;
|
||||
total: number;
|
||||
sentAnyHostMetricsData: boolean;
|
||||
isSendingK8SAgentMetrics: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(H4ad): Erase this whole file when migrating to openapi
|
||||
export const clustersMetaMap = [
|
||||
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
|
||||
{ dot: 'k8s.cluster.uid', under: 'k8s_cluster_uid' },
|
||||
] as const;
|
||||
|
||||
export function mapClustersMeta(
|
||||
raw: Record<string, unknown>,
|
||||
): K8sClusterData['meta'] {
|
||||
const out: Record<string, unknown> = { ...raw };
|
||||
clustersMetaMap.forEach(({ dot, under }) => {
|
||||
if (dot in raw) {
|
||||
const v = raw[dot];
|
||||
out[under] = typeof v === 'string' ? v : raw[under];
|
||||
}
|
||||
});
|
||||
return out as K8sClusterData['meta'];
|
||||
}
|
||||
|
||||
export const getK8sClustersList = async (
|
||||
props: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
): Promise<SuccessResponse<K8sClustersListResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const requestProps = dotMetricsEnabled
|
||||
? {
|
||||
...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);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as typeof props.filters.items,
|
||||
),
|
||||
},
|
||||
}
|
||||
: props;
|
||||
|
||||
const response = await axios.post('/clusters/list', requestProps, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
const payload: K8sClustersListResponse = response.data;
|
||||
|
||||
payload.data.records = payload.data.records.map((record) => ({
|
||||
...record,
|
||||
meta: mapClustersMeta(record.meta as Record<string, unknown>),
|
||||
}));
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload,
|
||||
params: requestProps,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
@@ -1,53 +1,39 @@
|
||||
import { InframonitoringtypesClusterRecordDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
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 { K8sClusterData } from './api';
|
||||
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
|
||||
|
||||
export const k8sClusterGetSelectedItemFilters = (
|
||||
export const k8sClusterGetSelectedItemExpression = (
|
||||
selectedItemId: string,
|
||||
): TagFilter => ({
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: 'k8s_cluster_name',
|
||||
key: {
|
||||
key: 'k8s_cluster_name',
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: selectedItemId,
|
||||
},
|
||||
],
|
||||
});
|
||||
): string => `k8s.cluster.name = ${formatValueForExpression(selectedItemId)}`;
|
||||
|
||||
export const k8sClusterDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sClusterData>[] =
|
||||
[{ label: 'Cluster Name', getValue: (p): string => p.meta.k8s_cluster_name }];
|
||||
export const k8sClusterDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesClusterRecordDTO>[] =
|
||||
[{ label: 'Cluster Name', getValue: (p): string => p.clusterName || '' }];
|
||||
|
||||
export const k8sClusterInitialEventsFilter = (
|
||||
item: K8sClusterData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Cluster'),
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_cluster_name),
|
||||
];
|
||||
export const k8sClusterInitialEventsExpression = (
|
||||
item: InframonitoringtypesClusterRecordDTO,
|
||||
): string => {
|
||||
const objectName = formatValueForExpression(item.clusterName || '');
|
||||
return `${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'Cluster' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${objectName}`;
|
||||
};
|
||||
|
||||
export const k8sClusterInitialLogTracesFilter = (
|
||||
item: K8sClusterData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_CLUSTER_NAME, item.meta.k8s_cluster_name),
|
||||
];
|
||||
export const k8sClusterInitialLogTracesExpression = (
|
||||
item: InframonitoringtypesClusterRecordDTO,
|
||||
): string => {
|
||||
const clusterName = formatValueForExpression(item.clusterName || '');
|
||||
return `${INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME} = ${clusterName}`;
|
||||
};
|
||||
|
||||
export const k8sClusterGetEntityName = (item: K8sClusterData): string =>
|
||||
item.meta.k8s_cluster_name;
|
||||
export const k8sClusterGetEntityName = (
|
||||
item: InframonitoringtypesClusterRecordDTO,
|
||||
): string => item.clusterName || '';
|
||||
|
||||
export const clusterWidgetInfo = [
|
||||
{
|
||||
@@ -85,7 +71,7 @@ export const clusterWidgetInfo = [
|
||||
];
|
||||
|
||||
export const getClusterMetricsQueryPayload = (
|
||||
cluster: K8sClusterData,
|
||||
cluster: InframonitoringtypesClusterRecordDTO,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
@@ -207,7 +193,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -246,7 +232,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -285,7 +271,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -324,7 +310,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -397,7 +383,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -436,7 +422,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -475,7 +461,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -514,7 +500,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -587,7 +573,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -673,7 +659,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -759,7 +745,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -811,7 +797,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -909,7 +895,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -961,7 +947,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1013,7 +999,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1065,7 +1051,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1187,7 +1173,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1233,7 +1219,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1279,7 +1265,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1383,7 +1369,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1435,7 +1421,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1487,7 +1473,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1539,7 +1525,7 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: cluster.meta.k8s_cluster_name,
|
||||
value: cluster.clusterName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
|
||||
@@ -1,42 +1,48 @@
|
||||
import { Tooltip } from 'antd';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Boxes } from '@signozhq/icons';
|
||||
import { InframonitoringtypesClusterRecordDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
|
||||
|
||||
import ColumnHeader from '../Base/ColumnHeader';
|
||||
import EntityGroupHeader from '../Base/EntityGroupHeader';
|
||||
import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { formatBytes } from '../commonUtils';
|
||||
import { ValidateColumnValueWrapper } from '../components';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { K8sClusterData, K8sClustersListPayload } from './api';
|
||||
import { Boxes } from '@signozhq/icons';
|
||||
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
|
||||
import {
|
||||
CellValueTooltip,
|
||||
GroupedStatusCounts,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../components';
|
||||
import {
|
||||
INFRA_MONITORING_ATTR_KEYS,
|
||||
InfraMonitoringEntity,
|
||||
} from '../constants';
|
||||
|
||||
export function getK8sClusterRowKey(cluster: K8sClusterData): string {
|
||||
export function getK8sClusterRowKey(
|
||||
cluster: InframonitoringtypesClusterRecordDTO,
|
||||
): string {
|
||||
return (
|
||||
cluster.clusterUID ||
|
||||
cluster.meta.k8s_cluster_uid ||
|
||||
cluster.meta.k8s_cluster_name
|
||||
cluster.clusterName ||
|
||||
cluster.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_UID] ||
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
export function getK8sClusterItemKey(cluster: K8sClusterData): string {
|
||||
return cluster.meta.k8s_cluster_name;
|
||||
export function getK8sClusterItemKey(
|
||||
cluster: InframonitoringtypesClusterRecordDTO,
|
||||
): string {
|
||||
return cluster.clusterName;
|
||||
}
|
||||
|
||||
export const getK8sClustersListQuery = (): K8sClustersListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
export const k8sClustersColumnsConfig: TableColumnDef<K8sClusterData>[] = [
|
||||
export type ClusterTableColumnConfig =
|
||||
TableColumnDef<InframonitoringtypesClusterRecordDTO>;
|
||||
export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
|
||||
{
|
||||
id: 'clusterGroup',
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="CLUSTER GROUP" />,
|
||||
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
|
||||
width: { min: 300 },
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="Cluster Group" />,
|
||||
accessorFn: (row): string => row.clusterName || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
@@ -59,9 +65,10 @@ export const k8sClustersColumnsConfig: TableColumnDef<K8sClusterData>[] = [
|
||||
<EntityGroupHeader
|
||||
title="Cluster Name"
|
||||
icon={<Boxes data-hide-expanded="true" size={14} />}
|
||||
docPath="/infrastructure-monitoring/kubernetes/clusters#cluster-name"
|
||||
/>
|
||||
),
|
||||
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
|
||||
accessorFn: (row): string => row.clusterName || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
@@ -71,55 +78,127 @@ export const k8sClustersColumnsConfig: TableColumnDef<K8sClusterData>[] = [
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const clusterName = value as string;
|
||||
return (
|
||||
<Tooltip title={clusterName}>
|
||||
<CellValueTooltip value={clusterName}>
|
||||
<TanStackTable.Text>{clusterName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
</CellValueTooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'nodeCountsByReadiness',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#node-readiness">
|
||||
Node Readiness
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (
|
||||
row,
|
||||
): InframonitoringtypesClusterRecordDTO['nodeCountsByReadiness'] =>
|
||||
row.nodeCountsByReadiness,
|
||||
width: { min: 180 },
|
||||
enableSort: false,
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
if (!row.nodeCountsByReadiness) {
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<GroupedStatusCounts
|
||||
items={[
|
||||
{
|
||||
value: row.nodeCountsByReadiness.ready,
|
||||
label: 'Ready',
|
||||
color: Color.BG_FOREST_500,
|
||||
},
|
||||
{
|
||||
value: row.nodeCountsByReadiness.notReady,
|
||||
label: 'Not Ready',
|
||||
color: Color.BG_AMBER_500,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'podCountsByPhase',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#pod-counts-by-phase">
|
||||
Pod Phases
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): InframonitoringtypesClusterRecordDTO['podCountsByPhase'] =>
|
||||
row.podCountsByPhase,
|
||||
width: { min: 250 },
|
||||
enableSort: false,
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
const podCountsByPhase = row.podCountsByPhase;
|
||||
if (!podCountsByPhase) {
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<GroupedStatusCounts items={getPodPhaseStatusItems(row.podCountsByPhase)} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cpu',
|
||||
header: 'CPU Usage (cores)',
|
||||
accessorFn: (row): number => row.cpuUsage,
|
||||
width: { min: 220 },
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#cpu-usage-cores">
|
||||
CPU Usage
|
||||
<br /> (cores)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.clusterCPU,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = value as number;
|
||||
const cpu = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpu}
|
||||
entity={InfraMonitoringEntity.CLUSTERS}
|
||||
attribute="CPU metric"
|
||||
>
|
||||
<TanStackTable.Text>{cpu}</TanStackTable.Text>
|
||||
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cpu_allocatable',
|
||||
header: 'CPU Alloc (cores)',
|
||||
accessorFn: (row): number => row.cpuAllocatable,
|
||||
width: { min: 220 },
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#cpu-alloc-cores">
|
||||
CPU Allocatable
|
||||
<br /> (cores)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.clusterCPUAllocatable,
|
||||
width: { min: 200 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuAllocatable = value as number;
|
||||
const cpuAllocatable = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpuAllocatable}
|
||||
entity={InfraMonitoringEntity.CLUSTERS}
|
||||
attribute="CPU allocatable metric"
|
||||
>
|
||||
<TanStackTable.Text>{cpuAllocatable}</TanStackTable.Text>
|
||||
<TanStackTable.Text>{cpuAllocatable.toFixed(2)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory',
|
||||
header: 'Memory Usage (WSS)',
|
||||
accessorFn: (row): number => row.memoryUsage,
|
||||
width: { min: 220 },
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#memory-usage-wss">
|
||||
Memory Usage
|
||||
<br /> (WSS)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.clusterMemory,
|
||||
width: { min: 180 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
@@ -136,9 +215,14 @@ export const k8sClustersColumnsConfig: TableColumnDef<K8sClusterData>[] = [
|
||||
},
|
||||
{
|
||||
id: 'memory_allocatable',
|
||||
header: 'Memory Allocatable',
|
||||
accessorFn: (row): number => row.memoryAllocatable,
|
||||
width: { min: 220 },
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#memory-allocatable">
|
||||
Memory
|
||||
<br /> Allocatable
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.clusterMemoryAllocatable,
|
||||
width: { min: 180 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryAllocatable = value as number;
|
||||
|
||||
@@ -1,93 +1,117 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { listDaemonSets } from 'api/generated/services/inframonitoring';
|
||||
import {
|
||||
InframonitoringtypesDaemonSetRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { getK8sDaemonSetsList, K8sDaemonSetsData } from './api';
|
||||
import {
|
||||
daemonSetWidgetInfo,
|
||||
getDaemonSetMetricsQueryPayload,
|
||||
k8sDaemonSetDetailsMetadataConfig,
|
||||
k8sDaemonSetGetEntityName,
|
||||
k8sDaemonSetGetSelectedItemFilters,
|
||||
k8sDaemonSetInitialEventsFilter,
|
||||
k8sDaemonSetInitialLogTracesFilter,
|
||||
k8sDaemonSetGetSelectedItemExpression,
|
||||
k8sDaemonSetInitialEventsExpression,
|
||||
k8sDaemonSetInitialLogTracesExpression,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sDaemonSetItemKey,
|
||||
getK8sDaemonSetRowKey,
|
||||
k8sDaemonSetsColumnsConfig,
|
||||
} from './table.config';
|
||||
|
||||
function K8sDaemonSetsList({
|
||||
controlListPrefix,
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
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 response = await getK8sDaemonSetsList(
|
||||
filters,
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.payload?.data.records || [],
|
||||
total: response.payload?.data.total || 0,
|
||||
error: response.error,
|
||||
rawData: response.payload?.data,
|
||||
};
|
||||
try {
|
||||
const response = await listDaemonSets(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch daemonsets';
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesDaemonSetRecordDTO[],
|
||||
total: 0,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ data: K8sDaemonSetsData | null; error?: string | null }> => {
|
||||
const response = await getK8sDaemonSetsList(
|
||||
{
|
||||
filters: filters.filters,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
const records = response.payload?.data.records || [];
|
||||
|
||||
return {
|
||||
data: records.length > 0 ? records[0] : null,
|
||||
error: response.error,
|
||||
};
|
||||
): Promise<{
|
||||
data: InframonitoringtypesDaemonSetRecordDTO | null;
|
||||
error?: string | null;
|
||||
}> => {
|
||||
try {
|
||||
const response = await listDaemonSets(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch daemonset';
|
||||
return {
|
||||
data: null,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<K8sDaemonSetsData>
|
||||
<K8sBaseList<InframonitoringtypesDaemonSetRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
tableColumns={k8sDaemonSetsColumnsConfig}
|
||||
@@ -96,15 +120,14 @@ function K8sDaemonSetsList({
|
||||
getItemKey={getK8sDaemonSetItemKey}
|
||||
eventCategory={InfraMonitoringEvents.DaemonSet}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails<K8sDaemonSetsData>
|
||||
<K8sBaseDetails<InframonitoringtypesDaemonSetRecordDTO>
|
||||
category={InfraMonitoringEntity.DAEMONSETS}
|
||||
eventCategory={InfraMonitoringEvents.DaemonSet}
|
||||
getSelectedItemFilters={k8sDaemonSetGetSelectedItemFilters}
|
||||
getSelectedItemExpression={k8sDaemonSetGetSelectedItemExpression}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sDaemonSetGetEntityName}
|
||||
getInitialLogTracesFilters={k8sDaemonSetInitialLogTracesFilter}
|
||||
getInitialEventsFilters={k8sDaemonSetInitialEventsFilter}
|
||||
getInitialLogTracesExpression={k8sDaemonSetInitialLogTracesExpression}
|
||||
getInitialEventsExpression={k8sDaemonSetInitialEventsExpression}
|
||||
metadataConfig={k8sDaemonSetDetailsMetadataConfig}
|
||||
entityWidgetInfo={daemonSetWidgetInfo}
|
||||
getEntityQueryPayload={getDaemonSetMetricsQueryPayload}
|
||||
@@ -113,5 +136,4 @@ function K8sDaemonSetsList({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sDaemonSetsList;
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
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 { K8sBaseFilters } from '../Base/types';
|
||||
|
||||
export interface K8sDaemonSetsData {
|
||||
daemonSetName: string;
|
||||
cpuUsage: number;
|
||||
memoryUsage: number;
|
||||
cpuRequest: number;
|
||||
memoryRequest: number;
|
||||
cpuLimit: number;
|
||||
memoryLimit: number;
|
||||
restarts: number;
|
||||
desiredNodes: number;
|
||||
availableNodes: number;
|
||||
meta: {
|
||||
k8s_cluster_name: string;
|
||||
k8s_daemonset_name: string;
|
||||
k8s_namespace_name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sDaemonSetsListResponse {
|
||||
status: string;
|
||||
data: {
|
||||
type: string;
|
||||
records: K8sDaemonSetsData[];
|
||||
groups: null;
|
||||
total: number;
|
||||
sentAnyHostMetricsData: boolean;
|
||||
isSendingK8SAgentMetrics: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(H4ad): Erase this whole file when migrating to openapi
|
||||
export const daemonSetsMetaMap = [
|
||||
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
|
||||
{ dot: 'k8s.daemonset.name', under: 'k8s_daemonset_name' },
|
||||
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
|
||||
] as const;
|
||||
|
||||
export function mapDaemonSetsMeta(
|
||||
raw: Record<string, unknown>,
|
||||
): K8sDaemonSetsData['meta'] {
|
||||
const out: Record<string, unknown> = { ...raw };
|
||||
daemonSetsMetaMap.forEach(({ dot, under }) => {
|
||||
if (dot in raw) {
|
||||
const v = raw[dot];
|
||||
out[under] = typeof v === 'string' ? v : raw[under];
|
||||
}
|
||||
});
|
||||
return out as K8sDaemonSetsData['meta'];
|
||||
}
|
||||
|
||||
export const getK8sDaemonSetsList = async (
|
||||
props: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
): Promise<SuccessResponse<K8sDaemonSetsListResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const requestProps = dotMetricsEnabled
|
||||
? {
|
||||
...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);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as typeof props.filters.items,
|
||||
),
|
||||
},
|
||||
}
|
||||
: props;
|
||||
|
||||
const response = await axios.post('/daemonsets/list', requestProps, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
const payload: K8sDaemonSetsListResponse = response.data;
|
||||
|
||||
payload.data.records = payload.data.records.map((record) => ({
|
||||
...record,
|
||||
meta: mapDaemonSetsMeta(record.meta as Record<string, unknown>),
|
||||
}));
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload,
|
||||
params: requestProps,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
@@ -1,67 +1,52 @@
|
||||
import { InframonitoringtypesDaemonSetRecordDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
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 { K8sDaemonSetsData } from './api';
|
||||
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
|
||||
|
||||
export const k8sDaemonSetGetSelectedItemFilters = (
|
||||
export const k8sDaemonSetGetSelectedItemExpression = (
|
||||
selectedItemId: string,
|
||||
): TagFilter => ({
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: 'k8s_daemonset_name',
|
||||
key: {
|
||||
key: 'k8s_daemonset_name',
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: selectedItemId,
|
||||
},
|
||||
],
|
||||
});
|
||||
): string =>
|
||||
`${INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME} = ${formatValueForExpression(selectedItemId)}`;
|
||||
|
||||
export const k8sDaemonSetDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sDaemonSetsData>[] =
|
||||
export const k8sDaemonSetDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesDaemonSetRecordDTO>[] =
|
||||
[
|
||||
{
|
||||
label: 'Daemonset Name',
|
||||
getValue: (p): string => p.meta.k8s_daemonset_name,
|
||||
getValue: (p): string =>
|
||||
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? '',
|
||||
},
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
getValue: (p): string => p.meta.k8s_cluster_name,
|
||||
getValue: (p): string =>
|
||||
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
getValue: (p): string => p.meta.k8s_namespace_name,
|
||||
getValue: (p): string =>
|
||||
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sDaemonSetInitialEventsFilter = (
|
||||
item: K8sDaemonSetsData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'DaemonSet'),
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_daemonset_name),
|
||||
];
|
||||
export const k8sDaemonSetInitialEventsExpression = (
|
||||
item: InframonitoringtypesDaemonSetRecordDTO,
|
||||
): string =>
|
||||
`${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'DaemonSet' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${formatValueForExpression(item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? '')}`;
|
||||
|
||||
export const k8sDaemonSetInitialLogTracesFilter = (
|
||||
item: K8sDaemonSetsData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_DAEMON_SET_NAME, item.meta.k8s_daemonset_name),
|
||||
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.k8s_namespace_name),
|
||||
];
|
||||
export const k8sDaemonSetInitialLogTracesExpression = (
|
||||
item: InframonitoringtypesDaemonSetRecordDTO,
|
||||
): string =>
|
||||
`${INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME} = ${formatValueForExpression(item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? '')} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${formatValueForExpression(item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '')}`;
|
||||
|
||||
export const k8sDaemonSetGetEntityName = (item: K8sDaemonSetsData): string =>
|
||||
item.meta.k8s_daemonset_name;
|
||||
export const k8sDaemonSetGetEntityName = (
|
||||
item: InframonitoringtypesDaemonSetRecordDTO,
|
||||
): string => item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? '';
|
||||
|
||||
export const daemonSetWidgetInfo = [
|
||||
{
|
||||
@@ -83,7 +68,7 @@ export const daemonSetWidgetInfo = [
|
||||
];
|
||||
|
||||
export const getDaemonSetMetricsQueryPayload = (
|
||||
daemonSet: K8sDaemonSetsData,
|
||||
daemonSet: InframonitoringtypesDaemonSetRecordDTO,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
@@ -159,7 +144,9 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: daemonSet.meta.k8s_daemonset_name,
|
||||
value:
|
||||
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
|
||||
'',
|
||||
},
|
||||
{
|
||||
id: '47b3adae',
|
||||
@@ -170,7 +157,9 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: daemonSet.meta.k8s_namespace_name,
|
||||
value:
|
||||
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -209,7 +198,9 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'contains',
|
||||
value: daemonSet.meta.k8s_daemonset_name,
|
||||
value:
|
||||
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
|
||||
'',
|
||||
},
|
||||
{
|
||||
id: '47b3adae',
|
||||
@@ -220,7 +211,9 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: daemonSet.meta.k8s_namespace_name,
|
||||
value:
|
||||
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -259,7 +252,9 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'contains',
|
||||
value: daemonSet.meta.k8s_daemonset_name,
|
||||
value:
|
||||
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
|
||||
'',
|
||||
},
|
||||
{
|
||||
id: '47b3adae',
|
||||
@@ -270,7 +265,9 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: daemonSet.meta.k8s_namespace_name,
|
||||
value:
|
||||
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -343,7 +340,9 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: daemonSet.meta.k8s_daemonset_name,
|
||||
value:
|
||||
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
|
||||
'',
|
||||
},
|
||||
{
|
||||
id: '47b3adae',
|
||||
@@ -354,7 +353,9 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: daemonSet.meta.k8s_namespace_name,
|
||||
value:
|
||||
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -393,7 +394,9 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'contains',
|
||||
value: daemonSet.meta.k8s_daemonset_name,
|
||||
value:
|
||||
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
|
||||
'',
|
||||
},
|
||||
{
|
||||
id: '47b3adae',
|
||||
@@ -404,7 +407,9 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: daemonSet.meta.k8s_namespace_name,
|
||||
value:
|
||||
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -443,7 +448,9 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'contains',
|
||||
value: daemonSet.meta.k8s_daemonset_name,
|
||||
value:
|
||||
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
|
||||
'',
|
||||
},
|
||||
{
|
||||
id: '47b3adae',
|
||||
@@ -454,7 +461,9 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: daemonSet.meta.k8s_namespace_name,
|
||||
value:
|
||||
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -527,7 +536,9 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: daemonSet.meta.k8s_daemonset_name,
|
||||
value:
|
||||
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
|
||||
'',
|
||||
},
|
||||
{
|
||||
id: '47b3adae',
|
||||
@@ -538,7 +549,9 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: daemonSet.meta.k8s_namespace_name,
|
||||
value:
|
||||
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -624,7 +637,9 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: daemonSet.meta.k8s_daemonset_name,
|
||||
value:
|
||||
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
|
||||
'',
|
||||
},
|
||||
{
|
||||
id: '47b3adae',
|
||||
@@ -635,7 +650,9 @@ export const getDaemonSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: daemonSet.meta.k8s_namespace_name,
|
||||
value:
|
||||
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
|
||||
@@ -1,34 +1,49 @@
|
||||
import { Tooltip } from 'antd';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { InframonitoringtypesDaemonSetRecordDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
|
||||
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
|
||||
|
||||
import ColumnHeader from '../Base/ColumnHeader';
|
||||
import EntityGroupHeader from '../Base/EntityGroupHeader';
|
||||
import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { formatBytes } from '../commonUtils';
|
||||
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { K8sDaemonSetsData } from './api';
|
||||
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
|
||||
import {
|
||||
CellValueTooltip,
|
||||
EntityProgressBar,
|
||||
GroupedStatusCounts,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../components';
|
||||
import {
|
||||
INFRA_MONITORING_ATTR_KEYS,
|
||||
InfraMonitoringEntity,
|
||||
} from '../constants';
|
||||
import { Group } from '@signozhq/icons';
|
||||
|
||||
export function getK8sDaemonSetRowKey(daemonSet: K8sDaemonSetsData): string {
|
||||
export function getK8sDaemonSetRowKey(
|
||||
daemonSet: InframonitoringtypesDaemonSetRecordDTO,
|
||||
): string {
|
||||
return (
|
||||
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ||
|
||||
daemonSet.daemonSetName ||
|
||||
daemonSet.meta.k8s_daemonset_name ||
|
||||
`${daemonSet.meta.k8s_namespace_name}-${daemonSet.meta.k8s_daemonset_name}`
|
||||
`${daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME]}-${daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME]}`
|
||||
);
|
||||
}
|
||||
|
||||
export function getK8sDaemonSetItemKey(daemonSet: K8sDaemonSetsData): string {
|
||||
return daemonSet.meta.k8s_daemonset_name;
|
||||
export function getK8sDaemonSetItemKey(
|
||||
daemonSet: InframonitoringtypesDaemonSetRecordDTO,
|
||||
): string {
|
||||
return daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] || '';
|
||||
}
|
||||
|
||||
export const k8sDaemonSetsColumnsConfig: TableColumnDef<K8sDaemonSetsData>[] = [
|
||||
export type DaemonSetTableColumnConfig =
|
||||
TableColumnDef<InframonitoringtypesDaemonSetRecordDTO>;
|
||||
export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
{
|
||||
id: 'daemonSetGroup',
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="DAEMONSET GROUP" />,
|
||||
accessorFn: (row): string => row.meta.k8s_daemonset_name || '',
|
||||
width: { min: 300 },
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="DaemonSet Group" />,
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
@@ -51,9 +66,11 @@ export const k8sDaemonSetsColumnsConfig: TableColumnDef<K8sDaemonSetsData>[] = [
|
||||
<EntityGroupHeader
|
||||
title="DaemonSet Name"
|
||||
icon={<Group data-hide-expanded="true" size={14} />}
|
||||
docPath="/infrastructure-monitoring/kubernetes/daemonsets#daemonset-name"
|
||||
/>
|
||||
),
|
||||
accessorFn: (row): string => row.meta.k8s_daemonset_name || '',
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
@@ -63,71 +80,98 @@ export const k8sDaemonSetsColumnsConfig: TableColumnDef<K8sDaemonSetsData>[] = [
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const daemonsetName = value as string;
|
||||
return (
|
||||
<Tooltip title={daemonsetName}>
|
||||
<CellValueTooltip value={daemonsetName}>
|
||||
<TanStackTable.Text>{daemonsetName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
</CellValueTooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'namespaceName',
|
||||
header: 'Namespace Name',
|
||||
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
|
||||
width: { default: 100 },
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#namespace-name">
|
||||
Namespace
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
width: { min: 160 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const namespaceName = value as string;
|
||||
return (
|
||||
<Tooltip title={namespaceName}>
|
||||
<CellValueTooltip value={namespaceName}>
|
||||
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
</CellValueTooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'available_nodes',
|
||||
header: 'Available',
|
||||
accessorFn: (row): number => row.availableNodes,
|
||||
width: { min: 140 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const availableNodes = value as number;
|
||||
id: 'pod_counts_by_phase',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#pod-counts-by-phase">
|
||||
Pod Phases
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (
|
||||
row,
|
||||
): InframonitoringtypesDaemonSetRecordDTO['podCountsByPhase'] =>
|
||||
row.podCountsByPhase,
|
||||
width: { min: 250 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
const podCountsByPhase = row.podCountsByPhase;
|
||||
if (!podCountsByPhase) {
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={availableNodes}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="available node"
|
||||
>
|
||||
<TanStackTable.Text>{availableNodes}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'desired_nodes',
|
||||
header: 'Desired',
|
||||
accessorFn: (row): number => row.desiredNodes,
|
||||
width: { min: 140 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const desiredNodes = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={desiredNodes}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="desired node"
|
||||
>
|
||||
<TanStackTable.Text>{desiredNodes}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
id: 'node_status',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#node-status">
|
||||
Node Status
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.currentNodes,
|
||||
width: { min: 180 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ row }): React.ReactNode => (
|
||||
<GroupedStatusCounts
|
||||
items={[
|
||||
{
|
||||
value: row.currentNodes,
|
||||
label: 'Current',
|
||||
color: Color.BG_FOREST_500,
|
||||
},
|
||||
{
|
||||
value: row.desiredNodes,
|
||||
label: 'Desired',
|
||||
color: Color.BG_ROBIN_500,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'cpu_request',
|
||||
header: 'CPU Req Usage (%)',
|
||||
accessorFn: (row): number => row.cpuRequest,
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-req-usage-">
|
||||
CPU Request
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.daemonSetCPURequest,
|
||||
width: { min: 200, default: 200 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuRequest = value as number;
|
||||
return (
|
||||
@@ -143,10 +187,16 @@ export const k8sDaemonSetsColumnsConfig: TableColumnDef<K8sDaemonSetsData>[] = [
|
||||
},
|
||||
{
|
||||
id: 'cpu_limit',
|
||||
header: 'CPU Limit Usage (%)',
|
||||
accessorFn: (row): number => row.cpuLimit,
|
||||
width: { min: 200, default: 200 },
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-limit-usage-">
|
||||
CPU Limit
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.daemonSetCPULimit,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuLimit = value as number;
|
||||
return (
|
||||
@@ -162,30 +212,42 @@ export const k8sDaemonSetsColumnsConfig: TableColumnDef<K8sDaemonSetsData>[] = [
|
||||
},
|
||||
{
|
||||
id: 'cpu',
|
||||
header: 'CPU Usage (cores)',
|
||||
accessorFn: (row): number => row.cpuUsage,
|
||||
width: { min: 190 },
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-usage-cores">
|
||||
CPU Usage
|
||||
<br /> (cores)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.daemonSetCPU,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = value as number;
|
||||
const cpu = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpu}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="CPU metric"
|
||||
>
|
||||
<TanStackTable.Text>{cpu}</TanStackTable.Text>
|
||||
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory_request',
|
||||
header: 'Mem Req Usage (%)',
|
||||
accessorFn: (row): number => row.memoryRequest,
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-req-usage-">
|
||||
Memory Request
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.daemonSetMemoryRequest,
|
||||
width: { min: 190 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryRequest = value as number;
|
||||
@@ -202,10 +264,16 @@ export const k8sDaemonSetsColumnsConfig: TableColumnDef<K8sDaemonSetsData>[] = [
|
||||
},
|
||||
{
|
||||
id: 'memory_limit',
|
||||
header: 'Mem Limit Usage (%)',
|
||||
accessorFn: (row): number => row.memoryLimit,
|
||||
width: { min: 180 },
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-limit-usage-">
|
||||
Memory Limit
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.daemonSetMemoryLimit,
|
||||
width: { min: 190 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryLimit = value as number;
|
||||
return (
|
||||
@@ -221,10 +289,16 @@ export const k8sDaemonSetsColumnsConfig: TableColumnDef<K8sDaemonSetsData>[] = [
|
||||
},
|
||||
{
|
||||
id: 'memory',
|
||||
header: 'Mem Usage (WSS)',
|
||||
accessorFn: (row): number => row.memoryUsage,
|
||||
width: { min: 160 },
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-usage-wss">
|
||||
Memory Usage
|
||||
<br /> (WSS)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.daemonSetMemory,
|
||||
width: { min: 180 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
return (
|
||||
@@ -238,4 +312,52 @@ export const k8sDaemonSetsColumnsConfig: TableColumnDef<K8sDaemonSetsData>[] = [
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'current_nodes',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#current">
|
||||
Current Nodes
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.currentNodes,
|
||||
width: { min: 140 },
|
||||
enableSort: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const currentNodes = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={currentNodes}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="current node"
|
||||
>
|
||||
<TanStackTable.Text>{currentNodes}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'desired_nodes',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#desired">
|
||||
Desired Nodes
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.desiredNodes,
|
||||
width: { min: 140 },
|
||||
enableSort: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const desiredNodes = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={desiredNodes}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="desired node"
|
||||
>
|
||||
<TanStackTable.Text>{desiredNodes}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { listDeployments } from 'api/generated/services/inframonitoring';
|
||||
import {
|
||||
InframonitoringtypesDeploymentRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { getK8sDeploymentsList, K8sDeploymentsData } from './api';
|
||||
import {
|
||||
deploymentWidgetInfo,
|
||||
getDeploymentMetricsQueryPayload,
|
||||
k8sDeploymentDetailsMetadataConfig,
|
||||
k8sDeploymentGetEntityName,
|
||||
k8sDeploymentGetSelectedItemFilters,
|
||||
k8sDeploymentInitialEventsFilter,
|
||||
k8sDeploymentInitialLogTracesFilter,
|
||||
k8sDeploymentGetSelectedItemExpression,
|
||||
k8sDeploymentInitialEventsExpression,
|
||||
k8sDeploymentInitialLogTracesExpression,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sDeploymentItemKey,
|
||||
@@ -28,66 +31,93 @@ function K8sDeploymentsList({
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
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',
|
||||
};
|
||||
try {
|
||||
const response = await listDeployments(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const response = await getK8sDeploymentsList(
|
||||
filters,
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.payload?.data.records || [],
|
||||
total: response.payload?.data.total || 0,
|
||||
error: response.error,
|
||||
rawData: response.payload?.data,
|
||||
};
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch deployments';
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesDeploymentRecordDTO[],
|
||||
total: 0,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ data: K8sDeploymentsData | null; error?: string | null }> => {
|
||||
const response = await getK8sDeploymentsList(
|
||||
{
|
||||
filters: filters.filters,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
): Promise<{
|
||||
data: InframonitoringtypesDeploymentRecordDTO | null;
|
||||
error?: string | null;
|
||||
}> => {
|
||||
try {
|
||||
const response = await listDeployments(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const records = response.payload?.data.records || [];
|
||||
|
||||
return {
|
||||
data: records.length > 0 ? records[0] : null,
|
||||
error: response.error,
|
||||
};
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch deployment';
|
||||
return {
|
||||
data: null,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<K8sDeploymentsData>
|
||||
<K8sBaseList<InframonitoringtypesDeploymentRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
tableColumns={k8sDeploymentsColumnsConfig}
|
||||
@@ -97,14 +127,14 @@ function K8sDeploymentsList({
|
||||
eventCategory={InfraMonitoringEvents.Deployment}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails<K8sDeploymentsData>
|
||||
<K8sBaseDetails<InframonitoringtypesDeploymentRecordDTO>
|
||||
category={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
eventCategory={InfraMonitoringEvents.Deployment}
|
||||
getSelectedItemFilters={k8sDeploymentGetSelectedItemFilters}
|
||||
getSelectedItemExpression={k8sDeploymentGetSelectedItemExpression}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sDeploymentGetEntityName}
|
||||
getInitialLogTracesFilters={k8sDeploymentInitialLogTracesFilter}
|
||||
getInitialEventsFilters={k8sDeploymentInitialEventsFilter}
|
||||
getInitialLogTracesExpression={k8sDeploymentInitialLogTracesExpression}
|
||||
getInitialEventsExpression={k8sDeploymentInitialEventsExpression}
|
||||
metadataConfig={k8sDeploymentDetailsMetadataConfig}
|
||||
entityWidgetInfo={deploymentWidgetInfo}
|
||||
getEntityQueryPayload={getDeploymentMetricsQueryPayload}
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
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/types';
|
||||
|
||||
export interface K8sDeploymentsListPayload {
|
||||
filters: TagFilter;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sDeploymentsData {
|
||||
deploymentName: string;
|
||||
cpuUsage: number;
|
||||
memoryUsage: number;
|
||||
desiredPods: number;
|
||||
availablePods: number;
|
||||
cpuRequest: number;
|
||||
memoryRequest: number;
|
||||
cpuLimit: number;
|
||||
memoryLimit: number;
|
||||
restarts: number;
|
||||
meta: {
|
||||
k8s_cluster_name: string;
|
||||
k8s_deployment_name: string;
|
||||
k8s_namespace_name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sDeploymentsListResponse {
|
||||
status: string;
|
||||
data: {
|
||||
type: string;
|
||||
records: K8sDeploymentsData[];
|
||||
groups: null;
|
||||
total: number;
|
||||
sentAnyHostMetricsData: boolean;
|
||||
isSendingK8SAgentMetrics: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(H4ad): Erase this whole file when migrating to openapi
|
||||
export const deploymentsMetaMap = [
|
||||
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
|
||||
{ dot: 'k8s.deployment.name', under: 'k8s_deployment_name' },
|
||||
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
|
||||
] as const;
|
||||
|
||||
export function mapDeploymentsMeta(
|
||||
raw: Record<string, unknown>,
|
||||
): K8sDeploymentsData['meta'] {
|
||||
const out: Record<string, unknown> = { ...raw };
|
||||
deploymentsMetaMap.forEach(({ dot, under }) => {
|
||||
if (dot in raw) {
|
||||
const v = raw[dot];
|
||||
out[under] = typeof v === 'string' ? v : raw[under];
|
||||
}
|
||||
});
|
||||
return out as K8sDeploymentsData['meta'];
|
||||
}
|
||||
|
||||
export const getK8sDeploymentsList = async (
|
||||
props: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
): Promise<SuccessResponse<K8sDeploymentsListResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const requestProps = dotMetricsEnabled
|
||||
? {
|
||||
...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);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as typeof props.filters.items,
|
||||
),
|
||||
},
|
||||
}
|
||||
: props;
|
||||
|
||||
const response = await axios.post('/deployments/list', requestProps, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
const payload: K8sDeploymentsListResponse = response.data;
|
||||
|
||||
payload.data.records = payload.data.records.map((record) => ({
|
||||
...record,
|
||||
meta: mapDeploymentsMeta(record.meta as Record<string, unknown>),
|
||||
}));
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload,
|
||||
params: requestProps,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
@@ -1,70 +1,63 @@
|
||||
import { InframonitoringtypesDeploymentRecordDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
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 { K8sDeploymentsData } from './api';
|
||||
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
|
||||
|
||||
export const k8sDeploymentGetSelectedItemFilters = (
|
||||
export const k8sDeploymentGetSelectedItemExpression = (
|
||||
selectedItemId: string,
|
||||
): TagFilter => ({
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: 'k8s_deployment_name',
|
||||
key: {
|
||||
key: 'k8s_deployment_name',
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: selectedItemId,
|
||||
},
|
||||
],
|
||||
});
|
||||
): string =>
|
||||
`${INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME} = ${formatValueForExpression(selectedItemId)}`;
|
||||
|
||||
export const k8sDeploymentDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sDeploymentsData>[] =
|
||||
export const k8sDeploymentDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesDeploymentRecordDTO>[] =
|
||||
[
|
||||
{
|
||||
label: 'Deployment Name',
|
||||
getValue: (p): string => p.meta.k8s_deployment_name,
|
||||
getValue: (p): string =>
|
||||
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? '',
|
||||
},
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
getValue: (p): string => p.meta.k8s_cluster_name,
|
||||
getValue: (p): string =>
|
||||
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
getValue: (p): string => p.meta.k8s_namespace_name,
|
||||
getValue: (p): string =>
|
||||
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sDeploymentInitialEventsFilter = (
|
||||
item: K8sDeploymentsData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Deployment'),
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_deployment_name),
|
||||
];
|
||||
export const k8sDeploymentInitialEventsExpression = (
|
||||
item: InframonitoringtypesDeploymentRecordDTO,
|
||||
): string => {
|
||||
const objectName = formatValueForExpression(
|
||||
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? '',
|
||||
);
|
||||
return `${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'Deployment' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${objectName}`;
|
||||
};
|
||||
|
||||
export const k8sDeploymentInitialLogTracesFilter = (
|
||||
item: K8sDeploymentsData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(
|
||||
QUERY_KEYS.K8S_DEPLOYMENT_NAME,
|
||||
item.meta.k8s_deployment_name,
|
||||
),
|
||||
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.k8s_namespace_name),
|
||||
];
|
||||
export const k8sDeploymentInitialLogTracesExpression = (
|
||||
item: InframonitoringtypesDeploymentRecordDTO,
|
||||
): string => {
|
||||
const deploymentName = formatValueForExpression(
|
||||
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? '',
|
||||
);
|
||||
const namespaceName = formatValueForExpression(
|
||||
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
|
||||
);
|
||||
return `${INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME} = ${deploymentName} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${namespaceName}`;
|
||||
};
|
||||
|
||||
export const k8sDeploymentGetEntityName = (item: K8sDeploymentsData): string =>
|
||||
item.meta.k8s_deployment_name;
|
||||
export const k8sDeploymentGetEntityName = (
|
||||
item: InframonitoringtypesDeploymentRecordDTO,
|
||||
): string => item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? '';
|
||||
|
||||
export const deploymentWidgetInfo = [
|
||||
{
|
||||
@@ -86,7 +79,7 @@ export const deploymentWidgetInfo = [
|
||||
];
|
||||
|
||||
export const getDeploymentMetricsQueryPayload = (
|
||||
deployment: K8sDeploymentsData,
|
||||
deployment: InframonitoringtypesDeploymentRecordDTO,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
@@ -158,7 +151,9 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: deployment.meta.k8s_deployment_name,
|
||||
value:
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -197,7 +192,9 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'contains',
|
||||
value: deployment.meta.k8s_deployment_name,
|
||||
value:
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -236,7 +233,9 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'contains',
|
||||
value: deployment.meta.k8s_deployment_name,
|
||||
value:
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -309,7 +308,9 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: deployment.meta.k8s_deployment_name,
|
||||
value:
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -348,7 +349,9 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'contains',
|
||||
value: deployment.meta.k8s_deployment_name,
|
||||
value:
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -387,7 +390,9 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'contains',
|
||||
value: deployment.meta.k8s_deployment_name,
|
||||
value:
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -460,7 +465,9 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: deployment.meta.k8s_deployment_name,
|
||||
value:
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -546,7 +553,9 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: deployment.meta.k8s_deployment_name,
|
||||
value:
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
|
||||
@@ -1,35 +1,50 @@
|
||||
import { Tooltip } from 'antd';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { InframonitoringtypesDeploymentRecordDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
|
||||
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
|
||||
|
||||
import ColumnHeader from '../Base/ColumnHeader';
|
||||
import EntityGroupHeader from '../Base/EntityGroupHeader';
|
||||
import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { formatBytes } from '../commonUtils';
|
||||
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { K8sDeploymentsData } from './api';
|
||||
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
|
||||
import {
|
||||
CellValueTooltip,
|
||||
EntityProgressBar,
|
||||
GroupedStatusCounts,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../components';
|
||||
import {
|
||||
INFRA_MONITORING_ATTR_KEYS,
|
||||
InfraMonitoringEntity,
|
||||
} from '../constants';
|
||||
import { Computer } from '@signozhq/icons';
|
||||
|
||||
export function getK8sDeploymentRowKey(deployment: K8sDeploymentsData): string {
|
||||
return deployment.meta.k8s_deployment_name || deployment.deploymentName;
|
||||
export function getK8sDeploymentRowKey(
|
||||
deployment: InframonitoringtypesDeploymentRecordDTO,
|
||||
): string {
|
||||
return (
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ||
|
||||
deployment.deploymentName ||
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
export function getK8sDeploymentItemKey(
|
||||
deployment: K8sDeploymentsData,
|
||||
deployment: InframonitoringtypesDeploymentRecordDTO,
|
||||
): string {
|
||||
return deployment.meta.k8s_deployment_name;
|
||||
return deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] || '';
|
||||
}
|
||||
|
||||
export const k8sDeploymentsColumnsConfig: TableColumnDef<K8sDeploymentsData>[] =
|
||||
export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDeploymentRecordDTO>[] =
|
||||
[
|
||||
{
|
||||
id: 'deploymentGroup',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader title="DEPLOYMENT GROUP" />
|
||||
<EntityGroupHeader title="Deployment Group" />
|
||||
),
|
||||
accessorFn: (row): string => row.meta.k8s_deployment_name || '',
|
||||
width: { min: 220 },
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
@@ -52,10 +67,12 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<K8sDeploymentsData>[] =
|
||||
<EntityGroupHeader
|
||||
title="Deployment Name"
|
||||
icon={<Computer data-hide-expanded="true" size={14} />}
|
||||
docPath="/infrastructure-monitoring/kubernetes/deployments#deployment-name"
|
||||
/>
|
||||
),
|
||||
accessorFn: (row): string => row.meta.k8s_deployment_name || '',
|
||||
width: { min: 210 },
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
@@ -64,17 +81,22 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<K8sDeploymentsData>[] =
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const deploymentName = value as string;
|
||||
return (
|
||||
<Tooltip title={deploymentName}>
|
||||
<CellValueTooltip value={deploymentName}>
|
||||
<TanStackTable.Text>{deploymentName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
</CellValueTooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'namespaceName',
|
||||
header: 'Namespace Name',
|
||||
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
|
||||
width: { default: 220 },
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#namespace-name">
|
||||
Namespace
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
width: { min: 160 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => (
|
||||
@@ -82,12 +104,216 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<K8sDeploymentsData>[] =
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'available_pods',
|
||||
header: 'Available',
|
||||
accessorFn: (row): number => row.availablePods,
|
||||
width: { min: 100 },
|
||||
id: 'podCountsByPhase',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#pod-counts-by-phase">
|
||||
Pod Phases
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): object | undefined => row.podCountsByPhase,
|
||||
width: { min: 250 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
const podCountsByPhase = row.podCountsByPhase;
|
||||
if (!podCountsByPhase) {
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'replica_status',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#replica-status">
|
||||
Replica Status
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.availablePods,
|
||||
width: { min: 180 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ row }): React.ReactNode => (
|
||||
<GroupedStatusCounts
|
||||
items={[
|
||||
{
|
||||
value: row.availablePods,
|
||||
label: 'Available',
|
||||
color: Color.BG_FOREST_500,
|
||||
},
|
||||
{
|
||||
value: row.desiredPods,
|
||||
label: 'Desired',
|
||||
color: Color.BG_ROBIN_500,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'cpu_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-req-usage-">
|
||||
CPU Request
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.deploymentCPURequest,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuRequest = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpuRequest}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<EntityProgressBar value={cpuRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cpu_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-limit-usage-">
|
||||
CPU Limit
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.deploymentCPULimit,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuLimit = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpuLimit}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<EntityProgressBar value={cpuLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cpu',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-usage-cores">
|
||||
CPU Usage
|
||||
<br /> (cores)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.deploymentCPU,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpu}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="CPU metric"
|
||||
>
|
||||
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-req-usage-">
|
||||
Memory Request
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.deploymentMemoryRequest,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryRequest = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={memoryRequest}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<EntityProgressBar value={memoryRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-limit-usage-">
|
||||
Memory Limit
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.deploymentMemoryLimit,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryLimit = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={memoryLimit}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<EntityProgressBar value={memoryLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-usage-wss">
|
||||
Memory Usage
|
||||
<br /> (WSS)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.deploymentMemory,
|
||||
width: { min: 180 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={memory}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="memory metric"
|
||||
>
|
||||
<TanStackTable.Text>{formatBytes(memory)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'available_pods',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#available">
|
||||
Available Pods
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.availablePods,
|
||||
width: { min: 120 },
|
||||
enableSort: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const availablePods = value as number;
|
||||
@@ -104,11 +330,14 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<K8sDeploymentsData>[] =
|
||||
},
|
||||
{
|
||||
id: 'desired_pods',
|
||||
header: 'Desired',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#desired">
|
||||
Desired Pods
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.desiredPods,
|
||||
width: { min: 80 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
width: { min: 120 },
|
||||
enableSort: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const desiredPods = value as number;
|
||||
@@ -123,124 +352,4 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<K8sDeploymentsData>[] =
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cpu_request',
|
||||
header: 'CPU Req Usage (%)',
|
||||
accessorFn: (row): number => row.cpuRequest,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuRequest = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpuRequest}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="CPU Request"
|
||||
>
|
||||
<EntityProgressBar value={cpuRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cpu_limit',
|
||||
header: 'CPU Limit Usage (%)',
|
||||
accessorFn: (row): number => row.cpuLimit,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuLimit = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpuLimit}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="CPU Limit"
|
||||
>
|
||||
<EntityProgressBar value={cpuLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cpu',
|
||||
header: 'CPU Usage (cores)',
|
||||
accessorFn: (row): number => row.cpuUsage,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpu}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="CPU metric"
|
||||
>
|
||||
<TanStackTable.Text>{cpu}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory_request',
|
||||
header: 'Mem Req Usage (%)',
|
||||
accessorFn: (row): number => row.memoryRequest,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryRequest = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={memoryRequest}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="Memory Request"
|
||||
>
|
||||
<EntityProgressBar value={memoryRequest} type="request" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory_limit',
|
||||
header: 'Mem Limit Usage (%)',
|
||||
accessorFn: (row): number => row.memoryLimit,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryLimit = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={memoryLimit}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="Memory Limit"
|
||||
>
|
||||
<EntityProgressBar value={memoryLimit} type="limit" />
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory',
|
||||
header: 'Mem Usage (WSS)',
|
||||
accessorFn: (row): number => row.memoryUsage,
|
||||
width: { min: 140 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={memory}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="memory metric"
|
||||
>
|
||||
<TanStackTable.Text>{formatBytes(memory)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -14,7 +14,12 @@ import {
|
||||
import APIError from 'types/api/error';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const K8S_EVENT_KEYS = ['k8s.object.kind', 'k8s.object.name'];
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from '../../constants';
|
||||
|
||||
const K8S_EVENT_KEYS = [
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND,
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME,
|
||||
];
|
||||
|
||||
export function isEventsKeyNotFoundError(error: unknown): boolean {
|
||||
if (!(error instanceof APIError)) {
|
||||
|
||||
@@ -1,144 +1,8 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import APIError from 'types/api/error';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { nanoToMilli } from 'utils/timeUtils';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export function isKeyNotFoundError(error: unknown): boolean {
|
||||
if (!(error instanceof APIError)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const errorDetails = error.getErrorDetails();
|
||||
if (errorDetails.error.code !== 'invalid_input') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const errors = errorDetails.error.errors || [];
|
||||
return errors.some((err) => err.message?.includes('not found'));
|
||||
}
|
||||
|
||||
export const QUERY_KEYS = {
|
||||
K8S_OBJECT_KIND: 'k8s.object.kind',
|
||||
K8S_OBJECT_NAME: 'k8s.object.name',
|
||||
K8S_POD_NAME: 'k8s.pod.name',
|
||||
K8S_NAMESPACE_NAME: 'k8s.namespace.name',
|
||||
K8S_CLUSTER_NAME: 'k8s.cluster.name',
|
||||
K8S_NODE_NAME: 'k8s.node.name',
|
||||
K8S_DEPLOYMENT_NAME: 'k8s.deployment.name',
|
||||
K8S_STATEFUL_SET_NAME: 'k8s.statefulset.name',
|
||||
K8S_JOB_NAME: 'k8s.job.name',
|
||||
K8S_DAEMON_SET_NAME: 'k8s.daemonset.name',
|
||||
K8S_PERSISTENT_VOLUME_CLAIM_NAME: 'k8s.persistentvolumeclaim.name',
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the payload configuration to fetch events for a K8s entity
|
||||
*/
|
||||
export const getEntityEventsOrLogsQueryPayload = (
|
||||
start: number,
|
||||
end: number,
|
||||
filters: IBuilderQuery['filters'],
|
||||
): GetQueryResultsProps => ({
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
query: {
|
||||
clickhouse_sql: [],
|
||||
promql: [],
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
dataSource: DataSource.LOGS,
|
||||
queryName: 'A',
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
id: '------false',
|
||||
dataType: DataTypes.String,
|
||||
key: '',
|
||||
type: '',
|
||||
},
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
functions: [],
|
||||
filters,
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
stepInterval: 60,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [
|
||||
{
|
||||
columnName: 'timestamp',
|
||||
order: 'desc',
|
||||
},
|
||||
],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
offset: 0,
|
||||
pageSize: 100,
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
id: uuidv4(),
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
},
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
export const entityTracesColumns = [
|
||||
{
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
title: 'Timestamp',
|
||||
width: 200,
|
||||
render: (timestamp: string): string => new Date(timestamp).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: 'Service Name',
|
||||
dataIndex: ['data', 'serviceName'],
|
||||
key: 'serviceName-string-tag',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: ['data', 'name'],
|
||||
key: 'name-string-tag',
|
||||
width: 145,
|
||||
},
|
||||
{
|
||||
title: 'Duration',
|
||||
dataIndex: ['data', 'durationNano'],
|
||||
key: 'durationNano-float64-tag',
|
||||
width: 145,
|
||||
render: (duration: number): string => `${nanoToMilli(duration)}ms`,
|
||||
},
|
||||
{
|
||||
title: 'HTTP Method',
|
||||
dataIndex: ['data', 'httpMethod'],
|
||||
key: 'httpMethod-string-tag',
|
||||
width: 145,
|
||||
},
|
||||
{
|
||||
title: 'Status Code',
|
||||
dataIndex: ['data', 'responseStatusCode'],
|
||||
key: 'responseStatusCode-string-tag',
|
||||
width: 145,
|
||||
},
|
||||
];
|
||||
|
||||
export const selectedEntityTracesColumns: BaseAutocompleteData[] = [
|
||||
{
|
||||
@@ -173,110 +37,16 @@ export const selectedEntityTracesColumns: BaseAutocompleteData[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const getEntityTracesQueryPayload = (
|
||||
start: number,
|
||||
end: number,
|
||||
offset = 0,
|
||||
filters: IBuilderQuery['filters'],
|
||||
): GetQueryResultsProps => ({
|
||||
query: {
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
dataSource: DataSource.TRACES,
|
||||
queryName: 'A',
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
id: '------false',
|
||||
dataType: DataTypes.EMPTY,
|
||||
key: '',
|
||||
type: '',
|
||||
},
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
functions: [],
|
||||
filters,
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
stepInterval: 60,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [
|
||||
{
|
||||
columnName: 'timestamp',
|
||||
order: 'desc',
|
||||
},
|
||||
],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
id: '572f1d91-6ac0-46c0-b726-c21488b34434',
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
},
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
start,
|
||||
end,
|
||||
params: {
|
||||
dataSource: DataSource.TRACES,
|
||||
},
|
||||
tableParams: {
|
||||
pagination: {
|
||||
limit: 10,
|
||||
offset,
|
||||
},
|
||||
selectColumns: [
|
||||
{
|
||||
key: 'serviceName',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
id: 'serviceName--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
id: 'name--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
{
|
||||
key: 'durationNano',
|
||||
dataType: 'float64',
|
||||
type: 'tag',
|
||||
id: 'durationNano--float64--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
{
|
||||
key: 'httpMethod',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
id: 'httpMethod--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
{
|
||||
key: 'responseStatusCode',
|
||||
dataType: 'string',
|
||||
type: 'tag',
|
||||
id: 'responseStatusCode--string--tag--true',
|
||||
isIndexed: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
export function isKeyNotFoundError(error: unknown): boolean {
|
||||
if (!(error instanceof APIError)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export const filterOutPrimaryFilters = (
|
||||
filters: TagFilterItem[],
|
||||
primaryKeys: string[],
|
||||
): TagFilterItem[] =>
|
||||
filters.filter(
|
||||
(filter) =>
|
||||
!primaryKeys.includes(filter.key?.key ?? '') && filter.key?.key !== 'id',
|
||||
);
|
||||
const errorDetails = error.getErrorDetails();
|
||||
if (errorDetails.error.code !== 'invalid_input') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const errors = errorDetails.error.errors || [];
|
||||
return errors.some((err) => err.message?.includes('not found'));
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
height: 0.1rem;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
@@ -35,11 +35,12 @@
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--accent-primary);
|
||||
background: color-mix(in srgb, var(--l1-foreground) 20%, transparent);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
|
||||
background: color-mix(in srgb, var(--l1-foreground) 30%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +85,7 @@
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,11 +100,12 @@
|
||||
|
||||
.listContainer {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
min-width: 595px;
|
||||
overflow-x: auto;
|
||||
|
||||
> * {
|
||||
min-width: 0;
|
||||
@@ -233,3 +235,7 @@
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.k8SOpenQuickFilters {
|
||||
grid-area: quickFilters;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import {
|
||||
ArrowUpDown,
|
||||
ArrowUpToLine,
|
||||
@@ -22,7 +21,7 @@ import {
|
||||
Workflow,
|
||||
} from '@signozhq/icons';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { FeatureKeys } from '../../constants/features';
|
||||
import { useAppContext } from '../../providers/App/App';
|
||||
@@ -43,9 +42,9 @@ import K8sDaemonSetsList from './DaemonSets/K8sDaemonSetsList';
|
||||
import K8sDeploymentsList from './Deployments/K8sDeploymentsList';
|
||||
import {
|
||||
useInfraMonitoringCategory,
|
||||
useInfraMonitoringFiltersK8s,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringSelectedItem,
|
||||
} from './hooks';
|
||||
import K8sJobsList from './Jobs/K8sJobsList';
|
||||
import K8sNamespacesList from './Namespaces/K8sNamespacesList';
|
||||
@@ -55,62 +54,60 @@ import K8sStatefulSetsList from './StatefulSets/K8sStatefulSetsList';
|
||||
import K8sVolumesList from './Volumes/K8sVolumesList';
|
||||
|
||||
import styles from './InfraMonitoringK8s.module.scss';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
export default function InfraMonitoringK8s(): JSX.Element {
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
|
||||
const [selectedCategory, setSelectedCategory] = useInfraMonitoringCategory();
|
||||
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
|
||||
const [, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
const [, setOrderBy] = useInfraMonitoringOrderBy();
|
||||
const [, setSelectedItem] = useInfraMonitoringSelectedItem();
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const handleFilterVisibilityChange = useCallback((): void => {
|
||||
setShowFilters((show) => !show);
|
||||
}, []);
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
// Track previous urlFilters to only sync when the value actually changes
|
||||
// (not when handleChangeQueryData changes due to query updates)
|
||||
const prevUrlFiltersRef = useRef<string | null>(null);
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
const isInitialized = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const currentFiltersJson = urlFilters ? JSON.stringify(urlFilters) : null;
|
||||
|
||||
// Only sync if urlFilters value has actually changed
|
||||
if (prevUrlFiltersRef.current !== currentFiltersJson) {
|
||||
prevUrlFiltersRef.current = currentFiltersJson;
|
||||
// Sync filters to query builder, using empty filter when urlFilters is null
|
||||
handleChangeQueryData('filters', urlFilters || { items: [], op: 'and' });
|
||||
if (isInitialized.current) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [urlFilters]); // handleChangeQueryData intentionally omitted - we call the current version but don't re-run when it changes
|
||||
isInitialized.current = true;
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
if (!compositeQuery) {
|
||||
const defaultQuery = initialQueriesMap[DataSource.METRICS];
|
||||
redirectWithQueryBuilderData({
|
||||
...defaultQuery,
|
||||
builder: {
|
||||
...defaultQuery.builder,
|
||||
queryData: defaultQuery.builder.queryData.map((query) => ({
|
||||
...query,
|
||||
filter: { expression: '' },
|
||||
filters: { items: [], op: 'AND' as const },
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [compositeQuery, redirectWithQueryBuilderData]);
|
||||
|
||||
const handleFilterChange = (query: Query): void => {
|
||||
// update the current query with the new filters
|
||||
// in infra monitoring k8s, we are using only one query, hence updating the 0th index of queryData
|
||||
const filters = query.builder.queryData[0].filters;
|
||||
// The useEffect will sync filters to query builder, avoiding double state updates
|
||||
setUrlFilters(filters || null);
|
||||
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
useEffect(() => {
|
||||
void logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: selectedCategory,
|
||||
view: InfraMonitoringEvents.QuickFiltersView,
|
||||
});
|
||||
};
|
||||
}, [selectedCategory]);
|
||||
|
||||
const handleFilterVisibilityChange = useCallback((): void => {
|
||||
setShowFilters((show) => !show);
|
||||
}, []);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const categories = useMemo(
|
||||
() => [
|
||||
@@ -179,12 +176,23 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
|
||||
const handleCategorySelect = (key: string): void => {
|
||||
if (key !== selectedCategory) {
|
||||
setSelectedCategory(key);
|
||||
// Reset filters
|
||||
setUrlFilters(null);
|
||||
setOrderBy(null);
|
||||
setGroupBy(null);
|
||||
handleChangeQueryData('filters', { items: [], op: 'and' });
|
||||
void setSelectedCategory(key as string);
|
||||
void setOrderBy(null);
|
||||
void setGroupBy(null);
|
||||
void setSelectedItem(null);
|
||||
redirectWithQueryBuilderData({
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...(currentQuery.builder.queryData[0] || {}),
|
||||
filter: { expression: '' },
|
||||
filters: { items: [], op: 'AND' as const },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -192,7 +200,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{!showFilters && (
|
||||
<div>
|
||||
<div className={styles.k8SOpenQuickFilters}>
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
type="text"
|
||||
@@ -261,7 +269,6 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={selectedCategoryConfig}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { listJobs } from 'api/generated/services/inframonitoring';
|
||||
import {
|
||||
InframonitoringtypesJobRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { getK8sJobsList, K8sJobsData } from './api';
|
||||
import {
|
||||
getJobMetricsQueryPayload,
|
||||
jobWidgetInfo,
|
||||
k8sJobDetailsMetadataConfig,
|
||||
k8sJobGetEntityName,
|
||||
k8sJobGetSelectedItemFilters,
|
||||
k8sJobInitialEventsFilter,
|
||||
k8sJobInitialLogTracesFilter,
|
||||
k8sJobGetSelectedItemExpression,
|
||||
k8sJobInitialEventsExpression,
|
||||
k8sJobInitialLogTracesExpression,
|
||||
} from './constants';
|
||||
import {
|
||||
getK8sJobItemKey,
|
||||
@@ -28,66 +31,93 @@ function K8sJobsList({
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
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',
|
||||
};
|
||||
try {
|
||||
const response = await listJobs(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const response = await getK8sJobsList(
|
||||
filters,
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.payload?.data.records || [],
|
||||
total: response.payload?.data.total || 0,
|
||||
error: response.error,
|
||||
rawData: response.payload?.data,
|
||||
};
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch jobs';
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesJobRecordDTO[],
|
||||
total: 0,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ data: K8sJobsData | null; error?: string | null }> => {
|
||||
const response = await getK8sJobsList(
|
||||
{
|
||||
filters: filters.filters,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
): Promise<{
|
||||
data: InframonitoringtypesJobRecordDTO | null;
|
||||
error?: string | null;
|
||||
}> => {
|
||||
try {
|
||||
const response = await listJobs(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const records = response.payload?.data.records || [];
|
||||
|
||||
return {
|
||||
data: records.length > 0 ? records[0] : null,
|
||||
error: response.error,
|
||||
};
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch job';
|
||||
return {
|
||||
data: null,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<K8sJobsData>
|
||||
<K8sBaseList<InframonitoringtypesJobRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
tableColumns={k8sJobsColumnsConfig}
|
||||
@@ -97,14 +127,14 @@ function K8sJobsList({
|
||||
eventCategory={InfraMonitoringEvents.Job}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails<K8sJobsData>
|
||||
<K8sBaseDetails<InframonitoringtypesJobRecordDTO>
|
||||
category={InfraMonitoringEntity.JOBS}
|
||||
eventCategory={InfraMonitoringEvents.Job}
|
||||
getSelectedItemFilters={k8sJobGetSelectedItemFilters}
|
||||
getSelectedItemExpression={k8sJobGetSelectedItemExpression}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sJobGetEntityName}
|
||||
getInitialLogTracesFilters={k8sJobInitialLogTracesFilter}
|
||||
getInitialEventsFilters={k8sJobInitialEventsFilter}
|
||||
getInitialLogTracesExpression={k8sJobInitialLogTracesExpression}
|
||||
getInitialEventsExpression={k8sJobInitialEventsExpression}
|
||||
metadataConfig={k8sJobDetailsMetadataConfig}
|
||||
entityWidgetInfo={jobWidgetInfo}
|
||||
getEntityQueryPayload={getJobMetricsQueryPayload}
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
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 { K8sBaseFilters } from '../Base/types';
|
||||
|
||||
export interface K8sJobsData {
|
||||
jobName: string;
|
||||
cpuUsage: number;
|
||||
memoryUsage: number;
|
||||
cpuRequest: number;
|
||||
memoryRequest: number;
|
||||
cpuLimit: number;
|
||||
memoryLimit: number;
|
||||
restarts: number;
|
||||
desiredSuccessfulPods: number;
|
||||
activePods: number;
|
||||
failedPods: number;
|
||||
successfulPods: number;
|
||||
meta: {
|
||||
k8s_cluster_name: string;
|
||||
k8s_job_name: string;
|
||||
k8s_namespace_name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sJobsListResponse {
|
||||
status: string;
|
||||
data: {
|
||||
type: string;
|
||||
records: K8sJobsData[];
|
||||
groups: null;
|
||||
total: number;
|
||||
sentAnyHostMetricsData: boolean;
|
||||
isSendingK8SAgentMetrics: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(H4ad): Erase this whole file when migrating to openapi
|
||||
export const jobsMetaMap = [
|
||||
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
|
||||
{ dot: 'k8s.job.name', under: 'k8s_job_name' },
|
||||
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
|
||||
] as const;
|
||||
|
||||
export function mapJobsMeta(raw: Record<string, unknown>): K8sJobsData['meta'] {
|
||||
const out: Record<string, unknown> = { ...raw };
|
||||
jobsMetaMap.forEach(({ dot, under }) => {
|
||||
if (dot in raw) {
|
||||
const v = raw[dot];
|
||||
out[under] = typeof v === 'string' ? v : raw[under];
|
||||
}
|
||||
});
|
||||
return out as K8sJobsData['meta'];
|
||||
}
|
||||
|
||||
export const getK8sJobsList = async (
|
||||
props: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
): Promise<SuccessResponse<K8sJobsListResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const requestProps = dotMetricsEnabled
|
||||
? {
|
||||
...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);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as typeof props.filters.items,
|
||||
),
|
||||
},
|
||||
}
|
||||
: props;
|
||||
|
||||
const response = await axios.post('/jobs/list', requestProps, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
const payload: K8sJobsListResponse = response.data;
|
||||
|
||||
payload.data.records = payload.data.records.map((record) => ({
|
||||
...record,
|
||||
meta: mapJobsMeta(record.meta as Record<string, unknown>),
|
||||
}));
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload,
|
||||
params: requestProps,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
@@ -1,67 +1,63 @@
|
||||
import { InframonitoringtypesJobRecordDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
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 { K8sJobsData } from './api';
|
||||
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
|
||||
|
||||
export const k8sJobGetSelectedItemFilters = (
|
||||
export const k8sJobGetSelectedItemExpression = (
|
||||
selectedItemId: string,
|
||||
): TagFilter => ({
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: 'k8s_job_name',
|
||||
key: {
|
||||
key: 'k8s_job_name',
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: selectedItemId,
|
||||
},
|
||||
],
|
||||
});
|
||||
): string =>
|
||||
`${INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME} = ${formatValueForExpression(selectedItemId)}`;
|
||||
|
||||
export const k8sJobDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sJobsData>[] =
|
||||
export const k8sJobDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesJobRecordDTO>[] =
|
||||
[
|
||||
{
|
||||
label: 'Job Name',
|
||||
getValue: (p): string => p.meta.k8s_job_name,
|
||||
getValue: (p): string =>
|
||||
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] ?? '',
|
||||
},
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
getValue: (p): string => p.meta.k8s_cluster_name,
|
||||
getValue: (p): string =>
|
||||
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
getValue: (p): string => p.meta.k8s_namespace_name,
|
||||
getValue: (p): string =>
|
||||
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sJobInitialEventsFilter = (
|
||||
item: K8sJobsData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Job'),
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_job_name),
|
||||
];
|
||||
export const k8sJobInitialEventsExpression = (
|
||||
item: InframonitoringtypesJobRecordDTO,
|
||||
): string => {
|
||||
const name = formatValueForExpression(
|
||||
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] ?? '',
|
||||
);
|
||||
return `${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'Job' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${name}`;
|
||||
};
|
||||
|
||||
export const k8sJobInitialLogTracesFilter = (
|
||||
item: K8sJobsData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_JOB_NAME, item.meta.k8s_job_name),
|
||||
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.k8s_namespace_name),
|
||||
];
|
||||
export const k8sJobInitialLogTracesExpression = (
|
||||
item: InframonitoringtypesJobRecordDTO,
|
||||
): string => {
|
||||
const jobName = formatValueForExpression(
|
||||
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] ?? '',
|
||||
);
|
||||
const namespaceName = formatValueForExpression(
|
||||
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
|
||||
);
|
||||
return `${INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME} = ${jobName} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${namespaceName}`;
|
||||
};
|
||||
|
||||
export const k8sJobGetEntityName = (item: K8sJobsData): string =>
|
||||
item.meta.k8s_job_name;
|
||||
export const k8sJobGetEntityName = (
|
||||
item: InframonitoringtypesJobRecordDTO,
|
||||
): string => item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] ?? '';
|
||||
|
||||
export const jobWidgetInfo = [
|
||||
{
|
||||
@@ -83,7 +79,7 @@ export const jobWidgetInfo = [
|
||||
];
|
||||
|
||||
export const getJobMetricsQueryPayload = (
|
||||
job: K8sJobsData,
|
||||
job: InframonitoringtypesJobRecordDTO,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
@@ -145,7 +141,8 @@ export const getJobMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: job.meta.k8s_namespace_name,
|
||||
value:
|
||||
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -229,7 +226,8 @@ export const getJobMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: job.meta.k8s_namespace_name,
|
||||
value:
|
||||
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -313,7 +311,8 @@ export const getJobMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: job.meta.k8s_namespace_name,
|
||||
value:
|
||||
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -410,7 +409,8 @@ export const getJobMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: job.meta.k8s_namespace_name,
|
||||
value:
|
||||
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
|
||||
@@ -1,30 +1,45 @@
|
||||
import { Tooltip } from 'antd';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { InframonitoringtypesJobRecordDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
|
||||
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
|
||||
|
||||
import ColumnHeader from '../Base/ColumnHeader';
|
||||
import EntityGroupHeader from '../Base/EntityGroupHeader';
|
||||
import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { formatBytes } from '../commonUtils';
|
||||
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { K8sJobsData } from './api';
|
||||
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
|
||||
import {
|
||||
CellValueTooltip,
|
||||
EntityProgressBar,
|
||||
GroupedStatusCounts,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../components';
|
||||
import {
|
||||
INFRA_MONITORING_ATTR_KEYS,
|
||||
InfraMonitoringEntity,
|
||||
} from '../constants';
|
||||
import { Bolt } from '@signozhq/icons';
|
||||
|
||||
export function getK8sJobRowKey(job: K8sJobsData): string {
|
||||
return job.jobName || job.meta.k8s_job_name || '';
|
||||
export function getK8sJobRowKey(job: InframonitoringtypesJobRecordDTO): string {
|
||||
return (
|
||||
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || job.jobName || ''
|
||||
);
|
||||
}
|
||||
|
||||
export function getK8sJobItemKey(job: K8sJobsData): string {
|
||||
return job.meta.k8s_job_name;
|
||||
export function getK8sJobItemKey(
|
||||
job: InframonitoringtypesJobRecordDTO,
|
||||
): string {
|
||||
return job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || '';
|
||||
}
|
||||
|
||||
export const k8sJobsColumnsConfig: TableColumnDef<K8sJobsData>[] = [
|
||||
export type JobTableColumnConfig =
|
||||
TableColumnDef<InframonitoringtypesJobRecordDTO>;
|
||||
export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
{
|
||||
id: 'jobGroup',
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="JOB GROUP" />,
|
||||
accessorFn: (row): string => row.meta.k8s_job_name || '',
|
||||
width: { min: 270 },
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="Job Group" />,
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
@@ -47,10 +62,12 @@ export const k8sJobsColumnsConfig: TableColumnDef<K8sJobsData>[] = [
|
||||
<EntityGroupHeader
|
||||
title="Job Name"
|
||||
icon={<Bolt data-hide-expanded="true" size={14} />}
|
||||
docPath="/infrastructure-monitoring/kubernetes/jobs#job-name"
|
||||
/>
|
||||
),
|
||||
accessorFn: (row): string => row.meta.k8s_job_name || '',
|
||||
width: { min: 260 },
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
@@ -59,109 +76,98 @@ export const k8sJobsColumnsConfig: TableColumnDef<K8sJobsData>[] = [
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const jobName = value as string;
|
||||
return (
|
||||
<Tooltip title={jobName}>
|
||||
<CellValueTooltip value={jobName}>
|
||||
<TanStackTable.Text>{jobName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
</CellValueTooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'namespaceName',
|
||||
header: 'Namespace Name',
|
||||
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
|
||||
width: { default: 150 },
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#namespace-name">
|
||||
Namespace
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
width: { min: 160 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const namespaceName = value as string;
|
||||
return (
|
||||
<Tooltip title={namespaceName}>
|
||||
<CellValueTooltip value={namespaceName}>
|
||||
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
</CellValueTooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'successful_pods',
|
||||
header: 'Successful',
|
||||
id: 'pod_counts_by_phase',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#pod-counts-by-phase">
|
||||
Pod Phases
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): InframonitoringtypesJobRecordDTO['podCountsByPhase'] =>
|
||||
row.podCountsByPhase,
|
||||
width: { min: 250 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
const podCountsByPhase = row.podCountsByPhase;
|
||||
if (!podCountsByPhase) {
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'completion_status',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#completion-status">
|
||||
Completion Status
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.successfulPods,
|
||||
width: { min: 120 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const successfulPods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={successfulPods}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
attribute="successful pod"
|
||||
>
|
||||
<TanStackTable.Text>{successfulPods}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'failed_pods',
|
||||
header: 'Failed',
|
||||
accessorFn: (row): number => row.failedPods,
|
||||
width: { min: 100 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const failedPods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={failedPods}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
attribute="failed pod"
|
||||
>
|
||||
<TanStackTable.Text>{failedPods}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'desired_successful_pods',
|
||||
header: 'Desired Successful',
|
||||
accessorFn: (row): number => row.desiredSuccessfulPods,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const desiredSuccessfulPods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={desiredSuccessfulPods}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
attribute="desired successful pod"
|
||||
>
|
||||
<TanStackTable.Text>{desiredSuccessfulPods}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'active_pods',
|
||||
header: 'Active',
|
||||
accessorFn: (row): number => row.activePods,
|
||||
width: { min: 100 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const activePods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={activePods}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
attribute="active pod"
|
||||
>
|
||||
<TanStackTable.Text>{activePods}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
width: { min: 210 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ row }): React.ReactNode => (
|
||||
<GroupedStatusCounts
|
||||
items={[
|
||||
{ value: row.activePods, label: 'Active', color: Color.BG_ROBIN_500 },
|
||||
{ value: row.failedPods, label: 'Failed', color: Color.BG_CHERRY_500 },
|
||||
{
|
||||
value: row.successfulPods,
|
||||
label: 'Successful',
|
||||
color: Color.BG_FOREST_500,
|
||||
},
|
||||
{
|
||||
value: row.desiredSuccessfulPods,
|
||||
label: 'Desired',
|
||||
color: Color.BG_ROBIN_500,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'cpu_request',
|
||||
header: 'CPU Req Usage (%)',
|
||||
accessorFn: (row): number => row.cpuRequest,
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-req-usage-">
|
||||
CPU Request
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.jobCPURequest,
|
||||
width: { min: 200, default: 200 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuRequest = value as number;
|
||||
return (
|
||||
@@ -177,10 +183,16 @@ export const k8sJobsColumnsConfig: TableColumnDef<K8sJobsData>[] = [
|
||||
},
|
||||
{
|
||||
id: 'cpu_limit',
|
||||
header: 'CPU Limit Usage (%)',
|
||||
accessorFn: (row): number => row.cpuLimit,
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-limit-usage-">
|
||||
CPU Limit
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.jobCPULimit,
|
||||
width: { min: 200, default: 200 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuLimit = value as number;
|
||||
return (
|
||||
@@ -196,29 +208,42 @@ export const k8sJobsColumnsConfig: TableColumnDef<K8sJobsData>[] = [
|
||||
},
|
||||
{
|
||||
id: 'cpu',
|
||||
header: 'CPU Usage (cores)',
|
||||
accessorFn: (row): number => row.cpuUsage,
|
||||
width: { min: 190 },
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-usage-cores">
|
||||
CPU Usage
|
||||
<br /> (cores)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.jobCPU,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = value as number;
|
||||
const cpu = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpu}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
attribute="CPU metric"
|
||||
>
|
||||
<TanStackTable.Text>{cpu}</TanStackTable.Text>
|
||||
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory_request',
|
||||
header: 'Mem Req Usage (%)',
|
||||
accessorFn: (row): number => row.memoryRequest,
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-req-usage-">
|
||||
Memory Request
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.jobMemoryRequest,
|
||||
width: { min: 190 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryRequest = value as number;
|
||||
return (
|
||||
@@ -234,10 +259,16 @@ export const k8sJobsColumnsConfig: TableColumnDef<K8sJobsData>[] = [
|
||||
},
|
||||
{
|
||||
id: 'memory_limit',
|
||||
header: 'Mem Limit Usage (%)',
|
||||
accessorFn: (row): number => row.memoryLimit,
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-limit-usage-">
|
||||
Memory Limit
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.jobMemoryLimit,
|
||||
width: { min: 180 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryLimit = value as number;
|
||||
return (
|
||||
@@ -253,10 +284,16 @@ export const k8sJobsColumnsConfig: TableColumnDef<K8sJobsData>[] = [
|
||||
},
|
||||
{
|
||||
id: 'memory',
|
||||
header: 'Mem Usage (WSS)',
|
||||
accessorFn: (row): number => row.memoryUsage,
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-usage-wss">
|
||||
Memory Usage
|
||||
<br /> (WSS)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.jobMemory,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
return (
|
||||
@@ -270,4 +307,100 @@ export const k8sJobsColumnsConfig: TableColumnDef<K8sJobsData>[] = [
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'active_pods',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#active">
|
||||
Active Pods
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.activePods,
|
||||
width: { min: 100 },
|
||||
enableSort: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const activePods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={activePods}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
attribute="active pod"
|
||||
>
|
||||
<TanStackTable.Text>{activePods}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'failed_pods',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#failed">
|
||||
Failed Pods
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.failedPods,
|
||||
width: { min: 100 },
|
||||
enableSort: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const failedPods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={failedPods}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
attribute="failed pod"
|
||||
>
|
||||
<TanStackTable.Text>{failedPods}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'successful_pods',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#successful">
|
||||
Successful Pods
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.successfulPods,
|
||||
width: { min: 120 },
|
||||
enableSort: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const successfulPods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={successfulPods}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
attribute="successful pod"
|
||||
>
|
||||
<TanStackTable.Text>{successfulPods}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'desired_successful_pods',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#desired-successful">
|
||||
Desired Successful Pods
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.desiredSuccessfulPods,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const desiredSuccessfulPods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={desiredSuccessfulPods}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
attribute="desired successful pod"
|
||||
>
|
||||
<TanStackTable.Text>{desiredSuccessfulPods}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { listNamespaces } from 'api/generated/services/inframonitoring';
|
||||
import {
|
||||
InframonitoringtypesNamespaceRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { getK8sNamespacesList, K8sNamespacesData } from './api';
|
||||
import {
|
||||
getNamespaceMetricsQueryPayload,
|
||||
k8sNamespaceDetailsMetadataConfig,
|
||||
k8sNamespaceGetEntityName,
|
||||
k8sNamespaceGetSelectedItemFilters,
|
||||
k8sNamespaceInitialEventsFilter,
|
||||
k8sNamespaceInitialLogTracesFilter,
|
||||
k8sNamespaceGetSelectedItemExpression,
|
||||
k8sNamespaceInitialEventsExpression,
|
||||
k8sNamespaceInitialLogTracesExpression,
|
||||
namespaceWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
@@ -28,66 +31,93 @@ function K8sNamespacesList({
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
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',
|
||||
};
|
||||
try {
|
||||
const response = await listNamespaces(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const response = await getK8sNamespacesList(
|
||||
filters,
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.payload?.data.records || [],
|
||||
total: response.payload?.data.total || 0,
|
||||
error: response.error,
|
||||
rawData: response.payload?.data,
|
||||
};
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch namespaces';
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesNamespaceRecordDTO[],
|
||||
total: 0,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
[],
|
||||
);
|
||||
|
||||
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,
|
||||
},
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
): Promise<{
|
||||
data: InframonitoringtypesNamespaceRecordDTO | null;
|
||||
error?: string | null;
|
||||
}> => {
|
||||
try {
|
||||
const response = await listNamespaces(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const records = response.payload?.data.records || [];
|
||||
|
||||
return {
|
||||
data: records.length > 0 ? records[0] : null,
|
||||
error: response.error,
|
||||
};
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch namespace';
|
||||
return {
|
||||
data: null,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<K8sNamespacesData>
|
||||
<K8sBaseList<InframonitoringtypesNamespaceRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.NAMESPACES}
|
||||
tableColumns={k8sNamespacesColumnsConfig}
|
||||
@@ -97,14 +127,14 @@ function K8sNamespacesList({
|
||||
eventCategory={InfraMonitoringEvents.Namespace}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails<K8sNamespacesData>
|
||||
<K8sBaseDetails<InframonitoringtypesNamespaceRecordDTO>
|
||||
category={InfraMonitoringEntity.NAMESPACES}
|
||||
eventCategory={InfraMonitoringEvents.Namespace}
|
||||
getSelectedItemFilters={k8sNamespaceGetSelectedItemFilters}
|
||||
getSelectedItemExpression={k8sNamespaceGetSelectedItemExpression}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sNamespaceGetEntityName}
|
||||
getInitialLogTracesFilters={k8sNamespaceInitialLogTracesFilter}
|
||||
getInitialEventsFilters={k8sNamespaceInitialEventsFilter}
|
||||
getInitialLogTracesExpression={k8sNamespaceInitialLogTracesExpression}
|
||||
getInitialEventsExpression={k8sNamespaceInitialEventsExpression}
|
||||
metadataConfig={k8sNamespaceDetailsMetadataConfig}
|
||||
entityWidgetInfo={namespaceWidgetInfo}
|
||||
getEntityQueryPayload={getNamespaceMetricsQueryPayload}
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
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/types';
|
||||
|
||||
export interface K8sNamespacesListPayload {
|
||||
filters: TagFilter;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sNamespacesData {
|
||||
namespaceName: string;
|
||||
cpuUsage: number;
|
||||
memoryUsage: number;
|
||||
meta: {
|
||||
k8s_cluster_name: string;
|
||||
k8s_namespace_name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sNamespacesListResponse {
|
||||
status: string;
|
||||
data: {
|
||||
type: string;
|
||||
records: K8sNamespacesData[];
|
||||
groups: null;
|
||||
total: number;
|
||||
sentAnyHostMetricsData: boolean;
|
||||
isSendingK8SAgentMetrics: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(H4ad): Erase this whole file when migrating to openapi
|
||||
export const namespacesMetaMap = [
|
||||
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
|
||||
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
|
||||
] as const;
|
||||
|
||||
export function mapNamespacesMeta(
|
||||
raw: Record<string, unknown>,
|
||||
): K8sNamespacesData['meta'] {
|
||||
const out: Record<string, unknown> = { ...raw };
|
||||
namespacesMetaMap.forEach(({ dot, under }) => {
|
||||
if (dot in raw) {
|
||||
const v = raw[dot];
|
||||
out[under] = typeof v === 'string' ? v : raw[under];
|
||||
}
|
||||
});
|
||||
return out as K8sNamespacesData['meta'];
|
||||
}
|
||||
|
||||
export const getK8sNamespacesList = async (
|
||||
props: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
): Promise<SuccessResponse<K8sNamespacesListResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const requestProps = dotMetricsEnabled
|
||||
? {
|
||||
...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);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as typeof props.filters.items,
|
||||
),
|
||||
},
|
||||
}
|
||||
: props;
|
||||
|
||||
const response = await axios.post('/namespaces/list', requestProps, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
const payload: K8sNamespacesListResponse = response.data;
|
||||
|
||||
payload.data.records = payload.data.records.map((record) => ({
|
||||
...record,
|
||||
meta: mapNamespacesMeta(record.meta as Record<string, unknown>),
|
||||
}));
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload,
|
||||
params: requestProps,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
@@ -1,67 +1,47 @@
|
||||
import { InframonitoringtypesNamespaceRecordDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
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';
|
||||
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
|
||||
|
||||
export const k8sNamespaceGetSelectedItemFilters = (
|
||||
export const k8sNamespaceGetSelectedItemExpression = (
|
||||
selectedItemId: string,
|
||||
): TagFilter => ({
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: 'k8s_namespace_name',
|
||||
key: {
|
||||
key: 'k8s_namespace_name',
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: selectedItemId,
|
||||
},
|
||||
],
|
||||
});
|
||||
): string =>
|
||||
`${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${formatValueForExpression(selectedItemId)}`;
|
||||
|
||||
export const k8sNamespaceDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sNamespacesData>[] =
|
||||
export const k8sNamespaceDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesNamespaceRecordDTO>[] =
|
||||
[
|
||||
{ label: 'Namespace Name', getValue: (p): string => p.namespaceName },
|
||||
{ label: 'Namespace Name', getValue: (p): string => p.namespaceName || '' },
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
getValue: (p): string => p.meta.k8s_cluster_name,
|
||||
getValue: (p): string =>
|
||||
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sNamespaceInitialFilters = [
|
||||
QUERY_KEYS.K8S_NAMESPACE_NAME,
|
||||
QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||
];
|
||||
export const k8sNamespaceInitialEventsExpression = (
|
||||
item: InframonitoringtypesNamespaceRecordDTO,
|
||||
): string => {
|
||||
const name = formatValueForExpression(item.namespaceName || '');
|
||||
return `${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'Namespace' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${name}`;
|
||||
};
|
||||
|
||||
export const k8sNamespaceInitialEventsFilter = (
|
||||
item: K8sNamespacesData,
|
||||
): TagFilterItem[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Namespace'),
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.namespaceName),
|
||||
];
|
||||
export const k8sNamespaceInitialLogTracesExpression = (
|
||||
item: InframonitoringtypesNamespaceRecordDTO,
|
||||
): string => {
|
||||
const name = formatValueForExpression(item.namespaceName || '');
|
||||
return `${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${name}`;
|
||||
};
|
||||
|
||||
export const k8sNamespaceInitialLogTracesFilter = (
|
||||
item: K8sNamespacesData,
|
||||
): TagFilterItem[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.namespaceName),
|
||||
];
|
||||
|
||||
export const k8sNamespaceGetEntityName = (item: K8sNamespacesData): string =>
|
||||
item.namespaceName;
|
||||
export const k8sNamespaceGetEntityName = (
|
||||
item: InframonitoringtypesNamespaceRecordDTO,
|
||||
): string => item.namespaceName || '';
|
||||
|
||||
export const namespaceWidgetInfo = [
|
||||
{
|
||||
@@ -107,7 +87,7 @@ export const namespaceWidgetInfo = [
|
||||
];
|
||||
|
||||
export const getNamespaceMetricsQueryPayload = (
|
||||
namespace: K8sNamespacesData,
|
||||
namespace: InframonitoringtypesNamespaceRecordDTO,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
|
||||
@@ -1,37 +1,46 @@
|
||||
import { Tooltip } from 'antd';
|
||||
import { FilePenLine } from '@signozhq/icons';
|
||||
import { InframonitoringtypesNamespaceRecordDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
|
||||
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
|
||||
|
||||
import ColumnHeader from '../Base/ColumnHeader';
|
||||
import EntityGroupHeader from '../Base/EntityGroupHeader';
|
||||
import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { formatBytes } from '../commonUtils';
|
||||
import { ValidateColumnValueWrapper } from '../components';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { K8sNamespacesData, K8sNamespacesListPayload } from './api';
|
||||
import { FilePenLine } from '@signozhq/icons';
|
||||
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
|
||||
import {
|
||||
CellValueTooltip,
|
||||
GroupedStatusCounts,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../components';
|
||||
import {
|
||||
INFRA_MONITORING_ATTR_KEYS,
|
||||
InfraMonitoringEntity,
|
||||
} from '../constants';
|
||||
|
||||
export function getK8sNamespaceRowKey(namespace: K8sNamespacesData): string {
|
||||
return namespace.namespaceName || namespace.meta.k8s_namespace_name;
|
||||
export function getK8sNamespaceRowKey(
|
||||
namespace: InframonitoringtypesNamespaceRecordDTO,
|
||||
): string {
|
||||
return (
|
||||
namespace.namespaceName ||
|
||||
namespace.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ||
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
export function getK8sNamespaceItemKey(namespace: K8sNamespacesData): string {
|
||||
return namespace.meta.k8s_namespace_name;
|
||||
export function getK8sNamespaceItemKey(
|
||||
namespace: InframonitoringtypesNamespaceRecordDTO,
|
||||
): string {
|
||||
return namespace.namespaceName;
|
||||
}
|
||||
|
||||
export const getK8sNamespacesListQuery = (): K8sNamespacesListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
export const k8sNamespacesColumnsConfig: TableColumnDef<K8sNamespacesData>[] = [
|
||||
export type NamespaceTableColumnConfig =
|
||||
TableColumnDef<InframonitoringtypesNamespaceRecordDTO>;
|
||||
export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
|
||||
{
|
||||
id: 'namespaceGroup',
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="NAMESPACE GROUP" />,
|
||||
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
|
||||
width: { min: 300 },
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="Namespace Group" />,
|
||||
accessorFn: (row): string => row.namespaceName || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
@@ -54,6 +63,7 @@ export const k8sNamespacesColumnsConfig: TableColumnDef<K8sNamespacesData>[] = [
|
||||
<EntityGroupHeader
|
||||
title="Namespace Name"
|
||||
icon={<FilePenLine data-hide-expanded="true" size={14} />}
|
||||
docPath="/infrastructure-monitoring/kubernetes/namespaces#namespace-name"
|
||||
/>
|
||||
),
|
||||
accessorFn: (row): string => row.namespaceName || '',
|
||||
@@ -66,49 +76,85 @@ export const k8sNamespacesColumnsConfig: TableColumnDef<K8sNamespacesData>[] = [
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const namespaceName = value as string;
|
||||
return (
|
||||
<Tooltip title={namespaceName}>
|
||||
<CellValueTooltip value={namespaceName}>
|
||||
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
</CellValueTooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'clusterName',
|
||||
header: 'Cluster Name',
|
||||
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/namespaces#cluster-name">
|
||||
Cluster Name
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
|
||||
width: { default: 150 },
|
||||
enableSort: false,
|
||||
cell: ({ value }): React.ReactNode => (
|
||||
<TanStackTable.Text>{value as string}</TanStackTable.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'podCountsByPhase',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/namespaces#pod-counts-by-phase">
|
||||
Pod Phases
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (
|
||||
row,
|
||||
): InframonitoringtypesNamespaceRecordDTO['podCountsByPhase'] =>
|
||||
row.podCountsByPhase,
|
||||
width: { min: 250 },
|
||||
enableSort: false,
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
const podCountsByPhase = row.podCountsByPhase;
|
||||
if (!podCountsByPhase) {
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<GroupedStatusCounts items={getPodPhaseStatusItems(row.podCountsByPhase)} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cpu',
|
||||
header: 'CPU Usage (cores)',
|
||||
accessorFn: (row): number => row.cpuUsage,
|
||||
width: { min: 220 },
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/namespaces#cpu-usage-cores">
|
||||
CPU Usage (cores)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.namespaceCPU,
|
||||
width: { min: 190 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = value as number;
|
||||
const cpu = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpu}
|
||||
entity={InfraMonitoringEntity.NAMESPACES}
|
||||
attribute="CPU metric"
|
||||
>
|
||||
<TanStackTable.Text>{cpu}</TanStackTable.Text>
|
||||
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory',
|
||||
header: 'Mem Usage (WSS)',
|
||||
accessorFn: (row): number => row.memoryUsage,
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/namespaces#mem-usage-wss">
|
||||
Memory Usage (WSS)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.namespaceMemory,
|
||||
width: { min: 220 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
const memory = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={memory}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { listNodes } from 'api/generated/services/inframonitoring';
|
||||
import {
|
||||
InframonitoringtypesNodeRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { getK8sNodesList, K8sNodeData } from './api';
|
||||
import {
|
||||
getNodeMetricsQueryPayload,
|
||||
k8sNodeDetailsMetadataConfig,
|
||||
k8sNodeGetEntityName,
|
||||
k8sNodeGetSelectedItemFilters,
|
||||
k8sNodeInitialEventsFilter,
|
||||
k8sNodeInitialLogTracesFilter,
|
||||
k8sNodeGetSelectedItemExpression,
|
||||
k8sNodeInitialEventsExpression,
|
||||
k8sNodeInitialLogTracesExpression,
|
||||
nodeWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
@@ -28,66 +31,93 @@ function K8sNodesList({
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
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',
|
||||
};
|
||||
try {
|
||||
const response = await listNodes(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const response = await getK8sNodesList(
|
||||
filters,
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.payload?.data.records || [],
|
||||
total: response.payload?.data.total || 0,
|
||||
error: response.error,
|
||||
rawData: response.payload?.data,
|
||||
};
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch nodes';
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesNodeRecordDTO[],
|
||||
total: 0,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ data: K8sNodeData | null; error?: string | null }> => {
|
||||
const response = await getK8sNodesList(
|
||||
{
|
||||
filters: filters.filters,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
): Promise<{
|
||||
data: InframonitoringtypesNodeRecordDTO | null;
|
||||
error?: string | null;
|
||||
}> => {
|
||||
try {
|
||||
const response = await listNodes(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const records = response.payload?.data.records || [];
|
||||
|
||||
return {
|
||||
data: records.length > 0 ? records[0] : null,
|
||||
error: response.error,
|
||||
};
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch node';
|
||||
return {
|
||||
data: null,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<K8sNodeData>
|
||||
<K8sBaseList<InframonitoringtypesNodeRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.NODES}
|
||||
tableColumns={k8sNodesColumnsConfig}
|
||||
@@ -97,14 +127,14 @@ function K8sNodesList({
|
||||
eventCategory={InfraMonitoringEvents.Node}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails<K8sNodeData>
|
||||
<K8sBaseDetails<InframonitoringtypesNodeRecordDTO>
|
||||
category={InfraMonitoringEntity.NODES}
|
||||
eventCategory={InfraMonitoringEvents.Node}
|
||||
getSelectedItemFilters={k8sNodeGetSelectedItemFilters}
|
||||
getSelectedItemExpression={k8sNodeGetSelectedItemExpression}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sNodeGetEntityName}
|
||||
getInitialLogTracesFilters={k8sNodeInitialLogTracesFilter}
|
||||
getInitialEventsFilters={k8sNodeInitialEventsFilter}
|
||||
getInitialLogTracesExpression={k8sNodeInitialLogTracesExpression}
|
||||
getInitialEventsExpression={k8sNodeInitialEventsExpression}
|
||||
metadataConfig={k8sNodeDetailsMetadataConfig}
|
||||
entityWidgetInfo={nodeWidgetInfo}
|
||||
getEntityQueryPayload={getNodeMetricsQueryPayload}
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
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/types';
|
||||
|
||||
export interface K8sNodesListPayload {
|
||||
filters: TagFilter;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sNodeData {
|
||||
nodeUID: string;
|
||||
nodeCPUUsage: number;
|
||||
nodeCPUAllocatable: number;
|
||||
nodeMemoryUsage: number;
|
||||
nodeMemoryAllocatable: number;
|
||||
meta: {
|
||||
k8s_node_name: string;
|
||||
k8s_node_uid: string;
|
||||
k8s_cluster_name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sNodesListResponse {
|
||||
status: string;
|
||||
data: {
|
||||
type: string;
|
||||
records: K8sNodeData[];
|
||||
groups: null;
|
||||
total: number;
|
||||
sentAnyHostMetricsData: boolean;
|
||||
isSendingK8SAgentMetrics: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(H4ad): Erase this whole file when migrating to openapi
|
||||
export const nodesMetaMap = [
|
||||
{ dot: 'k8s.node.name', under: 'k8s_node_name' },
|
||||
{ dot: 'k8s.node.uid', under: 'k8s_node_uid' },
|
||||
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
|
||||
] as const;
|
||||
|
||||
export function mapNodesMeta(
|
||||
raw: Record<string, unknown>,
|
||||
): K8sNodeData['meta'] {
|
||||
const out: Record<string, unknown> = { ...raw };
|
||||
nodesMetaMap.forEach(({ dot, under }) => {
|
||||
if (dot in raw) {
|
||||
const v = raw[dot];
|
||||
out[under] = typeof v === 'string' ? v : raw[under];
|
||||
}
|
||||
});
|
||||
return out as K8sNodeData['meta'];
|
||||
}
|
||||
|
||||
export const getK8sNodesList = async (
|
||||
props: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
): Promise<SuccessResponse<K8sNodesListResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const requestProps =
|
||||
dotMetricsEnabled && 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);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as typeof props.filters.items,
|
||||
),
|
||||
},
|
||||
}
|
||||
: props;
|
||||
|
||||
const response = await axios.post('/nodes/list', requestProps, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
const payload: K8sNodesListResponse = response.data;
|
||||
|
||||
// one-liner to map dot→underscore
|
||||
payload.data.records = payload.data.records.map((record) => ({
|
||||
...record,
|
||||
meta: mapNodesMeta(record.meta as Record<string, unknown>),
|
||||
}));
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload,
|
||||
params: requestProps,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
@@ -1,67 +1,43 @@
|
||||
import { InframonitoringtypesNodeRecordDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
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 { K8sNodeData } from './api';
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
|
||||
export const k8sNodeGetSelectedItemFilters = (
|
||||
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
|
||||
|
||||
export const k8sNodeGetSelectedItemExpression = (
|
||||
selectedItemId: string,
|
||||
): TagFilter => {
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: 'k8s_node_name',
|
||||
key: {
|
||||
key: 'k8s_node_name',
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: selectedItemId,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
): string => `k8s.node.name = ${formatValueForExpression(selectedItemId)}`;
|
||||
|
||||
export const k8sNodeDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sNodeData>[] =
|
||||
export const k8sNodeDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesNodeRecordDTO>[] =
|
||||
[
|
||||
{ label: 'Node Name', getValue: (p): string => p.meta.k8s_node_name },
|
||||
{ label: 'Node Name', getValue: (p): string => p.nodeName || '' },
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
getValue: (p): string => p.meta.k8s_cluster_name,
|
||||
getValue: (p): string =>
|
||||
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sNodeInitialFilters = [
|
||||
QUERY_KEYS.K8S_NODE_NAME,
|
||||
QUERY_KEYS.K8S_CLUSTER_NAME,
|
||||
];
|
||||
export const k8sNodeInitialEventsExpression = (
|
||||
item: InframonitoringtypesNodeRecordDTO,
|
||||
): string =>
|
||||
`${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'Node' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${formatValueForExpression(item.nodeName || '')}`;
|
||||
|
||||
export const k8sNodeInitialEventsFilter = (
|
||||
item: K8sNodeData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Node'),
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.meta.k8s_node_name),
|
||||
];
|
||||
export const k8sNodeInitialLogTracesExpression = (
|
||||
item: InframonitoringtypesNodeRecordDTO,
|
||||
): string =>
|
||||
`${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME} = ${formatValueForExpression(item.nodeName || '')} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME} = ${formatValueForExpression(item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '')}`;
|
||||
|
||||
export const k8sNodeInitialLogTracesFilter = (
|
||||
item: K8sNodeData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_NODE_NAME, item.meta.k8s_node_name),
|
||||
createFilterItem(QUERY_KEYS.K8S_CLUSTER_NAME, item.meta.k8s_cluster_name),
|
||||
];
|
||||
|
||||
export const k8sNodeGetEntityName = (item: K8sNodeData): string =>
|
||||
item.meta.k8s_node_name;
|
||||
export const k8sNodeGetEntityName = (
|
||||
item: InframonitoringtypesNodeRecordDTO,
|
||||
): string => item.nodeName || '';
|
||||
|
||||
export const nodeWidgetInfo = [
|
||||
{
|
||||
@@ -107,7 +83,7 @@ export const nodeWidgetInfo = [
|
||||
];
|
||||
|
||||
export const getNodeMetricsQueryPayload = (
|
||||
node: K8sNodeData,
|
||||
node: InframonitoringtypesNodeRecordDTO,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
@@ -222,7 +198,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -261,7 +237,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -300,7 +276,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -339,7 +315,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -378,7 +354,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -451,7 +427,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -490,7 +466,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -529,7 +505,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -568,7 +544,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -607,7 +583,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -646,7 +622,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -685,7 +661,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -758,7 +734,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -797,7 +773,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -836,7 +812,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -922,7 +898,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -961,7 +937,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1000,7 +976,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1086,7 +1062,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1166,7 +1142,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1246,7 +1222,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1332,7 +1308,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1418,7 +1394,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1457,7 +1433,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1496,7 +1472,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1569,7 +1545,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1608,7 +1584,7 @@ export const getNodeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: node.meta.k8s_node_name,
|
||||
value: node.nodeName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
|
||||
@@ -1,38 +1,56 @@
|
||||
import { Tooltip } from 'antd';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Badge, BadgeColor } from '@signozhq/ui/badge';
|
||||
import { InframonitoringtypesNodeRecordDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
|
||||
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
|
||||
|
||||
import ColumnHeader from '../Base/ColumnHeader';
|
||||
import EntityGroupHeader from '../Base/EntityGroupHeader';
|
||||
import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { formatBytes } from '../commonUtils';
|
||||
import { ValidateColumnValueWrapper } from '../components';
|
||||
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
|
||||
import {
|
||||
CellValueTooltip,
|
||||
GroupedStatusCounts,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../components';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { K8sNodeData, K8sNodesListPayload } from './api';
|
||||
import { Workflow } from '@signozhq/icons';
|
||||
|
||||
export function getK8sNodeRowKey(node: K8sNodeData): string {
|
||||
return node.nodeUID || node.meta.k8s_node_uid || node.meta.k8s_node_name;
|
||||
export function getK8sNodeRowKey(
|
||||
node: InframonitoringtypesNodeRecordDTO,
|
||||
): string {
|
||||
return (
|
||||
node.nodeName || node.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NODE_UID] || ''
|
||||
);
|
||||
}
|
||||
|
||||
export function getK8sNodeItemKey(node: K8sNodeData): string {
|
||||
return node.meta.k8s_node_name;
|
||||
export function getK8sNodeItemKey(
|
||||
node: InframonitoringtypesNodeRecordDTO,
|
||||
): string {
|
||||
return node.nodeName;
|
||||
}
|
||||
|
||||
export const getK8sNodesListQuery = (): K8sNodesListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'and',
|
||||
},
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
const NODE_CONDITION_COLORS: Record<string, BadgeColor> = {
|
||||
ready: 'forest',
|
||||
not_ready: 'amber',
|
||||
no_data: 'secondary',
|
||||
};
|
||||
|
||||
export const k8sNodesColumnsConfig: TableColumnDef<K8sNodeData>[] = [
|
||||
const NODE_CONDITION_LABEL_MAP: Record<string, string> = {
|
||||
ready: 'Ready',
|
||||
not_ready: 'Not Ready',
|
||||
no_data: 'No Data',
|
||||
};
|
||||
|
||||
export type NodeTableColumnConfig =
|
||||
TableColumnDef<InframonitoringtypesNodeRecordDTO>;
|
||||
export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
|
||||
{
|
||||
id: 'nodeGroup',
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="NODE GROUP" />,
|
||||
accessorFn: (row): string => row.meta.k8s_node_name || '',
|
||||
width: { min: 300 },
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="Node Group" />,
|
||||
accessorFn: (row): string => row.nodeName || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
@@ -55,9 +73,10 @@ export const k8sNodesColumnsConfig: TableColumnDef<K8sNodeData>[] = [
|
||||
<EntityGroupHeader
|
||||
title="Node Name"
|
||||
icon={<Workflow data-hide-expanded="true" size={14} />}
|
||||
docPath="/infrastructure-monitoring/kubernetes/nodes#node-name"
|
||||
/>
|
||||
),
|
||||
accessorFn: (row): string => row.meta.k8s_node_name || '',
|
||||
accessorFn: (row): string => row.nodeName || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
@@ -67,65 +86,146 @@ export const k8sNodesColumnsConfig: TableColumnDef<K8sNodeData>[] = [
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const nodeName = value as string;
|
||||
return (
|
||||
<Tooltip title={nodeName}>
|
||||
<CellValueTooltip value={nodeName}>
|
||||
<TanStackTable.Text>{nodeName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
</CellValueTooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'condition',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#node-condition">
|
||||
Status
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): string => row.condition,
|
||||
width: { min: 120 },
|
||||
enableSort: false,
|
||||
cell: ({ row, groupMeta }): React.ReactNode => {
|
||||
if (!groupMeta) {
|
||||
const color =
|
||||
NODE_CONDITION_COLORS[row.condition] || NODE_CONDITION_COLORS.no_data;
|
||||
return (
|
||||
<Badge color={color} variant="outline">
|
||||
{NODE_CONDITION_LABEL_MAP[row.condition] || 'Unknown'}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GroupedStatusCounts
|
||||
items={[
|
||||
{
|
||||
value: row.nodeCountsByReadiness?.ready ?? 0,
|
||||
label: 'Ready',
|
||||
color: Color.BG_FOREST_500,
|
||||
},
|
||||
{
|
||||
value: row.nodeCountsByReadiness?.notReady ?? 0,
|
||||
label: 'Not Ready',
|
||||
color: Color.BG_AMBER_500,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'podCountsByPhase',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#pod-counts-by-phase">
|
||||
Pod Phases
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): InframonitoringtypesNodeRecordDTO['podCountsByPhase'] =>
|
||||
row.podCountsByPhase,
|
||||
width: { min: 250 },
|
||||
enableSort: false,
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
const podCountsByPhase = row.podCountsByPhase;
|
||||
if (!podCountsByPhase) {
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<GroupedStatusCounts items={getPodPhaseStatusItems(row.podCountsByPhase)} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'clusterName',
|
||||
header: 'Cluster Name',
|
||||
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#cluster-name">
|
||||
Cluster Name
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
|
||||
width: { min: 150, default: 150 },
|
||||
enableSort: false,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => (
|
||||
<TanStackTable.Text>{value as string}</TanStackTable.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'cpu',
|
||||
header: 'CPU Usage (cores)',
|
||||
accessorFn: (row): number => row.nodeCPUUsage,
|
||||
width: { min: 200, default: 200 },
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#cpu-usage-cores">
|
||||
CPU Usage
|
||||
<br /> (cores)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.nodeCPU,
|
||||
width: { min: 160, default: 160 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = value as number;
|
||||
const cpu = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpu}
|
||||
entity={InfraMonitoringEntity.NODES}
|
||||
attribute="CPU metric"
|
||||
>
|
||||
<TanStackTable.Text>{cpu}</TanStackTable.Text>
|
||||
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cpu_allocatable',
|
||||
header: 'CPU Alloc (cores)',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#cpu-alloc-cores">
|
||||
CPU Allocatable
|
||||
<br /> (cores)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.nodeCPUAllocatable,
|
||||
width: { min: 200, default: 200 },
|
||||
width: { min: 160, default: 190 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuAllocatable = value as number;
|
||||
const cpuAllocatable = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpuAllocatable}
|
||||
entity={InfraMonitoringEntity.NODES}
|
||||
attribute="CPU allocatable metric"
|
||||
>
|
||||
<TanStackTable.Text>{cpuAllocatable}</TanStackTable.Text>
|
||||
<TanStackTable.Text>{cpuAllocatable.toFixed(2)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory',
|
||||
header: 'Memory Usage (WSS)',
|
||||
accessorFn: (row): number => row.nodeMemoryUsage,
|
||||
width: { min: 240, default: 240 },
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#memory-usage-wss">
|
||||
Memory Usage
|
||||
<br /> (WSS)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.nodeMemory,
|
||||
width: { min: 220, default: 220 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
@@ -142,9 +242,14 @@ export const k8sNodesColumnsConfig: TableColumnDef<K8sNodeData>[] = [
|
||||
},
|
||||
{
|
||||
id: 'memory_allocatable',
|
||||
header: 'Memory Allocatable',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#memory-allocatable">
|
||||
Memory
|
||||
<br /> Allocatable
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.nodeMemoryAllocatable,
|
||||
width: { min: 240, default: 240 },
|
||||
width: { min: 200, default: 200 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryAllocatable = value as number;
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { listPods } from 'api/generated/services/inframonitoring';
|
||||
import {
|
||||
InframonitoringtypesPodRecordDTO,
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { getK8sPodsList, K8sPodsData } from './api';
|
||||
import {
|
||||
getPodMetricsQueryPayload,
|
||||
k8sPodDetailsMetadataConfig,
|
||||
k8sPodGetEntityName,
|
||||
k8sPodGetSelectedItemFilters,
|
||||
k8sPodInitialEventsFilter,
|
||||
k8sPodInitialLogTracesFilter,
|
||||
k8sPodGetSelectedItemExpression,
|
||||
k8sPodInitialEventsExpression,
|
||||
k8sPodInitialLogTracesExpression,
|
||||
podWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
@@ -28,66 +31,93 @@ function K8sPodsList({
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
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',
|
||||
};
|
||||
try {
|
||||
const response = await listPods(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const response = await getK8sPodsList(
|
||||
filters,
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.payload?.data.records || [],
|
||||
total: response.payload?.data.total || 0,
|
||||
error: response.error,
|
||||
rawData: response.payload?.data,
|
||||
};
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch pods';
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesPodRecordDTO[],
|
||||
total: 0,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ data: K8sPodsData | null; error?: string | null }> => {
|
||||
const response = await getK8sPodsList(
|
||||
{
|
||||
filters: filters.filters,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
): Promise<{
|
||||
data: InframonitoringtypesPodRecordDTO | null;
|
||||
error?: string | null;
|
||||
}> => {
|
||||
try {
|
||||
const response = await listPods(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const records = response.payload?.data.records || [];
|
||||
|
||||
return {
|
||||
data: records.length > 0 ? records[0] : null,
|
||||
error: response.error,
|
||||
};
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch pod';
|
||||
return {
|
||||
data: null,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<K8sPodsData>
|
||||
<K8sBaseList<InframonitoringtypesPodRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.PODS}
|
||||
tableColumns={k8sPodColumnsConfig}
|
||||
@@ -97,14 +127,14 @@ function K8sPodsList({
|
||||
eventCategory={InfraMonitoringEvents.Pod}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails<K8sPodsData>
|
||||
<K8sBaseDetails<InframonitoringtypesPodRecordDTO>
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
eventCategory={InfraMonitoringEvents.Pod}
|
||||
getSelectedItemFilters={k8sPodGetSelectedItemFilters}
|
||||
getSelectedItemExpression={k8sPodGetSelectedItemExpression}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sPodGetEntityName}
|
||||
getInitialLogTracesFilters={k8sPodInitialLogTracesFilter}
|
||||
getInitialEventsFilters={k8sPodInitialEventsFilter}
|
||||
getInitialLogTracesExpression={k8sPodInitialLogTracesExpression}
|
||||
getInitialEventsExpression={k8sPodInitialEventsExpression}
|
||||
metadataConfig={k8sPodDetailsMetadataConfig}
|
||||
entityWidgetInfo={podWidgetInfo}
|
||||
getEntityQueryPayload={getPodMetricsQueryPayload}
|
||||
|
||||
148
frontend/src/container/InfraMonitoringK8sV2/Pods/api.ts
generated
148
frontend/src/container/InfraMonitoringK8sV2/Pods/api.ts
generated
@@ -1,148 +0,0 @@
|
||||
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 { K8sBaseFilters } from '../Base/types';
|
||||
|
||||
// TODO(H4ad): Erase this whole file when migrating to openapi
|
||||
export const podsMetaMap = [
|
||||
{ dot: 'k8s.cronjob.name', under: 'k8s_cronjob_name' },
|
||||
{ dot: 'k8s.daemonset.name', under: 'k8s_daemonset_name' },
|
||||
{ dot: 'k8s.deployment.name', under: 'k8s_deployment_name' },
|
||||
{ dot: 'k8s.job.name', under: 'k8s_job_name' },
|
||||
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
|
||||
{ dot: 'k8s.node.name', under: 'k8s_node_name' },
|
||||
{ dot: 'k8s.pod.name', under: 'k8s_pod_name' },
|
||||
{ dot: 'k8s.pod.uid', under: 'k8s_pod_uid' },
|
||||
{ dot: 'k8s.statefulset.name', under: 'k8s_statefulset_name' },
|
||||
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
|
||||
] as const;
|
||||
|
||||
export function mapPodsMeta(raw: Record<string, unknown>): K8sPodsData['meta'] {
|
||||
// clone everything
|
||||
const out: Record<string, unknown> = { ...raw };
|
||||
// overlay only the dot→under mappings
|
||||
podsMetaMap.forEach(({ dot, under }) => {
|
||||
if (dot in raw) {
|
||||
const v = raw[dot];
|
||||
out[under] = typeof v === 'string' ? v : raw[under];
|
||||
}
|
||||
});
|
||||
return out as K8sPodsData['meta'];
|
||||
}
|
||||
|
||||
export interface TimeSeriesValue {
|
||||
timestamp: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface TimeSeries {
|
||||
labels: Record<string, string>;
|
||||
labelsArray: Array<Record<string, string>>;
|
||||
values: TimeSeriesValue[];
|
||||
}
|
||||
|
||||
export interface K8sPodsData {
|
||||
podUID: string;
|
||||
podCPU: number;
|
||||
podCPURequest: number;
|
||||
podCPULimit: number;
|
||||
podMemory: number;
|
||||
podMemoryRequest: number;
|
||||
podMemoryLimit: number;
|
||||
restartCount: number;
|
||||
meta: {
|
||||
k8s_cronjob_name: string;
|
||||
k8s_daemonset_name: string;
|
||||
k8s_deployment_name: string;
|
||||
k8s_job_name: string;
|
||||
k8s_namespace_name: string;
|
||||
k8s_node_name: string;
|
||||
k8s_pod_name: string;
|
||||
k8s_pod_uid: string;
|
||||
k8s_statefulset_name: string;
|
||||
k8s_cluster_name: string;
|
||||
};
|
||||
countByPhase: {
|
||||
pending: number;
|
||||
running: number;
|
||||
succeeded: number;
|
||||
failed: number;
|
||||
unknown: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sPodsListResponse {
|
||||
status: string;
|
||||
data: {
|
||||
type: string;
|
||||
records: K8sPodsData[];
|
||||
groups: null;
|
||||
total: number;
|
||||
sentAnyHostMetricsData: boolean;
|
||||
isSendingK8SAgentMetrics: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const getK8sPodsList = async (
|
||||
props: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
): Promise<SuccessResponse<K8sPodsListResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const requestProps = dotMetricsEnabled
|
||||
? {
|
||||
...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);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as typeof props.filters.items,
|
||||
),
|
||||
},
|
||||
}
|
||||
: props;
|
||||
|
||||
const response = await axios.post('/pods/list', requestProps, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
const payload: K8sPodsListResponse = response.data;
|
||||
payload.data.records = payload.data.records.map((record) => ({
|
||||
...record,
|
||||
meta: mapPodsMeta(record.meta as Record<string, unknown>),
|
||||
}));
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload,
|
||||
params: requestProps,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
@@ -1,62 +1,59 @@
|
||||
import { InframonitoringtypesPodRecordDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
createFilterItem,
|
||||
K8sDetailsMetadataConfig,
|
||||
} from '../Base/K8sBaseDetails';
|
||||
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
|
||||
import { K8sPodsData } from './api';
|
||||
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
|
||||
|
||||
export const k8sPodGetSelectedItemFilters = (
|
||||
export const k8sPodGetSelectedItemExpression = (
|
||||
selectedItemId: string,
|
||||
): TagFilter => {
|
||||
return {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: 'k8s_pod_uid',
|
||||
key: {
|
||||
key: 'k8s_pod_uid',
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: selectedItemId,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
): string => `k8s.pod.uid = ${formatValueForExpression(selectedItemId)}`;
|
||||
|
||||
export const k8sPodDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sPodsData>[] =
|
||||
export const k8sPodDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesPodRecordDTO>[] =
|
||||
[
|
||||
{ label: 'NAMESPACE', getValue: (p): string => p.meta.k8s_namespace_name },
|
||||
{
|
||||
label: 'NAMESPACE',
|
||||
getValue: (p): string =>
|
||||
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
getValue: (p): string => p.meta.k8s_cluster_name,
|
||||
getValue: (p): string =>
|
||||
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
|
||||
},
|
||||
{
|
||||
label: 'Node',
|
||||
getValue: (p): string =>
|
||||
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME] || '',
|
||||
},
|
||||
{ label: 'Node', getValue: (p): string => p.meta.k8s_node_name },
|
||||
];
|
||||
|
||||
export const k8sPodInitialEventsFilter = (
|
||||
pod: K8sPodsData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'Pod'),
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, pod.meta.k8s_pod_name),
|
||||
];
|
||||
export const k8sPodInitialEventsExpression = (
|
||||
pod: InframonitoringtypesPodRecordDTO,
|
||||
): string => {
|
||||
const podName = formatValueForExpression(
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
);
|
||||
return `${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'Pod' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${podName}`;
|
||||
};
|
||||
|
||||
export const k8sPodInitialLogTracesFilter = (
|
||||
pod: K8sPodsData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_POD_NAME, pod.meta.k8s_pod_name),
|
||||
];
|
||||
export const k8sPodInitialLogTracesExpression = (
|
||||
pod: InframonitoringtypesPodRecordDTO,
|
||||
): string => {
|
||||
const podName = formatValueForExpression(
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
);
|
||||
return `${INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME} = ${podName}`;
|
||||
};
|
||||
|
||||
export const k8sPodGetEntityName = (pod: K8sPodsData): string =>
|
||||
pod.meta.k8s_pod_name;
|
||||
export const k8sPodGetEntityName = (
|
||||
pod: InframonitoringtypesPodRecordDTO,
|
||||
): string => pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '';
|
||||
|
||||
export const podWidgetInfo = [
|
||||
{
|
||||
@@ -114,7 +111,7 @@ export const podWidgetInfo = [
|
||||
];
|
||||
|
||||
export const getPodMetricsQueryPayload = (
|
||||
pod: K8sPodsData,
|
||||
pod: InframonitoringtypesPodRecordDTO,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
@@ -224,9 +221,15 @@ export const getPodMetricsQueryPayload = (
|
||||
'k8s_pod_filesystem_usage',
|
||||
);
|
||||
|
||||
const k8sPodNameKey = getKey('k8s.pod.name', 'k8s_pod_name');
|
||||
const k8sPodNameKey = getKey(
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME,
|
||||
'k8s_pod_name',
|
||||
);
|
||||
|
||||
const k8sNamespaceNameKey = getKey('k8s.namespace.name', 'k8s_namespace_name');
|
||||
const k8sNamespaceNameKey = getKey(
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME,
|
||||
'k8s_namespace_name',
|
||||
);
|
||||
return [
|
||||
{
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
@@ -256,7 +259,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '067b2dc4',
|
||||
@@ -267,7 +270,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -306,7 +310,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '379af416',
|
||||
@@ -317,7 +321,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -356,7 +361,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '39ee0dbd',
|
||||
@@ -367,7 +372,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -440,7 +446,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '755c8a9d',
|
||||
@@ -451,7 +458,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -490,7 +497,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '1e3d01ee',
|
||||
@@ -501,7 +509,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -540,7 +548,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '0c8b2662',
|
||||
@@ -551,7 +560,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -590,7 +599,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: 'e915da76',
|
||||
@@ -601,7 +611,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -640,7 +650,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '32c15c03',
|
||||
@@ -651,7 +662,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -690,7 +701,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '703fced1',
|
||||
@@ -701,7 +713,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -774,7 +786,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '5418405c',
|
||||
@@ -785,7 +798,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -824,7 +837,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '1ea5c602',
|
||||
@@ -835,7 +849,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -874,7 +888,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: 'd7632974',
|
||||
@@ -885,7 +900,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -958,7 +973,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '5a822bec',
|
||||
@@ -969,7 +985,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1008,7 +1024,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '3362c603',
|
||||
@@ -1019,7 +1036,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1058,7 +1075,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: 'ce88008b',
|
||||
@@ -1069,7 +1087,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1108,7 +1126,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '53fc92fd',
|
||||
@@ -1119,7 +1138,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1158,7 +1177,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '61dfa9f6',
|
||||
@@ -1169,7 +1189,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1208,7 +1228,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '508cdf26',
|
||||
@@ -1219,7 +1240,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1292,7 +1313,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '80528f79',
|
||||
@@ -1303,7 +1325,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1342,7 +1364,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '780cc786',
|
||||
@@ -1353,7 +1376,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1433,7 +1456,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '8b2a539b',
|
||||
@@ -1444,7 +1468,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1517,7 +1541,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '4b4382be',
|
||||
@@ -1528,7 +1553,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1608,7 +1633,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '9301d7c0',
|
||||
@@ -1619,7 +1645,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1665,7 +1691,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '92b99374',
|
||||
@@ -1676,7 +1703,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1722,7 +1749,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '1c7de95d',
|
||||
@@ -1733,7 +1761,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1826,7 +1854,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '964fd905',
|
||||
@@ -1837,7 +1866,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1883,7 +1912,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '76a586be',
|
||||
@@ -1894,7 +1924,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -1940,7 +1970,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '7c5f875b',
|
||||
@@ -1951,7 +1982,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -2031,7 +2062,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '80216712',
|
||||
@@ -2042,7 +2074,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -2088,7 +2120,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '84b59a9f',
|
||||
@@ -2099,7 +2132,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -2145,7 +2178,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: 'd649ad0c',
|
||||
@@ -2156,7 +2190,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -2249,7 +2283,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: 'bc9c5cf3',
|
||||
@@ -2260,7 +2295,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -2346,7 +2381,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: 'b54a3d78',
|
||||
@@ -2357,7 +2393,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -2443,7 +2479,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '1a97cb95',
|
||||
@@ -2454,7 +2491,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -2493,7 +2530,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: 'ddfbd379',
|
||||
@@ -2504,7 +2542,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -2543,7 +2581,8 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_namespace_name,
|
||||
value:
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: '1963fc96',
|
||||
@@ -2554,7 +2593,7 @@ export const getPodMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: pod.meta.k8s_pod_name,
|
||||
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -2562,7 +2601,7 @@ export const getPodMetricsQueryPayload = (
|
||||
functions: [],
|
||||
groupBy: [],
|
||||
having: [],
|
||||
legend: 'Uage',
|
||||
legend: 'Usage',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'C',
|
||||
|
||||
@@ -1,30 +1,63 @@
|
||||
import { Tooltip } from 'antd';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { Container } from '@signozhq/icons';
|
||||
import { Badge, BadgeColor } from '@signozhq/ui/badge';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import {
|
||||
InframonitoringtypesPodPhaseDTO,
|
||||
InframonitoringtypesPodRecordDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
|
||||
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
|
||||
|
||||
import ColumnHeader from '../Base/ColumnHeader';
|
||||
import EntityGroupHeader from '../Base/EntityGroupHeader';
|
||||
import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { formatBytes } from '../commonUtils';
|
||||
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { K8sPodsData } from './api';
|
||||
import { Container } from '@signozhq/icons';
|
||||
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
|
||||
import {
|
||||
CellValueTooltip,
|
||||
EntityProgressBar,
|
||||
GroupedStatusCounts,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../components';
|
||||
import {
|
||||
INFRA_MONITORING_ATTR_KEYS,
|
||||
InfraMonitoringEntity,
|
||||
} from '../constants';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { formatAge } from 'utils/timeUtils';
|
||||
|
||||
export function getK8sPodRowKey(pod: K8sPodsData): string {
|
||||
return pod.podUID || pod.meta.k8s_pod_uid || pod.meta.k8s_pod_name;
|
||||
export function getK8sPodRowKey(pod: InframonitoringtypesPodRecordDTO): string {
|
||||
return (
|
||||
pod.podUID ||
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_UID] ||
|
||||
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] ||
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
export function getK8sPodItemKey(pod: K8sPodsData): string {
|
||||
export function getK8sPodItemKey(
|
||||
pod: InframonitoringtypesPodRecordDTO,
|
||||
): string {
|
||||
return pod.podUID;
|
||||
}
|
||||
|
||||
export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
|
||||
const POD_PHASE_COLORS: Record<string, BadgeColor> = {
|
||||
running: 'forest',
|
||||
pending: 'amber',
|
||||
succeeded: 'robin',
|
||||
failed: 'cherry',
|
||||
unknown: 'vanilla',
|
||||
no_data: 'vanilla',
|
||||
};
|
||||
|
||||
export type PodTableColumnConfig =
|
||||
TableColumnDef<InframonitoringtypesPodRecordDTO>;
|
||||
export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
{
|
||||
id: 'podGroup',
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="POD GROUP" />,
|
||||
accessorFn: (row): string => row.meta.k8s_pod_name || '',
|
||||
width: { min: 300 },
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="Pod Group" />,
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
@@ -47,9 +80,11 @@ export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
|
||||
<EntityGroupHeader
|
||||
title="Pod Name"
|
||||
icon={<Container data-hide-expanded="true" size={14} />}
|
||||
docPath="/infrastructure-monitoring/kubernetes/pods#pod-name"
|
||||
/>
|
||||
),
|
||||
accessorFn: (row): string => row.meta.k8s_pod_name || '',
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
@@ -59,18 +94,92 @@ export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const podName = value as string;
|
||||
return (
|
||||
<Tooltip title={podName}>
|
||||
<CellValueTooltip value={podName}>
|
||||
<TanStackTable.Text>{podName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
</CellValueTooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'podPhase',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#pod-phase">
|
||||
Phase
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): string => row.podPhase,
|
||||
width: { min: 120 },
|
||||
enableSort: false,
|
||||
visibilityBehavior: 'hidden-on-expand',
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
if (!row.podPhase) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const color = POD_PHASE_COLORS[row.podPhase] || POD_PHASE_COLORS.unknown;
|
||||
const label =
|
||||
row.podPhase === InframonitoringtypesPodPhaseDTO.no_data
|
||||
? 'No Data'
|
||||
: row.podPhase.charAt(0).toUpperCase() + row.podPhase.slice(1);
|
||||
return (
|
||||
<Badge color={color} variant="outline">
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'podCountsByPhase',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#pod-phase">
|
||||
Phases
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): InframonitoringtypesPodRecordDTO['podCountsByPhase'] =>
|
||||
row.podCountsByPhase,
|
||||
width: { min: 250 },
|
||||
enableSort: false,
|
||||
visibilityBehavior: 'hidden-on-collapse',
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
const podCountsByPhase = row.podCountsByPhase;
|
||||
if (!podCountsByPhase) {
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<GroupedStatusCounts items={getPodPhaseStatusItems(row.podCountsByPhase)} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'podAge',
|
||||
header: 'Age',
|
||||
accessorFn: (row): number => row.podAge,
|
||||
width: { min: 100 },
|
||||
enableSort: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const age = value as number;
|
||||
if (age === -1) {
|
||||
return (
|
||||
<TooltipSimple title="No data">
|
||||
<Typography.Text>-</Typography.Text>
|
||||
</TooltipSimple>
|
||||
);
|
||||
}
|
||||
return <TanStackTable.Text>{formatAge(age)}</TanStackTable.Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cpu_request',
|
||||
header: 'CPU Req Usage (%)',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#cpu-req-usage-">
|
||||
CPU Request
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.podCPURequest,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuRequest = value as number;
|
||||
return (
|
||||
@@ -86,9 +195,14 @@ export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
|
||||
},
|
||||
{
|
||||
id: 'cpu_limit',
|
||||
header: 'CPU Limit Usage (%)',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#cpu-limit-usage-">
|
||||
CPU Limit
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.podCPULimit,
|
||||
width: { min: 210 },
|
||||
width: { min: 220 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuLimit = value as number;
|
||||
@@ -105,29 +219,40 @@ export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
|
||||
},
|
||||
{
|
||||
id: 'cpu',
|
||||
header: 'CPU Usage (cores)',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#cpu-usage-cores">
|
||||
CPU Usage
|
||||
<br /> (cores)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.podCPU,
|
||||
width: { min: 210 },
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = value as number;
|
||||
const cpu = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpu}
|
||||
entity={InfraMonitoringEntity.PODS}
|
||||
attribute="CPU metric"
|
||||
>
|
||||
<TanStackTable.Text>{cpu}</TanStackTable.Text>
|
||||
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory_request',
|
||||
header: 'Mem Req Usage (%)',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#mem-req-usage-">
|
||||
Memory Request
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.podMemoryRequest,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryRequest = value as number;
|
||||
return (
|
||||
@@ -143,9 +268,14 @@ export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
|
||||
},
|
||||
{
|
||||
id: 'memory_limit',
|
||||
header: 'Mem Limit Usage (%)',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#mem-limit-usage-">
|
||||
Memory Limit
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.podMemoryLimit,
|
||||
width: { min: 210 },
|
||||
width: { min: 220 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryLimit = value as number;
|
||||
@@ -162,7 +292,12 @@ export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
|
||||
},
|
||||
{
|
||||
id: 'memory',
|
||||
header: 'Mem Usage (WSS)',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#mem-usage-wss">
|
||||
Memory Usage
|
||||
<br /> (WSS)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.podMemory,
|
||||
width: { min: 210, default: '100%' },
|
||||
enableSort: true,
|
||||
@@ -182,7 +317,8 @@ export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
|
||||
{
|
||||
id: 'namespace',
|
||||
header: 'Namespace',
|
||||
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
width: { default: 100 },
|
||||
enableSort: false,
|
||||
defaultVisibility: false,
|
||||
@@ -193,7 +329,8 @@ export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
|
||||
{
|
||||
id: 'node',
|
||||
header: 'Node',
|
||||
accessorFn: (row): string => row.meta.k8s_node_name || '',
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME] || '',
|
||||
width: { default: 100 },
|
||||
enableSort: false,
|
||||
defaultVisibility: false,
|
||||
@@ -204,7 +341,8 @@ export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
|
||||
{
|
||||
id: 'cluster',
|
||||
header: 'Cluster',
|
||||
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
|
||||
width: { default: 100 },
|
||||
enableSort: false,
|
||||
defaultVisibility: false,
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { listStatefulSets } from 'api/generated/services/inframonitoring';
|
||||
import {
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
InframonitoringtypesStatefulSetRecordDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { getK8sStatefulSetsList, K8sStatefulSetsData } from './api';
|
||||
import {
|
||||
getStatefulSetMetricsQueryPayload,
|
||||
k8sStatefulSetDetailsMetadataConfig,
|
||||
k8sStatefulSetGetEntityName,
|
||||
k8sStatefulSetGetSelectedItemFilters,
|
||||
k8sStatefulSetInitialEventsFilter,
|
||||
k8sStatefulSetInitialLogTracesFilter,
|
||||
k8sStatefulSetGetSelectedItemExpression,
|
||||
k8sStatefulSetInitialEventsExpression,
|
||||
k8sStatefulSetInitialLogTracesExpression,
|
||||
statefulSetWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
@@ -28,66 +31,93 @@ function K8sStatefulSetsList({
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
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',
|
||||
};
|
||||
try {
|
||||
const response = await listStatefulSets(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const response = await getK8sStatefulSetsList(
|
||||
filters,
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.payload?.data.records || [],
|
||||
total: response.payload?.data.total || 0,
|
||||
error: response.error,
|
||||
rawData: response.payload?.data,
|
||||
};
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch statefulsets';
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesStatefulSetRecordDTO[],
|
||||
total: 0,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
[],
|
||||
);
|
||||
|
||||
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,
|
||||
);
|
||||
): Promise<{
|
||||
data: InframonitoringtypesStatefulSetRecordDTO | null;
|
||||
error?: string | null;
|
||||
}> => {
|
||||
try {
|
||||
const response = await listStatefulSets(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const records = response.payload?.data.records || [];
|
||||
|
||||
return {
|
||||
data: records.length > 0 ? records[0] : null,
|
||||
error: response.error,
|
||||
};
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch statefulset';
|
||||
return {
|
||||
data: null,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<K8sStatefulSetsData>
|
||||
<K8sBaseList<InframonitoringtypesStatefulSetRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
tableColumns={k8sStatefulSetsColumnsConfig}
|
||||
@@ -97,14 +127,14 @@ function K8sStatefulSetsList({
|
||||
eventCategory={InfraMonitoringEvents.StatefulSet}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails<K8sStatefulSetsData>
|
||||
<K8sBaseDetails<InframonitoringtypesStatefulSetRecordDTO>
|
||||
category={InfraMonitoringEntity.STATEFULSETS}
|
||||
eventCategory={InfraMonitoringEvents.StatefulSet}
|
||||
getSelectedItemFilters={k8sStatefulSetGetSelectedItemFilters}
|
||||
getSelectedItemExpression={k8sStatefulSetGetSelectedItemExpression}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sStatefulSetGetEntityName}
|
||||
getInitialLogTracesFilters={k8sStatefulSetInitialLogTracesFilter}
|
||||
getInitialEventsFilters={k8sStatefulSetInitialEventsFilter}
|
||||
getInitialLogTracesExpression={k8sStatefulSetInitialLogTracesExpression}
|
||||
getInitialEventsExpression={k8sStatefulSetInitialEventsExpression}
|
||||
metadataConfig={k8sStatefulSetDetailsMetadataConfig}
|
||||
entityWidgetInfo={statefulSetWidgetInfo}
|
||||
getEntityQueryPayload={getStatefulSetMetricsQueryPayload}
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
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/types';
|
||||
|
||||
export interface K8sStatefulSetsListPayload {
|
||||
filters: TagFilter;
|
||||
groupBy?: BaseAutocompleteData[];
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sStatefulSetsData {
|
||||
statefulSetName: string;
|
||||
cpuUsage: number;
|
||||
memoryUsage: number;
|
||||
desiredPods: number;
|
||||
availablePods: number;
|
||||
cpuRequest: number;
|
||||
memoryRequest: number;
|
||||
cpuLimit: number;
|
||||
memoryLimit: number;
|
||||
restarts: number;
|
||||
meta: {
|
||||
k8s_statefulset_name: string;
|
||||
k8s_namespace_name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sStatefulSetsListResponse {
|
||||
status: string;
|
||||
data: {
|
||||
type: string;
|
||||
records: K8sStatefulSetsData[];
|
||||
groups: null;
|
||||
total: number;
|
||||
sentAnyHostMetricsData: boolean;
|
||||
isSendingK8SAgentMetrics: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(H4ad): Erase this whole file when migrating to openapi
|
||||
export const statefulSetsMetaMap = [
|
||||
{ dot: 'k8s.statefulset.name', under: 'k8s_statefulset_name' },
|
||||
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
|
||||
] as const;
|
||||
|
||||
export function mapStatefulSetsMeta(
|
||||
raw: Record<string, unknown>,
|
||||
): K8sStatefulSetsData['meta'] {
|
||||
const out: Record<string, unknown> = { ...raw };
|
||||
statefulSetsMetaMap.forEach(({ dot, under }) => {
|
||||
if (dot in raw) {
|
||||
const v = raw[dot];
|
||||
out[under] = typeof v === 'string' ? v : raw[under];
|
||||
}
|
||||
});
|
||||
return out as K8sStatefulSetsData['meta'];
|
||||
}
|
||||
|
||||
export const getK8sStatefulSetsList = async (
|
||||
props: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
): Promise<SuccessResponse<K8sStatefulSetsListResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const requestProps = dotMetricsEnabled
|
||||
? {
|
||||
...props,
|
||||
filters: {
|
||||
...props.filters,
|
||||
items: props.filters.items.reduce<TagFilter['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);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as TagFilter['items'],
|
||||
),
|
||||
},
|
||||
}
|
||||
: props;
|
||||
|
||||
const response = await axios.post('/statefulsets/list', requestProps, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
const payload: K8sStatefulSetsListResponse = response.data;
|
||||
|
||||
payload.data.records = payload.data.records.map((record) => ({
|
||||
...record,
|
||||
meta: mapStatefulSetsMeta(record.meta as Record<string, unknown>),
|
||||
}));
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload,
|
||||
params: requestProps,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
@@ -1,67 +1,58 @@
|
||||
import { InframonitoringtypesStatefulSetRecordDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
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';
|
||||
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
|
||||
|
||||
export const k8sStatefulSetGetSelectedItemFilters = (
|
||||
export const k8sStatefulSetGetSelectedItemExpression = (
|
||||
selectedItemId: string,
|
||||
): TagFilter => ({
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: 'k8s_statefulset_name',
|
||||
key: {
|
||||
key: 'k8s_statefulset_name',
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: selectedItemId,
|
||||
},
|
||||
],
|
||||
});
|
||||
): string =>
|
||||
`${INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME} = ${formatValueForExpression(selectedItemId)}`;
|
||||
|
||||
export const k8sStatefulSetDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sStatefulSetsData>[] =
|
||||
export const k8sStatefulSetDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesStatefulSetRecordDTO>[] =
|
||||
[
|
||||
{
|
||||
label: 'Statefulset Name',
|
||||
getValue: (p): string => p.meta.k8s_statefulset_name,
|
||||
getValue: (p): string =>
|
||||
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ?? '',
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
getValue: (p): string => p.meta.k8s_namespace_name,
|
||||
getValue: (p): string =>
|
||||
p.meta?.[INFRA_MONITORING_ATTR_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 k8sStatefulSetInitialEventsExpression = (
|
||||
item: InframonitoringtypesStatefulSetRecordDTO,
|
||||
): string => {
|
||||
const objectName = formatValueForExpression(
|
||||
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ?? '',
|
||||
);
|
||||
return `${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'StatefulSet' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${objectName}`;
|
||||
};
|
||||
|
||||
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 k8sStatefulSetInitialLogTracesExpression = (
|
||||
item: InframonitoringtypesStatefulSetRecordDTO,
|
||||
): string => {
|
||||
const statefulSetName = formatValueForExpression(
|
||||
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ?? '',
|
||||
);
|
||||
const namespaceName = formatValueForExpression(
|
||||
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
|
||||
);
|
||||
return `${INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME} = ${statefulSetName} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${namespaceName}`;
|
||||
};
|
||||
|
||||
export const k8sStatefulSetGetEntityName = (
|
||||
item: K8sStatefulSetsData,
|
||||
): string => item.meta.k8s_statefulset_name;
|
||||
item: InframonitoringtypesStatefulSetRecordDTO,
|
||||
): string => item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ?? '';
|
||||
|
||||
export const statefulSetWidgetInfo = [
|
||||
{
|
||||
@@ -91,16 +82,16 @@ export const statefulSetWidgetInfo = [
|
||||
];
|
||||
|
||||
export const getStatefulSetMetricsQueryPayload = (
|
||||
statefulSet: K8sStatefulSetsData,
|
||||
statefulSet: InframonitoringtypesStatefulSetRecordDTO,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
): GetQueryResultsProps[] => {
|
||||
const k8sStatefulSetNameKey = dotMetricsEnabled
|
||||
? 'k8s.statefulset.name'
|
||||
? INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
|
||||
: 'k8s_statefulset_name';
|
||||
const k8sNamespaceNameKey = dotMetricsEnabled
|
||||
? 'k8s.namespace.name'
|
||||
? INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME
|
||||
: 'k8s_namespace_name';
|
||||
const k8sPodNameKey = dotMetricsEnabled ? 'k8s.pod.name' : 'k8s_pod_name';
|
||||
|
||||
@@ -172,7 +163,10 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: statefulSet.meta.k8s_statefulset_name,
|
||||
value:
|
||||
statefulSet.meta?.[
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
|
||||
] ?? '',
|
||||
},
|
||||
{
|
||||
id: 'f2',
|
||||
@@ -183,7 +177,9 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: statefulSet.meta.k8s_namespace_name,
|
||||
value:
|
||||
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -222,7 +218,10 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'contains',
|
||||
value: statefulSet.meta.k8s_statefulset_name,
|
||||
value:
|
||||
statefulSet.meta?.[
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
|
||||
] ?? '',
|
||||
},
|
||||
{
|
||||
id: 'f2',
|
||||
@@ -233,7 +232,9 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: statefulSet.meta.k8s_namespace_name,
|
||||
value:
|
||||
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -272,7 +273,10 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'contains',
|
||||
value: statefulSet.meta.k8s_statefulset_name,
|
||||
value:
|
||||
statefulSet.meta?.[
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
|
||||
] ?? '',
|
||||
},
|
||||
{
|
||||
id: 'f2',
|
||||
@@ -283,7 +287,9 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: statefulSet.meta.k8s_namespace_name,
|
||||
value:
|
||||
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -342,7 +348,10 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: statefulSet.meta.k8s_statefulset_name,
|
||||
value:
|
||||
statefulSet.meta?.[
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
|
||||
] ?? '',
|
||||
},
|
||||
{
|
||||
id: 'f2',
|
||||
@@ -353,7 +362,9 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: statefulSet.meta.k8s_namespace_name,
|
||||
value:
|
||||
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -392,7 +403,10 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: statefulSet.meta.k8s_statefulset_name,
|
||||
value:
|
||||
statefulSet.meta?.[
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
|
||||
] ?? '',
|
||||
},
|
||||
{
|
||||
id: 'f2',
|
||||
@@ -403,7 +417,9 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: statefulSet.meta.k8s_namespace_name,
|
||||
value:
|
||||
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -462,7 +478,10 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: statefulSet.meta.k8s_statefulset_name,
|
||||
value:
|
||||
statefulSet.meta?.[
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
|
||||
] ?? '',
|
||||
},
|
||||
{
|
||||
id: 'f2',
|
||||
@@ -473,7 +492,9 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: statefulSet.meta.k8s_namespace_name,
|
||||
value:
|
||||
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -512,7 +533,10 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'contains',
|
||||
value: statefulSet.meta.k8s_statefulset_name,
|
||||
value:
|
||||
statefulSet.meta?.[
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
|
||||
] ?? '',
|
||||
},
|
||||
{
|
||||
id: 'f2',
|
||||
@@ -523,7 +547,9 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: statefulSet.meta.k8s_namespace_name,
|
||||
value:
|
||||
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -562,7 +588,10 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'contains',
|
||||
value: statefulSet.meta.k8s_statefulset_name,
|
||||
value:
|
||||
statefulSet.meta?.[
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
|
||||
] ?? '',
|
||||
},
|
||||
{
|
||||
id: 'f2',
|
||||
@@ -573,7 +602,9 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: statefulSet.meta.k8s_namespace_name,
|
||||
value:
|
||||
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -632,7 +663,10 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: statefulSet.meta.k8s_statefulset_name,
|
||||
value:
|
||||
statefulSet.meta?.[
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
|
||||
] ?? '',
|
||||
},
|
||||
{
|
||||
id: 'f2',
|
||||
@@ -643,7 +677,9 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: statefulSet.meta.k8s_namespace_name,
|
||||
value:
|
||||
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -682,7 +718,10 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: statefulSet.meta.k8s_statefulset_name,
|
||||
value:
|
||||
statefulSet.meta?.[
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
|
||||
] ?? '',
|
||||
},
|
||||
{
|
||||
id: 'f2',
|
||||
@@ -693,7 +732,9 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: statefulSet.meta.k8s_namespace_name,
|
||||
value:
|
||||
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -752,7 +793,10 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: statefulSet.meta.k8s_statefulset_name,
|
||||
value:
|
||||
statefulSet.meta?.[
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
|
||||
] ?? '',
|
||||
},
|
||||
{
|
||||
id: 'f2',
|
||||
@@ -763,7 +807,9 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: statefulSet.meta.k8s_namespace_name,
|
||||
value:
|
||||
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
@@ -835,7 +881,10 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: statefulSet.meta.k8s_statefulset_name,
|
||||
value:
|
||||
statefulSet.meta?.[
|
||||
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
|
||||
] ?? '',
|
||||
},
|
||||
{
|
||||
id: 'f2',
|
||||
@@ -846,7 +895,9 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: statefulSet.meta.k8s_namespace_name,
|
||||
value:
|
||||
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
|
||||
'',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
|
||||
@@ -1,39 +1,52 @@
|
||||
import { Tooltip } from 'antd';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { InframonitoringtypesStatefulSetRecordDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
|
||||
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
|
||||
|
||||
import ColumnHeader from '../Base/ColumnHeader';
|
||||
import EntityGroupHeader from '../Base/EntityGroupHeader';
|
||||
import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { formatBytes } from '../commonUtils';
|
||||
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { K8sStatefulSetsData } from './api';
|
||||
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
|
||||
import {
|
||||
CellValueTooltip,
|
||||
EntityProgressBar,
|
||||
GroupedStatusCounts,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../components';
|
||||
import {
|
||||
INFRA_MONITORING_ATTR_KEYS,
|
||||
InfraMonitoringEntity,
|
||||
} from '../constants';
|
||||
import { ArrowUpDown } from '@signozhq/icons';
|
||||
|
||||
export function getK8sStatefulSetRowKey(
|
||||
statefulSet: K8sStatefulSetsData,
|
||||
statefulSet: InframonitoringtypesStatefulSetRecordDTO,
|
||||
): string {
|
||||
return (
|
||||
statefulSet.statefulSetName || statefulSet.meta.k8s_statefulset_name || ''
|
||||
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ||
|
||||
statefulSet.statefulSetName ||
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
export function getK8sStatefulSetItemKey(
|
||||
statefulSet: K8sStatefulSetsData,
|
||||
statefulSet: InframonitoringtypesStatefulSetRecordDTO,
|
||||
): string {
|
||||
return statefulSet.meta.k8s_statefulset_name;
|
||||
return (
|
||||
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] || ''
|
||||
);
|
||||
}
|
||||
|
||||
export const k8sStatefulSetsColumnsConfig: TableColumnDef<K8sStatefulSetsData>[] =
|
||||
export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesStatefulSetRecordDTO>[] =
|
||||
[
|
||||
{
|
||||
id: 'statefulSetGroup',
|
||||
header: (): React.ReactNode => (
|
||||
<EntityGroupHeader title="STATEFULSET GROUP" />
|
||||
<EntityGroupHeader title="StatefulSet Group" />
|
||||
),
|
||||
accessorFn: (row): string => row.meta.k8s_statefulset_name || '',
|
||||
width: { min: 210 },
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
@@ -56,10 +69,12 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<K8sStatefulSetsData>[]
|
||||
<EntityGroupHeader
|
||||
title="StatefulSet Name"
|
||||
icon={<ArrowUpDown data-hide-expanded="true" size={14} />}
|
||||
docPath="/infrastructure-monitoring/kubernetes/statefulsets#statefulset-name"
|
||||
/>
|
||||
),
|
||||
accessorFn: (row): string => row.meta.k8s_statefulset_name || '',
|
||||
width: { min: 200 },
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] || '',
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
@@ -68,75 +83,98 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<K8sStatefulSetsData>[]
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const statefulsetName = value as string;
|
||||
return (
|
||||
<Tooltip title={statefulsetName}>
|
||||
<CellValueTooltip value={statefulsetName}>
|
||||
<TanStackTable.Text>{statefulsetName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
</CellValueTooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'namespaceName',
|
||||
header: 'Namespace Name',
|
||||
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
|
||||
width: { default: 150 },
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#namespace-name">
|
||||
Namespace
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
width: { min: 180 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const namespaceName = value as string;
|
||||
return (
|
||||
<Tooltip title={namespaceName}>
|
||||
<CellValueTooltip value={namespaceName}>
|
||||
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
</CellValueTooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'available_pods',
|
||||
header: 'Available',
|
||||
accessorFn: (row): number => row.availablePods,
|
||||
width: { min: 100, default: 140 },
|
||||
enableSort: true,
|
||||
id: 'pod_counts_by_phase',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#pod-counts-by-phase">
|
||||
Pod Phases
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (
|
||||
row,
|
||||
): InframonitoringtypesStatefulSetRecordDTO['podCountsByPhase'] =>
|
||||
row.podCountsByPhase,
|
||||
width: { min: 250 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const availablePods = value as number;
|
||||
cell: ({ row }): React.ReactNode => {
|
||||
const podCountsByPhase = row.podCountsByPhase;
|
||||
if (!podCountsByPhase) {
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={availablePods}
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="available pod"
|
||||
>
|
||||
<TanStackTable.Text>{availablePods}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'desired_pods',
|
||||
header: 'Desired',
|
||||
accessorFn: (row): number => row.desiredPods,
|
||||
width: { min: 100, default: 140 },
|
||||
enableSort: true,
|
||||
id: 'pod_status',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#pod-status">
|
||||
Pod Status
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.currentPods,
|
||||
width: { min: 140 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const desiredPods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={desiredPods}
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="desired pod"
|
||||
>
|
||||
<TanStackTable.Text>{desiredPods}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
cell: ({ row }): React.ReactNode => (
|
||||
<GroupedStatusCounts
|
||||
items={[
|
||||
{
|
||||
value: row.currentPods,
|
||||
label: 'Current',
|
||||
color: Color.BG_FOREST_500,
|
||||
},
|
||||
{
|
||||
value: row.desiredPods,
|
||||
label: 'Desired',
|
||||
color: Color.BG_ROBIN_500,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'cpu_request',
|
||||
header: 'CPU Req Usage (%)',
|
||||
accessorFn: (row): number => row.cpuRequest,
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-req-usage-">
|
||||
CPU Request
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.statefulSetCPURequest,
|
||||
width: { min: 200, default: 200 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuRequest = value as number;
|
||||
return (
|
||||
@@ -152,8 +190,13 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<K8sStatefulSetsData>[]
|
||||
},
|
||||
{
|
||||
id: 'cpu_limit',
|
||||
header: 'CPU Limit Usage (%)',
|
||||
accessorFn: (row): number => row.cpuLimit,
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-limit-usage-">
|
||||
CPU Limit
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.statefulSetCPULimit,
|
||||
width: { min: 200, default: 200 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
@@ -172,32 +215,43 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<K8sStatefulSetsData>[]
|
||||
},
|
||||
{
|
||||
id: 'cpu',
|
||||
header: 'CPU Usage (cores)',
|
||||
accessorFn: (row): number => row.cpuUsage,
|
||||
width: { min: 190 },
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-usage-cores">
|
||||
CPU Usage
|
||||
<br /> (cores)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.statefulSetCPU,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = value as number;
|
||||
const cpu = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={cpu}
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="CPU metric"
|
||||
>
|
||||
<TanStackTable.Text>{cpu}</TanStackTable.Text>
|
||||
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory_request',
|
||||
header: 'Mem Req Usage (%)',
|
||||
accessorFn: (row): number => row.memoryRequest,
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-req-usage-">
|
||||
Memory Request
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.statefulSetMemoryRequest,
|
||||
width: { min: 190 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryRequest = value as number;
|
||||
return (
|
||||
@@ -213,8 +267,13 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<K8sStatefulSetsData>[]
|
||||
},
|
||||
{
|
||||
id: 'memory_limit',
|
||||
header: 'Mem Limit Usage (%)',
|
||||
accessorFn: (row): number => row.memoryLimit,
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-limit-usage-">
|
||||
Memory Limit
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.statefulSetMemoryLimit,
|
||||
width: { min: 180 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
@@ -233,8 +292,13 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<K8sStatefulSetsData>[]
|
||||
},
|
||||
{
|
||||
id: 'memory',
|
||||
header: 'Mem Usage (WSS)',
|
||||
accessorFn: (row): number => row.memoryUsage,
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-usage-wss">
|
||||
Memory Usage
|
||||
<br /> (WSS)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.statefulSetMemory,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
@@ -252,4 +316,52 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<K8sStatefulSetsData>[]
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'current_pods',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#current">
|
||||
Current Pods
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.currentPods,
|
||||
width: { min: 120 },
|
||||
enableSort: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const currentPods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={currentPods}
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="current pod"
|
||||
>
|
||||
<TanStackTable.Text>{currentPods}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'desired_pods',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#desired">
|
||||
Desired Pods
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.desiredPods,
|
||||
width: { min: 120 },
|
||||
enableSort: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const desiredPods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
value={desiredPods}
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
attribute="desired pod"
|
||||
>
|
||||
<TanStackTable.Text>{desiredPods}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { listVolumes } from 'api/generated/services/inframonitoring';
|
||||
import {
|
||||
InframonitoringtypesResponseTypeDTO,
|
||||
InframonitoringtypesVolumeRecordDTO,
|
||||
Querybuildertypesv5OrderDirectionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
|
||||
import { K8sBaseList } from '../Base/K8sBaseList';
|
||||
import { K8sBaseFilters } from '../Base/types';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { getK8sVolumesList, K8sVolumesData } from './api';
|
||||
import {
|
||||
getVolumeMetricsQueryPayload,
|
||||
k8sVolumeDetailsMetadataConfig,
|
||||
k8sVolumeGetEntityName,
|
||||
k8sVolumeGetSelectedItemFilters,
|
||||
k8sVolumeInitialEventsFilter,
|
||||
k8sVolumeInitialLogTracesFilter,
|
||||
k8sVolumeGetSelectedItemExpression,
|
||||
k8sVolumeInitialEventsExpression,
|
||||
k8sVolumeInitialLogTracesExpression,
|
||||
volumeWidgetInfo,
|
||||
} from './constants';
|
||||
import {
|
||||
@@ -28,66 +31,93 @@ function K8sVolumesList({
|
||||
}: {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
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: 'usage',
|
||||
order: 'desc',
|
||||
};
|
||||
try {
|
||||
const response = await listVolumes(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
groupBy: filters.groupBy?.map((g) => ({ name: g.name })),
|
||||
offset: filters.offset,
|
||||
limit: filters.limit ?? 10,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
orderBy: filters.orderBy
|
||||
? {
|
||||
key: { name: filters.orderBy.key.name },
|
||||
direction:
|
||||
filters.orderBy.direction === 'asc'
|
||||
? Querybuildertypesv5OrderDirectionDTO.asc
|
||||
: Querybuildertypesv5OrderDirectionDTO.desc,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const response = await getK8sVolumesList(
|
||||
filters,
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
|
||||
return {
|
||||
data: response.payload?.data.records || [],
|
||||
total: response.payload?.data.total || 0,
|
||||
error: response.error,
|
||||
rawData: response.payload?.data,
|
||||
};
|
||||
const data = response.data;
|
||||
return {
|
||||
type:
|
||||
data.type === InframonitoringtypesResponseTypeDTO.grouped_list
|
||||
? ('grouped_list' as const)
|
||||
: ('list' as const),
|
||||
records: data.records,
|
||||
total: data.total,
|
||||
endTimeBeforeRetention: data.endTimeBeforeRetention,
|
||||
warning: data.warning,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch volumes';
|
||||
return {
|
||||
type: 'list' as const,
|
||||
records: [] as InframonitoringtypesVolumeRecordDTO[],
|
||||
total: 0,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchEntityData = useCallback(
|
||||
async (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ data: K8sVolumesData | null; error?: string | null }> => {
|
||||
const response = await getK8sVolumesList(
|
||||
{
|
||||
filters: filters.filters,
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
undefined,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
): Promise<{
|
||||
data: InframonitoringtypesVolumeRecordDTO | null;
|
||||
error?: string | null;
|
||||
}> => {
|
||||
try {
|
||||
const response = await listVolumes(
|
||||
{
|
||||
filter: { expression: filters.filter.expression },
|
||||
start: filters.start,
|
||||
end: filters.end,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
|
||||
const records = response.payload?.data.records || [];
|
||||
|
||||
return {
|
||||
data: records.length > 0 ? records[0] : null,
|
||||
error: response.error,
|
||||
};
|
||||
return {
|
||||
data: response.data.records.length > 0 ? response.data.records[0] : null,
|
||||
};
|
||||
} catch (error) {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : 'Failed to fetch volume';
|
||||
return {
|
||||
data: null,
|
||||
error: errMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
[dotMetricsEnabled],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<K8sVolumesData>
|
||||
<K8sBaseList<InframonitoringtypesVolumeRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.VOLUMES}
|
||||
tableColumns={k8sVolumesColumnsConfig}
|
||||
@@ -97,14 +127,14 @@ function K8sVolumesList({
|
||||
eventCategory={InfraMonitoringEvents.Volumes}
|
||||
/>
|
||||
|
||||
<K8sBaseDetails<K8sVolumesData>
|
||||
<K8sBaseDetails<InframonitoringtypesVolumeRecordDTO>
|
||||
category={InfraMonitoringEntity.VOLUMES}
|
||||
eventCategory={InfraMonitoringEvents.Volume}
|
||||
getSelectedItemFilters={k8sVolumeGetSelectedItemFilters}
|
||||
getSelectedItemExpression={k8sVolumeGetSelectedItemExpression}
|
||||
fetchEntityData={fetchEntityData}
|
||||
getEntityName={k8sVolumeGetEntityName}
|
||||
getInitialLogTracesFilters={k8sVolumeInitialLogTracesFilter}
|
||||
getInitialEventsFilters={k8sVolumeInitialEventsFilter}
|
||||
getInitialLogTracesExpression={k8sVolumeInitialLogTracesExpression}
|
||||
getInitialEventsExpression={k8sVolumeInitialEventsExpression}
|
||||
metadataConfig={k8sVolumeDetailsMetadataConfig}
|
||||
entityWidgetInfo={volumeWidgetInfo}
|
||||
getEntityQueryPayload={getVolumeMetricsQueryPayload}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
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 { K8sBaseFilters } from '../Base/types';
|
||||
|
||||
export interface K8sVolumesData {
|
||||
persistentVolumeClaimName: string;
|
||||
volumeAvailable: number;
|
||||
volumeCapacity: number;
|
||||
volumeInodes: number;
|
||||
volumeInodesFree: number;
|
||||
volumeInodesUsed: number;
|
||||
volumeUsage: number;
|
||||
meta: {
|
||||
k8s_cluster_name: string;
|
||||
k8s_namespace_name: string;
|
||||
k8s_node_name: string;
|
||||
k8s_persistentvolumeclaim_name: string;
|
||||
k8s_pod_name: string;
|
||||
k8s_pod_uid: string;
|
||||
k8s_statefulset_name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface K8sVolumesListResponse {
|
||||
status: string;
|
||||
data: {
|
||||
type: string;
|
||||
records: K8sVolumesData[];
|
||||
groups: null;
|
||||
total: number;
|
||||
sentAnyHostMetricsData: boolean;
|
||||
isSendingK8SAgentMetrics: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(H4ad): Erase this whole file when migrating to openapi
|
||||
export const volumesMetaMap: Array<{
|
||||
dot: keyof Record<string, unknown>;
|
||||
under: keyof K8sVolumesData['meta'];
|
||||
}> = [
|
||||
{ dot: 'k8s.cluster.name', under: 'k8s_cluster_name' },
|
||||
{ dot: 'k8s.namespace.name', under: 'k8s_namespace_name' },
|
||||
{ dot: 'k8s.node.name', under: 'k8s_node_name' },
|
||||
{
|
||||
dot: 'k8s.persistentvolumeclaim.name',
|
||||
under: 'k8s_persistentvolumeclaim_name',
|
||||
},
|
||||
{ dot: 'k8s.pod.name', under: 'k8s_pod_name' },
|
||||
{ dot: 'k8s.pod.uid', under: 'k8s_pod_uid' },
|
||||
{ dot: 'k8s.statefulset.name', under: 'k8s_statefulset_name' },
|
||||
];
|
||||
|
||||
export function mapVolumesMeta(
|
||||
rawMeta: Record<string, unknown>,
|
||||
): K8sVolumesData['meta'] {
|
||||
const out: Record<string, unknown> = { ...rawMeta };
|
||||
|
||||
volumesMetaMap.forEach(({ dot, under }) => {
|
||||
if (dot in rawMeta) {
|
||||
const val = rawMeta[dot];
|
||||
out[under] = typeof val === 'string' ? val : rawMeta[under];
|
||||
}
|
||||
});
|
||||
|
||||
return out as K8sVolumesData['meta'];
|
||||
}
|
||||
|
||||
export const getK8sVolumesList = async (
|
||||
props: K8sBaseFilters,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
dotMetricsEnabled = false,
|
||||
): Promise<SuccessResponse<K8sVolumesListResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const { orderBy, ...rest } = props;
|
||||
const basePayload = {
|
||||
...rest,
|
||||
filters: props.filters ?? { items: [], op: 'and' },
|
||||
...(orderBy != null ? { orderBy } : {}),
|
||||
};
|
||||
|
||||
const requestProps = dotMetricsEnabled
|
||||
? {
|
||||
...basePayload,
|
||||
filters: {
|
||||
...basePayload.filters,
|
||||
items: basePayload.filters.items.reduce<typeof basePayload.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);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as typeof basePayload.filters.items,
|
||||
),
|
||||
},
|
||||
}
|
||||
: basePayload;
|
||||
|
||||
const response = await axios.post('/pvcs/list', requestProps, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
const payload: K8sVolumesListResponse = response.data;
|
||||
|
||||
payload.data.records = payload.data.records.map((record) => ({
|
||||
...record,
|
||||
meta: mapVolumesMeta(record.meta as Record<string, unknown>),
|
||||
}));
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload,
|
||||
params: requestProps,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
@@ -1,70 +1,61 @@
|
||||
import { InframonitoringtypesVolumeRecordDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
createFilterItem,
|
||||
K8sDetailsMetadataConfig,
|
||||
} from '../Base/K8sBaseDetails';
|
||||
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
|
||||
import { K8sVolumesData } from './api';
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
|
||||
export const k8sVolumeGetSelectedItemFilters = (
|
||||
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
|
||||
|
||||
export const k8sVolumeGetSelectedItemExpression = (
|
||||
selectedItemId: string,
|
||||
): TagFilter => ({
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
id: 'k8s_persistentvolumeclaim_name',
|
||||
key: {
|
||||
key: 'k8s_persistentvolumeclaim_name',
|
||||
type: null,
|
||||
},
|
||||
op: '=',
|
||||
value: selectedItemId,
|
||||
},
|
||||
],
|
||||
});
|
||||
): string =>
|
||||
`${INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME} = ${formatValueForExpression(selectedItemId)}`;
|
||||
|
||||
export const k8sVolumeDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sVolumesData>[] =
|
||||
export const k8sVolumeDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesVolumeRecordDTO>[] =
|
||||
[
|
||||
{
|
||||
label: 'PVC Name',
|
||||
getValue: (p): string => p.persistentVolumeClaimName,
|
||||
getValue: (p): string => p.persistentVolumeClaimName || '',
|
||||
},
|
||||
{
|
||||
label: 'Cluster Name',
|
||||
getValue: (p): string => p.meta.k8s_cluster_name,
|
||||
getValue: (p): string =>
|
||||
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
|
||||
},
|
||||
{
|
||||
label: 'Namespace Name',
|
||||
getValue: (p): string => p.meta.k8s_namespace_name,
|
||||
getValue: (p): string =>
|
||||
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
},
|
||||
];
|
||||
|
||||
export const k8sVolumeInitialEventsFilter = (
|
||||
item: K8sVolumesData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'PersistentVolumeClaim'),
|
||||
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.persistentVolumeClaimName),
|
||||
];
|
||||
export const k8sVolumeInitialEventsExpression = (
|
||||
item: InframonitoringtypesVolumeRecordDTO,
|
||||
): string => {
|
||||
const objectName = formatValueForExpression(
|
||||
item.persistentVolumeClaimName || '',
|
||||
);
|
||||
return `${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'PersistentVolumeClaim' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${objectName}`;
|
||||
};
|
||||
|
||||
export const k8sVolumeInitialLogTracesFilter = (
|
||||
item: K8sVolumesData,
|
||||
): ReturnType<typeof createFilterItem>[] => [
|
||||
createFilterItem(
|
||||
QUERY_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME,
|
||||
item.meta.k8s_persistentvolumeclaim_name,
|
||||
),
|
||||
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.k8s_namespace_name),
|
||||
];
|
||||
export const k8sVolumeInitialLogTracesExpression = (
|
||||
item: InframonitoringtypesVolumeRecordDTO,
|
||||
): string => {
|
||||
const pvcName = formatValueForExpression(item.persistentVolumeClaimName || '');
|
||||
const namespaceName = formatValueForExpression(
|
||||
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
);
|
||||
return `${INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME} = ${pvcName} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${namespaceName}`;
|
||||
};
|
||||
|
||||
export const k8sVolumeGetEntityName = (item: K8sVolumesData): string =>
|
||||
item.persistentVolumeClaimName;
|
||||
export const k8sVolumeGetEntityName = (
|
||||
item: InframonitoringtypesVolumeRecordDTO,
|
||||
): string => item.persistentVolumeClaimName || '';
|
||||
|
||||
export const volumeWidgetInfo = [
|
||||
{
|
||||
@@ -90,37 +81,37 @@ export const volumeWidgetInfo = [
|
||||
];
|
||||
|
||||
export const getVolumeMetricsQueryPayload = (
|
||||
volume: K8sVolumesData,
|
||||
volume: InframonitoringtypesVolumeRecordDTO,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
): GetQueryResultsProps[] => {
|
||||
const k8sClusterNameKey = dotMetricsEnabled
|
||||
? 'k8s.cluster.name'
|
||||
? INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME
|
||||
: 'k8s_cluster_name';
|
||||
const k8sNamespaceNameKey = dotMetricsEnabled
|
||||
? 'k8s.namespace.name'
|
||||
? INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME
|
||||
: 'k8s_namespace_name';
|
||||
const k8sVolumeAvailableKey = dotMetricsEnabled
|
||||
? 'k8s.volume.available'
|
||||
? INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_AVAILABLE
|
||||
: 'k8s_volume_available';
|
||||
const k8sVolumeCapacityKey = dotMetricsEnabled
|
||||
? 'k8s.volume.capacity'
|
||||
? INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_CAPACITY
|
||||
: 'k8s_volume_capacity';
|
||||
const k8sVolumeInodesUsedKey = dotMetricsEnabled
|
||||
? 'k8s.volume.inodes.used'
|
||||
? INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_INODES_USED
|
||||
: 'k8s_volume_inodes_used';
|
||||
const k8sVolumeInodesKey = dotMetricsEnabled
|
||||
? 'k8s.volume.inodes'
|
||||
? INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_INODES
|
||||
: 'k8s_volume_inodes';
|
||||
const k8sVolumeInodesFreeKey = dotMetricsEnabled
|
||||
? 'k8s.volume.inodes.free'
|
||||
? INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_INODES_FREE
|
||||
: 'k8s_volume_inodes_free';
|
||||
const k8sVolumeTypeKey = dotMetricsEnabled
|
||||
? 'k8s.volume.type'
|
||||
? INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_TYPE
|
||||
: 'k8s_volume_type';
|
||||
const k8sPVCNameKey = dotMetricsEnabled
|
||||
? 'k8s.persistentvolumeclaim.name'
|
||||
? INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME
|
||||
: 'k8s_persistentvolumeclaim_name';
|
||||
|
||||
return [
|
||||
@@ -152,7 +143,8 @@ export const getVolumeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: volume.meta.k8s_cluster_name,
|
||||
value:
|
||||
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: 'c2',
|
||||
@@ -163,7 +155,9 @@ export const getVolumeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'in',
|
||||
value: [volume.meta.k8s_namespace_name],
|
||||
value: [
|
||||
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'c3',
|
||||
@@ -244,7 +238,8 @@ export const getVolumeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: volume.meta.k8s_cluster_name,
|
||||
value:
|
||||
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: 'c2',
|
||||
@@ -255,7 +250,9 @@ export const getVolumeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'in',
|
||||
value: [volume.meta.k8s_namespace_name],
|
||||
value: [
|
||||
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'c3',
|
||||
@@ -336,7 +333,8 @@ export const getVolumeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: volume.meta.k8s_cluster_name,
|
||||
value:
|
||||
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: 'c2',
|
||||
@@ -347,7 +345,9 @@ export const getVolumeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'in',
|
||||
value: [volume.meta.k8s_namespace_name],
|
||||
value: [
|
||||
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'c3',
|
||||
@@ -428,7 +428,8 @@ export const getVolumeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: volume.meta.k8s_cluster_name,
|
||||
value:
|
||||
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: 'c2',
|
||||
@@ -439,7 +440,9 @@ export const getVolumeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'in',
|
||||
value: [volume.meta.k8s_namespace_name],
|
||||
value: [
|
||||
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'c3',
|
||||
@@ -520,7 +523,8 @@ export const getVolumeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: '=',
|
||||
value: volume.meta.k8s_cluster_name,
|
||||
value:
|
||||
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
|
||||
},
|
||||
{
|
||||
id: 'c2',
|
||||
@@ -531,7 +535,9 @@ export const getVolumeMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
op: 'in',
|
||||
value: [volume.meta.k8s_namespace_name],
|
||||
value: [
|
||||
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'c3',
|
||||
|
||||
@@ -1,34 +1,43 @@
|
||||
import { Tooltip } from 'antd';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
import { InframonitoringtypesVolumeRecordDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
|
||||
|
||||
import ColumnHeader from '../Base/ColumnHeader';
|
||||
import EntityGroupHeader from '../Base/EntityGroupHeader';
|
||||
import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { formatBytes } from '../commonUtils';
|
||||
import { ValidateColumnValueWrapper } from '../components';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { K8sVolumesData } from './api';
|
||||
import { CellValueTooltip, ValidateColumnValueWrapper } from '../components';
|
||||
import {
|
||||
INFRA_MONITORING_ATTR_KEYS,
|
||||
InfraMonitoringEntity,
|
||||
} from '../constants';
|
||||
import { HardDrive } from '@signozhq/icons';
|
||||
|
||||
export function getK8sVolumeRowKey(volume: K8sVolumesData): string {
|
||||
export function getK8sVolumeRowKey(
|
||||
volume: InframonitoringtypesVolumeRecordDTO,
|
||||
): string {
|
||||
return (
|
||||
volume.persistentVolumeClaimName ||
|
||||
volume.meta.k8s_persistentvolumeclaim_name ||
|
||||
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME] ||
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
export function getK8sVolumeItemKey(volume: K8sVolumesData): string {
|
||||
export function getK8sVolumeItemKey(
|
||||
volume: InframonitoringtypesVolumeRecordDTO,
|
||||
): string {
|
||||
return volume.persistentVolumeClaimName;
|
||||
}
|
||||
|
||||
export const k8sVolumesColumnsConfig: TableColumnDef<K8sVolumesData>[] = [
|
||||
export type VolumeTableColumnConfig =
|
||||
TableColumnDef<InframonitoringtypesVolumeRecordDTO>;
|
||||
export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
|
||||
{
|
||||
id: 'volumeGroup',
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="VOLUME GROUP" />,
|
||||
header: (): React.ReactNode => <EntityGroupHeader title="Volume Group" />,
|
||||
accessorFn: (row): string => row.persistentVolumeClaimName || '',
|
||||
width: { min: 300 },
|
||||
width: { min: 290 },
|
||||
enableSort: false,
|
||||
enableRemove: false,
|
||||
enableMove: false,
|
||||
@@ -51,6 +60,7 @@ export const k8sVolumesColumnsConfig: TableColumnDef<K8sVolumesData>[] = [
|
||||
<EntityGroupHeader
|
||||
title="PVC Name"
|
||||
icon={<HardDrive data-hide-expanded="true" size={14} />}
|
||||
docPath="/infrastructure-monitoring/kubernetes/volumes#pvc-name"
|
||||
/>
|
||||
),
|
||||
accessorFn: (row): string => row.persistentVolumeClaimName || '',
|
||||
@@ -63,30 +73,39 @@ export const k8sVolumesColumnsConfig: TableColumnDef<K8sVolumesData>[] = [
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const pvcName = value as string;
|
||||
return (
|
||||
<Tooltip title={pvcName}>
|
||||
<CellValueTooltip value={pvcName}>
|
||||
<TanStackTable.Text>{pvcName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
</CellValueTooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'namespaceName',
|
||||
header: 'Namespace Name',
|
||||
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#namespace-name">
|
||||
Namespace
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): string =>
|
||||
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
|
||||
width: { min: 220 },
|
||||
enableSort: false,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const namespaceName = value as string;
|
||||
return (
|
||||
<Tooltip title={namespaceName}>
|
||||
<CellValueTooltip value={namespaceName}>
|
||||
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
|
||||
</Tooltip>
|
||||
</CellValueTooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'capacity',
|
||||
header: 'Capacity',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-capacity">
|
||||
Capacity
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.volumeCapacity,
|
||||
width: { min: 140 },
|
||||
enableSort: true,
|
||||
@@ -105,7 +124,11 @@ export const k8sVolumesColumnsConfig: TableColumnDef<K8sVolumesData>[] = [
|
||||
},
|
||||
{
|
||||
id: 'usage',
|
||||
header: 'Used',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-used">
|
||||
Used
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.volumeUsage,
|
||||
width: { min: 140 },
|
||||
enableSort: true,
|
||||
@@ -124,7 +147,11 @@ export const k8sVolumesColumnsConfig: TableColumnDef<K8sVolumesData>[] = [
|
||||
},
|
||||
{
|
||||
id: 'available',
|
||||
header: 'Available',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-available">
|
||||
Available
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.volumeAvailable,
|
||||
width: { min: 140 },
|
||||
enableSort: true,
|
||||
@@ -143,7 +170,11 @@ export const k8sVolumesColumnsConfig: TableColumnDef<K8sVolumesData>[] = [
|
||||
},
|
||||
{
|
||||
id: 'inodes',
|
||||
header: 'Inodes',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-inodes">
|
||||
Inodes
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.volumeInodes,
|
||||
width: { min: 140 },
|
||||
enableSort: true,
|
||||
@@ -162,7 +193,11 @@ export const k8sVolumesColumnsConfig: TableColumnDef<K8sVolumesData>[] = [
|
||||
},
|
||||
{
|
||||
id: 'inodesUsed',
|
||||
header: 'Inodes Used',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-inodes-used">
|
||||
Inodes Used
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.volumeInodesUsed,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
@@ -181,7 +216,11 @@ export const k8sVolumesColumnsConfig: TableColumnDef<K8sVolumesData>[] = [
|
||||
},
|
||||
{
|
||||
id: 'inodesFree',
|
||||
header: 'Inodes Free',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-inodes-free">
|
||||
Inodes Free
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.volumeInodesFree,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { InframonitoringtypesPodCountsByPhaseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { StatusCountItem } from './components/GroupedStatusCounts';
|
||||
|
||||
/**
|
||||
* Converts size in bytes to a human-readable string with appropriate units
|
||||
@@ -60,3 +63,18 @@ export function getStrokeColorForLimitUtilization(value: number): string {
|
||||
// Red
|
||||
return Color.BG_SAKURA_500;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds StatusCountItem[] for GroupedStatusCounts from pod phase counts.
|
||||
*/
|
||||
export function getPodPhaseStatusItems(
|
||||
counts: InframonitoringtypesPodCountsByPhaseDTO,
|
||||
): StatusCountItem[] {
|
||||
return [
|
||||
{ value: counts.running, label: 'Running', color: Color.BG_FOREST_500 },
|
||||
{ value: counts.pending, label: 'Pending', color: Color.BG_AMBER_500 },
|
||||
{ value: counts.succeeded, label: 'Succeeded', color: Color.BG_ROBIN_500 },
|
||||
{ value: counts.failed, label: 'Failed', color: Color.BG_CHERRY_500 },
|
||||
{ value: counts.unknown, label: 'Unknown', color: Color.BG_SLATE_400 },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
.tooltipContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-1);
|
||||
padding: var(--spacing-2);
|
||||
justify-content: flex-end;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: var(--radius-1);
|
||||
background: transparent;
|
||||
color: var(--l2-foreground);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
color 0.15s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--l2-background-hover);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltipContentWrapper {
|
||||
--tooltip-background: var(--l3-background);
|
||||
--tooltip-border-radius: 4px;
|
||||
--tooltip-border-width: 1px;
|
||||
--tooltip-border-color: rgba(255, 255, 255, 0.14);
|
||||
--tooltip-padding: 0px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
--divider-color: rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { useCallback, type ReactNode, type MouseEvent } from 'react';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { Copy, Minus, Plus } from '@signozhq/icons';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
|
||||
import { useInfraMonitoringCellActionsStore } from './useInfraMonitoringCellActionsStore';
|
||||
|
||||
import styles from './CellValueTooltip.module.scss';
|
||||
import { Divider } from '@signozhq/ui/divider';
|
||||
|
||||
export interface CellValueTooltipProps {
|
||||
value: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function CellValueTooltip({
|
||||
value,
|
||||
children,
|
||||
}: CellValueTooltipProps): JSX.Element {
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
const { lineClamp, increaseLineClamp, decreaseLineClamp } =
|
||||
useInfraMonitoringCellActionsStore();
|
||||
|
||||
const handleCopy = useCallback(
|
||||
(e: MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
copyToClipboard(value);
|
||||
toast.success('Copied to clipboard');
|
||||
},
|
||||
[copyToClipboard, value],
|
||||
);
|
||||
|
||||
const handleIncreaseLineClamp = useCallback(
|
||||
(e: MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
increaseLineClamp();
|
||||
},
|
||||
[increaseLineClamp],
|
||||
);
|
||||
|
||||
const handleDecreaseLineClamp = useCallback(
|
||||
(e: MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
decreaseLineClamp();
|
||||
},
|
||||
[decreaseLineClamp],
|
||||
);
|
||||
|
||||
const tooltipContent = (
|
||||
<div className={styles.tooltipContent}>
|
||||
<div className={styles.actions}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.actionButton}
|
||||
onClick={handleDecreaseLineClamp}
|
||||
disabled={lineClamp <= 1}
|
||||
data-testid="cell-value-decrease-line-height"
|
||||
title="Decrease line height"
|
||||
>
|
||||
<Minus size={14} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.actionButton}
|
||||
onClick={handleIncreaseLineClamp}
|
||||
disabled={lineClamp >= 10}
|
||||
data-testid="cell-value-increase-line-height"
|
||||
title="Increase line height"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
|
||||
<Divider type="vertical" className={styles.divider} />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={styles.actionButton}
|
||||
onClick={handleCopy}
|
||||
data-testid="cell-value-copy"
|
||||
title="Copy value"
|
||||
>
|
||||
<Copy size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<TooltipSimple
|
||||
title={tooltipContent}
|
||||
arrow
|
||||
tooltipContentProps={{
|
||||
className: styles.tooltipContentWrapper,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TooltipSimple>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export {
|
||||
CellValueTooltip,
|
||||
type CellValueTooltipProps,
|
||||
} from './CellValueTooltip';
|
||||
export {
|
||||
useInfraMonitoringCellActionsStore,
|
||||
useInfraMonitoringLineClamp,
|
||||
} from './useInfraMonitoringCellActionsStore';
|
||||
@@ -0,0 +1,39 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
export interface IInfraMonitoringCellActionsStore {
|
||||
lineClamp: number;
|
||||
increaseLineClamp: () => void;
|
||||
decreaseLineClamp: () => void;
|
||||
}
|
||||
|
||||
const MIN_LINE_CLAMP = 1;
|
||||
const MAX_LINE_CLAMP = 10;
|
||||
const DEFAULT_LINE_CLAMP = 2;
|
||||
|
||||
export const useInfraMonitoringCellActionsStore =
|
||||
create<IInfraMonitoringCellActionsStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
lineClamp: DEFAULT_LINE_CLAMP,
|
||||
increaseLineClamp: (): void => {
|
||||
const current = get().lineClamp;
|
||||
if (current < MAX_LINE_CLAMP) {
|
||||
set({ lineClamp: current + 1 });
|
||||
}
|
||||
},
|
||||
decreaseLineClamp: (): void => {
|
||||
const current = get().lineClamp;
|
||||
if (current > MIN_LINE_CLAMP) {
|
||||
set({ lineClamp: current - 1 });
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: '@signoz/infra-monitoring-cell-actions',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export const useInfraMonitoringLineClamp = (): number =>
|
||||
useInfraMonitoringCellActionsStore((s) => s.lineClamp);
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
|
||||
import styles from './EntityProgressBar.module.scss';
|
||||
|
||||
type EntityProgressBarType = 'request' | 'limit' | 'cpu' | 'memory';
|
||||
type EntityProgressBarType = 'request' | 'limit' | 'cpu' | 'memory' | 'disk';
|
||||
|
||||
function getStrokeColor(type: EntityProgressBarType, value: number): string {
|
||||
switch (type) {
|
||||
@@ -24,6 +24,8 @@ function getStrokeColor(type: EntityProgressBarType, value: number): string {
|
||||
return getProgressColor(Number((value * 100).toFixed(1)));
|
||||
case 'memory':
|
||||
return getMemoryProgressColor(Number((value * 100).toFixed(1)));
|
||||
case 'disk':
|
||||
return getProgressColor(Number((value * 100).toFixed(1)));
|
||||
default:
|
||||
return getStrokeColorForRequestUtilization(value);
|
||||
}
|
||||
@@ -36,9 +38,8 @@ export function EntityProgressBar({
|
||||
value: number;
|
||||
type: EntityProgressBarType;
|
||||
}): JSX.Element {
|
||||
const percentage = Number.isNaN(+value)
|
||||
? null
|
||||
: Number((value * 100).toFixed(1));
|
||||
const isNoData = value === -1 || Number.isNaN(+value);
|
||||
const percentage = isNoData ? null : Number((value * 100).toFixed(1));
|
||||
|
||||
if (percentage === null) {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.itemWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
border-radius: 1px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-variant-numeric: tabular-nums;
|
||||
min-width: 4ch;
|
||||
text-align: left;
|
||||
cursor: default;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
|
||||
import styles from './GroupedStatusCounts.module.scss';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
|
||||
export interface StatusCountItem {
|
||||
value: number;
|
||||
label: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface GroupedStatusCountsProps {
|
||||
items: StatusCountItem[];
|
||||
showZeroValues?: boolean;
|
||||
}
|
||||
|
||||
export function GroupedStatusCounts({
|
||||
items,
|
||||
showZeroValues = true,
|
||||
}: GroupedStatusCountsProps): JSX.Element {
|
||||
const visibleItems =
|
||||
showZeroValues === false ? items.filter((item) => item.value > 0) : items;
|
||||
|
||||
if (visibleItems.length === 0) {
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{visibleItems.map((item) => (
|
||||
<div key={item.label} className={styles.itemWrapper}>
|
||||
<div
|
||||
className={styles.separator}
|
||||
style={{ backgroundColor: item.color }}
|
||||
/>
|
||||
<TooltipSimple title={`${item.label}: ${item.value}`}>
|
||||
<span>
|
||||
<TanStackTable.Text className={styles.value}>
|
||||
{item.value || '-'}
|
||||
</TanStackTable.Text>
|
||||
</span>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export function ValidateColumnValueWrapper({
|
||||
entity?: InfraMonitoringEntity;
|
||||
attribute?: string;
|
||||
}): JSX.Element {
|
||||
if (value === -1) {
|
||||
if (value === -1 || Number.isNaN(value)) {
|
||||
let element = <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
if (entity && attribute) {
|
||||
element = (
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
export { EntityProgressBar } from './EntityProgressBar';
|
||||
export { ValidateColumnValueWrapper } from './ValidateColumnValueWrapper';
|
||||
export { ExpandButtonWrapper } from './ExpandButtonWrapper';
|
||||
export {
|
||||
GroupedStatusCounts,
|
||||
type StatusCountItem,
|
||||
} from './GroupedStatusCounts';
|
||||
export {
|
||||
CellValueTooltip,
|
||||
useInfraMonitoringCellActionsStore,
|
||||
useInfraMonitoringLineClamp,
|
||||
} from './CellValueTooltip';
|
||||
|
||||
@@ -5,6 +5,108 @@ import {
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
// TODO(backend): Find a way to generate this via openapi
|
||||
export const INFRA_MONITORING_ATTR_KEYS = {
|
||||
// Host
|
||||
HOST_NAME: 'host.name',
|
||||
|
||||
// Cluster
|
||||
K8S_CLUSTER_NAME: 'k8s.cluster.name',
|
||||
K8S_CLUSTER_UID: 'k8s.cluster.uid',
|
||||
|
||||
// Namespace
|
||||
K8S_NAMESPACE_NAME: 'k8s.namespace.name',
|
||||
|
||||
// Node
|
||||
K8S_NODE_NAME: 'k8s.node.name',
|
||||
K8S_NODE_UID: 'k8s.node.uid',
|
||||
K8S_NODE_CPU_USAGE: 'k8s.node.cpu.usage',
|
||||
K8S_NODE_ALLOCATABLE_CPU: 'k8s.node.allocatable_cpu',
|
||||
K8S_NODE_ALLOCATABLE_MEMORY: 'k8s.node.allocatable_memory',
|
||||
K8S_NODE_CONDITION_READY: 'k8s.node.condition_ready',
|
||||
K8S_NODE_MEMORY_RSS: 'k8s.node.memory.rss',
|
||||
K8S_NODE_MEMORY_USAGE: 'k8s.node.memory.usage',
|
||||
K8S_NODE_MEMORY_WORKING_SET: 'k8s.node.memory.working_set',
|
||||
K8S_NODE_FILESYSTEM_AVAILABLE: 'k8s.node.filesystem.available',
|
||||
K8S_NODE_FILESYSTEM_CAPACITY: 'k8s.node.filesystem.capacity',
|
||||
K8S_NODE_FILESYSTEM_USAGE: 'k8s.node.filesystem.usage',
|
||||
K8S_NODE_NETWORK_IO: 'k8s.node.network.io',
|
||||
K8S_NODE_NETWORK_ERRORS: 'k8s.node.network.errors',
|
||||
|
||||
// Pod
|
||||
K8S_POD_NAME: 'k8s.pod.name',
|
||||
K8S_POD_UID: 'k8s.pod.uid',
|
||||
K8S_POD_CPU_USAGE: 'k8s.pod.cpu.usage',
|
||||
K8S_POD_CPU_LIMIT_UTILIZATION: 'k8s.pod.cpu_limit_utilization',
|
||||
K8S_POD_CPU_REQUEST_UTILIZATION: 'k8s.pod.cpu_request_utilization',
|
||||
K8S_POD_MEMORY_USAGE: 'k8s.pod.memory.usage',
|
||||
K8S_POD_MEMORY_RSS: 'k8s.pod.memory.rss',
|
||||
K8S_POD_MEMORY_WORKING_SET: 'k8s.pod.memory.working_set',
|
||||
K8S_POD_MEMORY_MAJOR_PAGE_FAULTS: 'k8s.pod.memory.major_page_faults',
|
||||
K8S_POD_MEMORY_LIMIT_UTILIZATION: 'k8s.pod.memory_limit_utilization',
|
||||
K8S_POD_MEMORY_REQUEST_UTILIZATION: 'k8s.pod.memory_request_utilization',
|
||||
K8S_POD_FILESYSTEM_AVAILABLE: 'k8s.pod.filesystem.available',
|
||||
K8S_POD_FILESYSTEM_CAPACITY: 'k8s.pod.filesystem.capacity',
|
||||
K8S_POD_FILESYSTEM_USAGE: 'k8s.pod.filesystem.usage',
|
||||
K8S_POD_NETWORK_IO: 'k8s.pod.network.io',
|
||||
K8S_POD_NETWORK_ERRORS: 'k8s.pod.network.errors',
|
||||
|
||||
// Container
|
||||
K8S_CONTAINER_NAME: 'k8s.container.name',
|
||||
K8S_CONTAINER_CPU_REQUEST: 'k8s.container.cpu_request',
|
||||
K8S_CONTAINER_CPU_LIMIT: 'k8s.container.cpu_limit',
|
||||
K8S_CONTAINER_MEMORY_REQUEST: 'k8s.container.memory_request',
|
||||
K8S_CONTAINER_MEMORY_LIMIT: 'k8s.container.memory_limit',
|
||||
|
||||
// Deployment
|
||||
K8S_DEPLOYMENT_NAME: 'k8s.deployment.name',
|
||||
K8S_DEPLOYMENT_AVAILABLE: 'k8s.deployment.available',
|
||||
K8S_DEPLOYMENT_DESIRED: 'k8s.deployment.desired',
|
||||
|
||||
// StatefulSet
|
||||
K8S_STATEFULSET_NAME: 'k8s.statefulset.name',
|
||||
K8S_STATEFULSET_CURRENT_PODS: 'k8s.statefulset.current_pods',
|
||||
K8S_STATEFULSET_DESIRED_PODS: 'k8s.statefulset.desired_pods',
|
||||
K8S_STATEFULSET_READY_PODS: 'k8s.statefulset.ready_pods',
|
||||
K8S_STATEFULSET_UPDATED_PODS: 'k8s.statefulset.updated_pods',
|
||||
|
||||
// DaemonSet
|
||||
K8S_DAEMONSET_NAME: 'k8s.daemonset.name',
|
||||
K8S_DAEMONSET_CURRENT_SCHEDULED_NODES: 'k8s.daemonset.current_scheduled_nodes',
|
||||
K8S_DAEMONSET_DESIRED_SCHEDULED_NODES: 'k8s.daemonset.desired_scheduled_nodes',
|
||||
K8S_DAEMONSET_MISSCHEDULED_NODES: 'k8s.daemonset.misscheduled_nodes',
|
||||
K8S_DAEMONSET_READY_NODES: 'k8s.daemonset.ready_nodes',
|
||||
|
||||
// ReplicaSet
|
||||
K8S_REPLICASET_NAME: 'k8s.replicaset.name',
|
||||
K8S_REPLICASET_AVAILABLE: 'k8s.replicaset.available',
|
||||
K8S_REPLICASET_DESIRED: 'k8s.replicaset.desired',
|
||||
|
||||
// Job
|
||||
K8S_JOB_NAME: 'k8s.job.name',
|
||||
K8S_CRONJOB_NAME: 'k8s.cronjob.name',
|
||||
K8S_JOB_ACTIVE_PODS: 'k8s.job.active_pods',
|
||||
K8S_JOB_DESIRED_SUCCESSFUL_PODS: 'k8s.job.desired_successful_pods',
|
||||
K8S_JOB_FAILED_PODS: 'k8s.job.failed_pods',
|
||||
K8S_JOB_SUCCESSFUL_PODS: 'k8s.job.successful_pods',
|
||||
|
||||
// Volume
|
||||
K8S_PERSISTENT_VOLUME_CLAIM_NAME: 'k8s.persistentvolumeclaim.name',
|
||||
K8S_VOLUME_AVAILABLE: 'k8s.volume.available',
|
||||
K8S_VOLUME_CAPACITY: 'k8s.volume.capacity',
|
||||
K8S_VOLUME_INODES: 'k8s.volume.inodes',
|
||||
K8S_VOLUME_INODES_FREE: 'k8s.volume.inodes.free',
|
||||
K8S_VOLUME_INODES_USED: 'k8s.volume.inodes.used',
|
||||
K8S_VOLUME_TYPE: 'k8s.volume.type',
|
||||
|
||||
// K8s events
|
||||
K8S_OBJECT_KIND: 'k8s.object.kind',
|
||||
K8S_OBJECT_NAME: 'k8s.object.name',
|
||||
|
||||
// Environment
|
||||
DEPLOYMENT_ENVIRONMENT: 'deployment.environment',
|
||||
} as const;
|
||||
|
||||
export const DEFAULT_PAGE_SIZE = 10;
|
||||
|
||||
export enum InfraMonitoringEntity {
|
||||
@@ -51,7 +153,7 @@ export const K8sCategories = {
|
||||
VOLUMES: 'volumes',
|
||||
};
|
||||
|
||||
export const underscoreMap = {
|
||||
const underscoreMap = {
|
||||
[InfraMonitoringEntity.HOSTS]: 'system_cpu_load_average_15m',
|
||||
[InfraMonitoringEntity.PODS]: 'k8s_pod_cpu_usage',
|
||||
[InfraMonitoringEntity.NODES]: 'k8s_node_cpu_usage',
|
||||
@@ -65,7 +167,7 @@ export const underscoreMap = {
|
||||
[InfraMonitoringEntity.VOLUMES]: 'k8s_volume_capacity',
|
||||
};
|
||||
|
||||
export const dotMap = {
|
||||
const dotMap = {
|
||||
[InfraMonitoringEntity.HOSTS]: 'system.cpu.load_average.15m',
|
||||
[InfraMonitoringEntity.PODS]: 'k8s.pod.cpu.usage',
|
||||
[InfraMonitoringEntity.NODES]: 'k8s.node.cpu.usage',
|
||||
@@ -752,19 +854,63 @@ export const getInvalidValueTooltipText = (
|
||||
attribute: string,
|
||||
): string => `Some ${entity} do not have ${attribute}s.`;
|
||||
|
||||
export const ENTITY_FILTER_PLACEHOLDERS: Record<InfraMonitoringEntity, string> =
|
||||
{
|
||||
[InfraMonitoringEntity.HOSTS]:
|
||||
"Enter your filter query (e.g., host.name = 'web-server-01' AND os.type = 'linux')",
|
||||
[InfraMonitoringEntity.PODS]:
|
||||
"Enter your filter query (e.g., k8s.namespace.name = 'production' AND k8s.deployment.name = 'api-server')",
|
||||
[InfraMonitoringEntity.NODES]:
|
||||
"Enter your filter query (e.g., k8s.node.name = 'node-01' AND k8s.cluster.name = 'prod-cluster')",
|
||||
[InfraMonitoringEntity.NAMESPACES]:
|
||||
"Enter your filter query (e.g., k8s.namespace.name = 'production' AND k8s.cluster.name = 'prod-cluster')",
|
||||
[InfraMonitoringEntity.CLUSTERS]:
|
||||
"Enter your filter query (e.g., k8s.cluster.name = 'prod-cluster' AND deployment.environment = 'production')",
|
||||
[InfraMonitoringEntity.DEPLOYMENTS]:
|
||||
"Enter your filter query (e.g., k8s.deployment.name = 'api-server' AND k8s.namespace.name = 'production')",
|
||||
[InfraMonitoringEntity.STATEFULSETS]:
|
||||
"Enter your filter query (e.g., k8s.statefulset.name = 'postgres' AND k8s.namespace.name = 'databases')",
|
||||
[InfraMonitoringEntity.DAEMONSETS]:
|
||||
"Enter your filter query (e.g., k8s.daemonset.name = 'fluentd' AND k8s.namespace.name = 'logging')",
|
||||
[InfraMonitoringEntity.CONTAINERS]:
|
||||
"Enter your filter query (e.g., k8s.container.name = 'nginx' AND k8s.namespace.name = 'production')",
|
||||
[InfraMonitoringEntity.JOBS]:
|
||||
"Enter your filter query (e.g., k8s.job.name = 'backup-job' AND k8s.namespace.name = 'cron-jobs')",
|
||||
[InfraMonitoringEntity.VOLUMES]:
|
||||
"Enter your filter query (e.g., k8s.persistentvolumeclaim.name = 'data-pvc' AND k8s.namespace.name = 'storage')",
|
||||
};
|
||||
|
||||
export const INFRA_MONITORING_K8S_PARAMS_KEYS = {
|
||||
CATEGORY: 'category',
|
||||
VIEW: 'view',
|
||||
FILTERS: 'filters',
|
||||
EXPRESSION: 'expression',
|
||||
GROUP_BY: 'groupBy',
|
||||
ORDER_BY: 'orderBy',
|
||||
LOG_FILTERS: 'logFilters',
|
||||
TRACES_FILTERS: 'tracesFilters',
|
||||
EVENTS_FILTERS: 'eventsFilters',
|
||||
HOSTS_FILTERS: 'hostsFilters',
|
||||
STATUS_FILTER: 'statusFilter',
|
||||
CURRENT_PAGE: 'currentPage',
|
||||
PAGE: 'page',
|
||||
PAGE_SIZE: 'pageSize',
|
||||
EXPANDED: 'expanded',
|
||||
SELECTED_ITEM: 'selectedItem',
|
||||
};
|
||||
|
||||
/** Metric namespace prefixes for /fields/keys and /fields/values APIs */
|
||||
export const METRIC_NAMESPACE_BY_ENTITY: Record<InfraMonitoringEntity, string> =
|
||||
{
|
||||
[InfraMonitoringEntity.HOSTS]: 'system.',
|
||||
[InfraMonitoringEntity.PODS]: 'k8s.pod.',
|
||||
[InfraMonitoringEntity.NODES]: 'k8s.node.',
|
||||
[InfraMonitoringEntity.NAMESPACES]: 'k8s.pod.',
|
||||
[InfraMonitoringEntity.CLUSTERS]: 'k8s.node.',
|
||||
[InfraMonitoringEntity.DEPLOYMENTS]: 'k8s.',
|
||||
[InfraMonitoringEntity.STATEFULSETS]: 'k8s.',
|
||||
[InfraMonitoringEntity.DAEMONSETS]: 'k8s.',
|
||||
[InfraMonitoringEntity.CONTAINERS]: 'k8s.pod.',
|
||||
[InfraMonitoringEntity.JOBS]: 'k8s.',
|
||||
[InfraMonitoringEntity.VOLUMES]: 'k8s.volume.',
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
createParser,
|
||||
Options,
|
||||
parseAsInteger,
|
||||
parseAsJson,
|
||||
@@ -6,7 +7,6 @@ import {
|
||||
useQueryState,
|
||||
UseQueryStateReturn,
|
||||
} from 'nuqs';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilter,
|
||||
@@ -20,35 +20,19 @@ import {
|
||||
} from './constants';
|
||||
import { orderBySchema, OrderBySchemaType } from './schemas';
|
||||
|
||||
export type StatusFilterValue = 'active' | 'inactive';
|
||||
|
||||
const defaultNuqsOptions: Options = {
|
||||
history: 'push',
|
||||
};
|
||||
|
||||
export const useInfraMonitoringCurrentPage = (): UseQueryStateReturn<
|
||||
number,
|
||||
number
|
||||
> =>
|
||||
useQueryState(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.CURRENT_PAGE,
|
||||
parseAsInteger.withDefault(1).withOptions(defaultNuqsOptions),
|
||||
);
|
||||
|
||||
export const useInfraMonitoringPageListing = (): UseQueryStateReturn<
|
||||
number,
|
||||
number
|
||||
number | undefined
|
||||
> =>
|
||||
useQueryState(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE,
|
||||
parseAsInteger.withDefault(1).withOptions(defaultNuqsOptions),
|
||||
);
|
||||
|
||||
export const useInfraMonitoringPageSizeListing = (): UseQueryStateReturn<
|
||||
number,
|
||||
number
|
||||
> =>
|
||||
useQueryState(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE_SIZE,
|
||||
parseAsInteger.withDefault(10).withOptions(defaultNuqsOptions),
|
||||
parseAsInteger.withOptions(defaultNuqsOptions),
|
||||
);
|
||||
|
||||
export const useInfraMonitoringOrderBy = (): UseQueryStateReturn<
|
||||
@@ -60,15 +44,40 @@ export const useInfraMonitoringOrderBy = (): UseQueryStateReturn<
|
||||
parseAsJson(orderBySchema).withOptions(defaultNuqsOptions),
|
||||
);
|
||||
|
||||
const parseAsGroupBy = createParser<string[]>({
|
||||
parse: (value: string): string[] | null => {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (!Array.isArray(parsed)) {
|
||||
return null;
|
||||
}
|
||||
return parsed
|
||||
.map((item: unknown) => {
|
||||
if (typeof item === 'string') {
|
||||
return item;
|
||||
}
|
||||
if (item && typeof item === 'object' && 'key' in item) {
|
||||
return String((item as { key: unknown }).key);
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.filter(Boolean);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
serialize: (value: string[]): string => JSON.stringify(value),
|
||||
eq: (a: string[], b: string[]): boolean =>
|
||||
JSON.stringify(a) === JSON.stringify(b),
|
||||
});
|
||||
|
||||
export const useInfraMonitoringGroupBy = (): UseQueryStateReturn<
|
||||
BaseAutocompleteData[],
|
||||
string[],
|
||||
[]
|
||||
> =>
|
||||
useQueryState(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY,
|
||||
parseAsJsonNoValidate<IBuilderQuery['groupBy']>()
|
||||
.withDefault([])
|
||||
.withOptions(defaultNuqsOptions),
|
||||
parseAsGroupBy.withDefault([]).withOptions(defaultNuqsOptions),
|
||||
);
|
||||
|
||||
export const useInfraMonitoringView = (): UseQueryStateReturn<string, string> =>
|
||||
@@ -119,15 +128,6 @@ export const useInfraMonitoringCategory = (): UseQueryStateReturn<
|
||||
parseAsString.withDefault(K8sCategories.PODS).withOptions(defaultNuqsOptions),
|
||||
);
|
||||
|
||||
export const useInfraMonitoringFiltersK8s = (): UseQueryStateReturn<
|
||||
TagFilter,
|
||||
undefined
|
||||
> =>
|
||||
useQueryState(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS,
|
||||
parseAsJsonNoValidate<TagFilter>().withOptions(defaultNuqsOptions),
|
||||
);
|
||||
|
||||
export const useInfraMonitoringSelectedItem = (): UseQueryStateReturn<
|
||||
string,
|
||||
string | undefined
|
||||
@@ -137,3 +137,12 @@ export const useInfraMonitoringSelectedItem = (): UseQueryStateReturn<
|
||||
parseAsString,
|
||||
);
|
||||
};
|
||||
|
||||
export const useInfraMonitoringStatusFilter = (): UseQueryStateReturn<
|
||||
string,
|
||||
string
|
||||
> =>
|
||||
useQueryState(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.STATUS_FILTER,
|
||||
parseAsString.withDefault('').withOptions(defaultNuqsOptions),
|
||||
);
|
||||
|
||||
@@ -39,12 +39,13 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
border: 1px solid var(--date-time-selector-border-color, var(--l1-border));
|
||||
background: var(--l3-background);
|
||||
margin-left: 8px;
|
||||
|
||||
.refresh-btn {
|
||||
border-right: 1px solid var(--l1-border);
|
||||
border-right: 1px solid
|
||||
var(--date-time-selector-border-color, var(--l1-border));
|
||||
}
|
||||
.ant-btn {
|
||||
display: flex;
|
||||
@@ -54,7 +55,7 @@
|
||||
border: none;
|
||||
|
||||
&.active-tab {
|
||||
background-color: var(--l1-border);
|
||||
background-color: var(--date-time-selector-border-color, var(--l1-border));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
import ActionsPopover from './ActionsPopover';
|
||||
|
||||
const baseProps = {
|
||||
link: '/dashboard/abc',
|
||||
dashboardId: 'abc',
|
||||
dashboardName: 'My Dashboard',
|
||||
createdBy: 'someone-else@signoz.io',
|
||||
isLocked: false,
|
||||
tags: [],
|
||||
canEdit: true,
|
||||
onView: jest.fn(),
|
||||
};
|
||||
|
||||
describe('ActionsPopover', () => {
|
||||
it('shows the full set of actions for a v2 dashboard', async () => {
|
||||
render(<ActionsPopover {...baseProps} />);
|
||||
await userEvent.click(screen.getByTestId('dashboard-action-icon'));
|
||||
|
||||
await screen.findByTestId('dashboard-action-view');
|
||||
expect(screen.getByTestId('dashboard-action-view')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId('dashboard-action-open-new-tab'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('dashboard-action-copy-link')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('dashboard-action-rename')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('dashboard-action-edit-tags')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('dashboard-action-duplicate')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('dashboard-action-delete')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps only Delete for a legacy dashboard', async () => {
|
||||
render(<ActionsPopover {...baseProps} isLegacy />);
|
||||
await userEvent.click(screen.getByTestId('dashboard-action-icon'));
|
||||
|
||||
// Delete is the one action that still applies to a legacy blob.
|
||||
await screen.findByTestId('dashboard-action-delete');
|
||||
expect(screen.getByTestId('dashboard-action-delete')).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByTestId('dashboard-action-view')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('dashboard-action-open-new-tab'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('dashboard-action-copy-link'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('dashboard-action-rename'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('dashboard-action-edit-tags'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('dashboard-action-duplicate'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('dashboard-action-lock')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -46,6 +46,10 @@ interface Props {
|
||||
// Edit permission (edit_dashboard). Read actions show regardless; edit actions are hidden without it.
|
||||
canEdit: boolean;
|
||||
onView: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
// A legacy (pre-v2) dashboard has no v2 spec, so the actions that operate on
|
||||
// one (view, open, copy link, rename, edit tags, duplicate, lock) don't apply —
|
||||
// only Delete is kept.
|
||||
isLegacy?: boolean;
|
||||
}
|
||||
|
||||
function ActionsPopover({
|
||||
@@ -57,6 +61,7 @@ function ActionsPopover({
|
||||
tags,
|
||||
canEdit,
|
||||
onView,
|
||||
isLegacy = false,
|
||||
}: Props): JSX.Element {
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
@@ -109,128 +114,132 @@ function ActionsPopover({
|
||||
// row's onClick, which would navigate to the dashboard.
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -- wrapper only guards propagation, not an interactive control
|
||||
<div className={styles.content} onClick={(e): void => e.stopPropagation()}>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<Expand size={14} />}
|
||||
onClick={onView}
|
||||
testId="dashboard-action-view"
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<SquareArrowOutUpRight size={14} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
openInNewTab(link);
|
||||
}}
|
||||
testId="dashboard-action-open-new-tab"
|
||||
>
|
||||
Open in New Tab
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<Link2 size={14} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setCopy(getAbsoluteUrl(link));
|
||||
}}
|
||||
testId="dashboard-action-copy-link"
|
||||
>
|
||||
Copy Link
|
||||
</Button>
|
||||
{canEdit && (
|
||||
<Tooltip
|
||||
placement="left"
|
||||
title={
|
||||
isLocked ? 'This dashboard is locked, so it cannot be renamed.' : ''
|
||||
}
|
||||
>
|
||||
<span className={styles.menuItemWrap}>
|
||||
{!isLegacy && (
|
||||
<>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<Expand size={14} />}
|
||||
onClick={onView}
|
||||
testId="dashboard-action-view"
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<SquareArrowOutUpRight size={14} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
openInNewTab(link);
|
||||
}}
|
||||
testId="dashboard-action-open-new-tab"
|
||||
>
|
||||
Open in New Tab
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<Link2 size={14} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setCopy(getAbsoluteUrl(link));
|
||||
}}
|
||||
testId="dashboard-action-copy-link"
|
||||
>
|
||||
Copy Link
|
||||
</Button>
|
||||
{canEdit && (
|
||||
<Tooltip
|
||||
placement="left"
|
||||
title={
|
||||
isLocked ? 'This dashboard is locked, so it cannot be renamed.' : ''
|
||||
}
|
||||
>
|
||||
<span className={styles.menuItemWrap}>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<PenLine size={14} />}
|
||||
disabled={isLocked}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (!isLocked) {
|
||||
setIsRenameOpen(true);
|
||||
}
|
||||
}}
|
||||
testId="dashboard-action-rename"
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Tooltip
|
||||
placement="left"
|
||||
title={
|
||||
isLocked
|
||||
? 'This dashboard is locked, so its tags cannot be edited.'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<span className={styles.menuItemWrap}>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<Tag size={14} />}
|
||||
disabled={isLocked}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (!isLocked) {
|
||||
setIsEditTagsOpen(true);
|
||||
}
|
||||
}}
|
||||
testId="dashboard-action-edit-tags"
|
||||
>
|
||||
{tags.length > 0 ? 'Edit Tags' : 'Add Tags'}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<PenLine size={14} />}
|
||||
disabled={isLocked}
|
||||
prefix={<Copy size={14} />}
|
||||
loading={isCloning}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (!isLocked) {
|
||||
setIsRenameOpen(true);
|
||||
}
|
||||
runClone();
|
||||
}}
|
||||
testId="dashboard-action-rename"
|
||||
testId="dashboard-action-duplicate"
|
||||
>
|
||||
Rename
|
||||
Duplicate
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Tooltip
|
||||
placement="left"
|
||||
title={
|
||||
isLocked
|
||||
? 'This dashboard is locked, so its tags cannot be edited.'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<span className={styles.menuItemWrap}>
|
||||
)}
|
||||
{canToggleLock && (
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<Tag size={14} />}
|
||||
disabled={isLocked}
|
||||
prefix={<LockKeyhole size={14} />}
|
||||
loading={isTogglingLock}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (!isLocked) {
|
||||
setIsEditTagsOpen(true);
|
||||
}
|
||||
runLockToggle();
|
||||
}}
|
||||
testId="dashboard-action-edit-tags"
|
||||
testId="dashboard-action-lock"
|
||||
>
|
||||
{tags.length > 0 ? 'Edit Tags' : 'Add Tags'}
|
||||
{isLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<Copy size={14} />}
|
||||
loading={isCloning}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
runClone();
|
||||
}}
|
||||
testId="dashboard-action-duplicate"
|
||||
>
|
||||
Duplicate
|
||||
</Button>
|
||||
)}
|
||||
{canToggleLock && (
|
||||
<Button
|
||||
color="secondary"
|
||||
className={styles.menuItem}
|
||||
prefix={<LockKeyhole size={14} />}
|
||||
loading={isTogglingLock}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
runLockToggle();
|
||||
}}
|
||||
testId="dashboard-action-lock"
|
||||
>
|
||||
{isLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{canEdit && (
|
||||
<DeleteActionItem
|
||||
@@ -238,6 +247,7 @@ function ActionsPopover({
|
||||
dashboardName={dashboardName}
|
||||
createdBy={createdBy}
|
||||
isLocked={isLocked}
|
||||
showDivider={!isLegacy}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,9 @@ interface Props {
|
||||
dashboardName: string;
|
||||
createdBy: string;
|
||||
isLocked: boolean;
|
||||
// Delete sits below the other actions, so it leads with a divider. When it's
|
||||
// the only item (a legacy dashboard), the divider is suppressed.
|
||||
showDivider?: boolean;
|
||||
}
|
||||
|
||||
function DeleteActionItem({
|
||||
@@ -30,6 +33,7 @@ function DeleteActionItem({
|
||||
dashboardName,
|
||||
createdBy,
|
||||
isLocked,
|
||||
showDivider = true,
|
||||
}: Props): JSX.Element {
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
const { user } = useAppContext();
|
||||
@@ -100,7 +104,7 @@ function DeleteActionItem({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider />
|
||||
{showDivider && <Divider />}
|
||||
<Tooltip placement="left" title={tooltip}>
|
||||
<span className={styles.menuItemWrap}>
|
||||
<Button
|
||||
|
||||
@@ -55,6 +55,10 @@
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.legacyBadge {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.tagsWithActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -71,6 +75,17 @@
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
/* Wrap so the tooltip has a hoverable target even when the pin button is disabled
|
||||
(a legacy dashboard can't be pinned). */
|
||||
.pinButtonWrap {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.pinButtonWrap button:disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pinButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
import type { DashboardListItem } from '../../utils/helpers';
|
||||
|
||||
import DashboardRow from './DashboardRow';
|
||||
|
||||
const mockSafeNavigate = jest.fn();
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockTogglePin = jest.fn();
|
||||
jest.mock('../../hooks/usePinDashboard', () => ({
|
||||
usePinDashboard: (): { togglePin: jest.Mock; isUpdating: boolean } => ({
|
||||
togglePin: mockTogglePin,
|
||||
isUpdating: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('api/common/logEvent', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
// Stub the actions menu so this suite stays focused on the row; the isLegacy
|
||||
// wiring is asserted via the recorded prop, and the menu itself is covered by
|
||||
// ActionsPopover's own tests.
|
||||
jest.mock('../ActionsPopover/ActionsPopover', () => ({
|
||||
__esModule: true,
|
||||
default: ({ isLegacy }: { isLegacy?: boolean }): JSX.Element => (
|
||||
<div data-testid="actions-popover" data-legacy={String(!!isLegacy)} />
|
||||
),
|
||||
}));
|
||||
|
||||
const makeDashboard = (
|
||||
overrides: Partial<DashboardListItem> = {},
|
||||
): DashboardListItem =>
|
||||
({
|
||||
id: 'dash-1',
|
||||
legacy: false,
|
||||
pinned: false,
|
||||
locked: false,
|
||||
image: '',
|
||||
createdBy: 'alice@signoz.io',
|
||||
updatedBy: 'alice@signoz.io',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-02T00:00:00Z',
|
||||
tags: [],
|
||||
spec: { display: { name: 'My Dashboard' } },
|
||||
...overrides,
|
||||
}) as unknown as DashboardListItem;
|
||||
|
||||
const renderRow = (dashboard: DashboardListItem): void => {
|
||||
render(
|
||||
<DashboardRow
|
||||
dashboard={dashboard}
|
||||
index={0}
|
||||
canEdit
|
||||
showUpdatedAt={false}
|
||||
showUpdatedBy={false}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
describe('DashboardRow', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('a v2 dashboard', () => {
|
||||
it('navigates on click and shows no legacy badge', async () => {
|
||||
renderRow(makeDashboard());
|
||||
|
||||
expect(screen.queryByTestId('dashboard-legacy-0')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('dashboard-pin-0')).not.toBeDisabled();
|
||||
expect(screen.getByTestId('actions-popover')).toHaveAttribute(
|
||||
'data-legacy',
|
||||
'false',
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('dashboard-title-0'));
|
||||
expect(mockSafeNavigate).toHaveBeenCalledTimes(1);
|
||||
expect(screen.queryByTestId('legacy-dashboard-id')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('a legacy dashboard', () => {
|
||||
it('badges the row, disables pin and gates the actions menu', () => {
|
||||
renderRow(makeDashboard({ legacy: true }));
|
||||
|
||||
expect(screen.getByTestId('dashboard-legacy-0')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('dashboard-pin-0')).toBeDisabled();
|
||||
expect(screen.getByTestId('actions-popover')).toHaveAttribute(
|
||||
'data-legacy',
|
||||
'true',
|
||||
);
|
||||
});
|
||||
|
||||
it('opens the explanatory dialog instead of navigating', async () => {
|
||||
renderRow(makeDashboard({ legacy: true }));
|
||||
|
||||
await userEvent.click(screen.getByTestId('dashboard-title-0'));
|
||||
|
||||
expect(mockSafeNavigate).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId('legacy-dashboard-id')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import TagBadge from 'components/TagBadge/TagBadge';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
@@ -18,6 +20,7 @@ import { useDashboardViewsStore } from '../../store/useDashboardViewsStore';
|
||||
import type { DashboardListItem } from '../../utils/helpers';
|
||||
import { lastUpdatedLabel, tagsToStrings } from '../../utils/helpers';
|
||||
import ActionsPopover from '../ActionsPopover/ActionsPopover';
|
||||
import LegacyDashboardDialog from '../LegacyDashboardDialog/LegacyDashboardDialog';
|
||||
|
||||
import styles from './DashboardRow.module.scss';
|
||||
|
||||
@@ -42,6 +45,11 @@ function DashboardRow({
|
||||
const markViewed = useDashboardViewsStore((s) => s.markViewed);
|
||||
const { togglePin, isUpdating } = usePinDashboard();
|
||||
|
||||
const [isLegacyDialogOpen, setIsLegacyDialogOpen] = useState(false);
|
||||
|
||||
// A legacy dashboard is stored in the pre-v2 shape and has no v2 spec, so it
|
||||
// can't be opened in the new experience — clicking it explains this instead.
|
||||
const isLegacy = !!dashboard.legacy;
|
||||
const isPinned = !!dashboard.pinned;
|
||||
const id = dashboard.id;
|
||||
const name = dashboard.spec?.display?.name ?? '';
|
||||
@@ -67,9 +75,17 @@ function DashboardRow({
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
if (isLegacy) {
|
||||
setIsLegacyDialogOpen(true);
|
||||
void logEvent('Dashboard List: Clicked on legacy dashboard', {
|
||||
dashboardId: id,
|
||||
dashboardName: name,
|
||||
});
|
||||
return;
|
||||
}
|
||||
markViewed(id);
|
||||
safeNavigate(link, { newTab: isModifierKeyPressed(event) });
|
||||
logEvent('Dashboard List: Clicked on dashboard', {
|
||||
void logEvent('Dashboard List: Clicked on dashboard', {
|
||||
dashboardId: id,
|
||||
dashboardName: name,
|
||||
});
|
||||
@@ -77,9 +93,17 @@ function DashboardRow({
|
||||
|
||||
const onTogglePin = (event: React.MouseEvent<HTMLElement>): void => {
|
||||
event.stopPropagation();
|
||||
if (isLegacy) {
|
||||
return;
|
||||
}
|
||||
togglePin(id, isPinned);
|
||||
};
|
||||
|
||||
const pinLabel = isPinned ? 'Unpin dashboard' : 'Pin dashboard';
|
||||
const pinTooltip = isLegacy
|
||||
? "This dashboard isn't available in the new experience, so it can't be pinned"
|
||||
: pinLabel;
|
||||
|
||||
// Only long titles are truncated, so only they need the full-name tooltip;
|
||||
// wrapping conditionally avoids an empty hanging tooltip for short names.
|
||||
const titleLink = (
|
||||
@@ -95,120 +119,144 @@ function DashboardRow({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.row} onClick={onClickHandler}>
|
||||
<div className={styles.titleWithAction}>
|
||||
<div className={styles.titleBlock}>
|
||||
{name.length > 50 ? (
|
||||
<TooltipSimple title={name} side="bottom" disableHoverableContent>
|
||||
{titleLink}
|
||||
<>
|
||||
<div className={styles.row} onClick={onClickHandler}>
|
||||
<div className={styles.titleWithAction}>
|
||||
<div className={styles.titleBlock}>
|
||||
{name.length > 50 ? (
|
||||
<TooltipSimple title={name} side="bottom" disableHoverableContent>
|
||||
{titleLink}
|
||||
</TooltipSimple>
|
||||
) : (
|
||||
titleLink
|
||||
)}
|
||||
{isLegacy && (
|
||||
<Badge
|
||||
color="amber"
|
||||
variant="outline"
|
||||
className={styles.legacyBadge}
|
||||
testId={`dashboard-legacy-${index}`}
|
||||
>
|
||||
Legacy
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.tagsWithActions}>
|
||||
{tags.length > 0 && (
|
||||
<div className={styles.tags}>
|
||||
{tags.slice(0, 3).map((tag) => (
|
||||
<TagBadge key={tag}>{tag}</TagBadge>
|
||||
))}
|
||||
{tags.length > 3 && (
|
||||
<TagBadge key={tags[3]}>+{tags.length - 3}</TagBadge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLocked && (
|
||||
<TooltipSimple
|
||||
title="This dashboard is locked"
|
||||
side="top"
|
||||
disableHoverableContent
|
||||
>
|
||||
<span
|
||||
className={styles.lockIcon}
|
||||
data-testid={`dashboard-lock-${index}`}
|
||||
>
|
||||
<LockKeyhole size={14} />
|
||||
</span>
|
||||
</TooltipSimple>
|
||||
) : (
|
||||
titleLink
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.tagsWithActions}>
|
||||
{tags.length > 0 && (
|
||||
<div className={styles.tags}>
|
||||
{tags.slice(0, 3).map((tag) => (
|
||||
<TagBadge key={tag}>{tag}</TagBadge>
|
||||
))}
|
||||
{tags.length > 3 && (
|
||||
<TagBadge key={tags[3]}>+{tags.length - 3}</TagBadge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLocked && (
|
||||
<TooltipSimple
|
||||
title="This dashboard is locked"
|
||||
side="top"
|
||||
disableHoverableContent
|
||||
>
|
||||
<span className={styles.lockIcon} data-testid={`dashboard-lock-${index}`}>
|
||||
<LockKeyhole size={14} />
|
||||
<TooltipSimple title={pinTooltip} side="top" disableHoverableContent>
|
||||
<span className={styles.pinButtonWrap}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
className={cx(styles.pinButton, {
|
||||
[styles.pinButtonOn]: isPinned && !isLegacy,
|
||||
})}
|
||||
aria-label={pinLabel}
|
||||
data-testid={`dashboard-pin-${index}`}
|
||||
disabled={isUpdating || isLegacy}
|
||||
onClick={onTogglePin}
|
||||
>
|
||||
{isPinned ? (
|
||||
<>
|
||||
<Pin size={14} className={styles.pinnedIcon} />
|
||||
<PinOff size={14} className={styles.unpinIcon} />
|
||||
</>
|
||||
) : (
|
||||
<Pin size={14} />
|
||||
)}
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
|
||||
<TooltipSimple
|
||||
title={isPinned ? 'Unpin dashboard' : 'Pin dashboard'}
|
||||
side="top"
|
||||
disableHoverableContent
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
className={cx(styles.pinButton, { [styles.pinButtonOn]: isPinned })}
|
||||
aria-label={isPinned ? 'Unpin dashboard' : 'Pin dashboard'}
|
||||
data-testid={`dashboard-pin-${index}`}
|
||||
disabled={isUpdating}
|
||||
onClick={onTogglePin}
|
||||
>
|
||||
{isPinned ? (
|
||||
<>
|
||||
<Pin size={14} className={styles.pinnedIcon} />
|
||||
<PinOff size={14} className={styles.unpinIcon} />
|
||||
</>
|
||||
) : (
|
||||
<Pin size={14} />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
|
||||
<ActionsPopover
|
||||
link={link}
|
||||
dashboardId={id}
|
||||
dashboardName={name}
|
||||
createdBy={createdBy}
|
||||
isLocked={isLocked}
|
||||
tags={tags}
|
||||
canEdit={canEdit}
|
||||
onView={onClickHandler}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.details}>
|
||||
<div className={styles.createdAt}>
|
||||
<CalendarClock size={14} />
|
||||
<Typography.Text>{formattedCreatedAt}</Typography.Text>
|
||||
<ActionsPopover
|
||||
link={link}
|
||||
dashboardId={id}
|
||||
dashboardName={name}
|
||||
createdBy={createdBy}
|
||||
isLocked={isLocked}
|
||||
tags={tags}
|
||||
canEdit={canEdit}
|
||||
onView={onClickHandler}
|
||||
isLegacy={isLegacy}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{createdBy && (
|
||||
<div className={styles.createdBy}>
|
||||
<div className={styles.avatar}>
|
||||
<Typography.Text className={styles.avatarText}>
|
||||
{createdBy.substring(0, 1).toUpperCase()}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text className={styles.byLabel}>{createdBy}</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showUpdatedAt && (
|
||||
<div className={styles.details}>
|
||||
<div className={styles.createdAt}>
|
||||
<CalendarClock size={14} />
|
||||
<Typography.Text>{lastUpdatedLabel(updatedAt)}</Typography.Text>
|
||||
<Typography.Text>{formattedCreatedAt}</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{updatedBy && showUpdatedBy && (
|
||||
<div className={styles.updatedBy}>
|
||||
<Typography.Text className={styles.byLabel}>
|
||||
Last Updated By -
|
||||
</Typography.Text>
|
||||
<div className={styles.avatar}>
|
||||
<Typography.Text className={styles.avatarText}>
|
||||
{updatedBy.substring(0, 1).toUpperCase()}
|
||||
</Typography.Text>
|
||||
{createdBy && (
|
||||
<div className={styles.createdBy}>
|
||||
<div className={styles.avatar}>
|
||||
<Typography.Text className={styles.avatarText}>
|
||||
{createdBy.substring(0, 1).toUpperCase()}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text className={styles.byLabel}>{createdBy}</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text className={styles.byLabel}>{updatedBy}</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{showUpdatedAt && (
|
||||
<div className={styles.createdAt}>
|
||||
<CalendarClock size={14} />
|
||||
<Typography.Text>{lastUpdatedLabel(updatedAt)}</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{updatedBy && showUpdatedBy && (
|
||||
<div className={styles.updatedBy}>
|
||||
<Typography.Text className={styles.byLabel}>
|
||||
Last Updated By -
|
||||
</Typography.Text>
|
||||
<div className={styles.avatar}>
|
||||
<Typography.Text className={styles.avatarText}>
|
||||
{updatedBy.substring(0, 1).toUpperCase()}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text className={styles.byLabel}>{updatedBy}</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isLegacy && (
|
||||
<LegacyDashboardDialog
|
||||
open={isLegacyDialogOpen}
|
||||
dashboardId={id}
|
||||
dashboardName={name}
|
||||
onClose={(): void => setIsLegacyDialogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
|
||||
strong {
|
||||
color: var(--l1-foreground);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.idField {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.idLabel {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.idRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 6px 6px 6px 12px;
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 4px;
|
||||
background: var(--l2-background);
|
||||
}
|
||||
|
||||
.idValue {
|
||||
color: var(--l1-foreground);
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
import LegacyDashboardDialog from './LegacyDashboardDialog';
|
||||
|
||||
const mockCopy = jest.fn();
|
||||
jest.mock('react-use', () => ({
|
||||
...jest.requireActual('react-use'),
|
||||
useCopyToClipboard: (): [unknown, (value: string) => void] => [{}, mockCopy],
|
||||
}));
|
||||
|
||||
const mockToastSuccess = jest.fn();
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
toast: { success: (message: string): void => mockToastSuccess(message) },
|
||||
}));
|
||||
|
||||
const mockContactSupport = jest.fn();
|
||||
jest.mock('container/Integrations/utils', () => ({
|
||||
handleContactSupport: (isCloud: boolean): void => mockContactSupport(isCloud),
|
||||
}));
|
||||
|
||||
const DASHBOARD_ID = '0f9a1b2c-3d4e-5f6a-7b8c-9d0e1f2a3b4c';
|
||||
|
||||
describe('LegacyDashboardDialog', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const setup = (open = true): void => {
|
||||
render(
|
||||
<LegacyDashboardDialog
|
||||
open={open}
|
||||
dashboardId={DASHBOARD_ID}
|
||||
dashboardName="My Legacy Dashboard"
|
||||
onClose={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
it('surfaces the dashboard name and id', () => {
|
||||
setup();
|
||||
expect(screen.getByText('My Legacy Dashboard')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('legacy-dashboard-id')).toHaveTextContent(
|
||||
DASHBOARD_ID,
|
||||
);
|
||||
});
|
||||
|
||||
it('copies the dashboard id and confirms with a toast', async () => {
|
||||
setup();
|
||||
await userEvent.click(screen.getByTestId('legacy-dashboard-copy-id'));
|
||||
expect(mockCopy).toHaveBeenCalledWith(DASHBOARD_ID);
|
||||
expect(mockToastSuccess).toHaveBeenCalledWith('Dashboard ID copied');
|
||||
});
|
||||
|
||||
it('routes the user to support', async () => {
|
||||
setup();
|
||||
await userEvent.click(screen.getByTestId('legacy-dashboard-contact-support'));
|
||||
expect(mockContactSupport).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders nothing when closed', () => {
|
||||
setup(false);
|
||||
expect(screen.queryByTestId('legacy-dashboard-id')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { ArrowUpRight, Copy } from '@signozhq/icons';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { handleContactSupport } from 'container/Integrations/utils';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
|
||||
import styles from './LegacyDashboardDialog.module.scss';
|
||||
|
||||
interface LegacyDashboardDialogProps {
|
||||
open: boolean;
|
||||
dashboardId: string;
|
||||
dashboardName: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explains why a legacy (pre-v2) dashboard can't be opened in the new experience
|
||||
* and hands the user the dashboard ID to share with support. Legacy rows are
|
||||
* surfaced by the list API with `legacy: true` but have no v2 spec to render.
|
||||
*/
|
||||
function LegacyDashboardDialog({
|
||||
open,
|
||||
dashboardId,
|
||||
dashboardName,
|
||||
onClose,
|
||||
}: LegacyDashboardDialogProps): JSX.Element {
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
const { isCloudUser } = useGetTenantLicense();
|
||||
|
||||
const onCopyId = (): void => {
|
||||
copyToClipboard(dashboardId);
|
||||
toast.success('Dashboard ID copied');
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogWrapper
|
||||
title="This dashboard isn't available in the new experience"
|
||||
open={open}
|
||||
width="narrow"
|
||||
onOpenChange={(next): void => {
|
||||
if (!next) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
footer={
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="md"
|
||||
onClick={onClose}
|
||||
testId="legacy-dashboard-close"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="md"
|
||||
suffix={<ArrowUpRight size={14} />}
|
||||
onClick={(): void => handleContactSupport(!!isCloudUser)}
|
||||
testId="legacy-dashboard-contact-support"
|
||||
>
|
||||
Contact Support
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={styles.body}>
|
||||
<Typography.Text className={styles.description}>
|
||||
<strong>{dashboardName || 'This dashboard'}</strong> hasn't been
|
||||
migrated to the new dashboard experience yet, so it can't be opened
|
||||
here. Share the dashboard ID below with support and we'll help you
|
||||
move it over.
|
||||
</Typography.Text>
|
||||
|
||||
<div className={styles.idField}>
|
||||
<Typography.Text className={styles.idLabel}>Dashboard ID</Typography.Text>
|
||||
<div className={styles.idRow}>
|
||||
<Typography.Text
|
||||
className={styles.idValue}
|
||||
data-testid="legacy-dashboard-id"
|
||||
>
|
||||
{dashboardId}
|
||||
</Typography.Text>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="icon"
|
||||
prefix={<Copy size={14} />}
|
||||
aria-label="Copy dashboard ID"
|
||||
onClick={onCopyId}
|
||||
testId="legacy-dashboard-copy-id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default LegacyDashboardDialog;
|
||||
@@ -28,6 +28,7 @@ export interface QueryKeyRequestProps {
|
||||
fieldContext?: 'resource' | 'scope' | 'attribute' | 'span';
|
||||
fieldDataType?: QUERY_BUILDER_KEY_TYPES;
|
||||
metricName?: string;
|
||||
metricNamespace?: string;
|
||||
signalSource?: 'meter' | '';
|
||||
}
|
||||
|
||||
|
||||
@@ -302,3 +302,30 @@ Shortcuts:
|
||||
endTimeMs,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats age in milliseconds to a human-readable string (e.g., "5d 3h", "2h 30m", "45s")
|
||||
*/
|
||||
export function formatAge(ms: number): string {
|
||||
if (ms < 0 || Number.isNaN(ms) || !Number.isFinite(ms)) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) {
|
||||
const remainingHours = hours % 24;
|
||||
return remainingHours > 0 ? `${days}d ${remainingHours}h` : `${days}d`;
|
||||
}
|
||||
if (hours > 0) {
|
||||
const remainingMinutes = minutes % 60;
|
||||
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ func (provider *provider) addInfraMonitoringRoutes(router *mux.Router) error {
|
||||
ID: "ListDaemonSets",
|
||||
Tags: []string{"inframonitoring"},
|
||||
Summary: "List DaemonSets for Infra Monitoring",
|
||||
Description: "Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on) and currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel when no data is available for that field.",
|
||||
Description: "Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on), currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on), readyNodes (k8s.daemonset.ready_nodes, the number of nodes running at least one ready daemon pod) and misscheduledNodes (k8s.daemonset.misscheduled_nodes, the number of nodes running the daemon pod but not supposed to) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes / ready_nodes / misscheduled_nodes, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes, readyNodes, misscheduledNodes) return -1 as a sentinel when no data is available for that field.",
|
||||
Request: new(inframonitoringtypes.PostableDaemonSets),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(inframonitoringtypes.DaemonSets),
|
||||
|
||||
@@ -71,7 +71,7 @@ func (module *module) ListV2(ctx context.Context, orgID valuer.UUID, params *das
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dashboardtypes.NewListableDashboardV2(dashboards, total, tagsByDashboard, allTags)
|
||||
return dashboardtypes.NewListableDashboardV2(dashboards, total, tagsByDashboard, allTags), nil
|
||||
}
|
||||
|
||||
func (module *module) ListForUserV2(ctx context.Context, orgID valuer.UUID, userID valuer.UUID, params *dashboardtypes.ListDashboardsV2Params) (*dashboardtypes.ListableDashboardForUserV2, error) {
|
||||
@@ -90,7 +90,7 @@ func (module *module) ListForUserV2(ctx context.Context, orgID valuer.UUID, user
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dashboardtypes.NewListableDashboardForUserV2(rows, total, tagsByDashboard, allTags)
|
||||
return dashboardtypes.NewListableDashboardForUserV2(rows, total, tagsByDashboard, allTags), nil
|
||||
}
|
||||
|
||||
func (module *module) fetchDashboardTags(ctx context.Context, orgID valuer.UUID, dashboardIDs []valuer.UUID) (map[valuer.UUID][]*tagtypes.Tag, []*tagtypes.Tag, error) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user