Compare commits

..

1 Commits

Author SHA1 Message Date
swapnil-signoz
1b7f1cb654 feat: adding metrics in definition and dashboard JSON 2026-07-10 16:23:30 +05:30
161 changed files with 8006 additions and 7326 deletions

View File

@@ -3148,8 +3148,6 @@ components:
type: string
image:
type: string
legacy:
type: boolean
locked:
type: boolean
name:
@@ -3182,7 +3180,6 @@ components:
- name
- tags
- spec
- legacy
- pinned
type: object
DashboardtypesListedDashboardV2:
@@ -3196,8 +3193,6 @@ components:
type: string
image:
type: string
legacy:
type: boolean
locked:
type: boolean
name:
@@ -3228,7 +3223,6 @@ components:
- name
- tags
- spec
- legacy
type: object
DashboardtypesListedDashboardV2Spec:
properties:
@@ -4484,14 +4478,10 @@ 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
@@ -4502,8 +4492,6 @@ components:
- daemonSetMemoryLimit
- desiredNodes
- currentNodes
- readyNodes
- misscheduledNodes
- podCountsByPhase
- podCountsByStatus
- meta
@@ -16005,24 +15993,21 @@ 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), 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,
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, readyNodes, misscheduledNodes)
return -1 as a sentinel when no data is available for that field.'
daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel
when no data is available for that field.'
operationId: ListDaemonSets
requestBody:
content:

View File

@@ -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), 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.
* 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.
* @summary List DaemonSets for Infra Monitoring
*/
export const listDaemonSets = (

View File

@@ -5005,10 +5005,6 @@ export interface DashboardtypesListedDashboardForUserV2DTO {
* @type string
*/
image?: string;
/**
* @type boolean
*/
legacy: boolean;
/**
* @type boolean
*/
@@ -5084,10 +5080,6 @@ export interface DashboardtypesListedDashboardV2DTO {
* @type string
*/
image?: string;
/**
* @type boolean
*/
legacy: boolean;
/**
* @type boolean
*/
@@ -6090,16 +6082,8 @@ export interface InframonitoringtypesDaemonSetRecordDTO {
* @type object,null
*/
meta: InframonitoringtypesDaemonSetRecordDTOMeta;
/**
* @type integer
*/
misscheduledNodes: number;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
/**
* @type integer
*/
readyNodes: number;
}
export interface InframonitoringtypesDaemonSetsDTO {

View File

@@ -22,7 +22,6 @@ export const getKeySuggestions = (
fieldContext = '',
fieldDataType = '',
signalSource = '',
metricNamespace = '',
} = props;
const encodedSignal = encodeURIComponent(signal);
@@ -31,9 +30,8 @@ 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}&metricNamespace=${encodedMetricNamespace}`,
`/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}&source=${encodedSource}`,
);
};

View File

@@ -2,7 +2,7 @@ import { Controller, useForm } from 'react-hook-form';
import { useQueryClient } from 'react-query';
import { X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import { SACreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import { DialogFooter, DialogWrapper } from '@signozhq/ui/dialog';
import { Input } from '@signozhq/ui/input';
@@ -134,17 +134,18 @@ function CreateServiceAccountModal(): JSX.Element {
Cancel
</Button>
<AuthZButton
checks={[SACreatePermission]}
type="submit"
form="create-sa-form"
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
>
Create Service Account
</AuthZButton>
<AuthZTooltip checks={[SACreatePermission]}>
<Button
type="submit"
form="create-sa-form"
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
>
Create Service Account
</Button>
</AuthZTooltip>
</DialogFooter>
</DialogWrapper>
);

View File

@@ -89,7 +89,6 @@ interface QuerySearchProps {
onChange: (value: string) => void;
queryData: IBuilderQuery;
dataSource: DataSource;
metricNamespace?: string;
signalSource?: string;
hardcodedAttributeKeys?: QueryKeyDataSuggestionsProps[];
onRun?: (query: string) => void;
@@ -108,7 +107,6 @@ function QuerySearch({
hardcodedAttributeKeys,
showFilterSuggestionsWithoutMetric,
initialExpression,
metricNamespace,
}: QuerySearchProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
@@ -301,7 +299,6 @@ function QuerySearch({
searchText: searchText || '',
metricName: debouncedMetricName ?? undefined,
signalSource: signalSource as 'meter' | '',
metricNamespace,
});
if (response.data.data) {
@@ -334,7 +331,6 @@ function QuerySearch({
signalSource,
hardcodedAttributeKeys,
showFilterSuggestionsWithoutMetric,
metricNamespace,
],
);

View File

@@ -20,7 +20,6 @@ function OtherFiltersSkeleton(): JSX.Element {
<Skeleton.Input
active
size="small"
className="qf-other-filters-skeleton"
// eslint-disable-next-line react/no-array-index-key
key={index}
/>

View File

@@ -4,7 +4,7 @@ import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { DatePicker } from 'antd';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import {
APIKeyCreatePermission,
buildSAAttachPermission,
@@ -109,21 +109,24 @@ function KeyFormPhase({
<Button variant="solid" color="secondary" onClick={onClose}>
Cancel
</Button>
<AuthZButton
<AuthZTooltip
checks={[
APIKeyCreatePermission,
buildSAAttachPermission(accountId ?? ''),
]}
authZEnabled={!!accountId}
type="submit"
form={FORM_ID}
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
enabled={!!accountId}
>
Create Key
</AuthZButton>
<Button
type="submit"
form={FORM_ID}
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
>
Create Key
</Button>
</AuthZTooltip>
</div>
</div>
</>

View File

@@ -1,7 +1,7 @@
import { useQueryClient } from 'react-query';
import { Trash2, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import { buildSADeletePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import { DialogWrapper } from '@signozhq/ui/dialog';
import { toast } from '@signozhq/ui/sonner';
@@ -84,17 +84,20 @@ function DeleteAccountModal(): JSX.Element {
<X size={12} />
Cancel
</Button>
<AuthZButton
<AuthZTooltip
checks={[buildSADeletePermission(accountId ?? '')]}
authZEnabled={!!accountId}
variant="solid"
color="destructive"
loading={isDeleting}
onClick={handleConfirm}
enabled={!!accountId}
>
<Trash2 size={12} />
Delete
</AuthZButton>
<Button
variant="solid"
color="destructive"
loading={isDeleting}
onClick={handleConfirm}
>
<Trash2 size={12} />
Delete
</Button>
</AuthZTooltip>
</div>
);

View File

@@ -7,7 +7,6 @@ import { Input } from '@signozhq/ui/input';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { DatePicker } from 'antd';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import {
buildAPIKeyDeletePermission,
@@ -159,36 +158,38 @@ function EditKeyForm({
</form>
<div className="edit-key-modal__footer">
<AuthZButton
<AuthZTooltip
checks={[
buildAPIKeyDeletePermission(keyItem?.id ?? ''),
buildSADetachPermission(accountId ?? ''),
]}
authZEnabled={!!accountId && !!keyItem?.id}
variant="link"
color="destructive"
onClick={onRevokeClick}
enabled={!!accountId && !!keyItem?.id}
>
<Trash2 size={12} />
Revoke Key
</AuthZButton>
<Button variant="link" color="destructive" onClick={onRevokeClick}>
<Trash2 size={12} />
Revoke Key
</Button>
</AuthZTooltip>
<div className="edit-key-modal__footer-right">
<Button variant="solid" color="secondary" onClick={onClose}>
<X size={12} />
Cancel
</Button>
<AuthZButton
<AuthZTooltip
checks={[buildAPIKeyUpdatePermission(keyItem?.id ?? '')]}
authZEnabled={!!accountId && !!keyItem?.id}
type="submit"
form={FORM_ID}
variant="solid"
color="primary"
loading={isSaving}
disabled={!isDirty}
enabled={!!accountId && !!keyItem?.id}
>
Save Changes
</AuthZButton>
<Button
type="submit"
form={FORM_ID}
variant="solid"
color="primary"
loading={isSaving}
disabled={!isDirty}
>
Save Changes
</Button>
</AuthZTooltip>
</div>
</div>
</>

View File

@@ -1,13 +1,12 @@
import React, { useCallback, useMemo } from 'react';
import { KeyRound, X } from '@signozhq/icons';
import { Pagination, Skeleton, Table, Tooltip } from 'antd';
import { Button } from '@signozhq/ui/button';
import { Skeleton, Table, Tooltip } from 'antd';
import type { ColumnsType } from 'antd/es/table/interface';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import { withAuthZContent } from 'lib/authz/components/withAuthZ/withAuthZContent';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import {
APIKeyCreatePermission,
APIKeyListPermission,
buildAPIKeyDeletePermission,
buildSAAttachPermission,
buildSADetachPermission,
@@ -25,10 +24,10 @@ interface KeysTabProps {
keys: ServiceaccounttypesGettableFactorAPIKeyDTO[];
isLoading: boolean;
isDisabled?: boolean;
canUpdate?: boolean;
accountId?: string;
currentPage: number;
pageSize: number;
onPageChange: (page: number) => void;
}
interface BuildColumnsParams {
@@ -114,26 +113,29 @@ function buildColumns({
render: (_, record): JSX.Element => {
const tooltipTitle = isDisabled ? 'Service account disabled' : 'Revoke Key';
return (
<Tooltip title={tooltipTitle}>
<AuthZButton
checks={[
buildAPIKeyDeletePermission(record.id),
buildSADetachPermission(accountId),
]}
authZEnabled={!isDisabled && !!accountId}
variant="ghost"
size="sm"
color="destructive"
disabled={isDisabled}
onClick={(e): void => {
e.stopPropagation();
onRevokeClick(record.id);
}}
className="keys-tab__revoke-btn"
>
<X size={12} />
</AuthZButton>
</Tooltip>
<AuthZTooltip
checks={[
buildAPIKeyDeletePermission(record.id),
buildSADetachPermission(accountId),
]}
enabled={!isDisabled && !!accountId}
>
<Tooltip title={tooltipTitle}>
<Button
variant="ghost"
size="sm"
color="destructive"
disabled={isDisabled}
onClick={(e): void => {
e.stopPropagation();
onRevokeClick(record.id);
}}
className="keys-tab__revoke-btn"
>
<X size={12} />
</Button>
</Tooltip>
</AuthZTooltip>
);
},
},
@@ -147,7 +149,6 @@ function KeysTab({
accountId = '',
currentPage,
pageSize,
onPageChange,
}: KeysTabProps): JSX.Element {
const [, setIsAddKeyOpen] = useQueryState(
'add-key',
@@ -211,18 +212,21 @@ function KeysTab({
Learn more
</a>
</p>
<AuthZButton
<AuthZTooltip
checks={[APIKeyCreatePermission, buildSAAttachPermission(accountId)]}
authZEnabled={!isDisabled && !!accountId}
variant="link"
color="primary"
onClick={async (): Promise<void> => {
await setIsAddKeyOpen(true);
}}
disabled={isDisabled}
enabled={!isDisabled && !!accountId}
>
+ Add your first key
</AuthZButton>
<Button
variant="link"
color="primary"
onClick={async (): Promise<void> => {
await setIsAddKeyOpen(true);
}}
disabled={isDisabled}
>
+ Add your first key
</Button>
</AuthZTooltip>
</div>
);
}
@@ -274,24 +278,6 @@ function KeysTab({
})}
/>
<Pagination
current={currentPage}
pageSize={pageSize}
total={keys.length}
showTotal={(total: number, range: number[]): JSX.Element => (
<>
<span className="sa-drawer__pagination-range">
{range[0]} &#8212; {range[1]}
</span>
<span className="sa-drawer__pagination-total"> of {total}</span>
</>
)}
showSizeChanger={false}
hideOnSinglePage
onChange={onPageChange}
className="sa-drawer__keys-pagination"
/>
<EditKeyModal keyItem={editKey} />
<RevokeKeyModal />
@@ -299,7 +285,4 @@ function KeysTab({
);
}
export default withAuthZContent(KeysTab, {
checks: [APIKeyListPermission],
fallbackOnLoading: <Skeleton active paragraph={{ rows: 6 }} />,
});
export default KeysTab;

View File

@@ -6,20 +6,15 @@ import { Input } from '@signozhq/ui/input';
import { useCopyToClipboard } from 'react-use';
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import { withAuthZContent } from 'lib/authz/components/withAuthZ/withAuthZContent';
import RolesSelect from 'components/RolesSelect';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { ServiceAccountRow } from 'container/ServiceAccountsSettings/utils';
import {
buildSAReadPermission,
buildSAUpdatePermission,
} from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import { buildSAUpdatePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import { useTimezone } from 'providers/Timezone';
import APIError from 'types/api/error';
import SaveErrorItem from './SaveErrorItem';
import type { SaveError } from './utils';
import { Skeleton } from 'antd';
interface OverviewTabProps {
account: ServiceAccountRow;
@@ -28,6 +23,7 @@ interface OverviewTabProps {
localRoles: string[];
onRolesChange: (v: string[]) => void;
isDisabled: boolean;
canUpdate?: boolean;
availableRoles: AuthtypesRoleDTO[];
rolesLoading?: boolean;
rolesError?: boolean;
@@ -43,6 +39,7 @@ function OverviewTab({
localRoles,
onRolesChange,
isDisabled,
canUpdate = true,
availableRoles,
rolesLoading,
rolesError,
@@ -89,22 +86,23 @@ function OverviewTab({
<label className="sa-drawer__label" htmlFor="sa-name">
Name
</label>
{isDisabled ? (
<AuthZTooltip checks={[buildSAUpdatePermission(account.id)]}>
{isDisabled || !canUpdate ? (
<AuthZTooltip
checks={[buildSAUpdatePermission(account.id)]}
enabled={!isDisabled && !canUpdate}
>
<div className="sa-drawer__input-wrapper sa-drawer__input-wrapper--disabled">
<span className="sa-drawer__input-text">{localName || '—'}</span>
<LockKeyhole size={14} className="sa-drawer__lock-icon" />
</div>
</AuthZTooltip>
) : (
<AuthZTooltip checks={[buildSAUpdatePermission(account.id)]}>
<Input
id="sa-name"
value={localName}
onChange={(e): void => onNameChange(e.target.value)}
placeholder="Enter name"
/>
</AuthZTooltip>
<Input
id="sa-name"
value={localName}
onChange={(e): void => onNameChange(e.target.value)}
placeholder="Enter name"
/>
)}
</div>
@@ -222,9 +220,4 @@ function OverviewTab({
);
}
export default withAuthZContent(OverviewTab, {
checks: (props): ReturnType<typeof buildSAReadPermission>[] => [
buildSAReadPermission(props.account.id),
],
fallbackOnLoading: <Skeleton active paragraph={{ rows: 6 }} />,
});
export default OverviewTab;

View File

@@ -1,7 +1,7 @@
import { useQueryClient } from 'react-query';
import { Trash2, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import {
buildAPIKeyDeletePermission,
buildSADetachPermission,
@@ -45,20 +45,23 @@ export function RevokeKeyFooter({
<X size={12} />
Cancel
</Button>
<AuthZButton
<AuthZTooltip
checks={[
buildAPIKeyDeletePermission(keyId ?? ''),
buildSADetachPermission(accountId ?? ''),
]}
authZEnabled={!!accountId && !!keyId}
variant="solid"
color="destructive"
loading={isRevoking}
onClick={onConfirm}
enabled={!!accountId && !!keyId}
>
<Trash2 size={12} />
Revoke Key
</AuthZButton>
<Button
variant="solid"
color="destructive"
loading={isRevoking}
onClick={onConfirm}
>
<Trash2 size={12} />
Revoke Key
</Button>
</AuthZTooltip>
</>
);
}
@@ -108,7 +111,7 @@ function RevokeKeyModal(): JSX.Element {
}
function handleCancel(): void {
void setRevokeKeyId(null);
setRevokeKeyId(null);
}
return (

View File

@@ -1,17 +1,11 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQueryClient } from 'react-query';
import { Key, LayoutGrid, Plus, Trash2, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DrawerWrapper } from '@signozhq/ui/drawer';
import { toast } from '@signozhq/ui/sonner';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Skeleton } from 'antd';
import { Pagination, Skeleton } from 'antd';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
getListServiceAccountsQueryKey,
@@ -22,6 +16,7 @@ import {
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import PermissionDeniedCallout from 'lib/authz/components/PermissionDeniedCallout/PermissionDeniedCallout';
import { useRoles } from 'components/RolesSelect';
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
import {
@@ -33,13 +28,15 @@ import {
RoleUpdateFailure,
useServiceAccountRoleManager,
} from 'hooks/serviceAccount/useServiceAccountRoleManager';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import {
APIKeyCreatePermission,
APIKeyListPermission,
buildSAAttachPermission,
buildSADeletePermission,
buildSAReadPermission,
buildSAUpdatePermission,
} from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
import {
parseAsBoolean,
parseAsInteger,
@@ -50,6 +47,7 @@ import {
import APIError from 'types/api/error';
import { toAPIError } from 'utils/errorUtils';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import AddKeyModal from './AddKeyModal';
import DeleteAccountModal from './DeleteAccountModal';
import KeysTab from './KeysTab';
@@ -72,12 +70,14 @@ function toSaveApiError(err: unknown): APIError {
);
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function ServiceAccountDrawer({
onSuccess,
}: ServiceAccountDrawerProps): JSX.Element {
const [selectedAccountId, setSelectedAccountId] = useQueryState(
SA_QUERY_PARAMS.ACCOUNT,
);
const open = !!selectedAccountId;
const [activeTab, setActiveTab] = useQueryState(
SA_QUERY_PARAMS.TAB,
parseAsStringEnum<ServiceAccountDrawerTab>(
@@ -100,14 +100,28 @@ function ServiceAccountDrawer({
SA_QUERY_PARAMS.DELETE_SA,
parseAsBoolean.withDefault(false),
);
const [localName, setLocalName] = useState('');
const [localRoles, setLocalRoles] = useState<string[]>([]);
const [isSaving, setIsSaving] = useState(false);
const [saveErrors, setSaveErrors] = useState<SaveError[]>([]);
const queryClient = useQueryClient();
const open = !!selectedAccountId;
const { permissions: drawerPermissions, isLoading: isAuthZLoading } = useAuthZ(
selectedAccountId
? [
buildSAReadPermission(selectedAccountId),
buildSAUpdatePermission(selectedAccountId),
buildSADeletePermission(selectedAccountId),
APIKeyListPermission,
]
: [],
{ enabled: !!selectedAccountId },
);
const canRead =
drawerPermissions?.[buildSAReadPermission(selectedAccountId ?? '')]
?.isGranted ?? false;
const {
data: accountData,
@@ -117,7 +131,7 @@ function ServiceAccountDrawer({
refetch: refetchAccount,
} = useGetServiceAccount(
{ id: selectedAccountId ?? '' },
{ query: { enabled: !!selectedAccountId } },
{ query: { enabled: canRead && !!selectedAccountId } },
);
const account = useMemo(
@@ -131,7 +145,7 @@ function ServiceAccountDrawer({
isLoading: isRolesLoading,
applyDiff,
} = useServiceAccountRoleManager(selectedAccountId ?? '', {
enabled: !!selectedAccountId,
enabled: canRead && !!selectedAccountId,
});
const roleSessionRef = useRef<string | null>(null);
@@ -180,9 +194,16 @@ function ServiceAccountDrawer({
refetch: refetchRoles,
} = useRoles();
const canListKeys =
drawerPermissions?.[APIKeyListPermission]?.isGranted ?? false;
const canUpdate =
drawerPermissions?.[buildSAUpdatePermission(selectedAccountId ?? '')]
?.isGranted ?? true;
const { data: keysData, isLoading: keysLoading } = useListServiceAccountKeys(
{ id: selectedAccountId ?? '' },
{ query: { enabled: !!selectedAccountId } },
{ query: { enabled: !!selectedAccountId && canListKeys } },
);
const keys = keysData?.data ?? [];
@@ -196,6 +217,7 @@ function ServiceAccountDrawer({
}
}, [keysLoading, keys.length, keysPage, setKeysPage]);
// the retry for this mutation is safe due to the api being idempotent on backend
const { mutateAsync: updateMutateAsync } = useUpdateServiceAccount();
const retryNameUpdate = useCallback(async (): Promise<void> => {
@@ -353,71 +375,23 @@ function ServiceAccountDrawer({
]);
const handleClose = useCallback((): void => {
setSaveErrors([]);
void setIsDeleteOpen(null);
void setIsAddKeyOpen(null);
void setSelectedAccountId(null);
void setActiveTab(null);
void setKeysPage(null);
void setEditKeyId(null);
void setIsAddKeyOpen(null);
void setIsDeleteOpen(null);
void setSelectedAccountId(null);
setSaveErrors([]);
}, [
setSelectedAccountId,
setActiveTab,
setKeysPage,
setEditKeyId,
setIsAddKeyOpen,
setIsDeleteOpen,
setSelectedAccountId,
]);
const footer = useMemo(
() =>
activeTab === ServiceAccountDrawerTab.Overview && !isDeleted && open ? (
<div className="sa-drawer__footer">
<AuthZButton
checks={[buildSADeletePermission(selectedAccountId ?? '')]}
authZEnabled={!!selectedAccountId}
variant="link"
color="destructive"
onClick={(): void => {
void setIsDeleteOpen(true);
}}
>
<Trash2 size={12} />
Delete Service Account
</AuthZButton>
<div className="sa-drawer__footer-right">
<Button variant="outlined" color="secondary" onClick={handleClose}>
<X size={14} />
Cancel
</Button>
<AuthZButton
checks={[buildSAUpdatePermission(selectedAccountId ?? '')]}
authZEnabled={!!selectedAccountId}
variant="solid"
color="primary"
loading={isSaving}
disabled={!isDirty}
onClick={handleSave}
>
Save Changes
</AuthZButton>
</div>
</div>
) : null,
[
activeTab,
isDeleted,
open,
selectedAccountId,
isSaving,
isDirty,
handleClose,
handleSave,
setIsDeleteOpen,
],
);
const body = (
const drawerContent = (
<div className="sa-drawer__layout">
<div className="sa-drawer__tabs">
<ToggleGroupSimple
@@ -459,23 +433,26 @@ function ServiceAccountDrawer({
]}
/>
{activeTab === ServiceAccountDrawerTab.Keys && (
<AuthZButton
<AuthZTooltip
checks={[
APIKeyCreatePermission,
buildSAAttachPermission(selectedAccountId ?? ''),
]}
authZEnabled={!isDeleted && !!selectedAccountId}
variant="outlined"
size="sm"
color="secondary"
disabled={isDeleted}
onClick={(): void => {
void setIsAddKeyOpen(true);
}}
enabled={!isDeleted && !!selectedAccountId}
>
<Plus size={12} />
Add Key
</AuthZButton>
<Button
variant="outlined"
size="sm"
color="secondary"
disabled={isDeleted}
onClick={(): void => {
void setIsAddKeyOpen(true);
}}
>
<Plus size={12} />
Add Key
</Button>
</AuthZTooltip>
)}
</div>
@@ -484,7 +461,9 @@ function ServiceAccountDrawer({
activeTab === ServiceAccountDrawerTab.Keys ? ' sa-drawer__body--keys' : ''
}`}
>
{isAccountLoading && <Skeleton active paragraph={{ rows: 6 }} />}
{(isAuthZLoading || isAccountLoading) && (
<Skeleton active paragraph={{ rows: 6 }} />
)}
{isAccountError && (
<ErrorInPlace
error={toAPIError(
@@ -493,73 +472,141 @@ function ServiceAccountDrawer({
)}
/>
)}
{!isAccountLoading && !isAccountError && (
<>
{activeTab === ServiceAccountDrawerTab.Overview &&
(account ? (
<OverviewTab
account={account}
localName={localName}
onNameChange={handleNameChange}
localRoles={localRoles}
onRolesChange={(roles): void => {
setLocalRoles(roles);
clearRoleErrors();
}}
isDisabled={isDeleted}
availableRoles={availableRoles}
rolesLoading={rolesLoading}
rolesError={rolesError}
rolesErrorObj={rolesErrorObj}
onRefetchRoles={refetchRoles}
saveErrors={saveErrors}
/>
) : (
<Skeleton active />
))}
{activeTab === ServiceAccountDrawerTab.Keys && (
<KeysTab
keys={keys}
isLoading={keysLoading}
isDisabled={isDeleted}
accountId={selectedAccountId ?? ''}
currentPage={keysPage}
pageSize={PAGE_SIZE}
onPageChange={(page): void => {
void setKeysPage(page);
}}
/>
)}
</>
)}
{!isAuthZLoading &&
!isAccountLoading &&
!isAccountError &&
selectedAccountId && (
<>
{activeTab === ServiceAccountDrawerTab.Overview &&
(canRead && account ? (
<OverviewTab
account={account}
localName={localName}
onNameChange={handleNameChange}
localRoles={localRoles}
onRolesChange={(roles): void => {
setLocalRoles(roles);
clearRoleErrors();
}}
isDisabled={isDeleted}
canUpdate={canUpdate}
availableRoles={availableRoles}
rolesLoading={rolesLoading}
rolesError={rolesError}
rolesErrorObj={rolesErrorObj}
onRefetchRoles={refetchRoles}
saveErrors={saveErrors}
/>
) : (
<PermissionDeniedCallout permissionName="serviceaccount:read" />
))}
{activeTab === ServiceAccountDrawerTab.Keys &&
(canListKeys ? (
<KeysTab
keys={keys}
isLoading={keysLoading}
isDisabled={isDeleted}
canUpdate={canUpdate}
accountId={selectedAccountId}
currentPage={keysPage}
pageSize={PAGE_SIZE}
/>
) : (
<PermissionDeniedCallout permissionName="factor-api-key:list" />
))}
</>
)}
</div>
</div>
);
return (
<DrawerWrapper
open={open}
onOpenChange={(isOpen): void => {
if (!isOpen) {
handleClose();
}
}}
direction="right"
showCloseButton
showOverlay={false}
title="Service Account Details"
className="sa-drawer"
width="wide"
footer={footer}
>
{open && (
const footer = (
<div className="sa-drawer__footer">
{activeTab === ServiceAccountDrawerTab.Keys ? (
<Pagination
current={keysPage}
pageSize={PAGE_SIZE}
total={keys.length}
showTotal={(total: number, range: number[]): JSX.Element => (
<>
<span className="sa-drawer__pagination-range">
{range[0]} &#8212; {range[1]}
</span>
<span className="sa-drawer__pagination-total"> of {total}</span>
</>
)}
showSizeChanger={false}
hideOnSinglePage
onChange={(page): void => {
void setKeysPage(page);
}}
className="sa-drawer__keys-pagination"
/>
) : (
<>
{body}
<DeleteAccountModal />
<AddKeyModal />
{!isDeleted && (
<AuthZTooltip
checks={[buildSADeletePermission(selectedAccountId ?? '')]}
enabled={!!selectedAccountId}
>
<Button
variant="link"
color="destructive"
onClick={(): void => {
void setIsDeleteOpen(true);
}}
>
<Trash2 size={12} />
Delete Service Account
</Button>
</AuthZTooltip>
)}
{!isDeleted && (
<div className="sa-drawer__footer-right">
<Button variant="outlined" color="secondary" onClick={handleClose}>
<X size={14} />
Cancel
</Button>
<Button
variant="solid"
color="primary"
loading={isSaving}
disabled={!isDirty}
onClick={handleSave}
>
Save Changes
</Button>
</div>
)}
</>
)}
</DrawerWrapper>
</div>
);
return (
<>
<DrawerWrapper
open={open}
onOpenChange={(isOpen): void => {
if (!isOpen) {
handleClose();
}
}}
direction="right"
showCloseButton
showOverlay={false}
title="Service Account Details"
className="sa-drawer"
width="wide"
footer={footer}
>
{drawerContent}
</DrawerWrapper>
<DeleteAccountModal />
<AddKeyModal />
</>
);
}

View File

@@ -1,5 +1,4 @@
import { toast } from '@signozhq/ui/sonner';
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import {
@@ -60,7 +59,6 @@ describe('AddKeyModal', () => {
rest.post(SA_KEYS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(201), ctx.json(createdKeyResponse)),
),
setupAuthzAdmin(),
);
});

View File

@@ -1,6 +1,5 @@
import { toast } from '@signozhq/ui/sonner';
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
@@ -62,7 +61,6 @@ describe('EditKeyModal (URL-controlled)', () => {
rest.delete(SA_KEY_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
setupAuthzAdmin(),
);
});

View File

@@ -1,6 +1,5 @@
import { toast } from '@signozhq/ui/sonner';
import { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
@@ -36,7 +35,7 @@ const keys: ServiceaccounttypesGettableFactorAPIKeyDTO[] = [
{
id: 'key-2',
name: 'Staging Key',
expiresAt: 1924948800, // 2030-12-31 12:00 UTC (noon to avoid timezone issues)
expiresAt: 1924905600, // 2030-12-31
lastObservedAt: '2026-03-10T10:00:00Z',
serviceAccountId: 'sa-1',
},
@@ -48,7 +47,6 @@ const defaultProps = {
isDisabled: false,
currentPage: 1,
pageSize: 10,
onPageChange: jest.fn(),
};
function renderKeysTab(
@@ -69,7 +67,6 @@ describe('KeysTab', () => {
rest.delete(SA_KEY_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: {} })),
),
setupAuthzAdmin(),
);
});
@@ -77,12 +74,9 @@ describe('KeysTab', () => {
server.resetHandlers();
});
it('renders loading state', async () => {
it('renders loading state', () => {
renderKeysTab({ isLoading: true });
// Wait for authz to complete, then check for skeleton
await waitFor(() => {
expect(document.querySelector('.ant-skeleton')).toBeInTheDocument();
});
expect(document.querySelector('.ant-skeleton')).toBeInTheDocument();
});
it('renders empty state when no keys and clicking add sets add-key param', async () => {
@@ -97,9 +91,9 @@ describe('KeysTab', () => {
</NuqsTestingAdapter>,
);
await expect(
screen.findByText(/No keys. Start by creating one./i),
).resolves.toBeInTheDocument();
expect(
screen.getByText(/No keys. Start by creating one./i),
).toBeInTheDocument();
const addBtn = screen.getByRole('button', { name: /\+ Add your first key/i });
await user.click(addBtn);
expect(onUrlUpdate).toHaveBeenCalledWith(
@@ -109,12 +103,10 @@ describe('KeysTab', () => {
);
});
it('renders table with keys', async () => {
it('renders table with keys', () => {
renderKeysTab();
await expect(
screen.findByText('Production Key'),
).resolves.toBeInTheDocument();
expect(screen.getByText('Production Key')).toBeInTheDocument();
expect(screen.getByText('Staging Key')).toBeInTheDocument();
expect(screen.getByText('Never')).toBeInTheDocument();
expect(screen.getByText('Dec 31, 2030')).toBeInTheDocument();
@@ -130,7 +122,7 @@ describe('KeysTab', () => {
</NuqsTestingAdapter>,
);
const row = (await screen.findByText('Production Key')).closest('tr');
const row = screen.getByText('Production Key').closest('tr');
if (!row) {
throw new Error('Row not found');
}
@@ -154,8 +146,6 @@ describe('KeysTab', () => {
</NuqsTestingAdapter>,
);
// Wait for authz to complete and table to render
await screen.findByText('Production Key');
const revokeBtns = screen
.getAllByRole('button')
.filter((btn) => btn.className.includes('keys-tab__revoke-btn'));
@@ -173,8 +163,7 @@ describe('KeysTab', () => {
renderKeysTab();
// Wait for authz to complete and table to render
await screen.findByText('Production Key');
// Seed the keys cache so RevokeKeyModal can read the key name
const revokeBtns = screen
.getAllByRole('button')
.filter((btn) => btn.className.includes('keys-tab__revoke-btn'));
@@ -188,11 +177,9 @@ describe('KeysTab', () => {
});
});
it('disables actions when isDisabled is true', async () => {
it('disables actions when isDisabled is true', () => {
renderKeysTab({ isDisabled: true });
// Wait for authz to complete and table to render
await screen.findByText('Production Key');
const revokeBtns = screen
.getAllByRole('button')
.filter((btn) => btn.className.includes('keys-tab__revoke-btn'));

View File

@@ -1,3 +1,4 @@
import type { ReactNode } from 'react';
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
@@ -31,6 +32,30 @@ const activeAccountResponse = {
updatedAt: '2026-01-02T00:00:00Z',
};
jest.mock('@signozhq/ui/drawer', () => ({
...jest.requireActual('@signozhq/ui/drawer'),
DrawerWrapper: ({
children,
footer,
open,
}: {
children?: ReactNode;
footer?: ReactNode;
open: boolean;
}): JSX.Element | null =>
open ? (
<div>
{children}
{footer}
</div>
) : null,
}));
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: { success: jest.fn(), error: jest.fn() },
}));
function renderDrawer(
searchParams: Record<string, string> = { account: 'sa-1' },
): ReturnType<typeof render> {
@@ -93,7 +118,7 @@ describe('ServiceAccountDrawer — permissions', () => {
renderDrawer();
await waitFor(() => {
expect(screen.getByText(/read:serviceaccount/)).toBeInTheDocument();
expect(screen.getByText(/serviceaccount:read/)).toBeInTheDocument();
});
});
@@ -115,7 +140,7 @@ describe('ServiceAccountDrawer — permissions', () => {
fireEvent.click(screen.getByRole('radio', { name: /keys/i }));
await waitFor(() => {
expect(screen.getByText(/list:factor-api-key/)).toBeInTheDocument();
expect(screen.getByText(/factor-api-key:list/)).toBeInTheDocument();
});
});

View File

@@ -1,3 +1,4 @@
import type { ReactNode } from 'react';
import { listRolesSuccessResponse } from 'mocks-server/__mockdata__/roles';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
@@ -6,6 +7,30 @@ import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
import ServiceAccountDrawer from '../ServiceAccountDrawer';
jest.mock('@signozhq/ui/drawer', () => ({
...jest.requireActual('@signozhq/ui/drawer'),
DrawerWrapper: ({
children,
footer,
open,
}: {
children?: ReactNode;
footer?: ReactNode;
open: boolean;
}): JSX.Element | null =>
open ? (
<div>
{children}
{footer}
</div>
) : null,
}));
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: { success: jest.fn(), error: jest.fn() },
}));
const ROLES_ENDPOINT = '*/api/v1/roles';
const SA_KEYS_ENDPOINT = '*/api/v1/service_accounts/:id/keys';
const SA_ENDPOINT = '*/api/v1/service_accounts/sa-1';

View File

@@ -2,22 +2,10 @@
position: sticky;
top: 0;
z-index: 2;
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)
);
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);
transform: translate3d(
var(--tanstack-header-translate-x, 0px),
var(--tanstack-header-translate-y, 0px),
@@ -251,7 +239,6 @@
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
width: var(--tanstack-table-header-label-width);
}
.tanstackSortIndicator {

View File

@@ -1,37 +1,33 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { listHosts } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesHostRecordDTO,
InframonitoringtypesHostStatusDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import logEvent from 'api/common/logEvent';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { initialQueriesMap } from 'constants/queryBuilder';
import K8sBaseDetails, {
K8sDetailsFilters,
} from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
import K8sBaseDetails 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 { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import {
useInfraMonitoringFiltersK8s,
useInfraMonitoringPageListing,
} from 'container/InfraMonitoringK8sV2/hooks';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { useAppContext } from 'providers/App/App';
import { DataSource } from 'types/common/queryBuilder';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import {
fetchHostEntityData,
fetchHostListData,
getHostMetricsQueryPayload,
hostDetailsMetadataConfig,
hostGetEntityName,
hostGetSelectedItemExpression,
hostInitialEventsExpression,
hostInitialLogTracesExpression,
hostGetSelectedItemFilters,
hostInitialEventsFilter,
hostInitialLogTracesFilter,
hostWidgetInfo,
} from './constants';
import {
@@ -46,135 +42,85 @@ import { ArrowUpToLine, Filter } from '@signozhq/icons';
function Hosts(): JSX.Element {
const [showFilters, setShowFilters] = useState(true);
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 [, setCurrentPage] = useInfraMonitoringPageListing();
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
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) => {
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,
);
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
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,
};
}
return fetchHostListData(filters, signal);
},
[],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
filters: Parameters<typeof fetchHostEntityData>[0],
signal?: AbortSignal,
): 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,
};
}
},
) => fetchHostEntityData(filters, signal),
[],
);
const getInitialLogTracesExpression = useCallback(
(host: InframonitoringtypesHostRecordDTO) =>
hostInitialLogTracesExpression(host, dotMetricsEnabled),
const getSelectedItemFilters = useCallback(
(selectedItem: string) =>
hostGetSelectedItemFilters(selectedItem, dotMetricsEnabled),
[dotMetricsEnabled],
);
const getInitialLogTracesFilters = useCallback(
(host: import('api/infraMonitoring/getHostLists').HostData) =>
hostInitialLogTracesFilter(host, dotMetricsEnabled),
[dotMetricsEnabled],
);
const controlListPrefix = !showFilters ? (
<div className={styles.quickFiltersToggleContainer}>
<Button
@@ -208,6 +154,7 @@ function Hosts(): JSX.Element {
source={QuickFiltersSource.INFRA_MONITORING}
config={getHostsQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleQuickFiltersChange}
/>
</div>
)}
@@ -216,9 +163,8 @@ function Hosts(): JSX.Element {
showFilters ? ` ${styles.listContainerFiltersVisible}` : ''
}`}
>
<K8sBaseList<InframonitoringtypesHostRecordDTO>
<K8sBaseList
controlListPrefix={controlListPrefix}
leftFilters={<StatusFilter />}
entity={InfraMonitoringEntity.HOSTS}
tableColumns={hostColumnsConfig}
fetchListData={fetchListData}
@@ -232,11 +178,11 @@ function Hosts(): JSX.Element {
<K8sBaseDetails
category={InfraMonitoringEntity.HOSTS}
eventCategory={InfraMonitoringEvents.HostEntity}
getSelectedItemExpression={hostGetSelectedItemExpression}
getSelectedItemFilters={getSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={hostGetEntityName}
getInitialLogTracesExpression={getInitialLogTracesExpression}
getInitialEventsExpression={hostInitialEventsExpression}
getInitialLogTracesFilters={getInitialLogTracesFilters}
getInitialEventsFilters={hostInitialEventsFilter}
metadataConfig={hostDetailsMetadataConfig}
entityWidgetInfo={hostWidgetInfo}
getEntityQueryPayload={getHostMetricsQueryPayload}

View File

@@ -26,8 +26,8 @@
}
&::-webkit-scrollbar {
width: 8px;
height: 8px;
width: 0.1rem;
height: 0.1rem;
}
&::-webkit-scrollbar-track {
@@ -35,12 +35,11 @@
}
&::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--l1-foreground) 20%, transparent);
border-radius: 9999px;
background: var(--accent-primary);
}
&::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--l1-foreground) 30%, transparent);
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
}
@@ -99,7 +98,7 @@
}
&::-webkit-scrollbar-thumb:hover {
background: var(--accent-primary);
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
}
}
@@ -114,17 +113,32 @@
.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 {
@@ -132,7 +146,7 @@
}
.quickFiltersToggleContainer {
grid-area: quickFilters;
padding: 0 8px;
}
.infraMonitoringTags {

View File

@@ -1,56 +0,0 @@
.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);
}

View File

@@ -1,152 +0,0 @@
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);
});
});
});

View File

@@ -1,63 +0,0 @@
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;

View File

@@ -4,16 +4,26 @@ import { Badge } from '@signozhq/ui/badge';
import { Progress } from '@signozhq/ui/progress';
import { Typography } from '@signozhq/ui/typography';
import {
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';
getHostLists,
HostData,
HostListPayload,
} from 'api/infraMonitoring/getHostLists';
import {
createFilterItem,
K8sDetailsFilters,
K8sDetailsMetadataConfig,
} from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
import { K8sBaseFilters } from 'container/InfraMonitoringK8sV2/Base/types';
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';
@@ -37,32 +47,24 @@ export function getMemoryProgressColor(percent: number): string {
return Color.BG_FOREST_500;
}
export type HostDetailMetadataConfigType =
K8sDetailsMetadataConfig<InframonitoringtypesHostRecordDTO>;
export const hostDetailsMetadataConfig: HostDetailMetadataConfigType[] = [
export const hostDetailsMetadataConfig: K8sDetailsMetadataConfig<HostData>[] = [
{
label: 'STATUS',
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>
);
},
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>
),
},
{
label: 'OPERATING SYSTEM',
getValue: (h): string => h.meta?.['os.type'] || '-',
getValue: (h): string => h.os || '-',
render: (value): React.ReactNode =>
value !== '-' ? (
<Badge variant="outline" className={infraHostsStyles.infraMonitoringTags}>
@@ -97,7 +99,7 @@ export const hostDetailsMetadataConfig: HostDetailMetadataConfigType[] = [
];
export function getHostMetricsQueryPayload(
host: InframonitoringtypesHostRecordDTO,
host: HostData,
start: number,
end: number,
dotMetricsEnabled: boolean,
@@ -107,26 +109,83 @@ export function getHostMetricsQueryPayload(
export { hostWidgetInfo };
export const hostGetSelectedItemExpression = (hostName: string): string =>
`host.name = ${formatValueForExpression(hostName)}`;
export function hostInitialLogTracesExpression(
host: InframonitoringtypesHostRecordDTO,
export function hostGetSelectedItemFilters(
selectedItem: string,
dotMetricsEnabled: boolean,
): string {
const hostKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.HOST_NAME
: 'host_name';
const hostName = formatValueForExpression(host.hostName || '');
return `${hostKey} = ${hostName}`;
): TagFilter {
const hostKey = dotMetricsEnabled ? 'host.name' : 'host_name';
return {
op: 'AND',
items: [createFilterItem(hostKey, selectedItem)],
};
}
export function hostInitialEventsExpression(
_host: InframonitoringtypesHostRecordDTO,
): string {
return '';
export function hostInitialLogTracesFilter(
host: HostData,
dotMetricsEnabled: boolean,
): TagFilterItem[] {
const hostKey = dotMetricsEnabled ? 'host.name' : 'host_name';
return [createFilterItem(hostKey, host.hostName || '')];
}
export const hostGetEntityName = (
host: InframonitoringtypesHostRecordDTO,
): string => host.hostName;
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,
};
}

View File

@@ -1,91 +1,55 @@
import React from 'react';
import { Color } from '@signozhq/design-tokens';
import { Container } from '@signozhq/icons';
import {
InframonitoringtypesHostRecordDTO,
InframonitoringtypesHostStatusDTO,
} from 'api/generated/services/sigNoz.schemas';
import { Tooltip } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import { HostData } from 'api/infraMonitoring/getHostLists';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { getGroupByEl } from 'container/InfraMonitoringK8sV2/Base/utils';
import {
EntityProgressBar,
ExpandButtonWrapper,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from 'container/InfraMonitoringK8sV2/components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from 'container/InfraMonitoringK8sV2/constants';
import { 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 { Badge, BadgeColor } from '@signozhq/ui/badge';
import { Container, Info } from '@signozhq/icons';
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>;
} {
function hostRowSource(host: HostData): { meta: Record<string, string> } {
return {
meta: {
...(host.meta ?? {}),
[INFRA_MONITORING_ATTR_KEYS.HOST_NAME]: host.hostName ?? '',
host_name: host.hostName ?? '',
'host.name': host.hostName ?? '',
os_type: host.os ?? '',
'os.type': host.os ?? '',
},
};
}
export function getHostRowKey(host: InframonitoringtypesHostRecordDTO): string {
export function getHostRowKey(host: HostData): string {
return host.hostName || 'unknown';
}
export function getHostItemKey(
host: InframonitoringtypesHostRecordDTO,
): string {
export function getHostItemKey(host: HostData): string {
return host.hostName ?? '';
}
function HostGroupCell({
row,
}: {
row: InframonitoringtypesHostRecordDTO;
}): JSX.Element {
function HostGroupCell({ row }: { row: HostData }): JSX.Element {
const [groupBy] = useInfraMonitoringGroupBy();
const synthetic = hostRowSource(row);
return getGroupByEl(synthetic, groupBy) as JSX.Element;
}
export type HostColumnConfigType =
TableColumnDef<InframonitoringtypesHostRecordDTO>;
export const hostColumnsConfig: HostColumnConfigType[] = [
export const hostColumnsConfig: TableColumnDef<HostData>[] = [
{
id: 'hostGroup',
header: (): React.ReactNode => <EntityGroupHeader title="Host Group" />,
header: (): React.ReactNode => <EntityGroupHeader title="HOST GROUP" />,
accessorFn: (row): string => row.hostName ?? '',
width: { min: 290 },
width: { min: 300 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -100,11 +64,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
{
id: 'hostName',
header: (): React.ReactNode => (
<EntityGroupHeader
title="Hostname"
icon={<Container size={14} />}
docPath="/infrastructure-monitoring/host-monitoring#hostname"
/>
<EntityGroupHeader title="Hostname" icon={<Container size={14} />} />
),
accessorFn: (row): string => row.hostName ?? '',
width: { min: 290 },
@@ -118,48 +78,27 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
),
},
{
id: 'status',
id: 'active',
header: (): React.ReactNode => (
<ColumnHeader
tooltip="Sent system metrics in last 10 mins."
docPath="/infrastructure-monitoring/host-monitoring#status"
>
<div className={styles.statusHeader}>
Status
</ColumnHeader>
<Tooltip title="Sent system metrics in last 10 mins">
<Info size="md" />
</Tooltip>
</div>
),
accessorFn: (row): string => row.status,
width: { min: 140 },
accessorFn: (row): boolean => row.active,
width: { min: 150, default: 150 },
enableSort: false,
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[''];
cell: ({ value }): React.ReactNode => {
const active = value as boolean;
return (
<Badge
variant="outline"
color={statusDetails.color}
className={`${styles.statusTag}`}
className={`${styles.statusTag} ${
active ? styles.statusTagActive : styles.statusTagInactive
}`}
>
{statusDetails.label}
{active ? 'ACTIVE' : 'INACTIVE'}
</Badge>
);
},
@@ -167,9 +106,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
{
id: 'cpu',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#cpu-usage">
CPU Usage
</ColumnHeader>
<div className={styles.columnHeaderRight}>CPU Usage</div>
),
accessorFn: (row): number => row.cpu,
width: { min: 220 },
@@ -192,16 +129,15 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
{
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader
tooltip="Excluding cache memory."
docPath="/infrastructure-monitoring/host-monitoring#memory-usage"
className={styles.memoryUsageHeader}
>
Memory Usage (WSS)
</ColumnHeader>
<div className={`${styles.columnHeaderRight} ${styles.memoryUsageHeader}`}>
Memory Usage
<Tooltip title="Excluding cache memory">
<Info size="md" />
</Tooltip>
</div>
),
accessorFn: (row): number => row.memory,
width: { min: 240 },
width: { min: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
@@ -221,12 +157,10 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
{
id: 'wait',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#iowait">
IOWait
</ColumnHeader>
<div className={styles.columnHeaderRight}>IOWait</div>
),
accessorFn: (row): number => row.wait,
width: { min: 120 },
width: { min: 100, default: 100 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const wait = value as number;
@@ -237,9 +171,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
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>
);
},
@@ -247,15 +179,13 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
{
id: 'load15',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#load-avg">
Load Avg (15min)
</ColumnHeader>
<div className={styles.columnHeaderRight}>Load Avg</div>
),
accessorFn: (row): number => row.load15,
width: { min: 200 },
width: { min: 100, default: 100 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const load15 = Number(value);
const load15 = value as number;
return (
<ValidateColumnValueWrapper
@@ -263,35 +193,9 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
entity={InfraMonitoringEntity.HOSTS}
attribute="load average metric"
>
<TanStackTable.Text>{load15.toFixed(2)}</TanStackTable.Text>
<TanStackTable.Text>{load15}</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>
);
},
},
];

View File

@@ -9,6 +9,13 @@
display: block;
}
.statusHeader {
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
width: 100%;
}
.columnHeaderRight {
text-align: right;
}

View File

@@ -2,16 +2,14 @@ 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';
@@ -23,15 +21,13 @@ export function HostnameCell({
}): React.ReactElement {
const isEmpty = !hostName || !hostName.trim();
if (!isEmpty) {
return (
<CellValueTooltip value={hostName}>
<TanStackTable.Text>{hostName}</TanStackTable.Text>
</CellValueTooltip>
);
return <div className="hostname-column-value">{hostName}</div>;
}
return (
<>
<Typography.Text color="muted">-</Typography.Text>
<div className="hostname-cell-missing">
<Typography.Text color="muted" className="hostname-cell-placeholder">
-
</Typography.Text>
<Tooltip
title={
<div>
@@ -64,16 +60,52 @@ 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
? INFRA_MONITORING_ATTR_KEYS.HOST_NAME
: 'host_name';
const hostNameKey = dotMetricsEnabled ? 'host.name' : 'host_name';
const osTypeKey = dotMetricsEnabled ? 'os.type' : 'os_type';
const metricName = dotMetricsEnabled
? 'system.cpu.load_average.15m'

View File

@@ -1,16 +0,0 @@
.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;
}

View File

@@ -1,90 +0,0 @@
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;

View File

@@ -2,16 +2,4 @@
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);
}
}

View File

@@ -1,70 +1,19 @@
import { Group, Info } from '@signozhq/icons';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Group } from '@signozhq/icons';
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} data-slot="entity-group-header">
<span data-slot="icon">
{icon || <Group size={14} data-hide-expanded="true" />}
</span>{' '}
{title}
{renderInfoIcon()}
<div className={styles.entityGroupHeader}>
{icon || <Group size={14} data-hide-expanded="true" />} {title}
</div>
);
}

View File

@@ -6,6 +6,7 @@ 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';
@@ -13,6 +14,7 @@ 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 {
@@ -38,11 +40,17 @@ 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';
@@ -72,7 +80,7 @@ export interface K8sDetailsMetadataConfig<T> {
}
export interface K8sDetailsFilters {
filter: { expression: string };
filters: TagFilter;
start: number;
end: number;
}
@@ -81,15 +89,15 @@ export interface K8sBaseDetailsProps<T> {
category: InfraMonitoringEntity;
eventCategory: string;
// Data fetching configuration
getSelectedItemExpression: (selectedItem: string) => string;
getSelectedItemFilters: (selectedItem: string) => TagFilter;
fetchEntityData: (
filters: K8sDetailsFilters,
signal?: AbortSignal,
) => Promise<{ data: T | null; error?: string | null }>;
// Entity configuration
getEntityName: (entity: T) => string;
getInitialLogTracesExpression: (entity: T) => string;
getInitialEventsExpression: (entity: T) => string;
getInitialLogTracesFilters: (entity: T) => TagFilterItem[];
getInitialEventsFilters: (entity: T) => TagFilterItem[];
metadataConfig: K8sDetailsMetadataConfig<T>[];
entityWidgetInfo: {
title: string;
@@ -126,15 +134,33 @@ 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,
getSelectedItemExpression,
getSelectedItemFilters,
fetchEntityData,
getEntityName,
getInitialLogTracesExpression,
getInitialEventsExpression,
getInitialLogTracesFilters,
getInitialEventsFilters,
metadataConfig,
entityWidgetInfo,
getEntityQueryPayload,
@@ -175,12 +201,17 @@ 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({ filter: { expression }, start, end }, signal);
return fetchEntityData(
{
filters,
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
},
signal,
);
},
enabled: !!selectedItem,
});
@@ -192,15 +223,23 @@ export default function K8sBaseDetails<T>({
if (!entity) {
return '';
}
return getInitialLogTracesExpression(entity);
}, [entity, getInitialLogTracesExpression]);
const primaryFiltersOnly = {
op: 'AND' as const,
items: getInitialLogTracesFilters(entity),
};
return convertFiltersToExpression(primaryFiltersOnly).expression;
}, [entity, getInitialLogTracesFilters]);
const eventsInitialExpression = useMemo(() => {
if (!entity) {
return '';
}
return getInitialEventsExpression(entity);
}, [entity, getInitialEventsExpression]);
const primaryFiltersOnly = {
op: 'AND' as const,
items: getInitialEventsFilters(entity),
};
return convertFiltersToExpression(primaryFiltersOnly).expression;
}, [entity, getInitialEventsFilters]);
const handleClose = useCallback((): void => {
setSelectedItem(null);

View File

@@ -1,10 +1,3 @@
.tableContainer {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
}
.emptyStateContainer {
display: flex;
flex: 1;
@@ -14,6 +7,7 @@
}
.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);
@@ -24,12 +18,9 @@
--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-header-padding-left-override: 5px;
--tanstack-cell-padding-left-override: 26px;
--tanstack-cell-padding-left-override: 5px;
--tanstack-cell-padding-right-override: 5px;
--tanstack-expansion-first-col-padding-left: 30px;

View File

@@ -1,19 +1,15 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useQuery, useQueryClient } from 'react-query';
import { useQuery } 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 {
@@ -21,15 +17,15 @@ import {
InfraMonitoringEntity,
} from '../constants';
import {
useInfraMonitoringFiltersK8s,
useInfraMonitoringGroupBy,
useInfraMonitoringOrderBy,
useInfraMonitoringStatusFilter,
useInfraMonitoringPageListing,
useInfraMonitoringPageSizeListing,
} 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';
@@ -42,30 +38,24 @@ 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> | null };
export type K8sEntityData = { meta?: Record<string, string> };
export type K8sBaseListProps<T extends K8sEntityData> = {
controlListPrefix?: React.ReactNode;
leftFilters?: React.ReactNode;
entity: InfraMonitoringEntity;
tableColumns: TableColumnDef<T>[];
fetchListData: (
filters: K8sBaseFilters,
signal?: AbortSignal,
) => Promise<{
type?: 'list' | 'grouped_list';
records?: T[];
data?: 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;
@@ -75,12 +65,10 @@ 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,
@@ -88,14 +76,12 @@ export function K8sBaseList<T extends K8sEntityData>({
getItemKey,
eventCategory,
renderEmptyState,
extraQueryKeyParts = [],
}: K8sBaseListProps<T>): JSX.Element {
const { currentQuery } = useQueryBuilder();
const expression = currentQuery.builder.queryData[0]?.filter?.expression || '';
const lineClamp = useInfraMonitoringLineClamp();
const [queryFilters] = useInfraMonitoringFiltersK8s();
const [currentPage] = useInfraMonitoringPageListing();
const [currentPageSize] = useInfraMonitoringPageSizeListing();
const [groupBy] = useInfraMonitoringGroupBy();
const [orderBy] = useInfraMonitoringOrderBy();
const [statusFilter] = useInfraMonitoringStatusFilter();
const [selectedItem, setSelectedItem] = useQueryState(
'selectedItem',
parseAsString,
@@ -104,27 +90,6 @@ 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);
@@ -140,11 +105,9 @@ export function K8sBaseList<T extends K8sEntityData>({
entity,
String(currentPageSize),
String(currentPage),
expression || '',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
statusFilter,
...extraQueryKeyParts,
);
}, [
getAutoRefreshQueryKey,
@@ -152,64 +115,35 @@ export function K8sBaseList<T extends K8sEntityData>({
entity,
currentPageSize,
currentPage,
expression,
queryFilters,
orderBy,
groupBy,
statusFilter,
extraQueryKeyParts,
]);
const queryClient = useQueryClient();
const { data, isLoading, isFetching, isError } = useQuery({
const { data, isLoading, isError } = useQuery({
queryKey,
queryFn: async ({ signal }) => {
queryFn: ({ 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(
return fetchListData(
{
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,
limit: currentPageSize,
start,
end,
orderBy: orderBy
? { key: { name: orderBy.columnName }, direction: orderBy.order }
: 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,
},
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 = !!expression?.trim();
const hasFilters = (queryFilters?.items?.length ?? 0) > 0;
const getGroupKeyFn = useCallback(
(item: T) => getGroupedByMeta(item, groupBy),
@@ -217,7 +151,7 @@ export function K8sBaseList<T extends K8sEntityData>({
);
useEffect(() => {
void logEvent(InfraMonitoringEvents.PageVisited, {
logEvent(InfraMonitoringEvents.PageVisited, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: eventCategory,
@@ -228,10 +162,10 @@ export function K8sBaseList<T extends K8sEntityData>({
const handleRowClick = useCallback(
(_record: T, itemKey: string): void => {
if (groupBy.length === 0) {
void setSelectedItem(itemKey);
setSelectedItem(itemKey);
}
void logEvent(InfraMonitoringEvents.ItemClicked, {
logEvent(InfraMonitoringEvents.ItemClicked, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: eventCategory,
@@ -251,7 +185,7 @@ export function K8sBaseList<T extends K8sEntityData>({
url.searchParams.set('selectedItem', itemKey);
openInNewTab(url.pathname + url.search);
void logEvent(InfraMonitoringEvents.ItemClicked, {
logEvent(InfraMonitoringEvents.ItemClicked, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: eventCategory,
@@ -280,19 +214,11 @@ export function K8sBaseList<T extends K8sEntityData>({
entity={entity}
tableColumns={expandedRowColumns}
fetchListData={fetchListData}
extraQueryKeyParts={extraQueryKeyParts}
getRowKey={getRowKey}
getItemKey={getItemKey}
/>
),
[
entity,
fetchListData,
getRowKey,
getItemKey,
expandedRowColumns,
extraQueryKeyParts,
],
[entity, fetchListData, getRowKey, getItemKey, expandedRowColumns],
);
const getRowCanExpand = useCallback(
@@ -308,78 +234,64 @@ export function K8sBaseList<T extends K8sEntityData>({
totalCount,
hasFilters,
isLoading: showTableLoadingState,
endTimeBeforeRetention: data?.endTimeBeforeRetention,
rawData: data?.rawData,
}) || (
<K8sEmptyState
isError={isError}
error={data?.error}
isLoading={showTableLoadingState}
endTimeBeforeRetention={data?.endTimeBeforeRetention}
rawData={data?.rawData}
/>
);
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}
/>
<div ref={containerRef} className={styles.tableContainer}>
{isError && (
<Typography>
{data?.error?.toString() || 'Something went wrong'}
</Typography>
)}
{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.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>
{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}
/>
)}
</>
);
}

View File

@@ -11,6 +11,12 @@ 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 => {
@@ -25,7 +31,7 @@ export function K8sEmptyState({
isError,
error,
isLoading,
endTimeBeforeRetention,
rawData,
}: K8sEmptyStateProps): JSX.Element | null {
const { isCloudUser } = useGetTenantLicense();
@@ -64,7 +70,47 @@ export function K8sEmptyState({
);
}
if (endTimeBeforeRetention) {
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) {
return (
<div className={styles.container}>
<div className={styles.content}>

View File

@@ -1,7 +1,5 @@
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, {
@@ -9,19 +7,16 @@ 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,
@@ -30,7 +25,6 @@ import {
import { K8sBaseFilters } from './types';
import styles from './K8sExpandedRow.module.scss';
import { buildExpressionFromGroupMeta } from './utils';
const EXPANDED_ROW_LIMIT = 10;
@@ -41,21 +35,15 @@ export type K8sExpandedRowProps<T> = {
groupMeta?: Record<string, string>;
entity: InfraMonitoringEntity;
tableColumns: TableColumnDef<T>[];
/** API fetch function for expanded row data */
fetchListData?: (
fetchListData: (
filters: K8sBaseFilters,
signal?: AbortSignal,
) => Promise<{
type?: 'list' | 'grouped_list';
records?: T[];
data?: 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. */
@@ -68,20 +56,14 @@ export function K8sExpandedRow<T>({
entity,
tableColumns,
fetchListData,
extraQueryKeyParts = [],
getRowKey,
getItemKey,
}: K8sExpandedRowProps<T>): JSX.Element {
const [, setGroupBy] = useInfraMonitoringGroupBy();
const [, setCurrentPage] = useInfraMonitoringPageListing();
const { currentQuery } = useQueryBuilder();
const parentExpression =
currentQuery.builder.queryData[0]?.filter?.expression || '';
const [queryFilters, setFilters] = useInfraMonitoringFiltersK8s();
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, '_')}`,
@@ -103,10 +85,35 @@ export function K8sExpandedRow<T>({
const storageKey = `k8s-${entity}-columns-expanded`;
const expressionForRecord = useMemo(
() => buildExpressionFromGroupMeta(parentExpression || '', groupMeta),
[parentExpression, groupMeta],
);
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 selectedTime = useGlobalTimeStore((s) => s.selectedTime);
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
@@ -123,9 +130,8 @@ export function K8sExpandedRow<T>({
'k8sExpandedRow',
JSON.stringify(groupMeta),
rowKey,
expressionForRecord,
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
...extraQueryKeyParts,
);
}, [
getAutoRefreshQueryKey,
@@ -133,83 +139,49 @@ export function K8sExpandedRow<T>({
entity,
groupMeta,
rowKey,
expressionForRecord,
queryFilters,
orderBy,
extraQueryKeyParts,
]);
const { data, isLoading, isError } = useQuery({
queryKey,
queryFn: async ({ signal }) => {
if (!fetchListData) {
return { data: [] as T[], total: 0 };
}
const { minTime, maxTime } = getMinMaxTime();
const response = await fetchListData(
return 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
? { key: { name: orderBy.columnName }, direction: orderBy.order }
: undefined,
orderBy: orderBy || 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 => {
void setSelectedItem(itemKey);
setSelectedItem(itemKey);
},
[setSelectedItem],
);
const handleViewAllClick = (): void => {
void setGroupBy([]);
void setCurrentPage(1);
const filters = createFiltersForRecord();
setGroupBy([]);
setCurrentPage(1);
setFilters(filters);
if (orderBy) {
void setMainOrderBy(orderBy);
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;

View File

@@ -24,37 +24,11 @@
.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 {

View File

@@ -1,4 +1,4 @@
import { ReactNode, useMemo } from 'react';
import { 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: ReactNode;
label: string;
canBeHidden: boolean;
visibilityBehavior:
| 'hidden-on-expand'
@@ -20,16 +20,15 @@ type ColumnPickerItem = {
| 'always-visible';
};
function renderHeader(header: string | (() => ReactNode)): ReactNode {
return typeof header === 'function' ? header() : header;
}
/**
* Converts TableColumnDef to column picker item format
*/
function toColumnPickerItems<T>(
columns: TableColumnDef<T>[],
): ColumnPickerItem[] {
return columns.map((col) => ({
id: col.id,
label: renderHeader(col.header),
label: typeof col.header === 'string' ? col.header : col.id,
canBeHidden: col.canBeHidden !== false && col.enableRemove !== false,
visibilityBehavior: col.visibilityBehavior ?? 'always-visible',
}));

View File

@@ -2,7 +2,7 @@ import { getGroupByEl } from './utils';
import { useInfraMonitoringGroupBy } from '../hooks';
interface K8sEntityWithMeta {
meta?: Record<string, string> | null;
meta?: Record<string, string>;
}
function K8sGroupCell<T extends K8sEntityWithMeta>({

View File

@@ -1,14 +1,13 @@
.k8SListControls {
padding: var(--spacing-4);
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: space-between;
align-items: center;
gap: var(--spacing-4);
width: 100%;
:global(.ant-select-selector) {
border-radius: 2px;
border: 1px solid var(--l2-border) !important;
border: 1px solid var(--border) !important;
background-color: var(--l2-background) !important;
input {
@@ -21,81 +20,28 @@
}
}
.k8SListControlsRow {
.k8SListControlsLeft {
flex: 1;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--spacing-4);
width: 100%;
align-items: center;
justify-content: flex-end;
}
.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;
.k8SQbSearchContainer {
flex: 1;
min-width: 240px;
max-width: 60%;
}
}
.k8SAttributeSearchContainer {
flex: 1 1 240px;
max-width: 400px;
flex: 1;
min-width: 240px;
max-width: 40%;
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);
@@ -104,7 +50,7 @@
letter-spacing: -0.07px;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--l2-border);
border: 1px solid var(--border);
border-right: none;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
@@ -118,8 +64,6 @@
}
.groupBySelect {
width: 100%;
:global(.ant-select-selector) {
border-left: none;
border-top-left-radius: 0px;

View File

@@ -1,215 +1,167 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useLocation } from 'react-router-dom';
import React, { useCallback, useMemo, useState } from 'react';
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 { QueryParams } from 'constants/query';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
import { FeatureKeys } from 'constants/features';
import { initialQueriesMap } from 'constants/queryBuilder';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
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 { useAppContext } from 'providers/App/App';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import {
ENTITY_FILTER_PLACEHOLDERS,
GetK8sEntityToAggregateAttribute,
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);
// 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 [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
const { currentQuery } = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
const location = useLocation();
const invalidateQueries = useGlobalTimeQueryInvalidate();
const currentQuery = initialQueriesMap[DataSource.METRICS];
const queryData = useMemo(
(): IBuilderQuery => ({
...currentQuery.builder.queryData[0],
aggregateOperator: 'noop',
}),
[currentQuery],
const updatedCurrentQuery = useMemo(() => {
let { filters } = currentQuery.builder.queryData[0];
if (urlFilters) {
filters = urlFilters;
}
return {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
aggregateOperator: 'noop',
aggregateAttribute: {
...currentQuery.builder.queryData[0].aggregateAttribute,
},
filters,
},
],
},
};
}, [currentQuery, urlFilters]);
const query = useMemo(
() => updatedCurrentQuery?.builder?.queryData[0] || null,
[updatedCurrentQuery],
);
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: currentQuery.builder.queryData[0],
entityVersion: '',
});
const [, setCurrentPage] = useInfraMonitoringPageListing();
const handleChangeTagFilters = useCallback(
(value: IBuilderQuery['filters']) => {
setUrlFilters(value || null);
handleChangeQueryData('filters', value);
setCurrentPage(1);
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, {
if (value?.items && value?.items?.length > 0) {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: entity,
category: InfraMonitoringEvents.Pod,
});
}
},
[
currentQuery,
safeNavigate,
location.pathname,
setCurrentPage,
entity,
invalidateQueries,
],
[handleChangeQueryData, setCurrentPage, setUrlFilters],
);
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 { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const { data: groupByFiltersData, isLoading: isLoadingGroupByFilters } =
useGetFieldsKeys(
useGetAggregateKeys(
{
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,
dataSource: currentQuery.builder.queryData[0].dataSource,
aggregateAttribute: GetK8sEntityToAggregateAttribute(
entity,
dotMetricsEnabled,
),
aggregateOperator: 'noop',
searchText: '',
tagType: '',
},
{
query: {
queryKey: ['getFieldsKeys', entity, startUnixMilli, endUnixMilli],
},
queryKey: [currentQuery.builder.queryData[0].dataSource, 'noop'],
},
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(
() =>
flatFieldKeys.map((field) => ({
value: field.name,
label: field.name,
})),
[flatFieldKeys],
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
value: filter.key,
label: filter.key,
})) || [],
[groupByFiltersData],
);
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
const handleGroupByChange = useCallback(
(value: string[]) => {
void setCurrentPage(1);
void setGroupBy(value);
(value: IBuilderQuery['groupBy']) => {
const newGroupBy = [];
void logEvent(InfraMonitoringEvents.GroupByChanged, {
for (let index = 0; index < value.length; index++) {
const element = value[index] as unknown as string;
const key = groupByFiltersData?.payload?.attributeKeys?.find(
(k) => k.key === element,
);
if (key) {
newGroupBy.push(key);
}
}
// Reset pagination on switching to groupBy
setCurrentPage(1);
setGroupBy(newGroupBy);
logEvent(InfraMonitoringEvents.GroupByChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: InfraMonitoringEvents.Pod,
});
},
[setCurrentPage, setGroupBy],
[groupByFiltersData, setCurrentPage, setGroupBy],
);
const onClickOutside = useCallback(() => {
@@ -218,69 +170,53 @@ function K8sHeader<TData>({
return (
<div className={styles.k8SListControls}>
<div className={styles.k8SListControlsRow}>
<div className={styles.k8SListControlsLeft}>
{controlListPrefix}
<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 className={styles.k8SQbSearchContainer}>
<QueryBuilderSearch
query={query as IBuilderQuery}
onChange={handleChangeTagFilters}
isInfraMonitoring
disableNavigationShortcuts
entity={entity}
/>
</div>
<RunQueryBtn
isLoadingQueries={isFetching}
onStageRunQuery={handleStageRunQuery}
handleCancelQuery={handleCancelQuery}
className={styles.k8SRunButton}
/>
<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 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.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 className={styles.k8SListControlsRight}>
<DateTimeSelectionV2
showAutoRefresh={showAutoRefresh}
showRefreshText={false}
hideShareModal
/>
<Button
type="button"
variant="ghost"
size="icon"
color="none"
data-testid="k8s-list-filters-button"
onClick={(): void => setIsFiltersSidePanelOpen(true)}
>
<SlidersHorizontal size={14} />
</Button>
</div>
<K8sFiltersSidePanel

View File

@@ -1,15 +0,0 @@
.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;
}

View File

@@ -1,37 +0,0 @@
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>
);
}

View File

@@ -22,15 +22,6 @@ 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', () => ({
@@ -215,10 +206,8 @@ describe('K8sBaseList', () => {
const itemId2 = Math.random().toString(36).slice(7);
const onUrlUpdateMock = jest.fn<void, [UrlUpdateEvent]>();
const fetchListDataMock = jest.fn<
ReturnType<
NonNullable<K8sBaseListProps<TestItemWithTitle>['fetchListData']>
>,
Parameters<NonNullable<K8sBaseListProps<TestItemWithTitle>['fetchListData']>>
ReturnType<K8sBaseListProps<TestItemWithTitle>['fetchListData']>,
Parameters<K8sBaseListProps<TestItemWithTitle>['fetchListData']>
>();
beforeEach(() => {
@@ -270,10 +259,7 @@ describe('K8sBaseList', () => {
const [filters] = fetchListDataMock.mock.calls[0];
expect(filters.limit).toBe(10);
expect(filters.offset).toBe(0);
expect(filters.filter).toStrictEqual({
expression: '',
filterByStatus: undefined,
});
expect(filters.filters).toStrictEqual({ items: [], op: 'AND' });
expect(filters.groupBy).toBeUndefined();
expect(filters.orderBy).toBeUndefined();
});
@@ -433,11 +419,11 @@ describe('K8sBaseList', () => {
});
});
describe('with URL params (orderBy, groupBy old format, pagination)', () => {
describe('with URL params (orderBy, groupBy, pagination)', () => {
const onUrlUpdateMock = jest.fn<void, [UrlUpdateEvent]>();
const fetchListDataMock = jest.fn<
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
>();
const groupByValue = [
{ key: 'k8s.namespace.name', dataType: 'string', type: 'resource' },
@@ -476,11 +462,8 @@ describe('K8sBaseList', () => {
});
const [filters] = fetchListDataMock.mock.calls[0];
expect(filters.orderBy).toStrictEqual({
key: { name: 'cpu' },
direction: 'desc',
});
expect(filters.groupBy).toStrictEqual([{ name: 'k8s.namespace.name' }]);
expect(filters.orderBy).toStrictEqual({ columnName: 'cpu', order: 'desc' });
expect(filters.groupBy).toStrictEqual(groupByValue);
expect(filters.offset).toBe(20); // (3 - 1) * 10 = 20
expect(filters.limit).toBe(10);
});
@@ -504,84 +487,14 @@ describe('K8sBaseList', () => {
(c) => c[0].groupBy && c[0].groupBy.length > 0,
);
expect(callWithGroupBy).toBeDefined();
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' },
]);
expect(callWithGroupBy?.[0].groupBy).toStrictEqual(groupByValue);
});
});
describe('with empty data', () => {
const fetchListDataMock = jest.fn<
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
>();
beforeEach(() => {
@@ -590,6 +503,10 @@ describe('K8sBaseList', () => {
data: [],
total: 0,
error: null,
rawData: {
sentAnyHostMetricsData: true,
isSendingK8SAgentMetrics: false,
},
});
renderComponent<TestItem>({
@@ -617,8 +534,8 @@ describe('K8sBaseList', () => {
describe('with error response', () => {
const fetchListDataMock = jest.fn<
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
>();
beforeEach(() => {
@@ -652,10 +569,10 @@ describe('K8sBaseList', () => {
});
});
describe('with end time before retention (endTimeBeforeRetention=true)', () => {
describe('with no metrics data (sentAnyHostMetricsData=false)', () => {
const fetchListDataMock = jest.fn<
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
>();
beforeEach(() => {
@@ -664,7 +581,96 @@ describe('K8sBaseList', () => {
data: [],
total: 0,
error: null,
endTimeBeforeRetention: true,
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']>
>();
beforeEach(() => {
fetchListDataMock.mockClear();
fetchListDataMock.mockResolvedValue({
data: [],
total: 0,
error: null,
rawData: {
sentAnyHostMetricsData: true,
isSendingK8SAgentMetrics: false,
endTimeBeforeRetention: true,
},
});
renderComponent<TestItem>({
@@ -691,8 +697,8 @@ describe('K8sBaseList', () => {
describe('column visibility based on TanStack columns', () => {
const fetchListDataMock = jest.fn<
ReturnType<NonNullable<K8sBaseListProps<TestItemWithName>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItemWithName>['fetchListData']>>
ReturnType<K8sBaseListProps<TestItemWithName>['fetchListData']>,
Parameters<K8sBaseListProps<TestItemWithName>['fetchListData']>
>();
beforeEach(() => {
@@ -730,10 +736,8 @@ describe('K8sBaseList', () => {
describe('column behavior with groupBy (expanded/collapsed)', () => {
const fetchListDataMock = jest.fn<
ReturnType<
NonNullable<K8sBaseListProps<TestItemWithGroup>['fetchListData']>
>,
Parameters<NonNullable<K8sBaseListProps<TestItemWithGroup>['fetchListData']>>
ReturnType<K8sBaseListProps<TestItemWithGroup>['fetchListData']>,
Parameters<K8sBaseListProps<TestItemWithGroup>['fetchListData']>
>();
beforeEach(() => {
@@ -803,8 +807,8 @@ describe('K8sBaseList', () => {
describe('column visibility in expanded row (nested table)', () => {
const fetchListDataMock = jest.fn<
ReturnType<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItem>['fetchListData']>>
ReturnType<K8sBaseListProps<TestItem>['fetchListData']>,
Parameters<K8sBaseListProps<TestItem>['fetchListData']>
>();
const groupByValue = [
{ key: 'k8s.namespace.name', dataType: 'string', type: 'resource' },
@@ -848,8 +852,8 @@ describe('K8sBaseList', () => {
describe('TanStack table column rendering', () => {
const fetchListDataMock = jest.fn<
ReturnType<NonNullable<K8sBaseListProps<TestItemWithName>['fetchListData']>>,
Parameters<NonNullable<K8sBaseListProps<TestItemWithName>['fetchListData']>>
ReturnType<K8sBaseListProps<TestItemWithName>['fetchListData']>,
Parameters<K8sBaseListProps<TestItemWithName>['fetchListData']>
>();
beforeEach(() => {
@@ -905,40 +909,4 @@ 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();
});
});
});
});

View File

@@ -1,25 +1,16 @@
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { OrderBySchemaType } from '../schemas';
export type K8sBaseFilters = {
filter: {
expression: string;
filterByStatus?: 'active' | 'inactive' | '';
};
groupBy?: Array<{ name: string }>;
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
start: number;
end: number;
orderBy?: {
key: { name: string };
direction: 'asc' | 'desc';
};
};
export type K8sListResponse<T> = {
type: 'list' | 'grouped_list';
records: T[];
total: number;
endTimeBeforeRetention?: boolean;
error?: string | null;
orderBy?: OrderBySchemaType;
};
/**

View File

@@ -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,13 +22,15 @@ const dotToUnder: Record<string, string> = {
'k8s.persistentvolumeclaim.name': 'k8s_persistentvolumeclaim_name',
};
export function getGroupedByMeta<
T extends { meta?: Record<string, string> | null },
>(itemData: T, groupBy: string[]): Record<string, string> {
export function getGroupedByMeta<T extends { meta?: Record<string, string> }>(
itemData: T,
groupBy: BaseAutocompleteData[],
): Record<string, string> {
const result: Record<string, string> = {};
const meta = itemData.meta ?? {};
groupBy.forEach((rawKey) => {
groupBy.forEach((group) => {
const rawKey = group.key as string;
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof meta;
result[rawKey] = (meta[metaKey] || meta[rawKey]) ?? '';
});
@@ -36,13 +38,45 @@ export function getGroupedByMeta<
return result;
}
export function getGroupByEl<
T extends { meta?: Record<string, string> | null },
>(itemData: T, groupBy: string[]): React.ReactNode {
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 {
const groupByValues: string[] = [];
const meta = itemData.meta ?? {};
groupBy.forEach((rawKey) => {
groupBy.forEach((group) => {
const rawKey = group.key as string;
// Choose mapped key if present, otherwise use rawKey
const metaKey = (dotToUnder[rawKey] ?? rawKey) as keyof typeof meta;
const value = meta[metaKey] || meta[rawKey] || '<no-value>';
@@ -64,28 +98,3 @@ export function getGroupByEl<
</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;
}

View File

@@ -1,24 +1,21 @@
import { useCallback } from 'react';
import { listClusters } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesClusterRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import React, { useCallback } from 'react';
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,
k8sClusterGetSelectedItemExpression,
k8sClusterInitialEventsExpression,
k8sClusterInitialLogTracesExpression,
k8sClusterGetSelectedItemFilters,
k8sClusterInitialEventsFilter,
k8sClusterInitialLogTracesFilter,
} from './constants';
import {
getK8sClusterItemKey,
@@ -31,93 +28,66 @@ 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) => {
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,
);
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
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,
};
}
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,
};
},
[],
[dotMetricsEnabled],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): 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,
);
): 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,
);
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,
};
}
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
},
[],
[dotMetricsEnabled],
);
return (
<>
<K8sBaseList<InframonitoringtypesClusterRecordDTO>
<K8sBaseList<K8sClusterData>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.CLUSTERS}
tableColumns={k8sClustersColumnsConfig}
@@ -127,14 +97,14 @@ function K8sClustersList({
eventCategory={InfraMonitoringEvents.Cluster}
/>
<K8sBaseDetails<InframonitoringtypesClusterRecordDTO>
<K8sBaseDetails<K8sClusterData>
category={InfraMonitoringEntity.CLUSTERS}
eventCategory={InfraMonitoringEvents.Cluster}
getSelectedItemExpression={k8sClusterGetSelectedItemExpression}
getSelectedItemFilters={k8sClusterGetSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={k8sClusterGetEntityName}
getInitialLogTracesExpression={k8sClusterInitialLogTracesExpression}
getInitialEventsExpression={k8sClusterInitialEventsExpression}
getInitialLogTracesFilters={k8sClusterInitialLogTracesFilter}
getInitialEventsFilters={k8sClusterInitialEventsFilter}
metadataConfig={k8sClusterDetailsMetadataConfig}
entityWidgetInfo={clusterWidgetInfo}
getEntityQueryPayload={getClusterMetricsQueryPayload}

View File

@@ -0,0 +1,125 @@
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);
}
};

View File

@@ -1,39 +1,53 @@
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 { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sClusterData } from './api';
export const k8sClusterGetSelectedItemExpression = (
export const k8sClusterGetSelectedItemFilters = (
selectedItemId: string,
): string => `k8s.cluster.name = ${formatValueForExpression(selectedItemId)}`;
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_cluster_name',
key: {
key: 'k8s_cluster_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
export const k8sClusterDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesClusterRecordDTO>[] =
[{ label: 'Cluster Name', getValue: (p): string => p.clusterName || '' }];
export const k8sClusterDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sClusterData>[] =
[{ label: 'Cluster Name', getValue: (p): string => p.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 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 k8sClusterInitialLogTracesExpression = (
item: InframonitoringtypesClusterRecordDTO,
): string => {
const clusterName = formatValueForExpression(item.clusterName || '');
return `${INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME} = ${clusterName}`;
};
export const k8sClusterInitialLogTracesFilter = (
item: K8sClusterData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_CLUSTER_NAME, item.meta.k8s_cluster_name),
];
export const k8sClusterGetEntityName = (
item: InframonitoringtypesClusterRecordDTO,
): string => item.clusterName || '';
export const k8sClusterGetEntityName = (item: K8sClusterData): string =>
item.meta.k8s_cluster_name;
export const clusterWidgetInfo = [
{
@@ -71,7 +85,7 @@ export const clusterWidgetInfo = [
];
export const getClusterMetricsQueryPayload = (
cluster: InframonitoringtypesClusterRecordDTO,
cluster: K8sClusterData,
start: number,
end: number,
dotMetricsEnabled: boolean,
@@ -193,7 +207,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -232,7 +246,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -271,7 +285,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -310,7 +324,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -383,7 +397,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -422,7 +436,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -461,7 +475,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -500,7 +514,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -573,7 +587,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -659,7 +673,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -745,7 +759,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -797,7 +811,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -895,7 +909,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -947,7 +961,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -999,7 +1013,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -1051,7 +1065,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -1173,7 +1187,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -1219,7 +1233,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -1265,7 +1279,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -1369,7 +1383,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -1421,7 +1435,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -1473,7 +1487,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',
@@ -1525,7 +1539,7 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: cluster.clusterName,
value: cluster.meta.k8s_cluster_name,
},
],
op: 'AND',

View File

@@ -1,48 +1,42 @@
import { Color } from '@signozhq/design-tokens';
import { Boxes } from '@signozhq/icons';
import { InframonitoringtypesClusterRecordDTO } from 'api/generated/services/sigNoz.schemas';
import { Tooltip } from 'antd';
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, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { formatBytes } from '../commonUtils';
import { ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sClusterData, K8sClustersListPayload } from './api';
import { Boxes } from '@signozhq/icons';
export function getK8sClusterRowKey(
cluster: InframonitoringtypesClusterRecordDTO,
): string {
export function getK8sClusterRowKey(cluster: K8sClusterData): string {
return (
cluster.clusterName ||
cluster.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_UID] ||
''
cluster.clusterUID ||
cluster.meta.k8s_cluster_uid ||
cluster.meta.k8s_cluster_name
);
}
export function getK8sClusterItemKey(
cluster: InframonitoringtypesClusterRecordDTO,
): string {
return cluster.clusterName;
export function getK8sClusterItemKey(cluster: K8sClusterData): string {
return cluster.meta.k8s_cluster_name;
}
export type ClusterTableColumnConfig =
TableColumnDef<InframonitoringtypesClusterRecordDTO>;
export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
export const getK8sClustersListQuery = (): K8sClustersListPayload => ({
filters: {
items: [],
op: 'and',
},
orderBy: { columnName: 'cpu', order: 'desc' },
});
export const k8sClustersColumnsConfig: TableColumnDef<K8sClusterData>[] = [
{
id: 'clusterGroup',
header: (): React.ReactNode => <EntityGroupHeader title="Cluster Group" />,
accessorFn: (row): string => row.clusterName || '',
width: { min: 290 },
header: (): React.ReactNode => <EntityGroupHeader title="CLUSTER GROUP" />,
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
width: { min: 300 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -65,10 +59,9 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
<EntityGroupHeader
title="Cluster Name"
icon={<Boxes data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/clusters#cluster-name"
/>
),
accessorFn: (row): string => row.clusterName || '',
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
@@ -78,127 +71,55 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
cell: ({ value }): React.ReactNode => {
const clusterName = value as string;
return (
<CellValueTooltip value={clusterName}>
<Tooltip title={clusterName}>
<TanStackTable.Text>{clusterName}</TanStackTable.Text>
</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)} />
</Tooltip>
);
},
},
{
id: 'cpu',
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 },
header: 'CPU Usage (cores)',
accessorFn: (row): number => row.cpuUsage,
width: { min: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
const cpu = value as number;
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.CLUSTERS}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'cpu_allocatable',
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 },
header: 'CPU Alloc (cores)',
accessorFn: (row): number => row.cpuAllocatable,
width: { min: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpuAllocatable = Number(value);
const cpuAllocatable = value as number;
return (
<ValidateColumnValueWrapper
value={cpuAllocatable}
entity={InfraMonitoringEntity.CLUSTERS}
attribute="CPU allocatable metric"
>
<TanStackTable.Text>{cpuAllocatable.toFixed(2)}</TanStackTable.Text>
<TanStackTable.Text>{cpuAllocatable}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory',
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 },
header: 'Memory Usage (WSS)',
accessorFn: (row): number => row.memoryUsage,
width: { min: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
@@ -215,14 +136,9 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
},
{
id: 'memory_allocatable',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#memory-allocatable">
Memory
<br /> Allocatable
</ColumnHeader>
),
accessorFn: (row): number => row.clusterMemoryAllocatable,
width: { min: 180 },
header: 'Memory Allocatable',
accessorFn: (row): number => row.memoryAllocatable,
width: { min: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memoryAllocatable = value as number;

View File

@@ -1,117 +1,93 @@
import { useCallback } from 'react';
import { listDaemonSets } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesDaemonSetRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import React, { useCallback } from 'react';
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,
k8sDaemonSetGetSelectedItemExpression,
k8sDaemonSetInitialEventsExpression,
k8sDaemonSetInitialLogTracesExpression,
k8sDaemonSetGetSelectedItemFilters,
k8sDaemonSetInitialEventsFilter,
k8sDaemonSetInitialLogTracesFilter,
} 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) => {
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,
};
}
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,
};
},
[],
[dotMetricsEnabled],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): 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,
};
}
): 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,
};
},
[],
[dotMetricsEnabled],
);
return (
<>
<K8sBaseList<InframonitoringtypesDaemonSetRecordDTO>
<K8sBaseList<K8sDaemonSetsData>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.DAEMONSETS}
tableColumns={k8sDaemonSetsColumnsConfig}
@@ -120,14 +96,15 @@ function K8sDaemonSetsList({
getItemKey={getK8sDaemonSetItemKey}
eventCategory={InfraMonitoringEvents.DaemonSet}
/>
<K8sBaseDetails<InframonitoringtypesDaemonSetRecordDTO>
<K8sBaseDetails<K8sDaemonSetsData>
category={InfraMonitoringEntity.DAEMONSETS}
eventCategory={InfraMonitoringEvents.DaemonSet}
getSelectedItemExpression={k8sDaemonSetGetSelectedItemExpression}
getSelectedItemFilters={k8sDaemonSetGetSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={k8sDaemonSetGetEntityName}
getInitialLogTracesExpression={k8sDaemonSetInitialLogTracesExpression}
getInitialEventsExpression={k8sDaemonSetInitialEventsExpression}
getInitialLogTracesFilters={k8sDaemonSetInitialLogTracesFilter}
getInitialEventsFilters={k8sDaemonSetInitialEventsFilter}
metadataConfig={k8sDaemonSetDetailsMetadataConfig}
entityWidgetInfo={daemonSetWidgetInfo}
getEntityQueryPayload={getDaemonSetMetricsQueryPayload}
@@ -136,4 +113,5 @@ function K8sDaemonSetsList({
</>
);
}
export default K8sDaemonSetsList;

View File

@@ -0,0 +1,119 @@
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);
}
};

View File

@@ -1,52 +1,67 @@
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 { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sDaemonSetsData } from './api';
export const k8sDaemonSetGetSelectedItemExpression = (
export const k8sDaemonSetGetSelectedItemFilters = (
selectedItemId: string,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME} = ${formatValueForExpression(selectedItemId)}`;
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_daemonset_name',
key: {
key: 'k8s_daemonset_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
export const k8sDaemonSetDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesDaemonSetRecordDTO>[] =
export const k8sDaemonSetDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sDaemonSetsData>[] =
[
{
label: 'Daemonset Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? '',
getValue: (p): string => p.meta.k8s_daemonset_name,
},
{
label: 'Cluster Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
getValue: (p): string => p.meta.k8s_cluster_name,
},
{
label: 'Namespace Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
getValue: (p): string => p.meta.k8s_namespace_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 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 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 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 k8sDaemonSetGetEntityName = (
item: InframonitoringtypesDaemonSetRecordDTO,
): string => item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? '';
export const k8sDaemonSetGetEntityName = (item: K8sDaemonSetsData): string =>
item.meta.k8s_daemonset_name;
export const daemonSetWidgetInfo = [
{
@@ -68,7 +83,7 @@ export const daemonSetWidgetInfo = [
];
export const getDaemonSetMetricsQueryPayload = (
daemonSet: InframonitoringtypesDaemonSetRecordDTO,
daemonSet: K8sDaemonSetsData,
start: number,
end: number,
dotMetricsEnabled: boolean,
@@ -144,9 +159,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
value: daemonSet.meta.k8s_daemonset_name,
},
{
id: '47b3adae',
@@ -157,9 +170,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: daemonSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -198,9 +209,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
value: daemonSet.meta.k8s_daemonset_name,
},
{
id: '47b3adae',
@@ -211,9 +220,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: daemonSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -252,9 +259,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
value: daemonSet.meta.k8s_daemonset_name,
},
{
id: '47b3adae',
@@ -265,9 +270,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: daemonSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -340,9 +343,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
value: daemonSet.meta.k8s_daemonset_name,
},
{
id: '47b3adae',
@@ -353,9 +354,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: daemonSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -394,9 +393,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
value: daemonSet.meta.k8s_daemonset_name,
},
{
id: '47b3adae',
@@ -407,9 +404,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: daemonSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -448,9 +443,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
value: daemonSet.meta.k8s_daemonset_name,
},
{
id: '47b3adae',
@@ -461,9 +454,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: daemonSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -536,9 +527,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
value: daemonSet.meta.k8s_daemonset_name,
},
{
id: '47b3adae',
@@ -549,9 +538,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: daemonSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -637,9 +624,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
value: daemonSet.meta.k8s_daemonset_name,
},
{
id: '47b3adae',
@@ -650,9 +635,7 @@ export const getDaemonSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: daemonSet.meta.k8s_namespace_name,
},
],
op: 'AND',

View File

@@ -1,49 +1,34 @@
import { Color } from '@signozhq/design-tokens';
import { InframonitoringtypesDaemonSetRecordDTO } from 'api/generated/services/sigNoz.schemas';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { Tooltip } from 'antd';
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, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { formatBytes } from '../commonUtils';
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sDaemonSetsData } from './api';
import { Group } from '@signozhq/icons';
export function getK8sDaemonSetRowKey(
daemonSet: InframonitoringtypesDaemonSetRecordDTO,
): string {
export function getK8sDaemonSetRowKey(daemonSet: K8sDaemonSetsData): string {
return (
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ||
daemonSet.daemonSetName ||
`${daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME]}-${daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME]}`
daemonSet.meta.k8s_daemonset_name ||
`${daemonSet.meta.k8s_namespace_name}-${daemonSet.meta.k8s_daemonset_name}`
);
}
export function getK8sDaemonSetItemKey(
daemonSet: InframonitoringtypesDaemonSetRecordDTO,
): string {
return daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] || '';
export function getK8sDaemonSetItemKey(daemonSet: K8sDaemonSetsData): string {
return daemonSet.meta.k8s_daemonset_name;
}
export type DaemonSetTableColumnConfig =
TableColumnDef<InframonitoringtypesDaemonSetRecordDTO>;
export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
export const k8sDaemonSetsColumnsConfig: TableColumnDef<K8sDaemonSetsData>[] = [
{
id: 'daemonSetGroup',
header: (): React.ReactNode => <EntityGroupHeader title="DaemonSet Group" />,
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] || '',
width: { min: 290 },
header: (): React.ReactNode => <EntityGroupHeader title="DAEMONSET GROUP" />,
accessorFn: (row): string => row.meta.k8s_daemonset_name || '',
width: { min: 300 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -66,11 +51,9 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
<EntityGroupHeader
title="DaemonSet Name"
icon={<Group data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/daemonsets#daemonset-name"
/>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] || '',
accessorFn: (row): string => row.meta.k8s_daemonset_name || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
@@ -80,98 +63,71 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
cell: ({ value }): React.ReactNode => {
const daemonsetName = value as string;
return (
<CellValueTooltip value={daemonsetName}>
<Tooltip title={daemonsetName}>
<TanStackTable.Text>{daemonsetName}</TanStackTable.Text>
</CellValueTooltip>
</Tooltip>
);
},
},
{
id: 'namespaceName',
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 },
header: 'Namespace Name',
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
width: { default: 100 },
enableSort: false,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return (
<CellValueTooltip value={namespaceName}>
<Tooltip title={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</CellValueTooltip>
</Tooltip>
);
},
},
{
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>;
}
id: 'available_nodes',
header: 'Available',
accessorFn: (row): number => row.availableNodes,
width: { min: 140 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const availableNodes = value as number;
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
<ValidateColumnValueWrapper
value={availableNodes}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="available node"
>
<TanStackTable.Text>{availableNodes}</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: '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: 'cpu_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-req-usage-">
CPU Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.daemonSetCPURequest,
header: 'CPU Req Usage (%)',
accessorFn: (row): number => row.cpuRequest,
width: { min: 200, default: 200 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const cpuRequest = value as number;
return (
@@ -187,16 +143,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
},
{
id: 'cpu_limit',
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 },
header: 'CPU Limit Usage (%)',
accessorFn: (row): number => row.cpuLimit,
width: { min: 200, default: 200 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const cpuLimit = value as number;
return (
@@ -212,42 +162,30 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
},
{
id: 'cpu',
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 },
header: 'CPU Usage (cores)',
accessorFn: (row): number => row.cpuUsage,
width: { min: 190 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
const cpu = value as number;
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-req-usage-">
Memory Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.daemonSetMemoryRequest,
header: 'Mem Req Usage (%)',
accessorFn: (row): number => row.memoryRequest,
width: { min: 190 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const memoryRequest = value as number;
@@ -264,16 +202,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
},
{
id: 'memory_limit',
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 },
header: 'Mem Limit Usage (%)',
accessorFn: (row): number => row.memoryLimit,
width: { min: 180 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memoryLimit = value as number;
return (
@@ -289,16 +221,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
},
{
id: 'memory',
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 },
header: 'Mem Usage (WSS)',
accessorFn: (row): number => row.memoryUsage,
width: { min: 160 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
return (
@@ -312,52 +238,4 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
);
},
},
{
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>
);
},
},
];

View File

@@ -1,24 +1,21 @@
import { useCallback } from 'react';
import { listDeployments } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesDeploymentRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import React, { useCallback } from 'react';
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,
k8sDeploymentGetSelectedItemExpression,
k8sDeploymentInitialEventsExpression,
k8sDeploymentInitialLogTracesExpression,
k8sDeploymentGetSelectedItemFilters,
k8sDeploymentInitialEventsFilter,
k8sDeploymentInitialLogTracesFilter,
} from './constants';
import {
getK8sDeploymentItemKey,
@@ -31,93 +28,66 @@ 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) => {
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,
);
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
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,
};
}
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,
};
},
[],
[dotMetricsEnabled],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): 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,
);
): 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,
);
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,
};
}
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
},
[],
[dotMetricsEnabled],
);
return (
<>
<K8sBaseList<InframonitoringtypesDeploymentRecordDTO>
<K8sBaseList<K8sDeploymentsData>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.DEPLOYMENTS}
tableColumns={k8sDeploymentsColumnsConfig}
@@ -127,14 +97,14 @@ function K8sDeploymentsList({
eventCategory={InfraMonitoringEvents.Deployment}
/>
<K8sBaseDetails<InframonitoringtypesDeploymentRecordDTO>
<K8sBaseDetails<K8sDeploymentsData>
category={InfraMonitoringEntity.DEPLOYMENTS}
eventCategory={InfraMonitoringEvents.Deployment}
getSelectedItemExpression={k8sDeploymentGetSelectedItemExpression}
getSelectedItemFilters={k8sDeploymentGetSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={k8sDeploymentGetEntityName}
getInitialLogTracesExpression={k8sDeploymentInitialLogTracesExpression}
getInitialEventsExpression={k8sDeploymentInitialEventsExpression}
getInitialLogTracesFilters={k8sDeploymentInitialLogTracesFilter}
getInitialEventsFilters={k8sDeploymentInitialEventsFilter}
metadataConfig={k8sDeploymentDetailsMetadataConfig}
entityWidgetInfo={deploymentWidgetInfo}
getEntityQueryPayload={getDeploymentMetricsQueryPayload}

View File

@@ -0,0 +1,132 @@
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);
}
};

View File

@@ -1,63 +1,70 @@
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 { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sDeploymentsData } from './api';
export const k8sDeploymentGetSelectedItemExpression = (
export const k8sDeploymentGetSelectedItemFilters = (
selectedItemId: string,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME} = ${formatValueForExpression(selectedItemId)}`;
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_deployment_name',
key: {
key: 'k8s_deployment_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
export const k8sDeploymentDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesDeploymentRecordDTO>[] =
export const k8sDeploymentDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sDeploymentsData>[] =
[
{
label: 'Deployment Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? '',
getValue: (p): string => p.meta.k8s_deployment_name,
},
{
label: 'Cluster Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
getValue: (p): string => p.meta.k8s_cluster_name,
},
{
label: 'Namespace Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
getValue: (p): string => p.meta.k8s_namespace_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 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 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 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 k8sDeploymentGetEntityName = (
item: InframonitoringtypesDeploymentRecordDTO,
): string => item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? '';
export const k8sDeploymentGetEntityName = (item: K8sDeploymentsData): string =>
item.meta.k8s_deployment_name;
export const deploymentWidgetInfo = [
{
@@ -79,7 +86,7 @@ export const deploymentWidgetInfo = [
];
export const getDeploymentMetricsQueryPayload = (
deployment: InframonitoringtypesDeploymentRecordDTO,
deployment: K8sDeploymentsData,
start: number,
end: number,
dotMetricsEnabled: boolean,
@@ -151,9 +158,7 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
@@ -192,9 +197,7 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
@@ -233,9 +236,7 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
@@ -308,9 +309,7 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
@@ -349,9 +348,7 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
@@ -390,9 +387,7 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
@@ -465,9 +460,7 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',
@@ -553,9 +546,7 @@ export const getDeploymentMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
value: deployment.meta.k8s_deployment_name,
},
],
op: 'AND',

View File

@@ -1,50 +1,35 @@
import { Color } from '@signozhq/design-tokens';
import { InframonitoringtypesDeploymentRecordDTO } from 'api/generated/services/sigNoz.schemas';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { Tooltip } from 'antd';
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, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { formatBytes } from '../commonUtils';
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sDeploymentsData } from './api';
import { Computer } from '@signozhq/icons';
export function getK8sDeploymentRowKey(
deployment: InframonitoringtypesDeploymentRecordDTO,
): string {
return (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ||
deployment.deploymentName ||
''
);
export function getK8sDeploymentRowKey(deployment: K8sDeploymentsData): string {
return deployment.meta.k8s_deployment_name || deployment.deploymentName;
}
export function getK8sDeploymentItemKey(
deployment: InframonitoringtypesDeploymentRecordDTO,
deployment: K8sDeploymentsData,
): string {
return deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] || '';
return deployment.meta.k8s_deployment_name;
}
export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDeploymentRecordDTO>[] =
export const k8sDeploymentsColumnsConfig: TableColumnDef<K8sDeploymentsData>[] =
[
{
id: 'deploymentGroup',
header: (): React.ReactNode => (
<EntityGroupHeader title="Deployment Group" />
<EntityGroupHeader title="DEPLOYMENT GROUP" />
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] || '',
width: { min: 290 },
accessorFn: (row): string => row.meta.k8s_deployment_name || '',
width: { min: 220 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -67,12 +52,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
<EntityGroupHeader
title="Deployment Name"
icon={<Computer data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/deployments#deployment-name"
/>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] || '',
width: { min: 290 },
accessorFn: (row): string => row.meta.k8s_deployment_name || '',
width: { min: 210 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -81,239 +64,30 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
cell: ({ value }): React.ReactNode => {
const deploymentName = value as string;
return (
<CellValueTooltip value={deploymentName}>
<Tooltip title={deploymentName}>
<TanStackTable.Text>{deploymentName}</TanStackTable.Text>
</CellValueTooltip>
</Tooltip>
);
},
},
{
id: 'namespaceName',
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 },
header: 'Namespace Name',
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
width: { default: 220 },
enableSort: false,
enableResize: true,
cell: ({ value }): React.ReactNode => (
<TanStackTable.Text>{value as string}</TanStackTable.Text>
),
},
{
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>
),
header: 'Available',
accessorFn: (row): number => row.availablePods,
width: { min: 120 },
enableSort: true,
width: { min: 100 },
enableSort: false,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const availablePods = value as number;
@@ -330,14 +104,11 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
},
{
id: 'desired_pods',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#desired">
Desired Pods
</ColumnHeader>
),
header: 'Desired',
accessorFn: (row): number => row.desiredPods,
width: { min: 120 },
enableSort: true,
width: { min: 80 },
enableSort: false,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const desiredPods = value as number;
@@ -352,4 +123,124 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
);
},
},
{
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>
);
},
},
];

View File

@@ -14,12 +14,7 @@ import {
import APIError from 'types/api/error';
import { v4 as uuidv4 } from 'uuid';
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,
];
const K8S_EVENT_KEYS = ['k8s.object.kind', 'k8s.object.name'];
export function isEventsKeyNotFoundError(error: unknown): boolean {
if (!(error instanceof APIError)) {

View File

@@ -1,8 +1,144 @@
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[] = [
{
@@ -37,16 +173,110 @@ export const selectedEntityTracesColumns: BaseAutocompleteData[] = [
},
];
export function isKeyNotFoundError(error: unknown): boolean {
if (!(error instanceof APIError)) {
return false;
}
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,
},
],
},
});
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 filterOutPrimaryFilters = (
filters: TagFilterItem[],
primaryKeys: string[],
): TagFilterItem[] =>
filters.filter(
(filter) =>
!primaryKeys.includes(filter.key?.key ?? '') && filter.key?.key !== 'id',
);

View File

@@ -26,8 +26,8 @@
}
&::-webkit-scrollbar {
width: 8px;
height: 8px;
width: 0.1rem;
height: 0.1rem;
}
&::-webkit-scrollbar-track {
@@ -35,12 +35,11 @@
}
&::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--l1-foreground) 20%, transparent);
border-radius: 9999px;
background: var(--accent-primary);
}
&::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--l1-foreground) 30%, transparent);
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
}
@@ -85,7 +84,7 @@
}
&::-webkit-scrollbar-thumb:hover {
background: var(--accent-primary);
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
}
}
@@ -100,12 +99,11 @@
.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;
@@ -235,7 +233,3 @@
background: var(--accent-primary);
}
}
.k8SOpenQuickFilters {
grid-area: quickFilters;
}

View File

@@ -2,11 +2,12 @@ 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 { initialQueriesMap } from 'constants/queryBuilder';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { InfraMonitoringEvents } from 'constants/events';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import {
ArrowUpDown,
ArrowUpToLine,
@@ -21,7 +22,7 @@ import {
Workflow,
} from '@signozhq/icons';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { DataSource } from 'types/common/queryBuilder';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { FeatureKeys } from '../../constants/features';
import { useAppContext } from '../../providers/App/App';
@@ -42,9 +43,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';
@@ -54,61 +55,63 @@ 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 compositeQuery = useGetCompositeQueryParam();
const { currentQuery, 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]);
useEffect(() => {
void logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: selectedCategory,
view: InfraMonitoringEvents.QuickFiltersView,
});
}, [selectedCategory]);
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);
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 { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
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, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: selectedCategory,
view: InfraMonitoringEvents.QuickFiltersView,
});
};
const categories = useMemo(
() => [
{
@@ -176,23 +179,12 @@ export default function InfraMonitoringK8s(): JSX.Element {
const handleCategorySelect = (key: string): void => {
if (key !== selectedCategory) {
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 },
},
],
},
});
setSelectedCategory(key);
// Reset filters
setUrlFilters(null);
setOrderBy(null);
setGroupBy(null);
handleChangeQueryData('filters', { items: [], op: 'and' });
}
};
@@ -200,7 +192,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
return (
<>
{!showFilters && (
<div className={styles.k8SOpenQuickFilters}>
<div>
<Button
className="periscope-btn ghost"
type="text"
@@ -269,6 +261,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
source={QuickFiltersSource.INFRA_MONITORING}
config={selectedCategoryConfig}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
)}
</div>

View File

@@ -1,24 +1,21 @@
import { useCallback } from 'react';
import { listJobs } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesJobRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import React, { useCallback } from 'react';
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,
k8sJobGetSelectedItemExpression,
k8sJobInitialEventsExpression,
k8sJobInitialLogTracesExpression,
k8sJobGetSelectedItemFilters,
k8sJobInitialEventsFilter,
k8sJobInitialLogTracesFilter,
} from './constants';
import {
getK8sJobItemKey,
@@ -31,93 +28,66 @@ 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) => {
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,
);
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
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,
};
}
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,
};
},
[],
[dotMetricsEnabled],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): 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,
);
): 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,
);
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,
};
}
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
},
[],
[dotMetricsEnabled],
);
return (
<>
<K8sBaseList<InframonitoringtypesJobRecordDTO>
<K8sBaseList<K8sJobsData>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.JOBS}
tableColumns={k8sJobsColumnsConfig}
@@ -127,14 +97,14 @@ function K8sJobsList({
eventCategory={InfraMonitoringEvents.Job}
/>
<K8sBaseDetails<InframonitoringtypesJobRecordDTO>
<K8sBaseDetails<K8sJobsData>
category={InfraMonitoringEntity.JOBS}
eventCategory={InfraMonitoringEvents.Job}
getSelectedItemExpression={k8sJobGetSelectedItemExpression}
getSelectedItemFilters={k8sJobGetSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={k8sJobGetEntityName}
getInitialLogTracesExpression={k8sJobInitialLogTracesExpression}
getInitialEventsExpression={k8sJobInitialEventsExpression}
getInitialLogTracesFilters={k8sJobInitialLogTracesFilter}
getInitialEventsFilters={k8sJobInitialEventsFilter}
metadataConfig={k8sJobDetailsMetadataConfig}
entityWidgetInfo={jobWidgetInfo}
getEntityQueryPayload={getJobMetricsQueryPayload}

View File

@@ -0,0 +1,119 @@
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);
}
};

View File

@@ -1,63 +1,67 @@
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 { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sJobsData } from './api';
export const k8sJobGetSelectedItemExpression = (
export const k8sJobGetSelectedItemFilters = (
selectedItemId: string,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME} = ${formatValueForExpression(selectedItemId)}`;
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_job_name',
key: {
key: 'k8s_job_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
export const k8sJobDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesJobRecordDTO>[] =
export const k8sJobDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sJobsData>[] =
[
{
label: 'Job Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] ?? '',
getValue: (p): string => p.meta.k8s_job_name,
},
{
label: 'Cluster Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
getValue: (p): string => p.meta.k8s_cluster_name,
},
{
label: 'Namespace Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
getValue: (p): string => p.meta.k8s_namespace_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 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 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 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 k8sJobGetEntityName = (
item: InframonitoringtypesJobRecordDTO,
): string => item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] ?? '';
export const k8sJobGetEntityName = (item: K8sJobsData): string =>
item.meta.k8s_job_name;
export const jobWidgetInfo = [
{
@@ -79,7 +83,7 @@ export const jobWidgetInfo = [
];
export const getJobMetricsQueryPayload = (
job: InframonitoringtypesJobRecordDTO,
job: K8sJobsData,
start: number,
end: number,
dotMetricsEnabled: boolean,
@@ -141,8 +145,7 @@ export const getJobMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
value: job.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -226,8 +229,7 @@ export const getJobMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
value: job.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -311,8 +313,7 @@ export const getJobMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
value: job.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -409,8 +410,7 @@ export const getJobMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
value: job.meta.k8s_namespace_name,
},
],
op: 'AND',

View File

@@ -1,45 +1,30 @@
import { Color } from '@signozhq/design-tokens';
import { InframonitoringtypesJobRecordDTO } from 'api/generated/services/sigNoz.schemas';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { Tooltip } from 'antd';
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, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { formatBytes } from '../commonUtils';
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sJobsData } from './api';
import { Bolt } from '@signozhq/icons';
export function getK8sJobRowKey(job: InframonitoringtypesJobRecordDTO): string {
return (
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || job.jobName || ''
);
export function getK8sJobRowKey(job: K8sJobsData): string {
return job.jobName || job.meta.k8s_job_name || '';
}
export function getK8sJobItemKey(
job: InframonitoringtypesJobRecordDTO,
): string {
return job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || '';
export function getK8sJobItemKey(job: K8sJobsData): string {
return job.meta.k8s_job_name;
}
export type JobTableColumnConfig =
TableColumnDef<InframonitoringtypesJobRecordDTO>;
export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
export const k8sJobsColumnsConfig: TableColumnDef<K8sJobsData>[] = [
{
id: 'jobGroup',
header: (): React.ReactNode => <EntityGroupHeader title="Job Group" />,
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || '',
width: { min: 290 },
header: (): React.ReactNode => <EntityGroupHeader title="JOB GROUP" />,
accessorFn: (row): string => row.meta.k8s_job_name || '',
width: { min: 270 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -62,12 +47,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
<EntityGroupHeader
title="Job Name"
icon={<Bolt data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/jobs#job-name"
/>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || '',
width: { min: 290 },
accessorFn: (row): string => row.meta.k8s_job_name || '',
width: { min: 260 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -76,98 +59,109 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
cell: ({ value }): React.ReactNode => {
const jobName = value as string;
return (
<CellValueTooltip value={jobName}>
<Tooltip title={jobName}>
<TanStackTable.Text>{jobName}</TanStackTable.Text>
</CellValueTooltip>
</Tooltip>
);
},
},
{
id: 'namespaceName',
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 },
header: 'Namespace Name',
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
width: { default: 150 },
enableSort: false,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return (
<CellValueTooltip value={namespaceName}>
<Tooltip title={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</CellValueTooltip>
</Tooltip>
);
},
},
{
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>
),
id: 'successful_pods',
header: 'Successful',
accessorFn: (row): number => row.successfulPods,
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,
},
]}
/>
),
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>
);
},
},
{
id: 'cpu_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-req-usage-">
CPU Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.jobCPURequest,
header: 'CPU Req Usage (%)',
accessorFn: (row): number => row.cpuRequest,
width: { min: 200, default: 200 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const cpuRequest = value as number;
return (
@@ -183,16 +177,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
},
{
id: 'cpu_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-limit-usage-">
CPU Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.jobCPULimit,
header: 'CPU Limit Usage (%)',
accessorFn: (row): number => row.cpuLimit,
width: { min: 200, default: 200 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const cpuLimit = value as number;
return (
@@ -208,42 +196,29 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
},
{
id: 'cpu',
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 },
header: 'CPU Usage (cores)',
accessorFn: (row): number => row.cpuUsage,
width: { min: 190 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
const cpu = value as number;
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.JOBS}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-req-usage-">
Memory Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.jobMemoryRequest,
header: 'Mem Req Usage (%)',
accessorFn: (row): number => row.memoryRequest,
width: { min: 190 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const memoryRequest = value as number;
return (
@@ -259,16 +234,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
},
{
id: 'memory_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-limit-usage-">
Memory Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.jobMemoryLimit,
header: 'Mem Limit Usage (%)',
accessorFn: (row): number => row.memoryLimit,
width: { min: 180 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memoryLimit = value as number;
return (
@@ -284,16 +253,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
},
{
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-usage-wss">
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.jobMemory,
header: 'Mem Usage (WSS)',
accessorFn: (row): number => row.memoryUsage,
width: { min: 160 },
enableSort: true,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
return (
@@ -307,100 +270,4 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
);
},
},
{
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>
);
},
},
];

View File

@@ -1,23 +1,20 @@
import { useCallback } from 'react';
import { listNamespaces } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesNamespaceRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import React, { useCallback } from 'react';
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,
k8sNamespaceGetSelectedItemExpression,
k8sNamespaceInitialEventsExpression,
k8sNamespaceInitialLogTracesExpression,
k8sNamespaceGetSelectedItemFilters,
k8sNamespaceInitialEventsFilter,
k8sNamespaceInitialLogTracesFilter,
namespaceWidgetInfo,
} from './constants';
import {
@@ -31,93 +28,66 @@ 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) => {
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,
);
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
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,
};
}
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,
};
},
[],
[dotMetricsEnabled],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): 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,
);
): 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,
);
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,
};
}
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
},
[],
[dotMetricsEnabled],
);
return (
<>
<K8sBaseList<InframonitoringtypesNamespaceRecordDTO>
<K8sBaseList<K8sNamespacesData>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.NAMESPACES}
tableColumns={k8sNamespacesColumnsConfig}
@@ -127,14 +97,14 @@ function K8sNamespacesList({
eventCategory={InfraMonitoringEvents.Namespace}
/>
<K8sBaseDetails<InframonitoringtypesNamespaceRecordDTO>
<K8sBaseDetails<K8sNamespacesData>
category={InfraMonitoringEntity.NAMESPACES}
eventCategory={InfraMonitoringEvents.Namespace}
getSelectedItemExpression={k8sNamespaceGetSelectedItemExpression}
getSelectedItemFilters={k8sNamespaceGetSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={k8sNamespaceGetEntityName}
getInitialLogTracesExpression={k8sNamespaceInitialLogTracesExpression}
getInitialEventsExpression={k8sNamespaceInitialEventsExpression}
getInitialLogTracesFilters={k8sNamespaceInitialLogTracesFilter}
getInitialEventsFilters={k8sNamespaceInitialEventsFilter}
metadataConfig={k8sNamespaceDetailsMetadataConfig}
entityWidgetInfo={namespaceWidgetInfo}
getEntityQueryPayload={getNamespaceMetricsQueryPayload}

View File

@@ -0,0 +1,123 @@
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);
}
};

View File

@@ -1,47 +1,67 @@
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 { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sNamespacesData } from './api';
export const k8sNamespaceGetSelectedItemExpression = (
export const k8sNamespaceGetSelectedItemFilters = (
selectedItemId: string,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${formatValueForExpression(selectedItemId)}`;
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_namespace_name',
key: {
key: 'k8s_namespace_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
export const k8sNamespaceDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesNamespaceRecordDTO>[] =
export const k8sNamespaceDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sNamespacesData>[] =
[
{ label: 'Namespace Name', getValue: (p): string => p.namespaceName || '' },
{ label: 'Namespace Name', getValue: (p): string => p.namespaceName },
{
label: 'Cluster Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
getValue: (p): string => p.meta.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 k8sNamespaceInitialFilters = [
QUERY_KEYS.K8S_NAMESPACE_NAME,
QUERY_KEYS.K8S_CLUSTER_NAME,
];
export const k8sNamespaceInitialLogTracesExpression = (
item: InframonitoringtypesNamespaceRecordDTO,
): string => {
const name = formatValueForExpression(item.namespaceName || '');
return `${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_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 k8sNamespaceGetEntityName = (
item: InframonitoringtypesNamespaceRecordDTO,
): string => item.namespaceName || '';
export const k8sNamespaceInitialLogTracesFilter = (
item: K8sNamespacesData,
): TagFilterItem[] => [
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.namespaceName),
];
export const k8sNamespaceGetEntityName = (item: K8sNamespacesData): string =>
item.namespaceName;
export const namespaceWidgetInfo = [
{
@@ -87,7 +107,7 @@ export const namespaceWidgetInfo = [
];
export const getNamespaceMetricsQueryPayload = (
namespace: InframonitoringtypesNamespaceRecordDTO,
namespace: K8sNamespacesData,
start: number,
end: number,
dotMetricsEnabled: boolean,

View File

@@ -1,46 +1,37 @@
import { FilePenLine } from '@signozhq/icons';
import { InframonitoringtypesNamespaceRecordDTO } from 'api/generated/services/sigNoz.schemas';
import { Tooltip } from 'antd';
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, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { formatBytes } from '../commonUtils';
import { ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sNamespacesData, K8sNamespacesListPayload } from './api';
import { FilePenLine } from '@signozhq/icons';
export function getK8sNamespaceRowKey(
namespace: InframonitoringtypesNamespaceRecordDTO,
): string {
return (
namespace.namespaceName ||
namespace.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ||
''
);
export function getK8sNamespaceRowKey(namespace: K8sNamespacesData): string {
return namespace.namespaceName || namespace.meta.k8s_namespace_name;
}
export function getK8sNamespaceItemKey(
namespace: InframonitoringtypesNamespaceRecordDTO,
): string {
return namespace.namespaceName;
export function getK8sNamespaceItemKey(namespace: K8sNamespacesData): string {
return namespace.meta.k8s_namespace_name;
}
export type NamespaceTableColumnConfig =
TableColumnDef<InframonitoringtypesNamespaceRecordDTO>;
export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
export const getK8sNamespacesListQuery = (): K8sNamespacesListPayload => ({
filters: {
items: [],
op: 'and',
},
orderBy: { columnName: 'cpu', order: 'desc' },
});
export const k8sNamespacesColumnsConfig: TableColumnDef<K8sNamespacesData>[] = [
{
id: 'namespaceGroup',
header: (): React.ReactNode => <EntityGroupHeader title="Namespace Group" />,
accessorFn: (row): string => row.namespaceName || '',
width: { min: 290 },
header: (): React.ReactNode => <EntityGroupHeader title="NAMESPACE GROUP" />,
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
width: { min: 300 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -63,7 +54,6 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
<EntityGroupHeader
title="Namespace Name"
icon={<FilePenLine data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/namespaces#namespace-name"
/>
),
accessorFn: (row): string => row.namespaceName || '',
@@ -76,85 +66,49 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return (
<CellValueTooltip value={namespaceName}>
<Tooltip title={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</CellValueTooltip>
</Tooltip>
);
},
},
{
id: 'clusterName',
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] || '',
header: 'Cluster Name',
accessorFn: (row): string => row.meta.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: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/namespaces#cpu-usage-cores">
CPU Usage (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.namespaceCPU,
width: { min: 190 },
header: 'CPU Usage (cores)',
accessorFn: (row): number => row.cpuUsage,
width: { min: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
const cpu = value as number;
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.NAMESPACES}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/namespaces#mem-usage-wss">
Memory Usage (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.namespaceMemory,
header: 'Mem Usage (WSS)',
accessorFn: (row): number => row.memoryUsage,
width: { min: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memory = Number(value);
const memory = value as number;
return (
<ValidateColumnValueWrapper
value={memory}

View File

@@ -1,23 +1,20 @@
import { useCallback } from 'react';
import { listNodes } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesNodeRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import React, { useCallback } from 'react';
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,
k8sNodeGetSelectedItemExpression,
k8sNodeInitialEventsExpression,
k8sNodeInitialLogTracesExpression,
k8sNodeGetSelectedItemFilters,
k8sNodeInitialEventsFilter,
k8sNodeInitialLogTracesFilter,
nodeWidgetInfo,
} from './constants';
import {
@@ -31,93 +28,66 @@ 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) => {
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,
);
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
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,
};
}
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,
};
},
[],
[dotMetricsEnabled],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): 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,
);
): 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,
);
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,
};
}
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
},
[],
[dotMetricsEnabled],
);
return (
<>
<K8sBaseList<InframonitoringtypesNodeRecordDTO>
<K8sBaseList<K8sNodeData>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.NODES}
tableColumns={k8sNodesColumnsConfig}
@@ -127,14 +97,14 @@ function K8sNodesList({
eventCategory={InfraMonitoringEvents.Node}
/>
<K8sBaseDetails<InframonitoringtypesNodeRecordDTO>
<K8sBaseDetails<K8sNodeData>
category={InfraMonitoringEntity.NODES}
eventCategory={InfraMonitoringEvents.Node}
getSelectedItemExpression={k8sNodeGetSelectedItemExpression}
getSelectedItemFilters={k8sNodeGetSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={k8sNodeGetEntityName}
getInitialLogTracesExpression={k8sNodeInitialLogTracesExpression}
getInitialEventsExpression={k8sNodeInitialEventsExpression}
getInitialLogTracesFilters={k8sNodeInitialLogTracesFilter}
getInitialEventsFilters={k8sNodeInitialEventsFilter}
metadataConfig={k8sNodeDetailsMetadataConfig}
entityWidgetInfo={nodeWidgetInfo}
getEntityQueryPayload={getNodeMetricsQueryPayload}

View File

@@ -0,0 +1,129 @@
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);
}
};

View File

@@ -1,43 +1,67 @@
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 { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sNodeData } from './api';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
export const k8sNodeGetSelectedItemExpression = (
export const k8sNodeGetSelectedItemFilters = (
selectedItemId: string,
): string => `k8s.node.name = ${formatValueForExpression(selectedItemId)}`;
): TagFilter => {
return {
op: 'AND',
items: [
{
id: 'k8s_node_name',
key: {
key: 'k8s_node_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
};
};
export const k8sNodeDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesNodeRecordDTO>[] =
export const k8sNodeDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sNodeData>[] =
[
{ label: 'Node Name', getValue: (p): string => p.nodeName || '' },
{ label: 'Node Name', getValue: (p): string => p.meta.k8s_node_name },
{
label: 'Cluster Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
getValue: (p): string => p.meta.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 k8sNodeInitialFilters = [
QUERY_KEYS.K8S_NODE_NAME,
QUERY_KEYS.K8S_CLUSTER_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 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 k8sNodeGetEntityName = (
item: InframonitoringtypesNodeRecordDTO,
): string => item.nodeName || '';
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 nodeWidgetInfo = [
{
@@ -83,7 +107,7 @@ export const nodeWidgetInfo = [
];
export const getNodeMetricsQueryPayload = (
node: InframonitoringtypesNodeRecordDTO,
node: K8sNodeData,
start: number,
end: number,
dotMetricsEnabled: boolean,
@@ -198,7 +222,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -237,7 +261,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -276,7 +300,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -315,7 +339,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -354,7 +378,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -427,7 +451,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -466,7 +490,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -505,7 +529,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -544,7 +568,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -583,7 +607,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -622,7 +646,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -661,7 +685,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -734,7 +758,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -773,7 +797,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -812,7 +836,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -898,7 +922,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -937,7 +961,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -976,7 +1000,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -1062,7 +1086,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -1142,7 +1166,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -1222,7 +1246,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -1308,7 +1332,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -1394,7 +1418,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -1433,7 +1457,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -1472,7 +1496,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -1545,7 +1569,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',
@@ -1584,7 +1608,7 @@ export const getNodeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: node.nodeName,
value: node.meta.k8s_node_name,
},
],
op: 'AND',

View File

@@ -1,56 +1,38 @@
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 { Tooltip } from 'antd';
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, getPodPhaseStatusItems } from '../commonUtils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
CellValueTooltip,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import { formatBytes } from '../commonUtils';
import { ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sNodeData, K8sNodesListPayload } from './api';
import { Workflow } from '@signozhq/icons';
export function getK8sNodeRowKey(
node: InframonitoringtypesNodeRecordDTO,
): string {
return (
node.nodeName || node.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NODE_UID] || ''
);
export function getK8sNodeRowKey(node: K8sNodeData): string {
return node.nodeUID || node.meta.k8s_node_uid || node.meta.k8s_node_name;
}
export function getK8sNodeItemKey(
node: InframonitoringtypesNodeRecordDTO,
): string {
return node.nodeName;
export function getK8sNodeItemKey(node: K8sNodeData): string {
return node.meta.k8s_node_name;
}
const NODE_CONDITION_COLORS: Record<string, BadgeColor> = {
ready: 'forest',
not_ready: 'amber',
no_data: 'secondary',
};
export const getK8sNodesListQuery = (): K8sNodesListPayload => ({
filters: {
items: [],
op: 'and',
},
orderBy: { columnName: 'cpu', order: 'desc' },
});
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[] = [
export const k8sNodesColumnsConfig: TableColumnDef<K8sNodeData>[] = [
{
id: 'nodeGroup',
header: (): React.ReactNode => <EntityGroupHeader title="Node Group" />,
accessorFn: (row): string => row.nodeName || '',
width: { min: 290 },
header: (): React.ReactNode => <EntityGroupHeader title="NODE GROUP" />,
accessorFn: (row): string => row.meta.k8s_node_name || '',
width: { min: 300 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -73,10 +55,9 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
<EntityGroupHeader
title="Node Name"
icon={<Workflow data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/nodes#node-name"
/>
),
accessorFn: (row): string => row.nodeName || '',
accessorFn: (row): string => row.meta.k8s_node_name || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
@@ -86,146 +67,65 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
cell: ({ value }): React.ReactNode => {
const nodeName = value as string;
return (
<CellValueTooltip value={nodeName}>
<Tooltip title={nodeName}>
<TanStackTable.Text>{nodeName}</TanStackTable.Text>
</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)} />
</Tooltip>
);
},
},
{
id: 'clusterName',
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] || '',
header: 'Cluster Name',
accessorFn: (row): string => row.meta.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: (): 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 },
header: 'CPU Usage (cores)',
accessorFn: (row): number => row.nodeCPUUsage,
width: { min: 200, default: 200 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
const cpu = value as number;
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.NODES}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'cpu_allocatable',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#cpu-alloc-cores">
CPU Allocatable
<br /> (cores)
</ColumnHeader>
),
header: 'CPU Alloc (cores)',
accessorFn: (row): number => row.nodeCPUAllocatable,
width: { min: 160, default: 190 },
width: { min: 200, default: 200 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpuAllocatable = Number(value);
const cpuAllocatable = value as number;
return (
<ValidateColumnValueWrapper
value={cpuAllocatable}
entity={InfraMonitoringEntity.NODES}
attribute="CPU allocatable metric"
>
<TanStackTable.Text>{cpuAllocatable.toFixed(2)}</TanStackTable.Text>
<TanStackTable.Text>{cpuAllocatable}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory',
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 },
header: 'Memory Usage (WSS)',
accessorFn: (row): number => row.nodeMemoryUsage,
width: { min: 240, default: 240 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
@@ -242,14 +142,9 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
},
{
id: 'memory_allocatable',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#memory-allocatable">
Memory
<br /> Allocatable
</ColumnHeader>
),
header: 'Memory Allocatable',
accessorFn: (row): number => row.nodeMemoryAllocatable,
width: { min: 200, default: 200 },
width: { min: 240, default: 240 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memoryAllocatable = value as number;

View File

@@ -1,23 +1,20 @@
import { useCallback } from 'react';
import { listPods } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesPodRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import React, { useCallback } from 'react';
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,
k8sPodGetSelectedItemExpression,
k8sPodInitialEventsExpression,
k8sPodInitialLogTracesExpression,
k8sPodGetSelectedItemFilters,
k8sPodInitialEventsFilter,
k8sPodInitialLogTracesFilter,
podWidgetInfo,
} from './constants';
import {
@@ -31,93 +28,66 @@ 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) => {
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,
);
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
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,
};
}
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,
};
},
[],
[dotMetricsEnabled],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): 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,
);
): 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,
);
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,
};
}
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
},
[],
[dotMetricsEnabled],
);
return (
<>
<K8sBaseList<InframonitoringtypesPodRecordDTO>
<K8sBaseList<K8sPodsData>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.PODS}
tableColumns={k8sPodColumnsConfig}
@@ -127,14 +97,14 @@ function K8sPodsList({
eventCategory={InfraMonitoringEvents.Pod}
/>
<K8sBaseDetails<InframonitoringtypesPodRecordDTO>
<K8sBaseDetails<K8sPodsData>
category={InfraMonitoringEntity.PODS}
eventCategory={InfraMonitoringEvents.Pod}
getSelectedItemExpression={k8sPodGetSelectedItemExpression}
getSelectedItemFilters={k8sPodGetSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={k8sPodGetEntityName}
getInitialLogTracesExpression={k8sPodInitialLogTracesExpression}
getInitialEventsExpression={k8sPodInitialEventsExpression}
getInitialLogTracesFilters={k8sPodInitialLogTracesFilter}
getInitialEventsFilters={k8sPodInitialEventsFilter}
metadataConfig={k8sPodDetailsMetadataConfig}
entityWidgetInfo={podWidgetInfo}
getEntityQueryPayload={getPodMetricsQueryPayload}

View File

@@ -0,0 +1,148 @@
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);
}
};

View File

@@ -1,59 +1,62 @@
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 { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sPodsData } from './api';
export const k8sPodGetSelectedItemExpression = (
export const k8sPodGetSelectedItemFilters = (
selectedItemId: string,
): string => `k8s.pod.uid = ${formatValueForExpression(selectedItemId)}`;
): TagFilter => {
return {
op: 'AND',
items: [
{
id: 'k8s_pod_uid',
key: {
key: 'k8s_pod_uid',
type: null,
},
op: '=',
value: selectedItemId,
},
],
};
};
export const k8sPodDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesPodRecordDTO>[] =
export const k8sPodDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sPodsData>[] =
[
{
label: 'NAMESPACE',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
},
{ label: 'NAMESPACE', getValue: (p): string => p.meta.k8s_namespace_name },
{
label: '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] || '',
getValue: (p): string => p.meta.k8s_cluster_name,
},
{ label: 'Node', getValue: (p): string => p.meta.k8s_node_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 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 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 k8sPodInitialLogTracesFilter = (
pod: K8sPodsData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_POD_NAME, pod.meta.k8s_pod_name),
];
export const k8sPodGetEntityName = (
pod: InframonitoringtypesPodRecordDTO,
): string => pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '';
export const k8sPodGetEntityName = (pod: K8sPodsData): string =>
pod.meta.k8s_pod_name;
export const podWidgetInfo = [
{
@@ -111,7 +114,7 @@ export const podWidgetInfo = [
];
export const getPodMetricsQueryPayload = (
pod: InframonitoringtypesPodRecordDTO,
pod: K8sPodsData,
start: number,
end: number,
dotMetricsEnabled: boolean,
@@ -221,15 +224,9 @@ export const getPodMetricsQueryPayload = (
'k8s_pod_filesystem_usage',
);
const k8sPodNameKey = getKey(
INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME,
'k8s_pod_name',
);
const k8sPodNameKey = getKey('k8s.pod.name', 'k8s_pod_name');
const k8sNamespaceNameKey = getKey(
INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME,
'k8s_namespace_name',
);
const k8sNamespaceNameKey = getKey('k8s.namespace.name', 'k8s_namespace_name');
return [
{
selectedTime: 'GLOBAL_TIME',
@@ -259,7 +256,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
{
id: '067b2dc4',
@@ -270,8 +267,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -310,7 +306,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
{
id: '379af416',
@@ -321,8 +317,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -361,7 +356,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
{
id: '39ee0dbd',
@@ -372,8 +367,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -446,8 +440,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '755c8a9d',
@@ -458,7 +451,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -497,8 +490,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '1e3d01ee',
@@ -509,7 +501,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -548,8 +540,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '0c8b2662',
@@ -560,7 +551,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -599,8 +590,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: 'e915da76',
@@ -611,7 +601,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -650,8 +640,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '32c15c03',
@@ -662,7 +651,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -701,8 +690,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '703fced1',
@@ -713,7 +701,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -786,8 +774,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '5418405c',
@@ -798,7 +785,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -837,8 +824,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '1ea5c602',
@@ -849,7 +835,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -888,8 +874,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: 'd7632974',
@@ -900,7 +885,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -973,8 +958,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '5a822bec',
@@ -985,7 +969,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -1024,8 +1008,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '3362c603',
@@ -1036,7 +1019,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -1075,8 +1058,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: 'ce88008b',
@@ -1087,7 +1069,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -1126,8 +1108,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '53fc92fd',
@@ -1138,7 +1119,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -1177,8 +1158,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '61dfa9f6',
@@ -1189,7 +1169,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -1228,8 +1208,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '508cdf26',
@@ -1240,7 +1219,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -1313,8 +1292,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '80528f79',
@@ -1325,7 +1303,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -1364,8 +1342,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '780cc786',
@@ -1376,7 +1353,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -1456,8 +1433,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '8b2a539b',
@@ -1468,7 +1444,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -1541,8 +1517,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '4b4382be',
@@ -1553,7 +1528,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -1633,8 +1608,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '9301d7c0',
@@ -1645,7 +1619,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -1691,8 +1665,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '92b99374',
@@ -1703,7 +1676,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -1749,8 +1722,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '1c7de95d',
@@ -1761,7 +1733,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -1854,8 +1826,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '964fd905',
@@ -1866,7 +1837,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -1912,8 +1883,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '76a586be',
@@ -1924,7 +1894,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -1970,8 +1940,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '7c5f875b',
@@ -1982,7 +1951,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -2062,8 +2031,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '80216712',
@@ -2074,7 +2042,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -2120,8 +2088,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '84b59a9f',
@@ -2132,7 +2099,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -2178,8 +2145,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: 'd649ad0c',
@@ -2190,7 +2156,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -2283,8 +2249,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: 'bc9c5cf3',
@@ -2295,7 +2260,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -2381,8 +2346,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: 'b54a3d78',
@@ -2393,7 +2357,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -2479,8 +2443,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '1a97cb95',
@@ -2491,7 +2454,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -2530,8 +2493,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: 'ddfbd379',
@@ -2542,7 +2504,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -2581,8 +2543,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
value: pod.meta.k8s_namespace_name,
},
{
id: '1963fc96',
@@ -2593,7 +2554,7 @@ export const getPodMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
value: pod.meta.k8s_pod_name,
},
],
op: 'AND',
@@ -2601,7 +2562,7 @@ export const getPodMetricsQueryPayload = (
functions: [],
groupBy: [],
having: [],
legend: 'Usage',
legend: 'Uage',
limit: null,
orderBy: [],
queryName: 'C',

View File

@@ -1,63 +1,30 @@
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 { Tooltip } from 'antd';
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, 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';
import { formatBytes } from '../commonUtils';
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sPodsData } from './api';
import { Container } from '@signozhq/icons';
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 getK8sPodRowKey(pod: K8sPodsData): string {
return pod.podUID || pod.meta.k8s_pod_uid || pod.meta.k8s_pod_name;
}
export function getK8sPodItemKey(
pod: InframonitoringtypesPodRecordDTO,
): string {
export function getK8sPodItemKey(pod: K8sPodsData): string {
return pod.podUID;
}
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[] = [
export const k8sPodColumnsConfig: TableColumnDef<K8sPodsData>[] = [
{
id: 'podGroup',
header: (): React.ReactNode => <EntityGroupHeader title="Pod Group" />,
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
width: { min: 290 },
header: (): React.ReactNode => <EntityGroupHeader title="POD GROUP" />,
accessorFn: (row): string => row.meta.k8s_pod_name || '',
width: { min: 300 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -80,11 +47,9 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
<EntityGroupHeader
title="Pod Name"
icon={<Container data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/pods#pod-name"
/>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
accessorFn: (row): string => row.meta.k8s_pod_name || '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
@@ -94,92 +59,18 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
cell: ({ value }): React.ReactNode => {
const podName = value as string;
return (
<CellValueTooltip value={podName}>
<Tooltip title={podName}>
<TanStackTable.Text>{podName}</TanStackTable.Text>
</CellValueTooltip>
</Tooltip>
);
},
},
{
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: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#cpu-req-usage-">
CPU Request
<br /> Usage (%)
</ColumnHeader>
),
header: 'CPU Req Usage (%)',
accessorFn: (row): number => row.podCPURequest,
width: { min: 210 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const cpuRequest = value as number;
return (
@@ -195,14 +86,9 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
},
{
id: 'cpu_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#cpu-limit-usage-">
CPU Limit
<br /> Usage (%)
</ColumnHeader>
),
header: 'CPU Limit Usage (%)',
accessorFn: (row): number => row.podCPULimit,
width: { min: 220 },
width: { min: 210 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpuLimit = value as number;
@@ -219,40 +105,29 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
},
{
id: 'cpu',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#cpu-usage-cores">
CPU Usage
<br /> (cores)
</ColumnHeader>
),
header: 'CPU Usage (cores)',
accessorFn: (row): number => row.podCPU,
width: { min: 160 },
width: { min: 210 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
const cpu = value as number;
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.PODS}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#mem-req-usage-">
Memory Request
<br /> Usage (%)
</ColumnHeader>
),
header: 'Mem Req Usage (%)',
accessorFn: (row): number => row.podMemoryRequest,
width: { min: 210 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const memoryRequest = value as number;
return (
@@ -268,14 +143,9 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
},
{
id: 'memory_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#mem-limit-usage-">
Memory Limit
<br /> Usage (%)
</ColumnHeader>
),
header: 'Mem Limit Usage (%)',
accessorFn: (row): number => row.podMemoryLimit,
width: { min: 220 },
width: { min: 210 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memoryLimit = value as number;
@@ -292,12 +162,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
},
{
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#mem-usage-wss">
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
header: 'Mem Usage (WSS)',
accessorFn: (row): number => row.podMemory,
width: { min: 210, default: '100%' },
enableSort: true,
@@ -317,8 +182,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
{
id: 'namespace',
header: 'Namespace',
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
width: { default: 100 },
enableSort: false,
defaultVisibility: false,
@@ -329,8 +193,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
{
id: 'node',
header: 'Node',
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME] || '',
accessorFn: (row): string => row.meta.k8s_node_name || '',
width: { default: 100 },
enableSort: false,
defaultVisibility: false,
@@ -341,8 +204,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
{
id: 'cluster',
header: 'Cluster',
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
accessorFn: (row): string => row.meta.k8s_cluster_name || '',
width: { default: 100 },
enableSort: false,
defaultVisibility: false,

View File

@@ -1,23 +1,20 @@
import { useCallback } from 'react';
import { listStatefulSets } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesResponseTypeDTO,
InframonitoringtypesStatefulSetRecordDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import React, { useCallback } from 'react';
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,
k8sStatefulSetGetSelectedItemExpression,
k8sStatefulSetInitialEventsExpression,
k8sStatefulSetInitialLogTracesExpression,
k8sStatefulSetGetSelectedItemFilters,
k8sStatefulSetInitialEventsFilter,
k8sStatefulSetInitialLogTracesFilter,
statefulSetWidgetInfo,
} from './constants';
import {
@@ -31,93 +28,66 @@ 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) => {
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,
);
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
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,
};
}
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,
};
},
[],
[dotMetricsEnabled],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): 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,
);
): 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,
);
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,
};
}
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
},
[],
[dotMetricsEnabled],
);
return (
<>
<K8sBaseList<InframonitoringtypesStatefulSetRecordDTO>
<K8sBaseList<K8sStatefulSetsData>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.STATEFULSETS}
tableColumns={k8sStatefulSetsColumnsConfig}
@@ -127,14 +97,14 @@ function K8sStatefulSetsList({
eventCategory={InfraMonitoringEvents.StatefulSet}
/>
<K8sBaseDetails<InframonitoringtypesStatefulSetRecordDTO>
<K8sBaseDetails<K8sStatefulSetsData>
category={InfraMonitoringEntity.STATEFULSETS}
eventCategory={InfraMonitoringEvents.StatefulSet}
getSelectedItemExpression={k8sStatefulSetGetSelectedItemExpression}
getSelectedItemFilters={k8sStatefulSetGetSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={k8sStatefulSetGetEntityName}
getInitialLogTracesExpression={k8sStatefulSetInitialLogTracesExpression}
getInitialEventsExpression={k8sStatefulSetInitialEventsExpression}
getInitialLogTracesFilters={k8sStatefulSetInitialLogTracesFilter}
getInitialEventsFilters={k8sStatefulSetInitialEventsFilter}
metadataConfig={k8sStatefulSetDetailsMetadataConfig}
entityWidgetInfo={statefulSetWidgetInfo}
getEntityQueryPayload={getStatefulSetMetricsQueryPayload}

View File

@@ -0,0 +1,127 @@
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);
}
};

View File

@@ -1,58 +1,67 @@
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 { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sStatefulSetsData } from './api';
export const k8sStatefulSetGetSelectedItemExpression = (
export const k8sStatefulSetGetSelectedItemFilters = (
selectedItemId: string,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME} = ${formatValueForExpression(selectedItemId)}`;
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_statefulset_name',
key: {
key: 'k8s_statefulset_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
export const k8sStatefulSetDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesStatefulSetRecordDTO>[] =
export const k8sStatefulSetDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sStatefulSetsData>[] =
[
{
label: 'Statefulset Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ?? '',
getValue: (p): string => p.meta.k8s_statefulset_name,
},
{
label: 'Namespace Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
getValue: (p): string => p.meta.k8s_namespace_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 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 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 k8sStatefulSetInitialLogTracesFilter = (
item: K8sStatefulSetsData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(
QUERY_KEYS.K8S_STATEFUL_SET_NAME,
item.meta.k8s_statefulset_name,
),
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.k8s_namespace_name),
];
export const k8sStatefulSetGetEntityName = (
item: InframonitoringtypesStatefulSetRecordDTO,
): string => item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ?? '';
item: K8sStatefulSetsData,
): string => item.meta.k8s_statefulset_name;
export const statefulSetWidgetInfo = [
{
@@ -82,16 +91,16 @@ export const statefulSetWidgetInfo = [
];
export const getStatefulSetMetricsQueryPayload = (
statefulSet: InframonitoringtypesStatefulSetRecordDTO,
statefulSet: K8sStatefulSetsData,
start: number,
end: number,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] => {
const k8sStatefulSetNameKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
? 'k8s.statefulset.name'
: 'k8s_statefulset_name';
const k8sNamespaceNameKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME
? 'k8s.namespace.name'
: 'k8s_namespace_name';
const k8sPodNameKey = dotMetricsEnabled ? 'k8s.pod.name' : 'k8s_pod_name';
@@ -163,10 +172,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
value: statefulSet.meta.k8s_statefulset_name,
},
{
id: 'f2',
@@ -177,9 +183,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: statefulSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -218,10 +222,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
value: statefulSet.meta.k8s_statefulset_name,
},
{
id: 'f2',
@@ -232,9 +233,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: statefulSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -273,10 +272,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
value: statefulSet.meta.k8s_statefulset_name,
},
{
id: 'f2',
@@ -287,9 +283,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: statefulSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -348,10 +342,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
value: statefulSet.meta.k8s_statefulset_name,
},
{
id: 'f2',
@@ -362,9 +353,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: statefulSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -403,10 +392,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
value: statefulSet.meta.k8s_statefulset_name,
},
{
id: 'f2',
@@ -417,9 +403,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: statefulSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -478,10 +462,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
value: statefulSet.meta.k8s_statefulset_name,
},
{
id: 'f2',
@@ -492,9 +473,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: statefulSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -533,10 +512,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
value: statefulSet.meta.k8s_statefulset_name,
},
{
id: 'f2',
@@ -547,9 +523,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: statefulSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -588,10 +562,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: 'contains',
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
value: statefulSet.meta.k8s_statefulset_name,
},
{
id: 'f2',
@@ -602,9 +573,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: statefulSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -663,10 +632,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
value: statefulSet.meta.k8s_statefulset_name,
},
{
id: 'f2',
@@ -677,9 +643,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: statefulSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -718,10 +682,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
value: statefulSet.meta.k8s_statefulset_name,
},
{
id: 'f2',
@@ -732,9 +693,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: statefulSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -793,10 +752,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
value: statefulSet.meta.k8s_statefulset_name,
},
{
id: 'f2',
@@ -807,9 +763,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: statefulSet.meta.k8s_namespace_name,
},
],
op: 'AND',
@@ -881,10 +835,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
value: statefulSet.meta.k8s_statefulset_name,
},
{
id: 'f2',
@@ -895,9 +846,7 @@ export const getStatefulSetMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
value: statefulSet.meta.k8s_namespace_name,
},
],
op: 'AND',

View File

@@ -1,52 +1,39 @@
import { Color } from '@signozhq/design-tokens';
import { InframonitoringtypesStatefulSetRecordDTO } from 'api/generated/services/sigNoz.schemas';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { Tooltip } from 'antd';
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, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
GroupedStatusCounts,
ValidateColumnValueWrapper,
} from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { formatBytes } from '../commonUtils';
import { EntityProgressBar, ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sStatefulSetsData } from './api';
import { ArrowUpDown } from '@signozhq/icons';
export function getK8sStatefulSetRowKey(
statefulSet: InframonitoringtypesStatefulSetRecordDTO,
statefulSet: K8sStatefulSetsData,
): string {
return (
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ||
statefulSet.statefulSetName ||
''
statefulSet.statefulSetName || statefulSet.meta.k8s_statefulset_name || ''
);
}
export function getK8sStatefulSetItemKey(
statefulSet: InframonitoringtypesStatefulSetRecordDTO,
statefulSet: K8sStatefulSetsData,
): string {
return (
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] || ''
);
return statefulSet.meta.k8s_statefulset_name;
}
export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesStatefulSetRecordDTO>[] =
export const k8sStatefulSetsColumnsConfig: TableColumnDef<K8sStatefulSetsData>[] =
[
{
id: 'statefulSetGroup',
header: (): React.ReactNode => (
<EntityGroupHeader title="StatefulSet Group" />
<EntityGroupHeader title="STATEFULSET GROUP" />
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] || '',
width: { min: 290 },
accessorFn: (row): string => row.meta.k8s_statefulset_name || '',
width: { min: 210 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -69,12 +56,10 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
<EntityGroupHeader
title="StatefulSet Name"
icon={<ArrowUpDown data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/statefulsets#statefulset-name"
/>
),
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] || '',
width: { min: 290 },
accessorFn: (row): string => row.meta.k8s_statefulset_name || '',
width: { min: 200 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -83,98 +68,75 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
cell: ({ value }): React.ReactNode => {
const statefulsetName = value as string;
return (
<CellValueTooltip value={statefulsetName}>
<Tooltip title={statefulsetName}>
<TanStackTable.Text>{statefulsetName}</TanStackTable.Text>
</CellValueTooltip>
</Tooltip>
);
},
},
{
id: 'namespaceName',
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 },
header: 'Namespace Name',
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
width: { default: 150 },
enableSort: false,
enableResize: true,
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return (
<CellValueTooltip value={namespaceName}>
<Tooltip title={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</CellValueTooltip>
</Tooltip>
);
},
},
{
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,
id: 'available_pods',
header: 'Available',
accessorFn: (row): number => row.availablePods,
width: { min: 100, default: 140 },
enableSort: true,
enableResize: true,
cell: ({ row }): React.ReactNode => {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
cell: ({ value }): React.ReactNode => {
const availablePods = value as number;
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
<ValidateColumnValueWrapper
value={availablePods}
entity={InfraMonitoringEntity.STATEFULSETS}
attribute="available pod"
>
<TanStackTable.Text>{availablePods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
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,
id: 'desired_pods',
header: 'Desired',
accessorFn: (row): number => row.desiredPods,
width: { min: 100, default: 140 },
enableSort: true,
enableResize: true,
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,
},
]}
/>
),
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>
);
},
},
{
id: 'cpu_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-req-usage-">
CPU Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.statefulSetCPURequest,
header: 'CPU Req Usage (%)',
accessorFn: (row): number => row.cpuRequest,
width: { min: 200, default: 200 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const cpuRequest = value as number;
return (
@@ -190,13 +152,8 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
},
{
id: 'cpu_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-limit-usage-">
CPU Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.statefulSetCPULimit,
header: 'CPU Limit Usage (%)',
accessorFn: (row): number => row.cpuLimit,
width: { min: 200, default: 200 },
enableSort: true,
enableResize: true,
@@ -215,43 +172,32 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
},
{
id: 'cpu',
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 },
header: 'CPU Usage (cores)',
accessorFn: (row): number => row.cpuUsage,
width: { min: 190 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
const cpu = value as number;
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.STATEFULSETS}
attribute="CPU metric"
>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
<TanStackTable.Text>{cpu}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'memory_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-req-usage-">
Memory Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.statefulSetMemoryRequest,
header: 'Mem Req Usage (%)',
accessorFn: (row): number => row.memoryRequest,
width: { min: 190 },
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const memoryRequest = value as number;
return (
@@ -267,13 +213,8 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
},
{
id: 'memory_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-limit-usage-">
Memory Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.statefulSetMemoryLimit,
header: 'Mem Limit Usage (%)',
accessorFn: (row): number => row.memoryLimit,
width: { min: 180 },
enableSort: true,
enableResize: true,
@@ -292,13 +233,8 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
},
{
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-usage-wss">
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.statefulSetMemory,
header: 'Mem Usage (WSS)',
accessorFn: (row): number => row.memoryUsage,
width: { min: 160 },
enableSort: true,
enableResize: true,
@@ -316,52 +252,4 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
);
},
},
{
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>
);
},
},
];

View File

@@ -1,23 +1,20 @@
import { useCallback } from 'react';
import { listVolumes } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesResponseTypeDTO,
InframonitoringtypesVolumeRecordDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import React, { useCallback } from 'react';
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,
k8sVolumeGetSelectedItemExpression,
k8sVolumeInitialEventsExpression,
k8sVolumeInitialLogTracesExpression,
k8sVolumeGetSelectedItemFilters,
k8sVolumeInitialEventsFilter,
k8sVolumeInitialLogTracesFilter,
volumeWidgetInfo,
} from './constants';
import {
@@ -31,93 +28,66 @@ 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) => {
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,
);
filters.orderBy ||= {
columnName: 'usage',
order: 'desc',
};
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,
};
}
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,
};
},
[],
[dotMetricsEnabled],
);
const fetchEntityData = useCallback(
async (
filters: K8sDetailsFilters,
signal?: AbortSignal,
): 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,
);
): 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,
);
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,
};
}
const records = response.payload?.data.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
},
[],
[dotMetricsEnabled],
);
return (
<>
<K8sBaseList<InframonitoringtypesVolumeRecordDTO>
<K8sBaseList<K8sVolumesData>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.VOLUMES}
tableColumns={k8sVolumesColumnsConfig}
@@ -127,14 +97,14 @@ function K8sVolumesList({
eventCategory={InfraMonitoringEvents.Volumes}
/>
<K8sBaseDetails<InframonitoringtypesVolumeRecordDTO>
<K8sBaseDetails<K8sVolumesData>
category={InfraMonitoringEntity.VOLUMES}
eventCategory={InfraMonitoringEvents.Volume}
getSelectedItemExpression={k8sVolumeGetSelectedItemExpression}
getSelectedItemFilters={k8sVolumeGetSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={k8sVolumeGetEntityName}
getInitialLogTracesExpression={k8sVolumeInitialLogTracesExpression}
getInitialEventsExpression={k8sVolumeInitialEventsExpression}
getInitialLogTracesFilters={k8sVolumeInitialLogTracesFilter}
getInitialEventsFilters={k8sVolumeInitialEventsFilter}
metadataConfig={k8sVolumeDetailsMetadataConfig}
entityWidgetInfo={volumeWidgetInfo}
getEntityQueryPayload={getVolumeMetricsQueryPayload}

View File

@@ -0,0 +1,136 @@
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);
}
};

View File

@@ -1,61 +1,70 @@
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 { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import {
createFilterItem,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { QUERY_KEYS } from '../EntityDetailsUtils/utils';
import { K8sVolumesData } from './api';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
export const k8sVolumeGetSelectedItemExpression = (
export const k8sVolumeGetSelectedItemFilters = (
selectedItemId: string,
): string =>
`${INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME} = ${formatValueForExpression(selectedItemId)}`;
): TagFilter => ({
op: 'AND',
items: [
{
id: 'k8s_persistentvolumeclaim_name',
key: {
key: 'k8s_persistentvolumeclaim_name',
type: null,
},
op: '=',
value: selectedItemId,
},
],
});
export const k8sVolumeDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesVolumeRecordDTO>[] =
export const k8sVolumeDetailsMetadataConfig: K8sDetailsMetadataConfig<K8sVolumesData>[] =
[
{
label: 'PVC Name',
getValue: (p): string => p.persistentVolumeClaimName || '',
getValue: (p): string => p.persistentVolumeClaimName,
},
{
label: 'Cluster Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
getValue: (p): string => p.meta.k8s_cluster_name,
},
{
label: 'Namespace Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
getValue: (p): string => p.meta.k8s_namespace_name,
},
];
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 k8sVolumeInitialEventsFilter = (
item: K8sVolumesData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(QUERY_KEYS.K8S_OBJECT_KIND, 'PersistentVolumeClaim'),
createFilterItem(QUERY_KEYS.K8S_OBJECT_NAME, item.persistentVolumeClaimName),
];
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 k8sVolumeInitialLogTracesFilter = (
item: K8sVolumesData,
): ReturnType<typeof createFilterItem>[] => [
createFilterItem(
QUERY_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME,
item.meta.k8s_persistentvolumeclaim_name,
),
createFilterItem(QUERY_KEYS.K8S_NAMESPACE_NAME, item.meta.k8s_namespace_name),
];
export const k8sVolumeGetEntityName = (
item: InframonitoringtypesVolumeRecordDTO,
): string => item.persistentVolumeClaimName || '';
export const k8sVolumeGetEntityName = (item: K8sVolumesData): string =>
item.persistentVolumeClaimName;
export const volumeWidgetInfo = [
{
@@ -81,37 +90,37 @@ export const volumeWidgetInfo = [
];
export const getVolumeMetricsQueryPayload = (
volume: InframonitoringtypesVolumeRecordDTO,
volume: K8sVolumesData,
start: number,
end: number,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] => {
const k8sClusterNameKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME
? 'k8s.cluster.name'
: 'k8s_cluster_name';
const k8sNamespaceNameKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME
? 'k8s.namespace.name'
: 'k8s_namespace_name';
const k8sVolumeAvailableKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_AVAILABLE
? 'k8s.volume.available'
: 'k8s_volume_available';
const k8sVolumeCapacityKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_CAPACITY
? 'k8s.volume.capacity'
: 'k8s_volume_capacity';
const k8sVolumeInodesUsedKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_INODES_USED
? 'k8s.volume.inodes.used'
: 'k8s_volume_inodes_used';
const k8sVolumeInodesKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_INODES
? 'k8s.volume.inodes'
: 'k8s_volume_inodes';
const k8sVolumeInodesFreeKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_INODES_FREE
? 'k8s.volume.inodes.free'
: 'k8s_volume_inodes_free';
const k8sVolumeTypeKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_TYPE
? 'k8s.volume.type'
: 'k8s_volume_type';
const k8sPVCNameKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME
? 'k8s.persistentvolumeclaim.name'
: 'k8s_persistentvolumeclaim_name';
return [
@@ -143,8 +152,7 @@ export const getVolumeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
value: volume.meta.k8s_cluster_name,
},
{
id: 'c2',
@@ -155,9 +163,7 @@ export const getVolumeMetricsQueryPayload = (
type: 'tag',
},
op: 'in',
value: [
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
],
value: [volume.meta.k8s_namespace_name],
},
{
id: 'c3',
@@ -238,8 +244,7 @@ export const getVolumeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
value: volume.meta.k8s_cluster_name,
},
{
id: 'c2',
@@ -250,9 +255,7 @@ export const getVolumeMetricsQueryPayload = (
type: 'tag',
},
op: 'in',
value: [
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
],
value: [volume.meta.k8s_namespace_name],
},
{
id: 'c3',
@@ -333,8 +336,7 @@ export const getVolumeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
value: volume.meta.k8s_cluster_name,
},
{
id: 'c2',
@@ -345,9 +347,7 @@ export const getVolumeMetricsQueryPayload = (
type: 'tag',
},
op: 'in',
value: [
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
],
value: [volume.meta.k8s_namespace_name],
},
{
id: 'c3',
@@ -428,8 +428,7 @@ export const getVolumeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
value: volume.meta.k8s_cluster_name,
},
{
id: 'c2',
@@ -440,9 +439,7 @@ export const getVolumeMetricsQueryPayload = (
type: 'tag',
},
op: 'in',
value: [
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
],
value: [volume.meta.k8s_namespace_name],
},
{
id: 'c3',
@@ -523,8 +520,7 @@ export const getVolumeMetricsQueryPayload = (
type: 'tag',
},
op: '=',
value:
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
value: volume.meta.k8s_cluster_name,
},
{
id: 'c2',
@@ -535,9 +531,7 @@ export const getVolumeMetricsQueryPayload = (
type: 'tag',
},
op: 'in',
value: [
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
],
value: [volume.meta.k8s_namespace_name],
},
{
id: 'c3',

View File

@@ -1,43 +1,34 @@
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 { CellValueTooltip, ValidateColumnValueWrapper } from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { ValidateColumnValueWrapper } from '../components';
import { InfraMonitoringEntity } from '../constants';
import { K8sVolumesData } from './api';
import { HardDrive } from '@signozhq/icons';
export function getK8sVolumeRowKey(
volume: InframonitoringtypesVolumeRecordDTO,
): string {
export function getK8sVolumeRowKey(volume: K8sVolumesData): string {
return (
volume.persistentVolumeClaimName ||
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME] ||
volume.meta.k8s_persistentvolumeclaim_name ||
''
);
}
export function getK8sVolumeItemKey(
volume: InframonitoringtypesVolumeRecordDTO,
): string {
export function getK8sVolumeItemKey(volume: K8sVolumesData): string {
return volume.persistentVolumeClaimName;
}
export type VolumeTableColumnConfig =
TableColumnDef<InframonitoringtypesVolumeRecordDTO>;
export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
export const k8sVolumesColumnsConfig: TableColumnDef<K8sVolumesData>[] = [
{
id: 'volumeGroup',
header: (): React.ReactNode => <EntityGroupHeader title="Volume Group" />,
header: (): React.ReactNode => <EntityGroupHeader title="VOLUME GROUP" />,
accessorFn: (row): string => row.persistentVolumeClaimName || '',
width: { min: 290 },
width: { min: 300 },
enableSort: false,
enableRemove: false,
enableMove: false,
@@ -60,7 +51,6 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
<EntityGroupHeader
title="PVC Name"
icon={<HardDrive data-hide-expanded="true" size={14} />}
docPath="/infrastructure-monitoring/kubernetes/volumes#pvc-name"
/>
),
accessorFn: (row): string => row.persistentVolumeClaimName || '',
@@ -73,39 +63,30 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
cell: ({ value }): React.ReactNode => {
const pvcName = value as string;
return (
<CellValueTooltip value={pvcName}>
<Tooltip title={pvcName}>
<TanStackTable.Text>{pvcName}</TanStackTable.Text>
</CellValueTooltip>
</Tooltip>
);
},
},
{
id: 'namespaceName',
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] || '',
header: 'Namespace Name',
accessorFn: (row): string => row.meta.k8s_namespace_name || '',
width: { min: 220 },
enableSort: false,
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return (
<CellValueTooltip value={namespaceName}>
<Tooltip title={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</CellValueTooltip>
</Tooltip>
);
},
},
{
id: 'capacity',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-capacity">
Capacity
</ColumnHeader>
),
header: 'Capacity',
accessorFn: (row): number => row.volumeCapacity,
width: { min: 140 },
enableSort: true,
@@ -124,11 +105,7 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
},
{
id: 'usage',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-used">
Used
</ColumnHeader>
),
header: 'Used',
accessorFn: (row): number => row.volumeUsage,
width: { min: 140 },
enableSort: true,
@@ -147,11 +124,7 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
},
{
id: 'available',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-available">
Available
</ColumnHeader>
),
header: 'Available',
accessorFn: (row): number => row.volumeAvailable,
width: { min: 140 },
enableSort: true,
@@ -170,11 +143,7 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
},
{
id: 'inodes',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-inodes">
Inodes
</ColumnHeader>
),
header: 'Inodes',
accessorFn: (row): number => row.volumeInodes,
width: { min: 140 },
enableSort: true,
@@ -193,11 +162,7 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
},
{
id: 'inodesUsed',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-inodes-used">
Inodes Used
</ColumnHeader>
),
header: 'Inodes Used',
accessorFn: (row): number => row.volumeInodesUsed,
width: { min: 160 },
enableSort: true,
@@ -216,11 +181,7 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
},
{
id: 'inodesFree',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/volumes#volume-inodes-free">
Inodes Free
</ColumnHeader>
),
header: 'Inodes Free',
accessorFn: (row): number => row.volumeInodesFree,
width: { min: 160 },
enableSort: true,

View File

@@ -1,7 +1,4 @@
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
@@ -63,18 +60,3 @@ 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 },
];
}

View File

@@ -1,54 +0,0 @@
.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);
}

View File

@@ -1,100 +0,0 @@
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>
);
}

View File

@@ -1,8 +0,0 @@
export {
CellValueTooltip,
type CellValueTooltipProps,
} from './CellValueTooltip';
export {
useInfraMonitoringCellActionsStore,
useInfraMonitoringLineClamp,
} from './useInfraMonitoringCellActionsStore';

View File

@@ -1,39 +0,0 @@
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);

View File

@@ -12,7 +12,7 @@ import {
import styles from './EntityProgressBar.module.scss';
type EntityProgressBarType = 'request' | 'limit' | 'cpu' | 'memory' | 'disk';
type EntityProgressBarType = 'request' | 'limit' | 'cpu' | 'memory';
function getStrokeColor(type: EntityProgressBarType, value: number): string {
switch (type) {
@@ -24,8 +24,6 @@ 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);
}
@@ -38,8 +36,9 @@ export function EntityProgressBar({
value: number;
type: EntityProgressBarType;
}): JSX.Element {
const isNoData = value === -1 || Number.isNaN(+value);
const percentage = isNoData ? null : Number((value * 100).toFixed(1));
const percentage = Number.isNaN(+value)
? null
: Number((value * 100).toFixed(1));
if (percentage === null) {
return (

View File

@@ -1,25 +0,0 @@
.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;
}

View File

@@ -1,47 +0,0 @@
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>
);
}

View File

@@ -17,7 +17,7 @@ export function ValidateColumnValueWrapper({
entity?: InfraMonitoringEntity;
attribute?: string;
}): JSX.Element {
if (value === -1 || Number.isNaN(value)) {
if (value === -1) {
let element = <TanStackTable.Text>-</TanStackTable.Text>;
if (entity && attribute) {
element = (

View File

@@ -1,12 +1,3 @@
export { EntityProgressBar } from './EntityProgressBar';
export { ValidateColumnValueWrapper } from './ValidateColumnValueWrapper';
export { ExpandButtonWrapper } from './ExpandButtonWrapper';
export {
GroupedStatusCounts,
type StatusCountItem,
} from './GroupedStatusCounts';
export {
CellValueTooltip,
useInfraMonitoringCellActionsStore,
useInfraMonitoringLineClamp,
} from './CellValueTooltip';

View File

@@ -5,108 +5,6 @@ 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 {
@@ -153,7 +51,7 @@ export const K8sCategories = {
VOLUMES: 'volumes',
};
const underscoreMap = {
export const underscoreMap = {
[InfraMonitoringEntity.HOSTS]: 'system_cpu_load_average_15m',
[InfraMonitoringEntity.PODS]: 'k8s_pod_cpu_usage',
[InfraMonitoringEntity.NODES]: 'k8s_node_cpu_usage',
@@ -167,7 +65,7 @@ const underscoreMap = {
[InfraMonitoringEntity.VOLUMES]: 'k8s_volume_capacity',
};
const dotMap = {
export const dotMap = {
[InfraMonitoringEntity.HOSTS]: 'system.cpu.load_average.15m',
[InfraMonitoringEntity.PODS]: 'k8s.pod.cpu.usage',
[InfraMonitoringEntity.NODES]: 'k8s.node.cpu.usage',
@@ -854,63 +752,19 @@ 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.',
};

View File

@@ -1,5 +1,4 @@
import {
createParser,
Options,
parseAsInteger,
parseAsJson,
@@ -7,6 +6,7 @@ import {
useQueryState,
UseQueryStateReturn,
} from 'nuqs';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
TagFilter,
@@ -20,19 +20,35 @@ 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 | undefined
number
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE,
parseAsInteger.withOptions(defaultNuqsOptions),
parseAsInteger.withDefault(1).withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringPageSizeListing = (): UseQueryStateReturn<
number,
number
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE_SIZE,
parseAsInteger.withDefault(10).withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringOrderBy = (): UseQueryStateReturn<
@@ -44,40 +60,15 @@ 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<
string[],
BaseAutocompleteData[],
[]
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY,
parseAsGroupBy.withDefault([]).withOptions(defaultNuqsOptions),
parseAsJsonNoValidate<IBuilderQuery['groupBy']>()
.withDefault([])
.withOptions(defaultNuqsOptions),
);
export const useInfraMonitoringView = (): UseQueryStateReturn<string, string> =>
@@ -128,6 +119,15 @@ 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,12 +137,3 @@ export const useInfraMonitoringSelectedItem = (): UseQueryStateReturn<
parseAsString,
);
};
export const useInfraMonitoringStatusFilter = (): UseQueryStateReturn<
string,
string
> =>
useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.STATUS_FILTER,
parseAsString.withDefault('').withOptions(defaultNuqsOptions),
);

Some files were not shown because too many files have changed in this diff Show More