Compare commits

..

2 Commits

Author SHA1 Message Date
Srikanth Chekuri
e6356b3b8c Merge branch 'main' into anomaly-v2 2026-07-14 08:07:28 +05:30
srikanthccv
90512748fb chore: add new alert experience support for anomaly alerts 2026-07-11 23:08:23 +05:30
347 changed files with 3670 additions and 15996 deletions

View File

@@ -1,6 +1,6 @@
services:
init-clickhouse:
image: clickhouse/clickhouse-server:25.12.5
image: clickhouse/clickhouse-server:25.5.6
container_name: init-clickhouse
command:
- bash
@@ -18,7 +18,7 @@ services:
volumes:
- ${PWD}/fs/tmp/var/lib/clickhouse/user_scripts/:/var/lib/clickhouse/user_scripts/
clickhouse:
image: clickhouse/clickhouse-server:25.12.5
image: clickhouse/clickhouse-server:25.5.6
container_name: clickhouse
volumes:
- ${PWD}/fs/etc/clickhouse-server/config.d/config.xml:/etc/clickhouse-server/config.d/config.xml
@@ -67,7 +67,7 @@ services:
timeout: 5s
retries: 3
telemetrystore-migrator:
image: signoz/signoz-otel-collector:v0.144.6
image: signoz/signoz-otel-collector:v0.142.0
container_name: telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000

View File

@@ -53,7 +53,6 @@ jobs:
- queriermetrics
- querierscalar
- queriercommon
- querierai
- rawexportdata
- role
- rootuser
@@ -61,17 +60,16 @@ jobs:
- querier_json_body
- querier_skip_resource_fingerprint
- ttl
- clickhousecluster
- metricreduction
sqlstore-provider:
- postgres
- sqlite
sqlite-mode:
- wal
clickhouse-version:
- 25.5.6
- 25.12.5
schema-migrator-version:
- v0.144.6
- v0.144.3
postgres-version:
- 15
if: |

View File

@@ -4259,34 +4259,6 @@ components:
type: number
clusterName:
type: string
counts:
properties:
daemonSets:
format: int64
type: integer
deployments:
format: int64
type: integer
jobs:
format: int64
type: integer
namespaces:
format: int64
type: integer
nodes:
format: int64
type: integer
statefulSets:
format: int64
type: integer
required:
- nodes
- namespaces
- deployments
- daemonSets
- jobs
- statefulSets
type: object
meta:
additionalProperties:
type: string
@@ -4307,7 +4279,6 @@ components:
- nodeCountsByReadiness
- podCountsByPhase
- podCountsByStatus
- counts
- meta
type: object
InframonitoringtypesClusters:
@@ -4829,26 +4800,6 @@ components:
type: object
InframonitoringtypesNamespaceRecord:
properties:
counts:
properties:
daemonSets:
format: int64
type: integer
deployments:
format: int64
type: integer
jobs:
format: int64
type: integer
statefulSets:
format: int64
type: integer
required:
- deployments
- daemonSets
- jobs
- statefulSets
type: object
meta:
additionalProperties:
type: string
@@ -4872,7 +4823,6 @@ components:
- namespaceMemory
- podCountsByPhase
- podCountsByStatus
- counts
- meta
type: object
InframonitoringtypesNamespaces:
@@ -8048,15 +7998,6 @@ components:
required:
- items
type: object
SpantypesGettableSpanMappers:
properties:
items:
items:
$ref: '#/components/schemas/SpantypesSpanMapper'
type: array
required:
- items
type: object
SpantypesGettableTraceAggregations:
properties:
aggregations:
@@ -8209,7 +8150,7 @@ components:
type: boolean
fieldContext:
$ref: '#/components/schemas/SpantypesFieldContext'
groupId:
group_id:
type: string
id:
type: string
@@ -8222,7 +8163,7 @@ components:
type: string
required:
- id
- groupId
- group_id
- name
- fieldContext
- config
@@ -8565,7 +8506,6 @@ components:
TelemetrytypesSource:
enum:
- meter
- ai
type: string
TelemetrytypesTelemetryFieldKey:
properties:
@@ -13802,7 +13742,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/SpantypesGettableSpanMappers'
$ref: '#/components/schemas/SpantypesGettableSpanMapperGroups'
status:
type: string
required:

View File

@@ -3631,7 +3631,6 @@ export enum Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQue
}
export enum TelemetrytypesSourceDTO {
meter = 'meter',
ai = 'ai',
}
export interface Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5LogAggregationDTO {
/**
@@ -5670,39 +5669,6 @@ export interface InframonitoringtypesChecksDTO {
type: InframonitoringtypesCheckTypeDTO;
}
export type InframonitoringtypesClusterRecordDTOCounts = {
/**
* @type integer
* @format int64
*/
daemonSets: number;
/**
* @type integer
* @format int64
*/
deployments: number;
/**
* @type integer
* @format int64
*/
jobs: number;
/**
* @type integer
* @format int64
*/
namespaces: number;
/**
* @type integer
* @format int64
*/
nodes: number;
/**
* @type integer
* @format int64
*/
statefulSets: number;
};
export type InframonitoringtypesClusterRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -5847,10 +5813,6 @@ export interface InframonitoringtypesClusterRecordDTO {
* @type string
*/
clusterName: string;
/**
* @type object
*/
counts: InframonitoringtypesClusterRecordDTOCounts;
/**
* @type object,null
*/
@@ -6406,29 +6368,6 @@ export interface InframonitoringtypesJobsDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export type InframonitoringtypesNamespaceRecordDTOCounts = {
/**
* @type integer
* @format int64
*/
daemonSets: number;
/**
* @type integer
* @format int64
*/
deployments: number;
/**
* @type integer
* @format int64
*/
jobs: number;
/**
* @type integer
* @format int64
*/
statefulSets: number;
};
export type InframonitoringtypesNamespaceRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6440,10 +6379,6 @@ export type InframonitoringtypesNamespaceRecordDTOMeta =
InframonitoringtypesNamespaceRecordDTOMetaAnyOf | null;
export interface InframonitoringtypesNamespaceRecordDTO {
/**
* @type object
*/
counts: InframonitoringtypesNamespaceRecordDTOCounts;
/**
* @type object,null
*/
@@ -9259,76 +9194,6 @@ export interface SpantypesGettableSpanMapperGroupsDTO {
items: SpantypesSpanMapperGroupDTO[];
}
export enum SpantypesSpanMapperOperationDTO {
move = 'move',
copy = 'copy',
}
export interface SpantypesSpanMapperSourceDTO {
context: SpantypesFieldContextDTO;
/**
* @type string
*/
key: string;
operation: SpantypesSpanMapperOperationDTO;
/**
* @type integer
*/
priority: number;
}
export interface SpantypesSpanMapperConfigDTO {
/**
* @type array,null
*/
sources: SpantypesSpanMapperSourceDTO[] | null;
}
export interface SpantypesSpanMapperDTO {
config: SpantypesSpanMapperConfigDTO;
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
/**
* @type boolean
*/
enabled: boolean;
fieldContext: SpantypesFieldContextDTO;
/**
* @type string
*/
groupId: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface SpantypesGettableSpanMappersDTO {
/**
* @type array
*/
items: SpantypesSpanMapperDTO[];
}
export enum SpantypesSpanAggregationTypeDTO {
span_count = 'span_count',
execution_time_percentage = 'execution_time_percentage',
@@ -9575,6 +9440,30 @@ export interface SpantypesPostableFlamegraphDTO {
selectedSpanId?: string;
}
export enum SpantypesSpanMapperOperationDTO {
move = 'move',
copy = 'copy',
}
export interface SpantypesSpanMapperSourceDTO {
context: SpantypesFieldContextDTO;
/**
* @type string
*/
key: string;
operation: SpantypesSpanMapperOperationDTO;
/**
* @type integer
*/
priority: number;
}
export interface SpantypesSpanMapperConfigDTO {
/**
* @type array,null
*/
sources: SpantypesSpanMapperSourceDTO[] | null;
}
export interface SpantypesPostableSpanMapperDTO {
config: SpantypesSpanMapperConfigDTO;
/**
@@ -9623,6 +9512,45 @@ export interface SpantypesPostableWaterfallDTO {
uncollapsedSpans?: string[] | null;
}
export interface SpantypesSpanMapperDTO {
config: SpantypesSpanMapperConfigDTO;
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
/**
* @type boolean
*/
enabled: boolean;
fieldContext: SpantypesFieldContextDTO;
/**
* @type string
*/
group_id: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface SpantypesUpdatableSpanMapperDTO {
config?: SpantypesSpanMapperConfigDTO;
/**
@@ -10924,7 +10852,7 @@ export type ListSpanMappersPathParameters = {
groupId: string;
};
export type ListSpanMappers200 = {
data: SpantypesGettableSpanMappersDTO;
data: SpantypesGettableSpanMapperGroupsDTO;
/**
* @type string
*/

View File

@@ -3,7 +3,6 @@ import { Button, Popover, Tooltip } from 'antd';
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
import { Typography } from '@signozhq/ui/typography';
import { TelemetryFieldKey } from 'api/v5/v5';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useExportRawData } from 'hooks/useExportData/useServerExport';
import { Download, LoaderCircle } from '@signozhq/icons';
import { DataSource } from 'types/common/queryBuilder';
@@ -19,13 +18,11 @@ import './DownloadOptionsMenu.styles.scss';
interface DownloadOptionsMenuProps {
dataSource: DataSource;
selectedColumns?: TelemetryFieldKey[];
panelType?: PANEL_TYPES;
}
export default function DownloadOptionsMenu({
dataSource,
selectedColumns,
panelType,
}: DownloadOptionsMenuProps): JSX.Element {
const [exportFormat, setExportFormat] = useState<string>(DownloadFormats.CSV);
const [rowLimit, setRowLimit] = useState<number>(DownloadRowCounts.TEN_K);
@@ -36,7 +33,6 @@ export default function DownloadOptionsMenu({
const { isDownloading, handleExportRawData } = useExportRawData({
dataSource,
panelType,
});
const handleExport = useCallback(async (): Promise<void> => {

View File

@@ -11,17 +11,17 @@ import { FlatItem, TableRowContext } from './types';
import tableStyles from './TanStackTable.module.scss';
type VirtuosoTableRowProps<TData, TItemKey = string> = ComponentProps<
type VirtuosoTableRowProps<TData> = ComponentProps<
NonNullable<
TableComponents<FlatItem<TData>, TableRowContext<TData, TItemKey>>['TableRow']
TableComponents<FlatItem<TData>, TableRowContext<TData>>['TableRow']
>
>;
function TanStackCustomTableRow<TData, TItemKey = string>({
function TanStackCustomTableRow<TData>({
item,
context,
...props
}: VirtuosoTableRowProps<TData, TItemKey>): JSX.Element {
}: VirtuosoTableRowProps<TData>): JSX.Element {
const rowId = item.row.id;
const rowData = item.row.original;
@@ -84,9 +84,9 @@ function TanStackCustomTableRow<TData, TItemKey = string>({
// This looks overkill but ensures the table is stable and doesn't re-render on every change
// If you add any new prop to context, remember to update this function
// eslint-disable-next-line sonarjs/cognitive-complexity
function areTableRowPropsEqual<TData, TItemKey = string>(
prev: Readonly<VirtuosoTableRowProps<TData, TItemKey>>,
next: Readonly<VirtuosoTableRowProps<TData, TItemKey>>,
function areTableRowPropsEqual<TData>(
prev: Readonly<VirtuosoTableRowProps<TData>>,
next: Readonly<VirtuosoTableRowProps<TData>>,
): boolean {
if (prev.item.row.id !== next.item.row.id) {
return false;
@@ -141,9 +141,7 @@ function areTableRowPropsEqual<TData, TItemKey = string>(
return true;
}
export default memo(TanStackCustomTableRow, areTableRowPropsEqual as any) as <
TData,
TItemKey = string,
>(
props: VirtuosoTableRowProps<TData, TItemKey>,
) => JSX.Element;
export default memo(
TanStackCustomTableRow,
areTableRowPropsEqual,
) as typeof TanStackCustomTableRow;

View File

@@ -8,23 +8,23 @@ import { TableRowContext } from './types';
import tableStyles from './TanStackTable.module.scss';
type TanStackRowCellsProps<TData, TItemKey = string> = {
type TanStackRowCellsProps<TData> = {
row: TanStackRowModel<TData>;
context: TableRowContext<TData, TItemKey> | undefined;
context: TableRowContext<TData> | undefined;
itemKind: 'row' | 'expansion';
hasSingleColumn: boolean;
columnOrderKey: string;
columnVisibilityKey: string;
};
function TanStackRowCellsInner<TData, TItemKey = string>({
function TanStackRowCellsInner<TData>({
row,
context,
itemKind,
hasSingleColumn,
columnOrderKey: _columnOrderKey,
columnVisibilityKey: _columnVisibilityKey,
}: TanStackRowCellsProps<TData, TItemKey>): JSX.Element {
}: TanStackRowCellsProps<TData>): JSX.Element {
const hasHovered = useIsRowHovered(row.id);
const rowData = row.original;
const visibleCells = row.getVisibleCells();
@@ -40,10 +40,8 @@ function TanStackRowCellsInner<TData, TItemKey = string>({
const handleClick = useCallback(
(event: MouseEvent<HTMLTableCellElement>) => {
// Fall back to an empty key so row clicks still fire for consumers
// that don't provide getRowKey (e.g. Logs Explorer / Live Logs).
const keyData = getRowKeyData?.(rowIndex);
const itemKey = keyData?.itemKey ?? ('' as TItemKey);
const itemKey = keyData?.itemKey ?? '';
// Handle ctrl+click or cmd+click (open in new tab)
if ((event.ctrlKey || event.metaKey) && onRowClickNewTab) {
@@ -133,8 +131,6 @@ function areRowCellsPropsEqual<TData>(
const TanStackRowCells = memo(
TanStackRowCellsInner,
areRowCellsPropsEqual as any,
) as <T, TItemKey = string>(
props: TanStackRowCellsProps<T, TItemKey>,
) => JSX.Element;
) as <T>(props: TanStackRowCellsProps<T>) => JSX.Element;
export default TanStackRowCells;

View File

@@ -67,7 +67,7 @@ const INCREASE_VIEWPORT_BY = { top: 500, bottom: 500 };
const noopColumnVisibility = (): void => {};
// eslint-disable-next-line sonarjs/cognitive-complexity
function TanStackTableInner<TData, TItemKey = string>(
function TanStackTableInner<TData>(
{
data,
columns,
@@ -107,7 +107,7 @@ function TanStackTableInner<TData, TItemKey = string>(
suffixPaginationContent,
enableAlternatingRowColors,
disableVirtualScroll,
}: TanStackTableProps<TData, TItemKey>,
}: TanStackTableProps<TData>,
forwardedRef: React.ForwardedRef<TanStackTableHandle>,
): JSX.Element {
if (disableVirtualScroll && onEndReached) {
@@ -193,7 +193,7 @@ function TanStackTableInner<TData, TItemKey = string>(
skeletonRowCount,
});
const { rowKeyData, getRowKeyData } = useRowKeyData<TData, TItemKey>({
const { rowKeyData, getRowKeyData } = useRowKeyData({
data: effectiveData,
isLoading,
getRowKey,
@@ -229,7 +229,7 @@ function TanStackTableInner<TData, TItemKey = string>(
const tanstackColumns = useMemo<ColumnDef<TData>[]>(
() =>
effectiveColumns.map((colDef) =>
buildTanstackColumnDef<TData, TItemKey>(colDef, isRowActive, getRowKeyData),
buildTanstackColumnDef(colDef, isRowActive, getRowKeyData),
),
[effectiveColumns, isRowActive, getRowKeyData],
);
@@ -356,7 +356,7 @@ function TanStackTableInner<TData, TItemKey = string>(
[effectiveVisibility, columnIds],
);
const virtuosoContext = useMemo<TableRowContext<TData, TItemKey>>(
const virtuosoContext = useMemo<TableRowContext<TData>>(
() => ({
getRowStyle,
getRowClassName,
@@ -520,15 +520,13 @@ function TanStackTableInner<TData, TItemKey = string>(
);
type VirtuosoTableComponentProps = ComponentProps<
NonNullable<
TableComponents<FlatItem<TData>, TableRowContext<TData, TItemKey>>['Table']
>
NonNullable<TableComponents<FlatItem<TData>, TableRowContext<TData>>['Table']>
>;
// Use refs in virtuosoComponents to keep the component reference stable during resize
// This prevents Virtuoso from re-rendering all rows when columns are resized
const virtuosoComponents = useMemo(
(): TableComponents<FlatItem<TData>, TableRowContext<TData, TItemKey>> => ({
() => ({
Table: ({ style, children }: VirtuosoTableComponentProps): JSX.Element => (
<table className={tableStyles.tanStackTable} style={style}>
<VirtuosoTableColGroup
@@ -584,7 +582,7 @@ function TanStackTableInner<TData, TItemKey = string>(
</table>
</div>
) : (
<TableVirtuoso<FlatItem<TData>, TableRowContext<TData, TItemKey>>
<TableVirtuoso<FlatItem<TData>, TableRowContext<TData>>
className={virtuosoClassName}
ref={virtuosoRef}
{...restTableScrollerProps}
@@ -662,11 +660,8 @@ function TanStackTableInner<TData, TItemKey = string>(
);
}
const TanStackTableForward = forwardRef(TanStackTableInner) as <
TData,
TItemKey = string,
>(
props: TanStackTableProps<TData, TItemKey> & {
const TanStackTableForward = forwardRef(TanStackTableInner) as <TData>(
props: TanStackTableProps<TData> & {
ref?: React.Ref<TanStackTableHandle>;
},
) => JSX.Element;

View File

@@ -57,39 +57,6 @@ describe('TanStackRowCells', () => {
});
it('calls onRowClick when a cell is clicked', async () => {
const user = userEvent.setup();
const onRowClick = jest.fn();
const ctx: TableRowContext<Row> = {
colCount: 1,
onRowClick,
getRowKeyData: () => ({ finalKey: 'r1', itemKey: 'r1' }),
hasSingleColumn: false,
columnOrderKey: '',
columnVisibilityKey: '',
};
const row = buildMockRow([{ id: 'body' }]);
render(
<table>
<tbody>
<tr>
<TanStackRowCells<Row>
row={row as never}
context={ctx}
itemKind="row"
hasSingleColumn={false}
columnOrderKey=""
columnVisibilityKey=""
/>
</tr>
</tbody>
</table>,
);
await user.click(screen.getAllByRole('cell')[0]);
expect(onRowClick).toHaveBeenCalledWith({ id: 'r1' }, 'r1');
});
it('fires onRowClick with empty itemKey when getRowKeyData is not provided', async () => {
// Mirrors Logs Explorer / Live Logs, which set onRowClick but no getRowKey.
const user = userEvent.setup();
const onRowClick = jest.fn();
const ctx: TableRowContext<Row> = {
@@ -117,6 +84,7 @@ describe('TanStackRowCells', () => {
</table>,
);
await user.click(screen.getAllByRole('cell')[0]);
// onRowClick receives (rowData, itemKey) - itemKey is empty when getRowKeyData not provided
expect(onRowClick).toHaveBeenCalledWith({ id: 'r1' }, '');
});
@@ -129,7 +97,6 @@ describe('TanStackRowCells', () => {
onRowClick,
onRowDeactivate,
isRowActive: () => true,
getRowKeyData: () => ({ finalKey: 'r1', itemKey: 'r1' }),
hasSingleColumn: false,
columnOrderKey: '',
columnVisibilityKey: '',
@@ -227,7 +194,6 @@ describe('TanStackRowCells', () => {
colCount: 1,
onRowClick,
onRowClickNewTab,
getRowKeyData: () => ({ finalKey: 'r1', itemKey: 'r1' }),
hasSingleColumn: false,
columnOrderKey: '',
columnVisibilityKey: '',
@@ -250,7 +216,7 @@ describe('TanStackRowCells', () => {
</table>,
);
fireEvent.click(screen.getAllByRole('cell')[0], { ctrlKey: true });
expect(onRowClickNewTab).toHaveBeenCalledWith({ id: 'r1' }, 'r1');
expect(onRowClickNewTab).toHaveBeenCalledWith({ id: 'r1' }, '');
expect(onRowClick).not.toHaveBeenCalled();
});
@@ -261,7 +227,6 @@ describe('TanStackRowCells', () => {
colCount: 1,
onRowClick,
onRowClickNewTab,
getRowKeyData: () => ({ finalKey: 'r1', itemKey: 'r1' }),
hasSingleColumn: false,
columnOrderKey: '',
columnVisibilityKey: '',
@@ -284,7 +249,7 @@ describe('TanStackRowCells', () => {
</table>,
);
fireEvent.click(screen.getAllByRole('cell')[0], { metaKey: true });
expect(onRowClickNewTab).toHaveBeenCalledWith({ id: 'r1' }, 'r1');
expect(onRowClickNewTab).toHaveBeenCalledWith({ id: 'r1' }, '');
expect(onRowClick).not.toHaveBeenCalled();
});
@@ -295,7 +260,6 @@ describe('TanStackRowCells', () => {
colCount: 1,
onRowClick,
onRowClickNewTab,
getRowKeyData: () => ({ finalKey: 'r1', itemKey: 'r1' }),
hasSingleColumn: false,
columnOrderKey: '',
columnVisibilityKey: '',

View File

@@ -614,34 +614,6 @@ describe('TanStackTableView Integration', () => {
);
});
it('calls onRowClick with object itemKey when getItemKey returns object', async () => {
type SelectionParams = { id: string; name: string };
const user = userEvent.setup();
const onRowClick = jest.fn<void, [unknown, SelectionParams]>();
renderTanStackTable<
(typeof import('./testUtils').defaultData)[0],
SelectionParams
>({
props: {
onRowClick,
getRowKey: (row) => row.id,
getItemKey: (row) => ({ id: row.id, name: row.name }),
},
});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
await user.click(screen.getByText('Item 1'));
expect(onRowClick).toHaveBeenCalledWith(
expect.objectContaining({ id: '1', name: 'Item 1' }),
{ id: '1', name: 'Item 1' },
);
});
it('applies active class when isRowActive returns true', async () => {
renderTanStackTable({
props: {

View File

@@ -42,14 +42,14 @@ export const defaultData: TestRow[] = [
{ id: '3', name: 'Item 3', value: 300 },
];
export type RenderTanStackTableOptions<T, TItemKey = string> = {
props?: Partial<TanStackTableProps<T, TItemKey>>;
export type RenderTanStackTableOptions<T> = {
props?: Partial<TanStackTableProps<T>>;
queryParams?: Record<string, string>;
onUrlUpdate?: OnUrlUpdateFunction;
};
export function renderTanStackTable<T = TestRow, TItemKey = string>(
options: RenderTanStackTableOptions<T, TItemKey> = {},
export function renderTanStackTable<T = TestRow>(
options: RenderTanStackTableOptions<T> = {},
): RenderResult {
const { props = {}, queryParams, onUrlUpdate } = options;
@@ -57,7 +57,7 @@ export function renderTanStackTable<T = TestRow, TItemKey = string>(
data: defaultData as unknown as T[],
columns: defaultColumns as unknown as TableColumnDef<T>[],
...props,
} as TanStackTableProps<T, TItemKey>;
} as TanStackTableProps<T>;
return render(
<NuqsTestingAdapter searchParams={queryParams} onUrlUpdate={onUrlUpdate}>
@@ -65,7 +65,7 @@ export function renderTanStackTable<T = TestRow, TItemKey = string>(
value={{ viewportHeight: 500, itemHeight: 50 }}
>
<TooltipProvider>
<TanStackTable<T, TItemKey> {...mergedProps} />
<TanStackTable<T> {...mergedProps} />
</TooltipProvider>
</VirtuosoMockContext.Provider>
</NuqsTestingAdapter>,

View File

@@ -123,22 +123,6 @@ export * from './useTableParams';
* />
* ```
*
* @example Object itemKey — use generic `TItemKey` when selection needs compound keys.
* ```tsx
* type SelectionParams = { id: string; cluster: string; namespace: string };
*
* <TanStackTable<Row, SelectionParams>
* data={data}
* columns={columns}
* getRowKey={(row) => row.uid}
* getItemKey={(row) => ({ id: row.name, cluster: row.cluster, namespace: row.namespace })}
* onRowClick={(row, itemKey) => {
* // itemKey is typed as SelectionParams
* setSelection(itemKey);
* }}
* />
* ```
*
* @example Expandable rows. `renderExpandedRow` receives `(row, rowKey, groupMeta?)`.
* ```tsx
* <TanStackTable

View File

@@ -24,15 +24,17 @@ export type TableCellContext<TData, TValue> = {
isExpanded: boolean;
canExpand: boolean;
toggleExpanded: () => void;
/** Business/selection key for the row */
itemKey: string;
/** Group metadata when row is part of a grouped view */
groupMeta?: Record<string, string>;
};
export type RowKeyData<TItemKey = string> = {
export type RowKeyData = {
/** Final unique key (with duplicate suffix if needed) */
finalKey: string;
/** Business/selection key */
itemKey: TItemKey;
itemKey: string;
/** Group metadata */
groupMeta?: Record<string, string>;
};
@@ -80,14 +82,14 @@ export type FlatItem<TData> =
| { kind: 'row'; row: TanStackRowType<TData> }
| { kind: 'expansion'; row: TanStackRowType<TData> };
export type TableRowContext<TData, TItemKey = string> = {
export type TableRowContext<TData> = {
getRowStyle?: (row: TData) => CSSProperties;
getRowClassName?: (row: TData) => string;
isRowActive?: (row: TData) => boolean;
renderRowActions?: (row: TData) => ReactNode;
onRowClick?: (row: TData, itemKey: TItemKey) => void;
onRowClick?: (row: TData, itemKey: string) => void;
/** Called when ctrl+click or cmd+click on a row */
onRowClickNewTab?: (row: TData, itemKey: TItemKey) => void;
onRowClickNewTab?: (row: TData, itemKey: string) => void;
onRowDeactivate?: () => void;
renderExpandedRow?: (
row: TData,
@@ -95,7 +97,7 @@ export type TableRowContext<TData, TItemKey = string> = {
groupMeta?: Record<string, string>,
) => ReactNode;
/** Get key data for a row by index */
getRowKeyData?: (index: number) => RowKeyData<TItemKey> | undefined;
getRowKeyData?: (index: number) => RowKeyData | undefined;
colCount: number;
isDarkMode?: boolean;
/** When set, primitive cell output (string/number/boolean) is wrapped with typography + line-clamp (see `plainTextCellLineClamp` on the table). */
@@ -145,7 +147,7 @@ export type TanstackTableQueryParamsConfig = {
expanded?: string;
};
export type TanStackTableProps<TData, TItemKey = string> = {
export type TanStackTableProps<TData> = {
data: TData[];
columns: TableColumnDef<TData>[];
/** Storage key for column state persistence (visibility, sizing, ordering). When set, enables unified column management. */
@@ -170,7 +172,7 @@ export type TanStackTableProps<TData, TItemKey = string> = {
* When set, enables automatic duplicate key detection and group-aware key composition. */
getRowKey?: (row: TData) => string;
/** Function to get the business/selection key. Defaults to getRowKey result. */
getItemKey?: (row: TData) => TItemKey;
getItemKey?: (row: TData) => string;
/** When set, enables group-aware key generation (prefixes rowKey with group values). */
groupBy?: Array<{ key: string }>;
/** Extract group metadata from a row. Required when groupBy is set. */
@@ -179,9 +181,9 @@ export type TanStackTableProps<TData, TItemKey = string> = {
getRowClassName?: (row: TData) => string;
isRowActive?: (row: TData) => boolean;
renderRowActions?: (row: TData) => ReactNode;
onRowClick?: (row: TData, itemKey: TItemKey) => void;
onRowClick?: (row: TData, itemKey: string) => void;
/** Called when ctrl+click or cmd+click on a row */
onRowClickNewTab?: (row: TData, itemKey: TItemKey) => void;
onRowClickNewTab?: (row: TData, itemKey: string) => void;
onRowDeactivate?: () => void;
activeRowIndex?: number;
renderExpandedRow?: (

View File

@@ -1,51 +1,51 @@
import { useCallback, useMemo } from 'react';
export interface RowKeyDataItem<TItemKey = string> {
export interface RowKeyDataItem {
/** Final unique key for the row (with dedup suffix if needed) */
finalKey: string;
/** Item key for tracking (may differ from finalKey) */
itemKey: TItemKey;
itemKey: string;
/** Group metadata when grouped */
groupMeta: Record<string, string> | undefined;
}
export interface UseRowKeyDataOptions<TData, TItemKey = string> {
export interface UseRowKeyDataOptions<TData> {
data: TData[];
isLoading: boolean;
getRowKey?: (item: TData) => string;
getItemKey?: (item: TData) => TItemKey;
getItemKey?: (item: TData) => string;
groupBy?: Array<{ key: string }>;
getGroupKey?: (item: TData) => Record<string, string>;
}
export interface UseRowKeyDataResult<TItemKey = string> {
export interface UseRowKeyDataResult {
/** Array of key data for each row, undefined if getRowKey not provided or loading */
rowKeyData: RowKeyDataItem<TItemKey>[] | undefined;
getRowKeyData: (index: number) => RowKeyDataItem<TItemKey> | undefined;
rowKeyData: RowKeyDataItem[] | undefined;
getRowKeyData: (index: number) => RowKeyDataItem | undefined;
}
/**
* Computes unique row keys with duplicate handling and group prefixes.
*/
export function useRowKeyData<TData, TItemKey = string>({
export function useRowKeyData<TData>({
data,
isLoading,
getRowKey,
getItemKey,
groupBy,
getGroupKey,
}: UseRowKeyDataOptions<TData, TItemKey>): UseRowKeyDataResult<TItemKey> {
}: UseRowKeyDataOptions<TData>): UseRowKeyDataResult {
// eslint-disable-next-line sonarjs/cognitive-complexity
const rowKeyData = useMemo((): RowKeyDataItem<TItemKey>[] | undefined => {
const rowKeyData = useMemo((): RowKeyDataItem[] | undefined => {
if (!getRowKey || isLoading) {
return undefined;
}
const keyCount = new Map<string, number>();
return data.map((item, index): RowKeyDataItem<TItemKey> => {
return data.map((item, index): RowKeyDataItem => {
const itemIdentifier = getRowKey(item);
const itemKey = getItemKey?.(item) ?? (itemIdentifier as TItemKey);
const itemKey = getItemKey?.(item) ?? itemIdentifier;
const groupMeta = groupBy?.length ? getGroupKey?.(item) : undefined;
// Build rowKey with group prefix when grouped

View File

@@ -86,10 +86,10 @@ const buildAccessorFn = <TData>(
};
};
export function buildTanstackColumnDef<TData, TItemKey = string>(
export function buildTanstackColumnDef<TData>(
colDef: TableColumnDef<TData>,
isRowActive?: (row: TData) => boolean,
getRowKeyData?: (index: number) => RowKeyData<TItemKey> | undefined,
getRowKeyData?: (index: number) => RowKeyData | undefined,
): ColumnDef<TData> {
const isFixed = colDef.width?.fixed != null;
const headerFn =
@@ -140,6 +140,7 @@ export function buildTanstackColumnDef<TData, TItemKey = string>(
toggleExpanded: (): void => {
row.toggleExpanded();
},
itemKey: keyData?.itemKey ?? '',
groupMeta: keyData?.groupMeta,
});
},

View File

@@ -7,11 +7,13 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
import { ALERT_TYPE_URL_MAP } from './constants';
// The setup-guide button only exists in the classic form, which is reachable
// via the unadvertised showClassicCreateAlertsPage param.
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string; search: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALERTS_NEW}`,
search: 'ruleType=anomaly_rule',
search: 'ruleType=anomaly_rule&showClassicCreateAlertsPage=true',
}),
}));
@@ -20,7 +22,7 @@ jest.mock('react-router-dom-v5-compat', () => ({
useNavigationType: jest.fn(() => 'PUSH'),
useLocation: jest.fn(() => ({
pathname: '/alerts/new',
search: 'ruleType=anomaly_rule',
search: 'ruleType=anomaly_rule&showClassicCreateAlertsPage=true',
hash: '',
state: null,
})),

View File

@@ -152,7 +152,7 @@ describe('CreateAlertRule', () => {
expect(screen.getByText(AlertTypes.METRICS_BASED_ALERT)).toBeInTheDocument();
});
it('should render classic flow when ruleType is anomaly_rule even if showClassicCreateAlertsPage is not true', () => {
it('should render new flow when ruleType is anomaly_rule', () => {
mockGetUrlQuery.mockImplementation((key: string) => {
if (key === QueryParams.showClassicCreateAlertsPage) {
return 'false';
@@ -163,8 +163,8 @@ describe('CreateAlertRule', () => {
return null;
});
render(<CreateAlertRule />);
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
expect(screen.queryByText(CREATE_ALERT_V2_TEXT)).not.toBeInTheDocument();
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
expect(screen.queryByText(FORM_ALERT_RULES_TEXT)).not.toBeInTheDocument();
});
it('should use alertType from URL when provided', () => {

View File

@@ -100,10 +100,9 @@ function CreateRules(): JSX.Element {
return <SelectAlertType onSelect={handleSelectType} />;
}
if (
showClassicCreateAlertsPageFlag ||
alertType === AlertTypes.ANOMALY_BASED_ALERT
) {
// The classic experience is no longer offered in the UI; the query param
// is kept as an unadvertised escape hatch until the flow is removed.
if (showClassicCreateAlertsPageFlag) {
return (
<FormAlertRules
alertType={alertType}

View File

@@ -2,7 +2,9 @@ import { useQuery } from 'react-query';
import { Button, Tooltip } from 'antd';
import getAllChannels from 'api/channels/getAll';
import classNames from 'classnames';
import { ChartLine } from '@signozhq/icons';
import { FeatureKeys } from 'constants/features';
import { Activity, ChartLine } from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';
import { SuccessResponseV2 } from 'types/api';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { Channels } from 'types/api/channels/getAll';
@@ -19,6 +21,7 @@ import './styles.scss';
function AlertCondition(): JSX.Element {
const { alertType, setAlertType } = useCreateAlertState();
const { featureFlags } = useAppContext();
const {
data,
@@ -30,9 +33,15 @@ function AlertCondition(): JSX.Element {
});
const channels = data?.data || [];
const isAnomalyDetectionEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
?.active || false;
// Anomaly alerts always show both tabs so existing rules stay editable;
// metric alerts only offer the anomaly tab when the feature is enabled.
const showMultipleTabs =
alertType === AlertTypes.ANOMALY_BASED_ALERT ||
alertType === AlertTypes.METRICS_BASED_ALERT;
(isAnomalyDetectionEnabled && alertType === AlertTypes.METRICS_BASED_ALERT);
const tabs = [
{
@@ -40,16 +49,15 @@ function AlertCondition(): JSX.Element {
icon: <ChartLine size={14} data-testid="threshold-view" />,
value: AlertTypes.METRICS_BASED_ALERT,
},
// Hide anomaly tab for now
// ...(showMultipleTabs
// ? [
// {
// label: 'Anomaly',
// icon: <Activity size={14} data-testid="anomaly-view" />,
// value: AlertTypes.ANOMALY_BASED_ALERT,
// },
// ]
// : []),
...(showMultipleTabs
? [
{
label: 'Anomaly',
icon: <Activity size={14} data-testid="anomaly-view" />,
value: AlertTypes.ANOMALY_BASED_ALERT,
},
]
: []),
];
const handleAlertTypeChange = (value: AlertTypes): void => {

View File

@@ -188,7 +188,7 @@ function AnomalyThreshold({
}}
options={ANOMALY_SEASONALITY_OPTIONS}
/>
{notificationSettings.routingPolicies ? (
{!notificationSettings.routingPolicies ? (
<>
<Typography.Text
data-testid="seasonality-text"

View File

@@ -1,7 +1,11 @@
import { QueryClient, QueryClientProvider } from 'react-query';
import { MemoryRouter } from 'react-router-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import { FeatureKeys } from 'constants/features';
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
import * as appHooks from 'providers/App/App';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { FeatureFlagProps } from 'types/api/features/getFeaturesFlags';
import { CreateAlertProvider } from '../../context';
import AlertCondition from '../AlertCondition';
@@ -96,6 +100,23 @@ const createTestQueryClient = (): QueryClient =>
},
});
const ANOMALY_DETECTION_FLAG: FeatureFlagProps = {
name: FeatureKeys.ANOMALY_DETECTION,
active: true,
usage: 0,
usage_limit: -1,
route: '',
};
const useAppContextSpy = jest.spyOn(appHooks, 'useAppContext');
const mockAppContext = (isAnomalyDetectionEnabled: boolean): void => {
useAppContextSpy.mockReturnValue({
...getAppContextMockState(),
featureFlags: isAnomalyDetectionEnabled ? [ANOMALY_DETECTION_FLAG] : [],
});
};
const renderAlertCondition = (
alertType?: string,
): ReturnType<typeof render> => {
@@ -113,6 +134,10 @@ const renderAlertCondition = (
};
describe('AlertCondition', () => {
beforeEach(() => {
mockAppContext(true);
});
it('renders the stepper with correct step number and label', () => {
renderAlertCondition();
expect(screen.getByTestId(STEPPER_TEST_ID)).toHaveTextContent(
@@ -125,10 +150,9 @@ describe('AlertCondition', () => {
// Verify default alertType is METRICS_BASED_ALERT (shows AlertThreshold component)
expect(screen.getByTestId(ALERT_THRESHOLD_TEST_ID)).toBeInTheDocument();
// TODO: uncomment this when anomaly tab is implemented
// expect(
// screen.queryByTestId(ANOMALY_THRESHOLD_TEST_ID),
// ).not.toBeInTheDocument();
expect(
screen.queryByTestId(ANOMALY_THRESHOLD_TEST_ID),
).not.toBeInTheDocument();
// Verify threshold tab is active by default
const thresholdTab = screen.getByText(THRESHOLD_TAB_TEXT);
@@ -136,8 +160,7 @@ describe('AlertCondition', () => {
// Verify both tabs are visible (METRICS_BASED_ALERT supports multiple tabs)
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
// TODO: uncomment this when anomaly tab is implemented
// expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
});
it('renders threshold tab by default', () => {
@@ -152,13 +175,27 @@ describe('AlertCondition', () => {
).not.toBeInTheDocument();
});
// TODO: Unskip this when anomaly tab is implemented
it.skip('renders anomaly tab when alert type supports multiple tabs', () => {
it('renders anomaly tab when alert type supports multiple tabs', () => {
renderAlertCondition();
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
});
it('does not offer the anomaly tab when anomaly detection is disabled', () => {
mockAppContext(false);
renderAlertCondition();
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
expect(screen.queryByText(ANOMALY_TAB_TEXT)).not.toBeInTheDocument();
});
it('shows both tabs for anomaly alerts even when anomaly detection is disabled', () => {
mockAppContext(false);
renderAlertCondition('ANOMALY_BASED_ALERT');
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
expect(screen.getByTestId(ANOMALY_THRESHOLD_TEST_ID)).toBeInTheDocument();
});
it('shows AlertThreshold component when alert type is not anomaly based', () => {
renderAlertCondition();
expect(screen.getByTestId(ALERT_THRESHOLD_TEST_ID)).toBeInTheDocument();
@@ -167,8 +204,7 @@ describe('AlertCondition', () => {
).not.toBeInTheDocument();
});
// TODO: Unskip this when anomaly tab is implemented
it.skip('shows AnomalyThreshold component when alert type is anomaly based', () => {
it('shows AnomalyThreshold component when alert type is anomaly based', () => {
renderAlertCondition();
// Click on anomaly tab to switch to anomaly-based alert
@@ -179,8 +215,7 @@ describe('AlertCondition', () => {
expect(screen.queryByTestId(ALERT_THRESHOLD_TEST_ID)).not.toBeInTheDocument();
});
// TODO: Unskip this when anomaly tab is implemented
it.skip('switches between threshold and anomaly tabs', () => {
it('switches between threshold and anomaly tabs', () => {
renderAlertCondition();
// Initially shows threshold component
@@ -205,8 +240,7 @@ describe('AlertCondition', () => {
).not.toBeInTheDocument();
});
// TODO: Unskip this when anomaly tab is implemented
it.skip('applies active tab styling correctly', () => {
it('applies active tab styling correctly', () => {
renderAlertCondition();
const thresholdTab = screen.getByText(THRESHOLD_TAB_TEXT);
@@ -227,11 +261,10 @@ describe('AlertCondition', () => {
it('shows multiple tabs for METRICS_BASED_ALERT', () => {
renderAlertCondition('METRIC_BASED_ALERT');
// TODO: uncomment this when anomaly tab is implemented
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
// expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
expect(screen.getByTestId(THRESHOLD_VIEW_TEST_ID)).toBeInTheDocument();
// expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
});
it('shows multiple tabs for ANOMALY_BASED_ALERT', () => {
@@ -239,9 +272,8 @@ describe('AlertCondition', () => {
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
expect(screen.getByTestId(THRESHOLD_VIEW_TEST_ID)).toBeInTheDocument();
// TODO: uncomment this when anomaly tab is implemented
// expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
// expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
});
it('shows only threshold tab for LOGS_BASED_ALERT', () => {

View File

@@ -1,14 +1,7 @@
import { useCallback, useMemo } from 'react';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import logEvent from 'api/common/logEvent';
import classNames from 'classnames';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { RotateCcw } from '@signozhq/icons';
import { useAlertRuleOptional } from 'providers/Alert';
import { Labels } from 'types/api/alerts/def';
@@ -22,8 +15,6 @@ function CreateAlertHeader(): JSX.Element {
const alertRuleContext = useAlertRuleOptional();
const { currentQuery } = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
const urlQuery = useUrlQuery();
const groupByLabels = useMemo(() => {
const labels = new Array<string>();
@@ -46,14 +37,6 @@ function CreateAlertHeader(): JSX.Element {
[groupByLabels],
);
const handleSwitchToClassicExperience = useCallback(() => {
logEvent('Alert: Switch to classic experience button clicked', {});
urlQuery.set(QueryParams.showClassicCreateAlertsPage, 'true');
const url = `${ROUTES.ALERTS_NEW}?${urlQuery.toString()}`;
safeNavigate(url, { replace: true });
}, [safeNavigate, urlQuery]);
return (
<div
className={classNames('alert-header', { 'edit-alert-header': isEditMode })}
@@ -61,15 +44,6 @@ function CreateAlertHeader(): JSX.Element {
{!isEditMode && (
<div className="alert-header__tab-bar">
<div className="alert-header__tab">New Alert Rule</div>
<Button
prefix={<RotateCcw size={12} />}
onClick={handleSwitchToClassicExperience}
variant="solid"
color="secondary"
size="sm"
>
Switch to Classic Experience
</Button>
</div>
)}
<div className="alert-header__content">

View File

@@ -1,20 +1,12 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { defaultPostableAlertRuleV2 } from 'container/CreateAlertV2/constants';
import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/utils';
import * as useSafeNavigateHook from 'hooks/useSafeNavigate';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import * as rulesHook from '../../../../api/generated/services/rules';
import { CreateAlertProvider } from '../../context';
import CreateAlertHeader from '../CreateAlertHeader';
const mockSafeNavigate = jest.fn();
jest.spyOn(useSafeNavigateHook, 'useSafeNavigate').mockReturnValue({
safeNavigate: mockSafeNavigate,
});
jest.spyOn(rulesHook, 'useCreateRule').mockReturnValue({
mutate: jest.fn(),
isLoading: false,
@@ -106,34 +98,8 @@ describe('CreateAlertHeader', () => {
).toHaveValue('TEST_ALERT');
});
it('should navigate to classic experience when button is clicked', () => {
it('should not render "switch to classic experience" button', () => {
renderCreateAlertHeader();
const switchToClassicExperienceButton = screen.getByText(
'Switch to Classic Experience',
);
expect(switchToClassicExperienceButton).toBeInTheDocument();
fireEvent.click(switchToClassicExperienceButton);
const params = new URLSearchParams();
params.set(QueryParams.showClassicCreateAlertsPage, 'true');
expect(mockSafeNavigate).toHaveBeenCalledWith(
`${ROUTES.ALERTS_NEW}?${params.toString()}`,
{ replace: true },
);
});
it('should not render "switch to classic experience" button when isEditMode is true', () => {
render(
<CreateAlertProvider
isEditMode
initialAlertType={AlertTypes.METRICS_BASED_ALERT}
initialAlertState={getCreateAlertLocalStateFromAlertDef(
defaultPostableAlertRuleV2,
)}
>
<CreateAlertHeader />
</CreateAlertProvider>,
);
expect(
screen.queryByText('Switch to Classic Experience'),
).not.toBeInTheDocument();

View File

@@ -14,10 +14,7 @@ import APIError from 'types/api/error';
import { isModifierKeyPressed } from 'utils/app';
import { useCreateAlertState } from '../context';
import {
buildCreateThresholdAlertRulePayload,
validateCreateAlertState,
} from './utils';
import { buildCreateAlertRulePayload, validateCreateAlertState } from './utils';
import './styles.scss';
import {
@@ -85,7 +82,7 @@ function Footer(): JSX.Element {
);
const handleTestNotification = useCallback((): void => {
const payload = buildCreateThresholdAlertRulePayload({
const payload = buildCreateAlertRulePayload({
alertType,
basicAlertState,
thresholdState,
@@ -122,7 +119,7 @@ function Footer(): JSX.Element {
const queryClient = useQueryClient();
const handleSaveAlert = useCallback((): void => {
const payload = buildCreateThresholdAlertRulePayload({
const payload = buildCreateAlertRulePayload({
alertType,
basicAlertState,
thresholdState,

View File

@@ -18,6 +18,8 @@ import { EQueryType } from 'types/common/dashboard';
import { BuildCreateAlertRulePayloadArgs } from '../types';
import {
buildCreateAlertRulePayload,
buildCreateAnomalyAlertRulePayload,
buildCreateThresholdAlertRulePayload,
getAlertOnAbsentProps,
getEnforceMinimumDatapointsProps,
@@ -550,4 +552,115 @@ describe('Footer utils', () => {
},
);
});
describe('buildCreateAnomalyAlertRulePayload', () => {
const mockCreateAlertContextState = createMockAlertContextState();
const ANOMALY_PAYLOAD_ARGS: BuildCreateAlertRulePayloadArgs = {
basicAlertState: mockCreateAlertContextState.alertState,
thresholdState: {
...mockCreateAlertContextState.thresholdState,
thresholds: [
{
...mockCreateAlertContextState.thresholdState.thresholds[0],
thresholdValue: 3,
},
],
},
advancedOptions: mockCreateAlertContextState.advancedOptions,
evaluationWindow: mockCreateAlertContextState.evaluationWindow,
notificationSettings: mockCreateAlertContextState.notificationSettings,
query: initialQueriesMap.metrics,
alertType: AlertTypes.ANOMALY_BASED_ALERT,
};
it('builds a v2alpha1 anomaly rule payload', () => {
const props = buildCreateAnomalyAlertRulePayload(ANOMALY_PAYLOAD_ARGS);
expect(props.ruleType).toBe('anomaly_rule');
expect(props.schemaVersion).toBe('v2alpha1');
expect(props.version).toBe('v5');
// The stored alertType is metric based; anomaly is a rule type
expect(props.alertType).toBe('METRIC_BASED_ALERT');
expect(props.condition.algorithm).toBe(
ANOMALY_PAYLOAD_ARGS.thresholdState.algorithm,
);
expect(props.condition.seasonality).toBe(
ANOMALY_PAYLOAD_ARGS.thresholdState.seasonality,
);
expect(props.condition.selectedQueryName).toBe(
ANOMALY_PAYLOAD_ARGS.thresholdState.selectedQuery,
);
// Evaluation comes from the anomaly condition's own window
expect(props.evaluation).toStrictEqual({
kind: 'rolling',
spec: {
evalWindow: ANOMALY_PAYLOAD_ARGS.thresholdState.evaluationWindow,
frequency: '1m',
},
});
});
it('keeps the target positive for the above operator', () => {
const props = buildCreateAnomalyAlertRulePayload({
...ANOMALY_PAYLOAD_ARGS,
thresholdState: {
...ANOMALY_PAYLOAD_ARGS.thresholdState,
operator: 'above',
},
});
expect(props.condition.thresholds?.spec[0].target).toBe(3);
expect(props.condition.thresholds?.spec[0].op).toBe('above');
});
it.each([['below'], ['2']])(
'negates the target for the below operator (%s)',
(op) => {
const props = buildCreateAnomalyAlertRulePayload({
...ANOMALY_PAYLOAD_ARGS,
thresholdState: {
...ANOMALY_PAYLOAD_ARGS.thresholdState,
operator: op,
},
});
expect(props.condition.thresholds?.spec[0].target).toBe(-3);
expect(props.condition.thresholds?.spec[0].op).toBe(op);
},
);
it('keeps the target positive for the outside_bounds operator', () => {
const props = buildCreateAnomalyAlertRulePayload({
...ANOMALY_PAYLOAD_ARGS,
thresholdState: {
...ANOMALY_PAYLOAD_ARGS.thresholdState,
operator: 'outside_bounds',
},
});
expect(props.condition.thresholds?.spec[0].target).toBe(3);
});
});
describe('buildCreateAlertRulePayload', () => {
const mockCreateAlertContextState = createMockAlertContextState();
const args: BuildCreateAlertRulePayloadArgs = {
basicAlertState: mockCreateAlertContextState.alertState,
thresholdState: mockCreateAlertContextState.thresholdState,
advancedOptions: mockCreateAlertContextState.advancedOptions,
evaluationWindow: mockCreateAlertContextState.evaluationWindow,
notificationSettings: mockCreateAlertContextState.notificationSettings,
query: initialQueriesMap.metrics,
alertType: mockCreateAlertContextState.alertType,
};
it('builds an anomaly payload for anomaly based alerts', () => {
const props = buildCreateAlertRulePayload({
...args,
alertType: AlertTypes.ANOMALY_BASED_ALERT,
});
expect(props.ruleType).toBe('anomaly_rule');
});
it('builds a threshold payload for other alert types', () => {
const props = buildCreateAlertRulePayload(args);
expect(props.ruleType).toBe('threshold_rule');
});
});
});

View File

@@ -2,6 +2,7 @@ import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { AlertDetectionTypes } from 'container/FormAlertRules';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import {
BasicThreshold,
PostableAlertRuleV2,
@@ -11,9 +12,11 @@ import { compositeQueryToQueryEnvelope } from 'utils/compositeQueryToQueryEnvelo
import {
AdvancedOptionsState,
AlertThresholdOperator,
EvaluationWindowState,
NotificationSettingsState,
} from '../context/types';
import { normalizeOperator } from '../utils';
import { BuildCreateAlertRulePayloadArgs } from './types';
// Get formatted time/unit pairs for create alert api payload
@@ -288,16 +291,15 @@ export function buildCreateThresholdAlertRulePayload(
}
// Build Create Anomaly Alert Rule Payload
// TODO: Update this function before enabling anomaly alert rule creation
export function buildCreateAnomalyAlertRulePayload(
args: BuildCreateAlertRulePayloadArgs,
): PostableAlertRuleV2 {
const {
alertType,
basicAlertState,
thresholdState,
query,
notificationSettings,
evaluationWindow,
advancedOptions,
} = args;
@@ -313,19 +315,55 @@ export function buildCreateAnomalyAlertRulePayload(
unit: basicAlertState.yAxisUnit,
});
// v2alpha1 thresholds are literal: "3 deviations below the predicted data"
// means the anomaly z-score must drop under -3, so the target is negated
// for the below operator (the deviations input is always positive).
const isBelowOperator =
normalizeOperator(thresholdState.operator) ===
AlertThresholdOperator.IS_BELOW;
const thresholds: BasicThreshold[] = thresholdState.thresholds.map(
(threshold) => {
const deviations = Math.abs(parseFloat(threshold.thresholdValue.toString()));
return {
name: threshold.label,
target: isBelowOperator ? -deviations : deviations,
matchType: thresholdState.matchType,
op: thresholdState.operator,
channels: threshold.channels,
targetUnit: threshold.unit,
};
},
);
const alertOnAbsentProps = getAlertOnAbsentProps(advancedOptions);
const enforceMinimumDatapointsProps =
getEnforceMinimumDatapointsProps(advancedOptions);
const evaluationProps = getEvaluationProps(evaluationWindow, advancedOptions);
const notificationSettingsProps =
getNotificationSettingsProps(notificationSettings);
// The anomaly condition carries its own evaluation window
// ("during the last X"), so the evaluation is always a rolling window.
const frequency = getFormattedTimeValue(
advancedOptions.evaluationCadence.default.value,
advancedOptions.evaluationCadence.default.timeUnit,
);
return {
alert: basicAlertState.name,
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
alertType,
alertType:
alertType === AlertTypes.ANOMALY_BASED_ALERT
? AlertTypes.METRICS_BASED_ALERT
: alertType,
condition: {
thresholds: {
kind: 'basic',
spec: thresholds,
},
compositeQuery,
selectedQueryName: thresholdState.selectedQuery,
algorithm: thresholdState.algorithm,
seasonality: thresholdState.seasonality,
...alertOnAbsentProps,
...enforceMinimumDatapointsProps,
},
@@ -335,9 +373,25 @@ export function buildCreateAnomalyAlertRulePayload(
summary: notificationSettings.description,
},
notificationSettings: notificationSettingsProps,
evaluation: evaluationProps,
version: '',
schemaVersion: '',
evaluation: {
kind: 'rolling',
spec: {
evalWindow: thresholdState.evaluationWindow,
frequency,
},
},
version: 'v5',
schemaVersion: 'v2alpha1',
source: window?.location.toString(),
};
}
// Build the create/test alert rule payload for the selected alert type
export function buildCreateAlertRulePayload(
args: BuildCreateAlertRulePayloadArgs,
): PostableAlertRuleV2 {
if (args.alertType === AlertTypes.ANOMALY_BASED_ALERT) {
return buildCreateAnomalyAlertRulePayload(args);
}
return buildCreateThresholdAlertRulePayload(args);
}

View File

@@ -316,6 +316,60 @@ describe('CreateAlertV2 utils', () => {
});
});
describe('getThresholdStateFromAlertDef for anomaly rules', () => {
const anomalyAlertDef: PostableAlertRuleV2 = {
...defaultPostableAlertRuleV2,
ruleType: 'anomaly_rule',
condition: {
...defaultPostableAlertRuleV2.condition,
algorithm: 'standard',
seasonality: 'daily',
selectedQueryName: 'A',
thresholds: {
kind: 'basic',
spec: [
{
name: 'critical',
target: -3,
targetUnit: '',
channels: ['email'],
matchType: AlertThresholdMatchType.AT_LEAST_ONCE,
op: AlertThresholdOperator.IS_BELOW,
},
],
},
},
evaluation: {
kind: 'rolling',
spec: {
evalWindow: '1h0m0s',
frequency: '1m',
},
},
};
it('shows the absolute deviation value for negative anomaly targets', () => {
const props = getThresholdStateFromAlertDef(anomalyAlertDef);
expect(props.thresholds[0].thresholdValue).toBe(3);
expect(props.operator).toBe(AlertThresholdOperator.IS_BELOW);
});
it('hydrates the anomaly evaluation window, algorithm and seasonality', () => {
const props = getThresholdStateFromAlertDef(anomalyAlertDef);
expect(props.evaluationWindow).toBe('1h0m0s');
expect(props.algorithm).toBe('standard');
expect(props.seasonality).toBe('daily');
});
it('does not touch the target for non-anomaly rules', () => {
const props = getThresholdStateFromAlertDef({
...anomalyAlertDef,
ruleType: 'threshold_rule',
});
expect(props.thresholds[0].thresholdValue).toBe(-3);
});
});
describe('normalizeOperator', () => {
it.each([
['1', AlertThresholdOperator.IS_ABOVE],

View File

@@ -239,6 +239,39 @@ describe('CreateAlertV2 Context Utils', () => {
});
});
it('should set evaluation window', () => {
const result = alertThresholdReducer(INITIAL_ALERT_THRESHOLD_STATE, {
type: 'SET_EVALUATION_WINDOW',
payload: TimeDuration.ONE_HOUR,
});
expect(result).toStrictEqual({
...INITIAL_ALERT_THRESHOLD_STATE,
evaluationWindow: TimeDuration.ONE_HOUR,
});
});
it('should set algorithm', () => {
const result = alertThresholdReducer(INITIAL_ALERT_THRESHOLD_STATE, {
type: 'SET_ALGORITHM',
payload: Algorithm.STANDARD,
});
expect(result).toStrictEqual({
...INITIAL_ALERT_THRESHOLD_STATE,
algorithm: Algorithm.STANDARD,
});
});
it('should set seasonality', () => {
const result = alertThresholdReducer(INITIAL_ALERT_THRESHOLD_STATE, {
type: 'SET_SEASONALITY',
payload: Seasonality.WEEKLY,
});
expect(result).toStrictEqual({
...INITIAL_ALERT_THRESHOLD_STATE,
seasonality: Seasonality.WEEKLY,
});
});
it('should set thresholds', () => {
const newThresholds = [
{

View File

@@ -124,6 +124,12 @@ export const alertThresholdReducer = (
return { ...state, operator: action.payload };
case 'SET_MATCH_TYPE':
return { ...state, matchType: action.payload };
case 'SET_EVALUATION_WINDOW':
return { ...state, evaluationWindow: action.payload };
case 'SET_ALGORITHM':
return { ...state, algorithm: action.payload };
case 'SET_SEASONALITY':
return { ...state, seasonality: action.payload };
case 'SET_THRESHOLDS':
return { ...state, thresholds: action.payload };
case 'RESET':

View File

@@ -4,6 +4,7 @@ import { Spin } from 'antd';
import { TIMEZONE_DATA } from 'components/CustomTimePicker/timezoneUtils';
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
import { getRandomColor } from 'container/ExplorerOptions/utils';
import { AlertDetectionTypes } from 'container/FormAlertRules';
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
import { v4 } from 'uuid';
@@ -303,13 +304,19 @@ export function normalizeMatchType(
export function getThresholdStateFromAlertDef(
alertDef: PostableAlertRuleV2,
): AlertThresholdState {
// Anomaly targets are stored as literal z-scores (negative for the below
// operator); the deviations select always shows the positive value.
const isAnomalyRule =
alertDef.ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT;
return {
...INITIAL_ALERT_THRESHOLD_STATE,
thresholds:
alertDef.condition.thresholds?.spec.map((threshold) => ({
id: v4(),
label: threshold.name,
thresholdValue: threshold.target,
thresholdValue: isAnomalyRule
? Math.abs(threshold.target)
: threshold.target,
recoveryThresholdValue: null,
unit: threshold.targetUnit,
color: getColorForThreshold(threshold.name),
@@ -321,6 +328,18 @@ export function getThresholdStateFromAlertDef(
matchType:
alertDef.condition.thresholds?.spec[0].matchType ||
AlertThresholdMatchType.AT_LEAST_ONCE,
...(isAnomalyRule
? {
evaluationWindow:
alertDef.evaluation?.spec?.evalWindow ||
INITIAL_ALERT_THRESHOLD_STATE.evaluationWindow,
algorithm:
alertDef.condition.algorithm || INITIAL_ALERT_THRESHOLD_STATE.algorithm,
seasonality:
alertDef.condition.seasonality ||
INITIAL_ALERT_THRESHOLD_STATE.seasonality,
}
: {}),
};
}

View File

@@ -1,17 +1,13 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listHosts } from 'api/generated/services/inframonitoring';
import {
InframonitoringtypesHostRecordDTO,
InframonitoringtypesHostStatusDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
RenderErrorResponseDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import APIError from 'types/api/error';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { InfraMonitoringEvents } from 'constants/events';
@@ -126,12 +122,13 @@ function Hosts(): JSX.Element {
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:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},
@@ -144,7 +141,7 @@ function Hosts(): JSX.Element {
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesHostRecordDTO | null;
error?: APIError | null;
error?: string | null;
}> => {
try {
const response = await listHosts(
@@ -162,10 +159,11 @@ function Hosts(): JSX.Element {
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:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},

View File

@@ -10,7 +10,6 @@ import {
import { K8sDetailsMetadataConfig } from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
import { SelectedItemParams } from 'container/InfraMonitoringK8sV2/hooks';
import {
getHostQueryPayload,
hostWidgetInfo,
@@ -108,10 +107,8 @@ export function getHostMetricsQueryPayload(
export { hostWidgetInfo };
export const hostGetSelectedItemExpression = (
params: SelectedItemParams,
): string =>
`host.name = ${formatValueForExpression(params.selectedItem ?? '')}`;
export const hostGetSelectedItemExpression = (hostName: string): string =>
`host.name = ${formatValueForExpression(hostName)}`;
export function hostInitialLogTracesExpression(
host: InframonitoringtypesHostRecordDTO,

View File

@@ -11,6 +11,8 @@ import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/const
import { CellValueTooltip } from 'container/InfraMonitoringK8sV2/components';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import TanStackTable from 'components/TanStackTableView';
const HOSTNAME_DOCS_URL =
'https://signoz.io/docs/infrastructure-monitoring/hostmetrics/#host-name-is-blankempty';
@@ -21,7 +23,11 @@ export function HostnameCell({
}): React.ReactElement {
const isEmpty = !hostName || !hostName.trim();
if (!isEmpty) {
return <CellValueTooltip value={hostName} />;
return (
<CellValueTooltip value={hostName}>
<TanStackTable.Text>{hostName}</TanStackTable.Text>
</CellValueTooltip>
);
}
return (
<>

View File

@@ -11,6 +11,6 @@
}
.columnHeaderLabel {
text-align: left;
text-align: center;
padding: var(--spacing-2) var(--spacing-2) var(--spacing-2) 0px;
}

View File

@@ -51,7 +51,7 @@ function ColumnHeader({
<a
href={`${DOCS_BASE_URL}${docPath}`}
target="_blank"
rel="noopener"
rel="noopener noreferrer"
onClick={(e): void => e.stopPropagation()}
>
Learn more.

View File

@@ -30,7 +30,7 @@ function EntityGroupHeader({
<a
href={`${DOCS_BASE_URL}${docPath}`}
target="_blank"
rel="noopener"
rel="noopener noreferrer"
onClick={(e): void => e.stopPropagation()}
>
Learn more.

View File

@@ -12,8 +12,6 @@ import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import APIError from 'types/api/error';
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
import { InfraMonitoringEvents } from 'constants/events';
import { QueryParams } from 'constants/query';
@@ -54,10 +52,9 @@ import EntityMetrics from '../EntityDetailsUtils/EntityMetrics';
import EntityTraces from '../EntityDetailsUtils/EntityTraces';
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityTraces/hooks';
import {
SelectedItemParams,
useInfraMonitoringEventsFilters,
useInfraMonitoringLogFilters,
useInfraMonitoringSelectedItemParams,
useInfraMonitoringSelectedItem,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from '../hooks';
@@ -65,10 +62,6 @@ import LoadingContainer from '../LoadingContainer';
import '../EntityDetailsUtils/entityDetails.styles.scss';
import { parseAsString, useQueryState } from 'nuqs';
import {
EntityCountConfig,
EntityCountsSection,
} from './components/EntityCountsSection/EntityCountsSection';
const TimeRangeOffset = 1000000000;
@@ -78,51 +71,29 @@ export interface K8sDetailsMetadataConfig<T> {
render?: (value: string | number, entity: T) => React.ReactNode;
}
export type K8sDetailsCountConfig<T> = EntityCountConfig<T>;
export interface K8sDetailsFilters {
filter: { expression: string };
start: number;
end: number;
}
export interface CustomTabRenderProps<T> {
entity: T;
timeRange: { startTime: number; endTime: number };
selectedInterval: Time;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
}
export interface CustomTab<T> {
key: string;
label: string;
icon: React.ReactNode;
render: (props: CustomTabRenderProps<T>) => React.ReactNode;
}
export interface K8sBaseDetailsProps<T> {
category: InfraMonitoringEntity;
eventCategory: string;
// Data fetching configuration
getSelectedItemExpression: (params: SelectedItemParams) => string;
getSelectedItemExpression: (selectedItem: string) => string;
fetchEntityData: (
filters: K8sDetailsFilters,
signal?: AbortSignal,
) => Promise<{ data: T | null; error?: APIError | null }>;
) => Promise<{ data: T | null; error?: string | null }>;
// Entity configuration
getEntityName: (entity: T) => string;
getInitialLogTracesExpression: (entity: T) => string;
getInitialEventsExpression: (entity: T) => string;
metadataConfig: K8sDetailsMetadataConfig<T>[];
countsConfig?: K8sDetailsCountConfig<T>[];
getCountsFilterExpression?: (entity: T) => string;
entityWidgetInfo: {
title: string;
yAxisUnit: string;
docPath?: string;
}[];
getEntityQueryPayload: (
entity: T,
@@ -139,7 +110,20 @@ export interface K8sBaseDetailsProps<T> {
showTraces?: boolean;
showEvents?: boolean;
};
customTabs?: Array<CustomTab<T>>;
customTabs?: Array<{
key: string;
label: string;
icon: React.ReactNode;
render: (props: {
entity: T;
timeRange: { startTime: number; endTime: number };
selectedInterval: Time;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
}) => React.ReactNode;
}>;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
@@ -152,8 +136,6 @@ export default function K8sBaseDetails<T>({
getInitialLogTracesExpression,
getInitialEventsExpression,
metadataConfig,
countsConfig,
getCountsFilterExpression,
entityWidgetInfo,
getEntityQueryPayload,
queryKeyPrefix,
@@ -170,9 +152,7 @@ export default function K8sBaseDetails<T>({
const isDarkMode = useIsDarkMode();
const [selectedItemParams, setSelectedItemParams] =
useInfraMonitoringSelectedItemParams();
const selectedItem = selectedItemParams.selectedItem;
const [selectedItem, setSelectedItem] = useInfraMonitoringSelectedItem();
const entityQueryKey = useMemo(
() =>
@@ -180,17 +160,8 @@ export default function K8sBaseDetails<T>({
selectedTime,
`${queryKeyPrefix}EntityDetails`,
selectedItem,
selectedItemParams.clusterName,
selectedItemParams.namespaceName,
),
[
queryKeyPrefix,
selectedItem,
selectedItemParams.clusterName,
selectedItemParams.namespaceName,
selectedTime,
getAutoRefreshQueryKey,
],
[queryKeyPrefix, selectedItem, selectedTime, getAutoRefreshQueryKey],
);
const {
@@ -207,7 +178,7 @@ export default function K8sBaseDetails<T>({
const { minTime, maxTime } = getMinMaxTime();
const start = Math.floor(minTime / NANO_SECOND_MULTIPLIER);
const end = Math.floor(maxTime / NANO_SECOND_MULTIPLIER);
const expression = getSelectedItemExpression(selectedItemParams);
const expression = getSelectedItemExpression(selectedItem);
return fetchEntityData({ filter: { expression }, start, end }, signal);
},
@@ -232,8 +203,8 @@ export default function K8sBaseDetails<T>({
}, [entity, getInitialEventsExpression]);
const handleClose = useCallback((): void => {
setSelectedItemParams(null);
}, [setSelectedItemParams]);
setSelectedItem(null);
}, [setSelectedItem]);
const entityName = entity ? getEntityName(entity) : '';
@@ -275,33 +246,6 @@ export default function K8sBaseDetails<T>({
const [selectedView, setSelectedView] = useInfraMonitoringView();
const effectiveView = hideDetailViewTabs ? VIEW_TYPES.METRICS : selectedView;
const validTabs = useMemo(() => {
const tabs: string[] = [];
if (tabVisibility.showMetrics) {
tabs.push(VIEW_TYPES.METRICS);
}
if (tabVisibility.showLogs) {
tabs.push(VIEW_TYPES.LOGS);
}
if (tabVisibility.showTraces) {
tabs.push(VIEW_TYPES.TRACES);
}
if (tabVisibility.showEvents) {
tabs.push(VIEW_TYPES.EVENTS);
}
if (customTabs) {
tabs.push(...customTabs.map((t) => t.key));
}
return tabs;
}, [tabVisibility, customTabs]);
useEffect(() => {
if (!hideDetailViewTabs && !validTabs.includes(selectedView)) {
const firstValid = validTabs[0] || VIEW_TYPES.METRICS;
void setSelectedView(firstValid);
}
}, [hideDetailViewTabs, selectedView, validTabs, setSelectedView]);
const [, setLogFiltersParam] = useInfraMonitoringLogFilters();
const [, setTracesFiltersParam] = useInfraMonitoringTracesFilters();
const [, setEventsFiltersParam] = useInfraMonitoringEventsFilters();
@@ -337,10 +281,7 @@ export default function K8sBaseDetails<T>({
}
}, [getMinMaxTime, selectedTime]);
const handleTabChange = (value: string | null): void => {
if (!value) {
return;
}
const handleTabChange = (value: string): void => {
setSelectedView(value);
setLogFiltersParam(null);
setTracesFiltersParam(null);
@@ -483,18 +424,12 @@ export default function K8sBaseDetails<T>({
{isEntityLoading && <LoadingContainer />}
{(isEntityError || hasResponseError) && (
<div className="entity-error-container">
<ErrorContent
error={
entityResponse?.error ??
(entityError instanceof APIError ? entityError : null) ?? {
code: 500,
message:
entityError instanceof Error
? entityError.message
: 'Failed to load entity details',
}
}
/>
<Typography.Text color="danger">
{entityResponse?.error ||
(entityError instanceof Error
? entityError.message
: 'Failed to load entity details')}
</Typography.Text>
</div>
)}
{entity && !isEntityLoading && !hasResponseError && (
@@ -532,19 +467,6 @@ export default function K8sBaseDetails<T>({
})}
</div>
</div>
{countsConfig &&
countsConfig.length > 0 &&
selectedItem &&
getCountsFilterExpression && (
<EntityCountsSection
entity={entity}
countsConfig={countsConfig}
selectedItem={selectedItem}
filterExpression={getCountsFilterExpression(entity)}
closeDrawer={handleClose}
/>
)}
</div>
{!hideDetailViewTabs && (

View File

@@ -10,21 +10,19 @@ import TanStackTable, {
} 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 APIError from 'types/api/error';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
InfraMonitoringEntity,
} from '../constants';
import {
SelectedItemParams,
useInfraMonitoringGroupBy,
useInfraMonitoringOrderBy,
useInfraMonitoringSelectedItemParams,
useInfraMonitoringStatusFilter,
} from '../hooks';
import { useInfraMonitoringLineClamp } from '../components';
@@ -34,14 +32,13 @@ import K8sHeader from './K8sHeader';
import { K8sPaginationWarning } from './K8sPaginationWarning';
import { K8sBaseFilters } from './types';
import { getGroupedByMeta } from './utils';
import { K8sInstrumentationChecksCallout } from './components/K8sInstrumentationChecksCallout/K8sInstrumentationChecksCallout';
import styles from './K8sBaseList.module.scss';
import cx from 'classnames';
export type K8sBaseListEmptyStateContext = {
isError: boolean;
error?: APIError | null;
error?: string | null;
totalCount: number;
hasFilters: boolean;
isLoading: boolean;
@@ -52,10 +49,7 @@ export type K8sBaseListEmptyStateContext = {
/** Base type constraint for K8s entity data */
export type K8sEntityData = { meta?: Record<string, string> | null };
export type K8sBaseListProps<
T extends K8sEntityData,
TItemKey extends string | SelectedItemParams = string,
> = {
export type K8sBaseListProps<T extends K8sEntityData> = {
controlListPrefix?: React.ReactNode;
leftFilters?: React.ReactNode;
entity: InfraMonitoringEntity;
@@ -68,15 +62,15 @@ export type K8sBaseListProps<
records?: T[];
data?: T[];
total: number;
error?: APIError | null;
error?: string | null;
rawData?: unknown;
endTimeBeforeRetention?: boolean;
warning?: Querybuildertypesv5QueryWarnDataDTO | null;
}>;
/** Function to get the unique key for a row. */
getRowKey?: (record: T) => string;
/** Function to get the item key used for selection. Can return string or SelectedItemParams. */
getItemKey?: (record: T) => TItemKey;
/** Function to get the item key used for selection. Defaults to getRowKey if not provided. */
getItemKey?: (record: T) => string;
eventCategory: InfraMonitoringEvents;
renderEmptyState?: (
context: K8sBaseListEmptyStateContext,
@@ -84,10 +78,7 @@ export type K8sBaseListProps<
extraQueryKeyParts?: string[];
};
export function K8sBaseList<
T extends K8sEntityData,
TItemKey extends string | SelectedItemParams = string,
>({
export function K8sBaseList<T extends K8sEntityData>({
controlListPrefix,
leftFilters,
entity,
@@ -98,16 +89,17 @@ export function K8sBaseList<
eventCategory,
renderEmptyState,
extraQueryKeyParts = [],
}: K8sBaseListProps<T, TItemKey>): JSX.Element {
}: K8sBaseListProps<T>): JSX.Element {
const { currentQuery } = useQueryBuilder();
const expression = currentQuery.builder.queryData[0]?.filter?.expression || '';
const lineClamp = useInfraMonitoringLineClamp();
const [groupBy] = useInfraMonitoringGroupBy();
const [orderBy] = useInfraMonitoringOrderBy();
const [statusFilter] = useInfraMonitoringStatusFilter();
const [selectedItemParams, setSelectedItemParams] =
useInfraMonitoringSelectedItemParams();
const selectedItem = selectedItemParams.selectedItem;
const [selectedItem, setSelectedItem] = useQueryState(
'selectedItem',
parseAsString,
);
const columnStorageKey = `k8s-${entity}-columns`;
const hiddenColumnIds = useHiddenColumnIds(columnStorageKey);
@@ -234,17 +226,9 @@ export function K8sBaseList<
}, [eventCategory, totalCount]);
const handleRowClick = useCallback(
(_record: T, itemKey: TItemKey): void => {
(_record: T, itemKey: string): void => {
if (groupBy.length === 0) {
if (typeof itemKey === 'object' && itemKey !== null) {
setSelectedItemParams(itemKey);
} else {
setSelectedItemParams({
selectedItem: itemKey,
clusterName: null,
namespaceName: null,
});
}
void setSelectedItem(itemKey);
}
void logEvent(InfraMonitoringEvents.ItemClicked, {
@@ -253,43 +237,18 @@ export function K8sBaseList<
category: eventCategory,
});
},
[eventCategory, groupBy.length, setSelectedItemParams],
[eventCategory, groupBy.length, setSelectedItem],
);
const handleRowClickNewTab = useCallback(
(_record: T, itemKey: TItemKey): void => {
(_record: T, itemKey: string): void => {
if (groupBy.length > 0) {
return;
}
// Build URL with selectedItem params
// Build URL with selectedItem param
const url = new URL(window.location.href);
if (typeof itemKey === 'object' && itemKey !== null) {
const params = itemKey;
if (params.selectedItem) {
url.searchParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM,
params.selectedItem,
);
}
if (params.clusterName) {
url.searchParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM_CLUSTER_NAME,
params.clusterName,
);
}
if (params.namespaceName) {
url.searchParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM_NAMESPACE_NAME,
params.namespaceName,
);
}
} else {
url.searchParams.set(
INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM,
itemKey,
);
}
url.searchParams.set('selectedItem', itemKey);
openInNewTab(url.pathname + url.search);
void logEvent(InfraMonitoringEvents.ItemClicked, {
@@ -315,7 +274,7 @@ export function K8sBaseList<
rowKey: string,
groupMeta?: Record<string, string>,
): JSX.Element => (
<K8sExpandedRow<T, TItemKey>
<K8sExpandedRow<T>
rowKey={rowKey}
groupMeta={groupMeta}
entity={entity}
@@ -379,8 +338,6 @@ export function K8sBaseList<
cancelQuery={cancelQuery}
/>
<div ref={containerRef} className={styles.tableContainer}>
<K8sInstrumentationChecksCallout entity={entity} />
{isError && (
<Typography>
{data?.error?.toString() || 'Something went wrong'}
@@ -390,7 +347,7 @@ export function K8sBaseList<
{showEmptyState ? (
<div className={styles.emptyStateContainer}>{emptyTableMessage}</div>
) : (
<TanStackTable<T, TItemKey>
<TanStackTable<T>
data={pageData}
columns={tableColumns}
columnStorageKey={columnStorageKey}

View File

@@ -44,11 +44,13 @@
width: 32px;
}
.errorContent {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-3);
max-width: 500px;
padding: 24px;
.errorIcon {
color: var(--danger-background);
}
.actions {
display: flex;
align-items: center;
gap: 8px;
margin-top: 16px;
}

View File

@@ -1,4 +1,8 @@
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import { useCallback } from 'react';
import { Button } from '@signozhq/ui/button';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import history from 'lib/history';
import { LifeBuoy, TriangleAlert } from '@signozhq/icons';
import emptyStateUrl from '@/assets/Icons/emptyState.svg';
import eyesEmojiUrl from '@/assets/Images/eyesEmoji.svg';
@@ -9,12 +13,26 @@ import styles from './K8sEmptyState.module.scss';
type K8sEmptyStateProps = Partial<K8sBaseListEmptyStateContext>;
const handleContactSupport = (isCloudUser: boolean): void => {
if (isCloudUser) {
history.push('/support');
} else {
window.open('https://signoz.io/slack', '_blank');
}
};
export function K8sEmptyState({
isError,
error,
isLoading,
endTimeBeforeRetention,
}: K8sEmptyStateProps): JSX.Element | null {
const { isCloudUser } = useGetTenantLicense();
const handleSupport = useCallback(() => {
handleContactSupport(isCloudUser);
}, [isCloudUser]);
if (isLoading) {
return null;
}
@@ -22,15 +40,25 @@ export function K8sEmptyState({
if (isError || error) {
return (
<div className={styles.container}>
<div className={styles.errorContent}>
<ErrorContent
error={
error ?? {
code: 500,
message: 'An error occurred while fetching data.',
}
}
/>
<div className={styles.content}>
<TriangleAlert size={32} className={styles.errorIcon} />
<span className={styles.message}>
{error || 'An error occurred while fetching data.'}
</span>
<p>
Our team is getting on top to resolve this. Please reach out to support if
the issue persists.
</p>
<div className={styles.actions}>
<Button
onClick={handleSupport}
variant="solid"
color="secondary"
prefix={<LifeBuoy size={14} />}
>
Contact Support
</Button>
</div>
</div>
</div>
);

View File

@@ -4,7 +4,6 @@ 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 APIError from 'types/api/error';
import TanStackTable, {
SortState,
TableColumnDef,
@@ -23,11 +22,10 @@ import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
import { InfraMonitoringEntity } from '../constants';
import {
SelectedItemParams,
useInfraMonitoringGroupBy,
useInfraMonitoringOrderBy,
useInfraMonitoringPageListing,
useInfraMonitoringSelectedItemParams,
useInfraMonitoringSelectedItem,
} from '../hooks';
import { K8sBaseFilters } from './types';
@@ -36,7 +34,7 @@ import { buildExpressionFromGroupMeta } from './utils';
const EXPANDED_ROW_LIMIT = 10;
export type K8sExpandedRowProps<T, TItemKey = string> = {
export type K8sExpandedRowProps<T> = {
/** Pre-computed row key from parent table (includes group prefix + duplicate handling) */
rowKey: string;
/** Group metadata for building filters */
@@ -52,7 +50,7 @@ export type K8sExpandedRowProps<T, TItemKey = string> = {
records?: T[];
data?: T[];
total: number;
error?: APIError | null;
error?: string | null;
rawData?: unknown;
warning?: Querybuildertypesv5QueryWarnDataDTO | null;
}>;
@@ -61,10 +59,10 @@ export type K8sExpandedRowProps<T, TItemKey = 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. */
getItemKey?: (record: T) => TItemKey;
getItemKey?: (record: T) => string;
};
export function K8sExpandedRow<T, TItemKey = string>({
export function K8sExpandedRow<T>({
rowKey,
groupMeta,
entity,
@@ -73,13 +71,13 @@ export function K8sExpandedRow<T, TItemKey = string>({
extraQueryKeyParts = [],
getRowKey,
getItemKey,
}: K8sExpandedRowProps<T, TItemKey>): JSX.Element {
}: K8sExpandedRowProps<T>): JSX.Element {
const [, setGroupBy] = useInfraMonitoringGroupBy();
const [, setCurrentPage] = useInfraMonitoringPageListing();
const { currentQuery } = useQueryBuilder();
const parentExpression =
currentQuery.builder.queryData[0]?.filter?.expression || '';
const [, setSelectedItemParams] = useInfraMonitoringSelectedItemParams();
const [, setSelectedItem] = useInfraMonitoringSelectedItem();
const [, setMainOrderBy] = useInfraMonitoringOrderBy();
const { safeNavigate } = useSafeNavigate();
const urlQuery = useUrlQuery();
@@ -177,18 +175,10 @@ export function K8sExpandedRow<T, TItemKey = string>({
const expandedData = data?.data ?? [];
const handleRowClick = useCallback(
(_row: T, itemKey: TItemKey): void => {
if (typeof itemKey === 'object' && itemKey !== null) {
setSelectedItemParams(itemKey as unknown as SelectedItemParams);
} else {
setSelectedItemParams({
selectedItem: itemKey as string,
clusterName: null,
namespaceName: null,
});
}
(_row: T, itemKey: string): void => {
void setSelectedItem(itemKey);
},
[setSelectedItemParams],
[setSelectedItem],
);
const handleViewAllClick = (): void => {
@@ -249,7 +239,7 @@ export function K8sExpandedRow<T, TItemKey = string>({
<div data-testid="expanded-table">
<TanStackTableStateProvider>
<TanStackTable<T, TItemKey>
<TanStackTable<T>
data={expandedData}
columns={tableColumns}
columnStorageKey={storageKey}

View File

@@ -1,221 +0,0 @@
import { Box } from '@signozhq/icons';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { act, render, waitFor } from 'tests/test-utils';
import {
InfraMonitoringEntity,
INFRA_MONITORING_K8S_PARAMS_KEYS,
VIEW_TYPES,
} from '../../constants';
import K8sBaseDetails from '../K8sBaseDetails';
jest.mock('container/TopNav/DateTimeSelectionV2/index.tsx', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="mock-datetime" />,
}));
type TestEntity = {
name: string;
namespace: string;
cluster: string;
};
const mockEntity: TestEntity = {
name: 'test-pod',
namespace: 'default',
cluster: 'test-cluster',
};
function createBaseProps() {
return {
category: InfraMonitoringEntity.PODS,
eventCategory: 'Pod',
getSelectedItemExpression: (): string => 'k8s.pod.name = "test-pod"',
fetchEntityData: jest
.fn()
.mockResolvedValue({ data: mockEntity, error: null }),
getEntityName: (e: TestEntity): string => e.name,
getInitialLogTracesExpression: (): string => 'k8s.pod.name = "test-pod"',
getInitialEventsExpression: (): string => 'k8s.pod.name = "test-pod"',
metadataConfig: [
{ label: 'Name', getValue: (e: TestEntity): string => e.name },
],
entityWidgetInfo: [{ title: 'CPU', yAxisUnit: 'percent' }],
getEntityQueryPayload: jest.fn().mockReturnValue([]),
queryKeyPrefix: 'testPod',
};
}
interface RenderOptions {
view?: string;
tabsConfig?: {
showMetrics?: boolean;
showLogs?: boolean;
showTraces?: boolean;
showEvents?: boolean;
};
customTabs?: Array<{
key: string;
label: string;
icon: React.ReactNode;
render: () => React.ReactNode;
}>;
}
function renderK8sBaseDetails({
view = VIEW_TYPES.METRICS,
tabsConfig,
customTabs,
}: RenderOptions = {}) {
const searchParams: Record<string, string> = {
[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM]: 'test-pod',
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
};
return render(
<NuqsTestingAdapter searchParams={searchParams}>
<K8sBaseDetails<TestEntity>
{...createBaseProps()}
tabsConfig={tabsConfig}
customTabs={customTabs}
/>
</NuqsTestingAdapter>,
);
}
function getSelectedTabText(): string | null {
const selectedTab = document.querySelector('[aria-checked="true"]');
return selectedTab?.textContent ?? null;
}
describe('K8sBaseDetails - Tab Validation', () => {
it('should reset view to METRICS when selected view is invalid', async () => {
act(() => {
renderK8sBaseDetails({ view: 'invalid-tab' });
});
await waitFor(() => {
expect(screen.getAllByText('test-pod').length).toBeGreaterThan(0);
});
await waitFor(() => {
expect(getSelectedTabText()).toContain('Metrics');
});
});
it('should reset to first available tab when METRICS is disabled and view is invalid', async () => {
act(() => {
renderK8sBaseDetails({
view: 'invalid-tab',
tabsConfig: { showMetrics: false },
});
});
await waitFor(() => {
expect(screen.getAllByText('test-pod').length).toBeGreaterThan(0);
});
await waitFor(() => {
expect(getSelectedTabText()).toContain('Logs');
});
});
it('should reset to custom tab when all standard tabs disabled and custom tab exists', async () => {
const customTabKey = 'pod-metrics';
act(() => {
renderK8sBaseDetails({
view: 'invalid-tab',
tabsConfig: {
showMetrics: false,
showLogs: false,
showTraces: false,
showEvents: false,
},
customTabs: [
{
key: customTabKey,
label: 'Pod Metrics',
icon: <Box size={14} />,
render: (): React.ReactNode => <div>Custom Tab</div>,
},
],
});
});
await waitFor(() => {
expect(screen.getAllByText('test-pod').length).toBeGreaterThan(0);
});
await waitFor(() => {
expect(getSelectedTabText()).toContain('Pod Metrics');
});
});
it('should NOT reset view when selected view is valid', async () => {
act(() => {
renderK8sBaseDetails({ view: VIEW_TYPES.LOGS });
});
await waitFor(() => {
expect(screen.getAllByText('test-pod').length).toBeGreaterThan(0);
});
await waitFor(() => {
expect(getSelectedTabText()).toContain('Logs');
});
});
it('should NOT reset view when custom tab is selected and exists', async () => {
const customTabKey = 'pod-metrics';
act(() => {
renderK8sBaseDetails({
view: customTabKey,
customTabs: [
{
key: customTabKey,
label: 'Pod Metrics',
icon: <Box size={14} />,
render: (): React.ReactNode => <div>Custom Tab</div>,
},
],
});
});
await waitFor(() => {
expect(screen.getAllByText('test-pod').length).toBeGreaterThan(0);
});
await waitFor(() => {
expect(getSelectedTabText()).toContain('Pod Metrics');
});
});
it('should keep the selected tab when the active tab is clicked again (untoggle guard)', async () => {
const user = userEvent.setup();
act(() => {
renderK8sBaseDetails({ view: VIEW_TYPES.LOGS });
});
await waitFor(() => {
expect(screen.getAllByText('test-pod').length).toBeGreaterThan(0);
});
await waitFor(() => {
expect(getSelectedTabText()).toContain('Logs');
});
const selectedTab = document.querySelector('[aria-checked="true"]');
expect(selectedTab).not.toBeNull();
await user.click(selectedTab as Element);
await waitFor(() => {
expect(getSelectedTabText()).toContain('Logs');
});
});
});

View File

@@ -9,8 +9,6 @@ import { TooltipProvider } from '@signozhq/ui/tooltip';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { InfraMonitoringEvents } from 'constants/events';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import {
NuqsTestingAdapter,
OnUrlUpdateFunction,
@@ -19,13 +17,11 @@ import {
import { AppProvider } from 'providers/App/App';
import TimezoneProvider from 'providers/Timezone';
import store from 'store';
import APIError from 'types/api/error';
import { openInNewTab } from 'utils/navigation';
import { TableColumnDef } from 'components/TanStackTableView';
import { InfraMonitoringEntity } from '../../constants';
import { SelectedItemParams } from '../../hooks';
window.ResizeObserver =
window.ResizeObserver ||
@@ -169,14 +165,11 @@ function createTestColumnsWithGroup(): TableColumnDef<TestItemWithGroup>[] {
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function renderComponent<
T extends K8sEntityData,
TItemKey extends string | SelectedItemParams = string,
>({
function renderComponent<T extends K8sEntityData>({
queryParams,
onUrlUpdate,
...props
}: K8sBaseListProps<T, TItemKey> & {
}: K8sBaseListProps<T> & {
queryParams?: Record<string, string>;
onUrlUpdate?: OnUrlUpdateFunction;
}) {
@@ -203,7 +196,7 @@ function renderComponent<
value={{ viewportHeight: 800, itemHeight: 50 }}
>
<TooltipProvider>
<K8sBaseList<T, TItemKey> {...props} />
<K8sBaseList {...props} />
</TooltipProvider>
</VirtuosoMockContext.Provider>
</NuqsTestingAdapter>
@@ -633,15 +626,7 @@ describe('K8sBaseList', () => {
fetchListDataMock.mockResolvedValue({
data: [],
total: 0,
error: new APIError({
httpStatusCode: 500,
error: {
code: '500',
message: 'Failed to fetch pods',
url: '',
errors: [],
},
}),
error: 'Failed to fetch pods',
});
renderComponent<TestItem>({
@@ -956,413 +941,4 @@ describe('K8sBaseList', () => {
});
});
});
describe('with object itemKey (selectedItem + cluster + namespace params)', () => {
const itemId = 'obj-item';
const onUrlUpdateMock = jest.fn<void, [UrlUpdateEvent]>();
const fetchListDataMock = jest.fn<
ReturnType<
NonNullable<K8sBaseListProps<TestItemWithTitle>['fetchListData']>
>,
Parameters<NonNullable<K8sBaseListProps<TestItemWithTitle>['fetchListData']>>
>();
const getLatestParam = (key: string): string | undefined =>
onUrlUpdateMock.mock.calls
.map((call) => call[0].searchParams.get(key))
.filter(Boolean)
.pop() as string | undefined;
beforeEach(() => {
onUrlUpdateMock.mockClear();
fetchListDataMock.mockClear();
openInNewTabMock.mockClear();
fetchListDataMock.mockResolvedValue({
data: [{ id: `PodId:${itemId}`, title: `PodTitle:${itemId}` }],
total: 1,
error: null,
});
});
it('should set selectedItem, cluster and namespace params on row click', async () => {
const user = userEvent.setup();
renderComponent<TestItemWithTitle, SelectedItemParams>({
onUrlUpdate: onUrlUpdateMock,
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumnsWithTitle(),
getRowKey: (row): string => row.id,
getItemKey: (row): SelectedItemParams => ({
selectedItem: row.id,
clusterName: 'prod-cluster',
namespaceName: 'default-ns',
}),
});
const firstRowEl = await screen.findByText(`PodId:${itemId}`);
await user.click(firstRowEl);
await waitFor(() => {
expect(getLatestParam('selectedItem')).toBe(`PodId:${itemId}`);
expect(getLatestParam('selectedItemClusterName')).toBe('prod-cluster');
expect(getLatestParam('selectedItemNamespaceName')).toBe('default-ns');
});
});
it('should include cluster and namespace params in new tab URL on ctrl+click', async () => {
renderComponent<TestItemWithTitle, SelectedItemParams>({
onUrlUpdate: onUrlUpdateMock,
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumnsWithTitle(),
getRowKey: (row): string => row.id,
getItemKey: (row): SelectedItemParams => ({
selectedItem: row.id,
clusterName: 'prod-cluster',
namespaceName: 'default-ns',
}),
});
const firstRow = await screen.findByText(`PodId:${itemId}`);
fireEvent.click(firstRow, { ctrlKey: true });
await waitFor(() => {
expect(openInNewTabMock).toHaveBeenCalledTimes(1);
});
const url = openInNewTabMock.mock.calls[0][0] as string;
expect(url).toContain(`selectedItem=PodId%3A${itemId}`);
expect(url).toContain('selectedItemClusterName=prod-cluster');
expect(url).toContain('selectedItemNamespaceName=default-ns');
});
it('should omit null cluster/namespace params in new tab URL', async () => {
renderComponent<TestItemWithTitle, SelectedItemParams>({
onUrlUpdate: onUrlUpdateMock,
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumnsWithTitle(),
getRowKey: (row): string => row.id,
getItemKey: (row): SelectedItemParams => ({
selectedItem: row.id,
clusterName: null,
namespaceName: null,
}),
});
const firstRow = await screen.findByText(`PodId:${itemId}`);
fireEvent.click(firstRow, { ctrlKey: true });
await waitFor(() => {
expect(openInNewTabMock).toHaveBeenCalledTimes(1);
});
const url = openInNewTabMock.mock.calls[0][0] as string;
expect(url).toContain(`selectedItem=PodId%3A${itemId}`);
expect(url).not.toContain('selectedItemClusterName');
expect(url).not.toContain('selectedItemNamespaceName');
});
});
describe('instrumentation checks callout', () => {
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,
});
});
it('should not render callout when ready is true', async () => {
server.use(
rest.get('http://localhost/api/v2/infra_monitoring/checks', (_, res, ctx) =>
res(
ctx.json({
status: 'success',
data: {
ready: true,
type: 'pods',
presentDefaultEnabledMetrics: [
{
associatedComponent: { name: 'otel-collector' },
metrics: ['k8s.pod.cpu.usage'],
},
],
},
}),
),
),
);
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
await screen.findByText('item-1');
expect(screen.queryByText('Instrumentation checks')).not.toBeInTheDocument();
});
it('should not render callout when no entries exist', async () => {
server.use(
rest.get('http://localhost/api/v2/infra_monitoring/checks', (_, res, ctx) =>
res(
ctx.json({
status: 'success',
data: {
ready: false,
type: 'pods',
presentDefaultEnabledMetrics: null,
presentOptionalMetrics: null,
presentRequiredAttributes: null,
missingDefaultEnabledMetrics: null,
missingOptionalMetrics: null,
missingRequiredAttributes: null,
},
}),
),
),
);
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
await screen.findByText('item-1');
expect(screen.queryByText('Instrumentation checks')).not.toBeInTheDocument();
});
it('should render callout with present entries', async () => {
server.use(
rest.get('http://localhost/api/v2/infra_monitoring/checks', (_, res, ctx) =>
res(
ctx.json({
status: 'success',
data: {
ready: false,
type: 'pods',
presentDefaultEnabledMetrics: [
{
associatedComponent: { name: 'otel-collector' },
metrics: ['k8s.pod.cpu.usage', 'k8s.pod.memory.usage'],
},
],
presentOptionalMetrics: null,
presentRequiredAttributes: null,
missingDefaultEnabledMetrics: null,
missingOptionalMetrics: null,
missingRequiredAttributes: null,
},
}),
),
),
);
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
await screen.findByText('Instrumentation checks');
await expect(
screen.findByText('Default enabled metrics'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('k8s.pod.cpu.usage, k8s.pod.memory.usage'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('otel-collector'),
).resolves.toBeInTheDocument();
});
it('should render callout with missing entries', async () => {
server.use(
rest.get('http://localhost/api/v2/infra_monitoring/checks', (_, res, ctx) =>
res(
ctx.json({
status: 'success',
data: {
ready: false,
type: 'pods',
presentDefaultEnabledMetrics: null,
presentOptionalMetrics: null,
presentRequiredAttributes: null,
missingDefaultEnabledMetrics: [
{
associatedComponent: { name: 'otel-collector' },
metrics: ['k8s.pod.cpu.limit'],
documentationLink: 'https://example.com/docs',
},
],
missingOptionalMetrics: null,
missingRequiredAttributes: null,
},
}),
),
),
);
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
await screen.findByText('Instrumentation checks');
await expect(
screen.findByText('Missing default metrics'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('k8s.pod.cpu.limit'),
).resolves.toBeInTheDocument();
await expect(screen.findByText('Learn here')).resolves.toBeInTheDocument();
});
it('should trigger recheck on button click', async () => {
let recheckCallCount = 0;
server.use(
rest.get(
'http://localhost/api/v2/infra_monitoring/checks',
(_, res, ctx) => {
recheckCallCount++;
return res(
ctx.json({
status: 'success',
data: {
ready: false,
type: 'pods',
presentDefaultEnabledMetrics: [
{
associatedComponent: { name: 'otel-collector' },
metrics: ['k8s.pod.cpu.usage'],
},
],
},
}),
);
},
),
);
const user = userEvent.setup();
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
await screen.findByText('Instrumentation checks');
const initialCallCount = recheckCallCount;
const recheckBtn = screen.getByTestId('instrumentation-checks-recheck-btn');
await user.click(recheckBtn);
await waitFor(() => {
expect(recheckCallCount).toBeGreaterThan(initialCallCount);
});
});
it('should render both present and missing entries', async () => {
server.use(
rest.get('http://localhost/api/v2/infra_monitoring/checks', (_, res, ctx) =>
res(
ctx.json({
status: 'success',
data: {
ready: false,
type: 'pods',
presentDefaultEnabledMetrics: [
{
associatedComponent: { name: 'otel-collector' },
metrics: ['k8s.pod.cpu.usage'],
},
],
presentOptionalMetrics: null,
presentRequiredAttributes: [
{
associatedComponent: { name: 'otel-collector' },
attributes: ['k8s.namespace.name'],
},
],
missingDefaultEnabledMetrics: [
{
associatedComponent: { name: 'otel-collector' },
metrics: ['k8s.pod.memory.limit'],
},
],
missingOptionalMetrics: null,
missingRequiredAttributes: null,
},
}),
),
),
);
renderComponent<TestItem>({
entity: InfraMonitoringEntity.PODS,
eventCategory: InfraMonitoringEvents.Pod,
fetchListData: fetchListDataMock,
tableColumns: createTestColumns(),
getRowKey: (row): string => row.id,
getItemKey: (row): string => row.id,
});
await screen.findByText('Instrumentation checks');
// Present entries
await expect(
screen.findByText('Default enabled metrics'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('k8s.pod.cpu.usage'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('Required attributes'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('k8s.namespace.name'),
).resolves.toBeInTheDocument();
// Missing entries
await expect(
screen.findByText('Missing default metrics'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('k8s.pod.memory.limit'),
).resolves.toBeInTheDocument();
});
});
});

View File

@@ -1,132 +0,0 @@
import {
buildEventsExpression,
buildExpressionFromSelectedItemParams,
buildLogsTracesExpression,
} from 'container/InfraMonitoringK8sV2/Base/utils';
describe('buildExpressionFromSelectedItemParams', () => {
it('should build expression from params with all values', () => {
const result = buildExpressionFromSelectedItemParams(
{
selectedItem: 'nginx',
clusterName: 'prod',
namespaceName: 'default',
},
'k8s.deployment.name',
);
expect(result).toBe(
"k8s.deployment.name = 'nginx' AND k8s.cluster.name = 'prod' AND k8s.namespace.name = 'default'",
);
});
it('should skip null values', () => {
const result = buildExpressionFromSelectedItemParams(
{
selectedItem: 'nginx',
clusterName: null,
namespaceName: 'default',
},
'k8s.deployment.name',
);
expect(result).toBe(
"k8s.deployment.name = 'nginx' AND k8s.namespace.name = 'default'",
);
});
it('should handle only selectedItem', () => {
const result = buildExpressionFromSelectedItemParams(
{
selectedItem: 'pod-123',
clusterName: null,
namespaceName: null,
},
'k8s.pod.uid',
);
expect(result).toBe("k8s.pod.uid = 'pod-123'");
});
});
describe('buildEventsExpression', () => {
it('should build expression with kind, name, cluster and attribute-context namespace', () => {
const result = buildEventsExpression({
objectKind: 'Deployment',
objectName: 'nginx',
clusterName: 'prod',
namespaceName: 'default',
});
expect(result).toBe(
"k8s.object.kind = 'Deployment' AND k8s.object.name = 'nginx' AND k8s.cluster.name = 'prod' AND attribute.k8s.namespace.name = 'default'",
);
});
it('should always include kind and name, even when name is empty', () => {
const result = buildEventsExpression({
objectKind: 'Node',
objectName: '',
});
expect(result).toBe("k8s.object.kind = 'Node' AND k8s.object.name = ''");
});
it('should skip cluster and namespace clauses when values are missing', () => {
const result = buildEventsExpression({
objectKind: 'Pod',
objectName: 'pod-1',
clusterName: null,
namespaceName: undefined,
});
expect(result).toBe("k8s.object.kind = 'Pod' AND k8s.object.name = 'pod-1'");
});
it('should escape values with quotes', () => {
const result = buildEventsExpression({
objectKind: 'Pod',
objectName: "po'd",
namespaceName: 'ns-1',
});
expect(result).toBe(
`k8s.object.kind = 'Pod' AND k8s.object.name = 'po\\'d' AND attribute.k8s.namespace.name = 'ns-1'`,
);
});
});
describe('buildLogsTracesExpression', () => {
it('should build expression with main attribute, cluster and namespace', () => {
const result = buildLogsTracesExpression({
mainAttributeKey: 'k8s.deployment.name',
mainAttributeValue: 'nginx',
clusterName: 'prod',
namespaceName: 'default',
});
expect(result).toBe(
"k8s.deployment.name = 'nginx' AND k8s.cluster.name = 'prod' AND k8s.namespace.name = 'default'",
);
});
it('should skip clauses with empty values', () => {
const result = buildLogsTracesExpression({
mainAttributeKey: 'k8s.node.name',
mainAttributeValue: 'node-1',
clusterName: '',
namespaceName: null,
});
expect(result).toBe("k8s.node.name = 'node-1'");
});
it('should return empty string when all values are missing', () => {
const result = buildLogsTracesExpression({
mainAttributeKey: 'k8s.pod.name',
mainAttributeValue: '',
});
expect(result).toBe('');
});
});

View File

@@ -1,41 +0,0 @@
.countsContainer {
display: flex;
gap: var(--spacing-6);
margin-top: var(--spacing-8);
flex-wrap: wrap;
}
.countCard {
position: relative;
flex: 1;
min-width: 120px;
max-width: 180px;
border: 1px solid var(--l3-border);
border-radius: 4px;
display: flex;
flex-direction: column;
gap: var(--spacing-2);
padding: var(--spacing-6);
}
.countLabel {
color: var(--l2-foreground);
letter-spacing: var(--letter-spacing-wide);
text-transform: uppercase;
}
.countValue {
color: var(--l1-foreground);
font-family: var(--periscope-font-family-mono);
font-size: var(--font-size-xl);
}
.navigateButton {
position: absolute;
top: var(--spacing-4);
right: var(--spacing-4);
--button-padding: var(--spacing-1);
--button-height: 24px;
--button-width: 24px;
}

View File

@@ -1,107 +0,0 @@
import { Tooltip } from 'antd';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import { Compass } from '@signozhq/icons';
import { QueryParams } from 'constants/query';
import { initialQueriesMap } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { Link } from 'react-router-dom';
import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
InfraMonitoringEntity,
} from '../../../constants';
import styles from './EntityCountsSection.module.scss';
export interface EntityCountConfig<T> {
label: string;
getValue: (entity: T) => number;
targetCategory: InfraMonitoringEntity;
}
interface EntityCountsSectionProps<T> {
entity: T;
countsConfig: EntityCountConfig<T>[];
selectedItem: string;
filterExpression: string;
closeDrawer: () => void;
}
export function EntityCountsSection<T>({
entity,
countsConfig,
selectedItem,
filterExpression,
closeDrawer,
}: EntityCountsSectionProps<T>): JSX.Element {
const buildNavigationUrl = (targetCategory: InfraMonitoringEntity): string => {
const defaultQuery = initialQueriesMap[DataSource.METRICS];
const compositeQuery = {
...defaultQuery,
id: uuid(),
builder: {
...defaultQuery.builder,
queryData: defaultQuery.builder.queryData.map((query) => ({
...query,
filter: { expression: filterExpression },
filters: { items: [], op: 'AND' as const },
})),
},
};
// TODO(H4ad): After https://github.com/SigNoz/signoz/pull/12038, inherit custom time of drawer to list
const urlParams = new URLSearchParams();
urlParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.CATEGORY, targetCategory);
urlParams.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(compositeQuery)),
);
return `${ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES}?${urlParams.toString()}`;
};
return (
<div className={styles.countsContainer}>
{countsConfig.map((config) => (
<div
key={config.label}
className={styles.countCard}
data-testid={`count-card-${config.label.toLowerCase().replace(/\s+/g, '-')}`}
>
<Typography.Text
color="muted"
size="small"
weight="medium"
className={styles.countLabel}
>
{config.label}
</Typography.Text>
<Typography.Text className={styles.countValue} size="xl" weight="semibold">
{config.getValue(entity) || '-'}
</Typography.Text>
<Link
to={buildNavigationUrl(config.targetCategory)}
onClick={closeDrawer}
data-testid={`navigate-${config.label.toLowerCase().replace(/\s+/g, '-')}`}
>
<Tooltip
title={`View ${config.label.toLowerCase()} of '${selectedItem}'`}
placement="top"
>
<Button
size="icon"
variant="ghost"
color="secondary"
className={styles.navigateButton}
prefix={<Compass size={14} />}
/>
</Tooltip>
</Link>
</div>
))}
</div>
);
}

View File

@@ -1,24 +0,0 @@
.checkContainer {
padding: 0px var(--spacing-4) var(--spacing-4) var(--spacing-4);
position: relative;
}
.container {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
.entriesList {
display: flex;
flex-direction: column;
gap: 4px;
}

View File

@@ -1,162 +0,0 @@
import { MouseEvent, useCallback, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import { Button } from '@signozhq/ui/button';
import { Callout } from '@signozhq/ui/callout';
import { RefreshCw } from '@signozhq/icons';
import setLocalStorage from 'api/browser/localstorage/set';
import {
invalidateGetChecks,
useGetChecks,
} from 'api/generated/services/inframonitoring';
import { InframonitoringtypesCheckTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEntity } from '../../../constants';
import styles from './K8sInstrumentationChecksCallout.module.scss';
import { MissingEntryRow } from './MissingEntryRow';
import { PresentEntryRow } from './PresentEntryRow';
import {
ENTITY_TO_CHECK_TYPE,
getStorageKey,
hasAnyEntries,
hasMissingEntries,
readExpandedState,
} from './utils';
export interface InstrumentationChecksCalloutProps {
entity: InfraMonitoringEntity;
}
export function K8sInstrumentationChecksCallout({
entity,
}: InstrumentationChecksCalloutProps): JSX.Element | null {
const checkType = ENTITY_TO_CHECK_TYPE[entity];
const queryClient = useQueryClient();
const [isExpanded, setIsExpanded] = useState(() => readExpandedState(entity));
const { data, isLoading, isFetching } = useGetChecks(
{ type: checkType as InframonitoringtypesCheckTypeDTO },
{ query: { enabled: !!checkType } },
);
const handleRecheck = useCallback(
(e: MouseEvent): void => {
e.preventDefault();
e.stopPropagation();
if (checkType) {
void invalidateGetChecks(queryClient, { type: checkType });
}
},
[queryClient, checkType],
);
const handleExpandToggle = useCallback((): void => {
setIsExpanded((prev) => {
const next = !prev;
setLocalStorage(getStorageKey(entity), String(next));
return next;
});
}, [entity]);
const checksData = data?.data;
const hasMissingItems = useMemo(
() => (checksData ? hasMissingEntries(checksData) : false),
[checksData],
);
if (!checkType || isLoading) {
return null;
}
if (!checksData || checksData.ready) {
return null;
}
if (!hasAnyEntries(checksData)) {
return null;
}
return (
<div className={styles.checkContainer}>
<Callout
type={hasMissingItems ? 'warning' : 'info'}
showIcon
size="medium"
title={
<div className={styles.header}>
Instrumentation checks
<Button
variant="outlined"
color="warning"
size="sm"
onClick={handleRecheck}
loading={isFetching}
prefix={<RefreshCw size={12} />}
data-testid="instrumentation-checks-recheck-btn"
>
Recheck
</Button>
</div>
}
action="expandable"
defaultExpanded={isExpanded}
onClick={handleExpandToggle}
>
<div className={styles.container} onClick={(e) => e.stopPropagation()}>
<div className={styles.entriesList}>
{checksData.presentDefaultEnabledMetrics?.map((entry) => (
<PresentEntryRow
key={`present-default-${entry.associatedComponent.name}`}
entry={entry}
typeLabel="Default enabled metrics"
itemType="metrics"
/>
))}
{checksData.presentOptionalMetrics?.map((entry) => (
<PresentEntryRow
key={`present-optional-${entry.associatedComponent.name}`}
entry={entry}
typeLabel="Optional metrics"
itemType="metrics"
/>
))}
{checksData.presentRequiredAttributes?.map((entry) => (
<PresentEntryRow
key={`present-attrs-${entry.associatedComponent.name}`}
entry={entry}
typeLabel="Required attributes"
itemType="attributes"
/>
))}
{checksData.missingDefaultEnabledMetrics?.map((entry) => (
<MissingEntryRow
key={`missing-default-${entry.associatedComponent.name}`}
entry={entry}
typeLabel="Missing default metrics"
itemType="metrics"
/>
))}
{checksData.missingOptionalMetrics?.map((entry) => (
<MissingEntryRow
key={`missing-optional-${entry.associatedComponent.name}`}
entry={entry}
typeLabel="Missing optional metrics"
itemType="metrics"
/>
))}
{checksData.missingRequiredAttributes?.map((entry) => (
<MissingEntryRow
key={`missing-attrs-${entry.associatedComponent.name}`}
entry={entry}
typeLabel="Missing required attributes"
itemType="attributes"
/>
))}
</div>
</div>
</Callout>
</div>
);
}

View File

@@ -1,27 +0,0 @@
.entryRow {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
color: var(--l1-foreground);
}
.missingIcon {
color: var(--warning-background);
flex-shrink: 0;
}
.learnMoreLink {
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: 4px;
svg {
margin-left: 4px;
}
}
.divider {
--divider-color: var(--callout-warning-border);
}

View File

@@ -1,66 +0,0 @@
import styles from './MissingEntryRow.module.scss';
import { ExternalLink, TriangleAlert } from '@signozhq/icons';
import { Typography } from '@signozhq/ui/typography';
import { Divider } from '@signozhq/ui/divider';
import type {
InframonitoringtypesMissingAttributesComponentEntryDTO,
InframonitoringtypesMissingMetricsComponentEntryDTO,
} from 'api/generated/services/sigNoz.schemas';
type MissingEntryRowProps =
| {
entry: InframonitoringtypesMissingMetricsComponentEntryDTO;
typeLabel: string;
itemType: 'metrics';
}
| {
entry: InframonitoringtypesMissingAttributesComponentEntryDTO;
typeLabel: string;
itemType: 'attributes';
};
export function MissingEntryRow({
entry,
typeLabel,
itemType,
}: MissingEntryRowProps): JSX.Element {
const items = itemType === 'metrics' ? entry.metrics : entry.attributes;
return (
<div className={styles.entryRow}>
<TriangleAlert size={14} className={styles.missingIcon} />
<Typography.Text size="base" weight="medium">
{typeLabel}
</Typography.Text>
<Typography.Text size="base" color="muted">
-
</Typography.Text>
<Typography.Text
size="base"
weight="semibold"
className={styles.entryRowMetric}
>
{items?.join(', ')}
</Typography.Text>
<Typography.Text size="base" color="muted">
-
</Typography.Text>
<Typography.Text size="base" italic color="muted">
{entry.associatedComponent.name}
</Typography.Text>
{entry.documentationLink && (
<Typography.Link
href={entry.documentationLink}
target="_blank"
rel="noopener noreferrer"
size="base"
className={styles.learnMoreLink}
>
Learn here
<ExternalLink size={12} />
</Typography.Link>
)}
<Divider className={styles.divider} />
</div>
);
}

View File

@@ -1,16 +0,0 @@
.entryRow {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
color: var(--l1-foreground);
}
.presentIcon {
color: var(--success-background);
flex-shrink: 0;
}
.divider {
--divider-color: var(--callout-warning-border);
}

View File

@@ -1,54 +0,0 @@
import styles from './PresentEntryRow.module.scss';
import { CircleCheck } from '@signozhq/icons';
import { Typography } from '@signozhq/ui/typography';
import { Divider } from '@signozhq/ui/divider';
import type {
InframonitoringtypesAttributesComponentEntryDTO,
InframonitoringtypesMetricsComponentEntryDTO,
} from 'api/generated/services/sigNoz.schemas';
type PresentEntryRowProps =
| {
entry: InframonitoringtypesMetricsComponentEntryDTO;
typeLabel: string;
itemType: 'metrics';
}
| {
entry: InframonitoringtypesAttributesComponentEntryDTO;
typeLabel: string;
itemType: 'attributes';
};
export function PresentEntryRow({
entry,
typeLabel,
itemType,
}: PresentEntryRowProps): JSX.Element {
const items = itemType === 'metrics' ? entry.metrics : entry.attributes;
return (
<div className={styles.entryRow}>
<CircleCheck size={14} className={styles.presentIcon} />
<Typography.Text size="base" weight="medium">
{typeLabel}
</Typography.Text>
<Typography.Text size="base" color="muted">
-
</Typography.Text>
<Typography.Text
size="base"
weight="semibold"
className={styles.entryRowMetric}
>
{items?.join(', ')}
</Typography.Text>
<Typography.Text size="base" color="muted">
-
</Typography.Text>
<Typography.Text size="base" italic color="muted">
{entry.associatedComponent.name}
</Typography.Text>
<Divider className={styles.divider} />
</div>
);
}

View File

@@ -1,60 +0,0 @@
import getLocalStorage from 'api/browser/localstorage/get';
import {
InframonitoringtypesChecksDTO,
InframonitoringtypesCheckTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEntity } from '../../../constants';
export const ENTITY_TO_CHECK_TYPE: Record<
InfraMonitoringEntity,
InframonitoringtypesCheckTypeDTO
> = {
[InfraMonitoringEntity.HOSTS]: InframonitoringtypesCheckTypeDTO.hosts,
[InfraMonitoringEntity.PODS]: InframonitoringtypesCheckTypeDTO.pods,
[InfraMonitoringEntity.NODES]: InframonitoringtypesCheckTypeDTO.nodes,
[InfraMonitoringEntity.NAMESPACES]:
InframonitoringtypesCheckTypeDTO.namespaces,
[InfraMonitoringEntity.CLUSTERS]: InframonitoringtypesCheckTypeDTO.clusters,
[InfraMonitoringEntity.DEPLOYMENTS]:
InframonitoringtypesCheckTypeDTO.deployments,
[InfraMonitoringEntity.STATEFULSETS]:
InframonitoringtypesCheckTypeDTO.statefulsets,
[InfraMonitoringEntity.DAEMONSETS]:
InframonitoringtypesCheckTypeDTO.daemonsets,
[InfraMonitoringEntity.JOBS]: InframonitoringtypesCheckTypeDTO.jobs,
[InfraMonitoringEntity.VOLUMES]: InframonitoringtypesCheckTypeDTO.volumes,
[InfraMonitoringEntity.CONTAINERS]:
InframonitoringtypesCheckTypeDTO.kube_containers,
};
export function hasMissingEntries(
data: InframonitoringtypesChecksDTO,
): boolean {
return (
(data.missingDefaultEnabledMetrics?.length ?? 0) > 0 ||
(data.missingOptionalMetrics?.length ?? 0) > 0 ||
(data.missingRequiredAttributes?.length ?? 0) > 0
);
}
export function hasAnyEntries(data: InframonitoringtypesChecksDTO): boolean {
return (
(data.presentDefaultEnabledMetrics?.length ?? 0) > 0 ||
(data.presentOptionalMetrics?.length ?? 0) > 0 ||
(data.presentRequiredAttributes?.length ?? 0) > 0 ||
(data.missingDefaultEnabledMetrics?.length ?? 0) > 0 ||
(data.missingOptionalMetrics?.length ?? 0) > 0 ||
(data.missingRequiredAttributes?.length ?? 0) > 0
);
}
const STORAGE_PREFIX = '@signozhq/k8s-instrumentation-checks-expanded';
export function getStorageKey(entity: InfraMonitoringEntity): string {
return `${STORAGE_PREFIX}-${entity}`;
}
export function readExpandedState(entity: InfraMonitoringEntity): boolean {
const stored = getLocalStorage(getStorageKey(entity));
return stored === null ? true : stored === 'true';
}

View File

@@ -2,12 +2,7 @@ import { Badge } from '@signozhq/ui/badge';
import styles from './utils.module.scss';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import {
convertFiltersToExpression,
formatValueForExpression,
} from 'components/QueryBuilderV2/utils';
import { SelectedItemParams } from 'container/InfraMonitoringK8sV2/hooks';
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
const dotToUnder: Record<string, string> = {
'os.type': 'os_type',
@@ -94,91 +89,3 @@ export function buildExpressionFromGroupMeta(
}
return parent || metaExpression;
}
export interface EventsExpressionParams {
objectKind: string;
objectName: string;
clusterName?: string | null;
namespaceName?: string | null;
}
export function buildEventsExpression(params: EventsExpressionParams): string {
const clauses: string[] = [
`${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = ${formatValueForExpression(params.objectKind)}`,
`${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${formatValueForExpression(params.objectName)}`,
];
if (params.clusterName) {
clauses.push(
`${INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME} = ${formatValueForExpression(params.clusterName)}`,
);
}
// the other attributes are resource., and fallbacks correctly without prefix
// this one needs attribute. prefix otherwise it fails the query
if (params.namespaceName) {
clauses.push(
`attribute.${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${formatValueForExpression(params.namespaceName)}`,
);
}
return clauses.join(' AND ');
}
export interface LogsTracesExpressionParams {
mainAttributeKey: string;
mainAttributeValue?: string | null;
clusterName?: string | null;
namespaceName?: string | null;
}
export function buildLogsTracesExpression(
params: LogsTracesExpressionParams,
): string {
const clauses: string[] = [];
if (params.mainAttributeValue) {
clauses.push(
`${params.mainAttributeKey} = ${formatValueForExpression(params.mainAttributeValue)}`,
);
}
if (params.clusterName) {
clauses.push(
`${INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME} = ${formatValueForExpression(params.clusterName)}`,
);
}
if (params.namespaceName) {
clauses.push(
`${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${formatValueForExpression(params.namespaceName)}`,
);
}
return clauses.join(' AND ');
}
export function buildExpressionFromSelectedItemParams(
params: SelectedItemParams,
mainAttributeKey: string,
): string {
const clauses: string[] = [];
if (params.selectedItem) {
clauses.push(
`${mainAttributeKey} = ${formatValueForExpression(params.selectedItem)}`,
);
}
if (params.clusterName) {
clauses.push(
`${INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME} = ${formatValueForExpression(params.clusterName)}`,
);
}
if (params.namespaceName) {
clauses.push(
`${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${formatValueForExpression(params.namespaceName)}`,
);
}
return clauses.join(' AND ');
}

View File

@@ -1,15 +1,11 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listClusters } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesClusterRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
@@ -18,9 +14,7 @@ import { InfraMonitoringEntity } from '../constants';
import {
clusterWidgetInfo,
getClusterMetricsQueryPayload,
k8sClusterDetailsCountsConfig,
k8sClusterDetailsMetadataConfig,
k8sClusterGetCountsFilterExpression,
k8sClusterGetEntityName,
k8sClusterGetSelectedItemExpression,
k8sClusterInitialEventsExpression,
@@ -73,12 +67,13 @@ function K8sClustersList({
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:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},
@@ -91,7 +86,7 @@ function K8sClustersList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesClusterRecordDTO | null;
error?: APIError | null;
error?: string | null;
}> => {
try {
const response = await listClusters(
@@ -109,10 +104,11 @@ function K8sClustersList({
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:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},
@@ -140,8 +136,6 @@ function K8sClustersList({
getInitialLogTracesExpression={k8sClusterInitialLogTracesExpression}
getInitialEventsExpression={k8sClusterInitialEventsExpression}
metadataConfig={k8sClusterDetailsMetadataConfig}
countsConfig={k8sClusterDetailsCountsConfig}
getCountsFilterExpression={k8sClusterGetCountsFilterExpression}
entityWidgetInfo={clusterWidgetInfo}
getEntityQueryPayload={getClusterMetricsQueryPayload}
queryKeyPrefix="cluster"

View File

@@ -6,131 +6,67 @@ import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import {
K8sDetailsCountConfig,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { SelectedItemParams } from '../hooks';
import {
buildEventsExpression,
buildLogsTracesExpression,
} from 'container/InfraMonitoringK8sV2/Base/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
export const k8sClusterGetSelectedItemExpression = (
params: SelectedItemParams,
): string =>
`k8s.cluster.name = ${formatValueForExpression(params.selectedItem ?? '')}`;
selectedItemId: string,
): string => `k8s.cluster.name = ${formatValueForExpression(selectedItemId)}`;
export const k8sClusterDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesClusterRecordDTO>[] =
[{ label: 'Cluster Name', getValue: (p): string => p.clusterName || '' }];
export const k8sClusterDetailsCountsConfig: K8sDetailsCountConfig<InframonitoringtypesClusterRecordDTO>[] =
[
{
label: 'Namespaces',
getValue: (p): number => p.counts?.namespaces ?? 0,
targetCategory: InfraMonitoringEntity.NAMESPACES,
},
{
label: 'Nodes',
getValue: (p): number => p.counts?.nodes ?? 0,
targetCategory: InfraMonitoringEntity.NODES,
},
{
label: 'Deployments',
getValue: (p): number => p.counts?.deployments ?? 0,
targetCategory: InfraMonitoringEntity.DEPLOYMENTS,
},
{
label: 'StatefulSets',
getValue: (p): number => p.counts?.statefulSets ?? 0,
targetCategory: InfraMonitoringEntity.STATEFULSETS,
},
{
label: 'DaemonSets',
getValue: (p): number => p.counts?.daemonSets ?? 0,
targetCategory: InfraMonitoringEntity.DAEMONSETS,
},
{
label: 'Jobs',
getValue: (p): number => p.counts?.jobs ?? 0,
targetCategory: InfraMonitoringEntity.JOBS,
},
];
export const k8sClusterInitialEventsExpression = (
item: InframonitoringtypesClusterRecordDTO,
): string =>
buildEventsExpression({
objectKind: 'Cluster',
objectName: item.clusterName || '',
});
): 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 k8sClusterInitialLogTracesExpression = (
item: InframonitoringtypesClusterRecordDTO,
): string =>
buildLogsTracesExpression({
mainAttributeKey: INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME,
mainAttributeValue: item.clusterName,
});
): string => {
const clusterName = formatValueForExpression(item.clusterName || '');
return `${INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME} = ${clusterName}`;
};
export const k8sClusterGetEntityName = (
item: InframonitoringtypesClusterRecordDTO,
): string => item.clusterName || '';
export const k8sClusterGetCountsFilterExpression = (
item: InframonitoringtypesClusterRecordDTO,
): string =>
`k8s.cluster.name = ${formatValueForExpression(item.clusterName ?? '')}`;
export const clusterWidgetInfo = [
{
title: 'CPU Usage, allocatable',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/clusters/#cpu-usage-allocatable',
},
{
title: 'Memory Usage, allocatable',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/clusters/#memory-usage-allocatable',
},
{
title: 'Ready Nodes',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/clusters/#ready-nodes',
},
{
title: 'NotReady Nodes',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/clusters/#notready-nodes',
},
{
title: 'Deployments available and desired',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/clusters/#deployments-available-and-desired',
},
{
title: 'Statefulset pods',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/clusters/#statefulset-pods',
},
{
title: 'Daemonset nodes',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/clusters/#daemonset-nodes',
},
{
title: 'Jobs',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/clusters/#jobs',
},
];

View File

@@ -8,7 +8,7 @@ import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
GroupedStatusCounts,
@@ -77,7 +77,11 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => {
const clusterName = value as string;
return <CellValueTooltip value={clusterName} />;
return (
<CellValueTooltip value={clusterName}>
<TanStackTable.Text>{clusterName}</TanStackTable.Text>
</CellValueTooltip>
);
},
},
{
@@ -117,25 +121,23 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
},
},
{
id: 'podCountsByStatus',
id: 'podCountsByPhase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#pod-counts-by-status">
Pod Status
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#pod-counts-by-phase">
Pod Phases
</ColumnHeader>
),
accessorFn: (
row,
): InframonitoringtypesClusterRecordDTO['podCountsByStatus'] =>
row.podCountsByStatus,
accessorFn: (row): InframonitoringtypesClusterRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
cell: ({ row }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodStatusItems(row.podCountsByStatus)} />
<GroupedStatusCounts items={getPodPhaseStatusItems(row.podCountsByPhase)} />
);
},
},

View File

@@ -1,25 +1,18 @@
import { useCallback, useMemo } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useCallback } from 'react';
import { listDaemonSets } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesDaemonSetRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
daemonSetWidgetInfo,
getDaemonSetMetricsQueryPayload,
getDaemonSetPodMetricsQueryPayload,
k8sDaemonSetDetailsMetadataConfig,
k8sDaemonSetGetEntityName,
k8sDaemonSetGetSelectedItemExpression,
@@ -31,8 +24,6 @@ import {
getK8sDaemonSetRowKey,
k8sDaemonSetsColumnsConfig,
} from './table.config';
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
function K8sDaemonSetsList({
controlListPrefix,
}: {
@@ -73,12 +64,13 @@ function K8sDaemonSetsList({
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:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},
@@ -90,7 +82,7 @@ function K8sDaemonSetsList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesDaemonSetRecordDTO | null;
error?: APIError | null;
error?: string | null;
}> => {
try {
const response = await listDaemonSets(
@@ -107,29 +99,19 @@ function K8sDaemonSetsList({
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:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},
[],
);
const customTabs = useMemo(
() => [
createPodMetricsTab<InframonitoringtypesDaemonSetRecordDTO>({
getQueryPayload: getDaemonSetPodMetricsQueryPayload,
category: InfraMonitoringEntity.DAEMONSETS,
queryKey: 'daemonSetPodMetrics',
}),
],
[],
);
return (
<>
<K8sBaseList<InframonitoringtypesDaemonSetRecordDTO, SelectedItemParams>
<K8sBaseList<InframonitoringtypesDaemonSetRecordDTO>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.DAEMONSETS}
tableColumns={k8sDaemonSetsColumnsConfig}
@@ -150,7 +132,6 @@ function K8sDaemonSetsList({
entityWidgetInfo={daemonSetWidgetInfo}
getEntityQueryPayload={getDaemonSetMetricsQueryPayload}
queryKeyPrefix="daemonset"
customTabs={customTabs}
/>
</>
);

View File

@@ -7,24 +7,13 @@ import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import {
getPodUtilizationByPodQueryPayloads,
INFRA_MONITORING_ATTR_KEYS,
} from '../constants';
import { SelectedItemParams } from '../hooks';
import {
buildEventsExpression,
buildExpressionFromSelectedItemParams,
buildLogsTracesExpression,
} from 'container/InfraMonitoringK8sV2/Base/utils';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
export const k8sDaemonSetGetSelectedItemExpression = (
params: SelectedItemParams,
selectedItemId: string,
): string =>
buildExpressionFromSelectedItemParams(
params,
INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME,
);
`${INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME} = ${formatValueForExpression(selectedItemId)}`;
export const k8sDaemonSetDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesDaemonSetRecordDTO>[] =
[
@@ -48,23 +37,12 @@ export const k8sDaemonSetDetailsMetadataConfig: K8sDetailsMetadataConfig<Inframo
export const k8sDaemonSetInitialEventsExpression = (
item: InframonitoringtypesDaemonSetRecordDTO,
): string =>
buildEventsExpression({
objectKind: 'DaemonSet',
objectName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? '',
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
`${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 k8sDaemonSetInitialLogTracesExpression = (
item: InframonitoringtypesDaemonSetRecordDTO,
): string =>
buildLogsTracesExpression({
mainAttributeKey: INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME,
mainAttributeValue:
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME],
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
`${INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME} = ${formatValueForExpression(item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? '')} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${formatValueForExpression(item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '')}`;
export const k8sDaemonSetGetEntityName = (
item: InframonitoringtypesDaemonSetRecordDTO,
@@ -74,25 +52,18 @@ export const daemonSetWidgetInfo = [
{
title: 'CPU usage, request, limits',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/daemonsets/#cpu-usage-request-limits',
},
{
title: 'Memory usage, request, limits',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/daemonsets/#memory-usage-request-limits',
},
{
title: 'Network IO',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/daemonsets/#network-io',
},
{
title: 'Network errors count',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/daemonsets/#network-errors-count',
},
];
@@ -144,40 +115,6 @@ export const getDaemonSetMetricsQueryPayload = (
? 'k8s.namespace.name'
: 'k8s_namespace_name';
const k8sClusterNameKey = dotMetricsEnabled
? 'k8s.cluster.name'
: 'k8s_cluster_name';
const clusterName =
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '';
const namespaceName =
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '';
const filters = [
{
id: 'f1',
key: {
dataType: DataTypes.String,
id: 'k8s_cluster_name--string--tag--false',
key: k8sClusterNameKey,
type: 'tag',
},
op: '=',
value: clusterName,
},
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value: namespaceName,
},
];
return [
{
selectedTime: 'GLOBAL_TIME',
@@ -211,7 +148,19 @@ export const getDaemonSetMetricsQueryPayload = (
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
...filters,
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
},
@@ -253,7 +202,19 @@ export const getDaemonSetMetricsQueryPayload = (
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
...filters,
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
},
@@ -295,7 +256,19 @@ export const getDaemonSetMetricsQueryPayload = (
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
...filters,
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
},
@@ -371,7 +344,19 @@ export const getDaemonSetMetricsQueryPayload = (
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
...filters,
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
},
@@ -413,7 +398,19 @@ export const getDaemonSetMetricsQueryPayload = (
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
...filters,
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
},
@@ -455,7 +452,19 @@ export const getDaemonSetMetricsQueryPayload = (
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
...filters,
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
},
@@ -531,7 +540,19 @@ export const getDaemonSetMetricsQueryPayload = (
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
...filters,
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
},
@@ -620,7 +641,19 @@ export const getDaemonSetMetricsQueryPayload = (
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ??
'',
},
...filters,
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
},
@@ -679,29 +712,3 @@ export const getDaemonSetMetricsQueryPayload = (
},
];
};
export const getDaemonSetPodMetricsQueryPayload = (
daemonSet: InframonitoringtypesDaemonSetRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] => {
const k8sDaemonSetNameKey = dotMetricsEnabled
? 'k8s.daemonset.name'
: 'k8s_daemonset_name';
return getPodUtilizationByPodQueryPayloads(
{
workloadNameKey: k8sDaemonSetNameKey,
workloadNameValue:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? '',
clusterName:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
namespaceName:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
start,
end,
dotMetricsEnabled,
);
};

View File

@@ -6,8 +6,7 @@ import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { SelectedItemParams } from '../hooks';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
@@ -32,15 +31,8 @@ export function getK8sDaemonSetRowKey(
export function getK8sDaemonSetItemKey(
daemonSet: InframonitoringtypesDaemonSetRecordDTO,
): SelectedItemParams {
return {
selectedItem:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] ?? null,
clusterName:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? null,
namespaceName:
daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? null,
};
): string {
return daemonSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DAEMONSET_NAME] || '';
}
export type DaemonSetTableColumnConfig =
@@ -87,7 +79,11 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => {
const daemonsetName = value as string;
return <CellValueTooltip value={daemonsetName} />;
return (
<CellValueTooltip value={daemonsetName}>
<TanStackTable.Text>{daemonsetName}</TanStackTable.Text>
</CellValueTooltip>
);
},
},
{
@@ -104,64 +100,60 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
enableResize: true,
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return <CellValueTooltip value={namespaceName} />;
return (
<CellValueTooltip value={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</CellValueTooltip>
);
},
},
{
id: 'pod_counts_by_status',
id: 'pod_counts_by_phase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#pod-counts-by-status">
Pod Status
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#pod-counts-by-phase">
Pod Phases
</ColumnHeader>
),
accessorFn: (
row,
): InframonitoringtypesDaemonSetRecordDTO['podCountsByStatus'] =>
row.podCountsByStatus,
): InframonitoringtypesDaemonSetRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return <GroupedStatusCounts items={getPodStatusItems(podCountsByStatus)} />;
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
);
},
},
{
id: 'scheduled_nodes',
id: 'node_status',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#scheduled-nodes">
Scheduled Nodes
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#node-status">
Node Status
</ColumnHeader>
),
accessorFn: (row): number => row.currentNodes,
width: { min: 210 },
width: { min: 180 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => (
<GroupedStatusCounts
items={[
{
value: row.readyNodes,
label: 'Ready',
color: Color.BG_FOREST_500,
},
{
value: row.currentNodes,
label: 'Current',
color: Color.BG_ROBIN_500,
color: Color.BG_FOREST_500,
},
{
value: row.desiredNodes,
label: 'Desired',
color: Color.BG_SAKURA_400,
},
{
value: row.misscheduledNodes,
label: 'Misscheduled',
color: Color.BG_AMBER_500,
color: Color.BG_ROBIN_500,
},
]}
/>
@@ -320,30 +312,6 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
);
},
},
{
id: 'ready_nodes',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#ready">
Ready Nodes
</ColumnHeader>
),
accessorFn: (row): number => row.readyNodes,
width: { min: 140 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const readyNodes = value as number;
return (
<ValidateColumnValueWrapper
value={readyNodes}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="ready node"
>
<TanStackTable.Text>{readyNodes}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'current_nodes',
header: (): React.ReactNode => (
@@ -392,28 +360,4 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
);
},
},
{
id: 'misscheduled_nodes',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#misscheduled">
Misscheduled Nodes
</ColumnHeader>
),
accessorFn: (row): number => row.misscheduledNodes,
width: { min: 140 },
enableSort: true,
defaultVisibility: false,
cell: ({ value }): React.ReactNode => {
const misscheduledNodes = value as number;
return (
<ValidateColumnValueWrapper
value={misscheduledNodes}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="misscheduled node"
>
<TanStackTable.Text>{misscheduledNodes}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
];

View File

@@ -1,25 +1,19 @@
import { useCallback, useMemo } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useCallback } from 'react';
import { listDeployments } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesDeploymentRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
deploymentWidgetInfo,
getDeploymentMetricsQueryPayload,
getDeploymentPodMetricsQueryPayload,
k8sDeploymentDetailsMetadataConfig,
k8sDeploymentGetEntityName,
k8sDeploymentGetSelectedItemExpression,
@@ -31,7 +25,6 @@ import {
getK8sDeploymentRowKey,
k8sDeploymentsColumnsConfig,
} from './table.config';
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
function K8sDeploymentsList({
controlListPrefix,
@@ -74,12 +67,13 @@ function K8sDeploymentsList({
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:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},
@@ -92,7 +86,7 @@ function K8sDeploymentsList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesDeploymentRecordDTO | null;
error?: APIError | null;
error?: string | null;
}> => {
try {
const response = await listDeployments(
@@ -110,30 +104,20 @@ function K8sDeploymentsList({
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:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},
[],
);
const customTabs = useMemo(
() => [
createPodMetricsTab<InframonitoringtypesDeploymentRecordDTO>({
getQueryPayload: getDeploymentPodMetricsQueryPayload,
category: InfraMonitoringEntity.DEPLOYMENTS,
queryKey: 'deploymentPodMetrics',
}),
],
[],
);
return (
<>
<K8sBaseList<InframonitoringtypesDeploymentRecordDTO, SelectedItemParams>
<K8sBaseList<InframonitoringtypesDeploymentRecordDTO>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.DEPLOYMENTS}
tableColumns={k8sDeploymentsColumnsConfig}
@@ -155,7 +139,6 @@ function K8sDeploymentsList({
entityWidgetInfo={deploymentWidgetInfo}
getEntityQueryPayload={getDeploymentMetricsQueryPayload}
queryKeyPrefix="deployment"
customTabs={customTabs}
/>
</>
);

View File

@@ -7,24 +7,13 @@ import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import {
getPodUtilizationByPodQueryPayloads,
INFRA_MONITORING_ATTR_KEYS,
} from '../constants';
import { SelectedItemParams } from '../hooks';
import {
buildEventsExpression,
buildExpressionFromSelectedItemParams,
buildLogsTracesExpression,
} from 'container/InfraMonitoringK8sV2/Base/utils';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
export const k8sDeploymentGetSelectedItemExpression = (
params: SelectedItemParams,
selectedItemId: string,
): string =>
buildExpressionFromSelectedItemParams(
params,
INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME,
);
`${INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME} = ${formatValueForExpression(selectedItemId)}`;
export const k8sDeploymentDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesDeploymentRecordDTO>[] =
[
@@ -47,24 +36,24 @@ export const k8sDeploymentDetailsMetadataConfig: K8sDetailsMetadataConfig<Infram
export const k8sDeploymentInitialEventsExpression = (
item: InframonitoringtypesDeploymentRecordDTO,
): string =>
buildEventsExpression({
objectKind: 'Deployment',
objectName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? '',
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
): 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 k8sDeploymentInitialLogTracesExpression = (
item: InframonitoringtypesDeploymentRecordDTO,
): string =>
buildLogsTracesExpression({
mainAttributeKey: INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME,
mainAttributeValue:
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME],
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
): string => {
const deploymentName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? '',
);
const namespaceName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
);
return `${INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME} = ${deploymentName} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${namespaceName}`;
};
export const k8sDeploymentGetEntityName = (
item: InframonitoringtypesDeploymentRecordDTO,
@@ -74,25 +63,18 @@ export const deploymentWidgetInfo = [
{
title: 'CPU usage, request, limits',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/deployments/#cpu-usage-request-limits',
},
{
title: 'Memory usage, request, limits',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/deployments/#memory-usage-request-limits',
},
{
title: 'Network IO',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/deployments/#network-io',
},
{
title: 'Network error count',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/deployments/#network-error-count',
},
];
@@ -139,43 +121,6 @@ export const getDeploymentMetricsQueryPayload = (
: 'k8s_deployment_name';
const k8sPodNameKey = dotMetricsEnabled ? 'k8s.pod.name' : 'k8s_pod_name';
const k8sClusterNameKey = dotMetricsEnabled
? 'k8s.cluster.name'
: 'k8s_cluster_name';
const k8sNamespaceNameKey = dotMetricsEnabled
? 'k8s.namespace.name'
: 'k8s_namespace_name';
const clusterName =
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '';
const namespaceName =
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '';
const filters = [
{
id: 'f1',
key: {
dataType: DataTypes.String,
id: 'k8s_cluster_name--string--tag--false',
key: k8sClusterNameKey,
type: 'tag',
},
op: '=',
value: clusterName,
},
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value: namespaceName,
},
];
return [
{
@@ -210,7 +155,6 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -252,7 +196,6 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -294,7 +237,6 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -370,7 +312,6 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -412,7 +353,6 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -454,7 +394,6 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -530,7 +469,6 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -619,7 +557,6 @@ export const getDeploymentMetricsQueryPayload = (
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
'',
},
...filters,
],
op: 'AND',
},
@@ -678,29 +615,3 @@ export const getDeploymentMetricsQueryPayload = (
},
];
};
export const getDeploymentPodMetricsQueryPayload = (
deployment: InframonitoringtypesDeploymentRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] => {
const k8sDeploymentNameKey = dotMetricsEnabled
? 'k8s.deployment.name'
: 'k8s_deployment_name';
return getPodUtilizationByPodQueryPayloads(
{
workloadNameKey: k8sDeploymentNameKey,
workloadNameValue:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? '',
clusterName:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
namespaceName:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
start,
end,
dotMetricsEnabled,
);
};

View File

@@ -6,8 +6,7 @@ import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { SelectedItemParams } from '../hooks';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
@@ -32,15 +31,8 @@ export function getK8sDeploymentRowKey(
export function getK8sDeploymentItemKey(
deployment: InframonitoringtypesDeploymentRecordDTO,
): SelectedItemParams {
return {
selectedItem:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ?? null,
clusterName:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? null,
namespaceName:
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? null,
};
): string {
return deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] || '';
}
export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDeploymentRecordDTO>[] =
@@ -88,7 +80,11 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => {
const deploymentName = value as string;
return <CellValueTooltip value={deploymentName} />;
return (
<CellValueTooltip value={deploymentName}>
<TanStackTable.Text>{deploymentName}</TanStackTable.Text>
</CellValueTooltip>
);
},
},
{
@@ -108,29 +104,31 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
),
},
{
id: 'podCountsByStatus',
id: 'podCountsByPhase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#pod-counts-by-status">
Pod Status
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#pod-counts-by-phase">
Pod Phases
</ColumnHeader>
),
accessorFn: (row): object | undefined => row.podCountsByStatus,
accessorFn: (row): object | undefined => row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return <GroupedStatusCounts items={getPodStatusItems(podCountsByStatus)} />;
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
);
},
},
{
id: 'pod_replicas',
id: 'replica_status',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#pod-replicas">
Pod Replicas
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#replica-status">
Replica Status
</ColumnHeader>
),
accessorFn: (row): number => row.availablePods,

View File

@@ -1,32 +0,0 @@
.chartHeader {
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
}
.infoIcon {
display: inline-flex;
align-items: center;
color: var(--l2-foreground);
cursor: pointer;
&:hover {
color: var(--text-slate-primary);
}
}
.metricsExplorerLink {
display: inline-flex;
align-items: center;
color: var(--l2-foreground);
transition: opacity 0.2s;
&:hover {
color: var(--l3-foreground);
}
}
.chartHeaderLabel {
font-size: var(--periscope-font-size-base);
color: var(--l2-foreground);
}

View File

@@ -1,83 +0,0 @@
import { Link } from 'react-router-dom';
import { Compass, Info } from '@signozhq/icons';
import { Tooltip } from 'antd';
import styles from './ChartHeader.module.scss';
const DOCS_BASE_URL = `${process.env.DOCS_BASE_URL}/docs`;
interface ChartHeaderProps {
title: string;
docPath?: string;
tooltip?: string;
metricsExplorerUrl?: string;
metricsExplorerTestId?: string;
}
function ChartHeader({
title,
docPath,
tooltip,
metricsExplorerUrl,
metricsExplorerTestId = 'open-metrics-explorer',
}: ChartHeaderProps): JSX.Element {
const renderInfoIcon = (): React.ReactNode => {
if (docPath) {
const tooltipTitle = tooltip || 'Not sure what this represents?';
return (
<Tooltip
arrow
title={
<>
{tooltipTitle}{' '}
<a
href={`${DOCS_BASE_URL}${docPath}`}
target="_blank"
rel="noopener"
onClick={(e): void => e.stopPropagation()}
>
Learn more.
</a>
</>
}
>
<span className={styles.infoIcon} data-testid="chart-header-info-icon">
<Info size="md" />
</span>
</Tooltip>
);
}
if (tooltip) {
return (
<Tooltip title={tooltip}>
<span className={styles.infoIcon} data-testid="chart-header-info-icon">
<Info size="md" />
</span>
</Tooltip>
);
}
return null;
};
return (
<div className={styles.chartHeader} data-testid="chart-header">
<span className={styles.chartHeaderLabel}>{title}</span>
{renderInfoIcon()}
{metricsExplorerUrl && (
<Tooltip title="Open in Metrics Explorer">
<Link
to={metricsExplorerUrl}
className={styles.metricsExplorerLink}
data-testid={metricsExplorerTestId}
>
<Compass size={14} />
</Link>
</Tooltip>
)}
</div>
);
}
export default ChartHeader;

View File

@@ -14,6 +14,28 @@
box-sizing: border-box;
}
.entityMetricsTitleContainer {
display: flex;
align-items: center;
gap: 8px;
}
.entityMetricsTitle {
font-size: var(--periscope-font-size-base);
color: var(--l2-foreground);
}
.metricsExplorerLink {
display: flex;
align-items: center;
color: var(--l2-foreground);
transition: opacity 0.2s;
&:hover {
color: var(--l3-foreground);
}
}
.metricsHeader {
display: flex;
justify-content: flex-end;

View File

@@ -1,6 +1,8 @@
import { useCallback, useMemo, useRef } from 'react';
import { UseQueryResult } from 'react-query';
import { Skeleton } from 'antd';
import { Link } from 'react-router-dom';
import { Compass } from '@signozhq/icons';
import { Skeleton, Tooltip } from 'antd';
import cx from 'classnames';
import { PANEL_TYPES } from 'constants/queryBuilder';
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
@@ -22,7 +24,6 @@ import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { getMetricsExplorerUrl } from 'utils/explorerUtils';
import { buildEntityMetricsChartConfig } from './configBuilder';
import ChartHeader from './ChartHeader';
import { useEntityMetrics } from './hooks';
import { isKeyNotFoundError } from '../utils';
@@ -46,7 +47,6 @@ interface EntityMetricsProps<T> {
entityWidgetInfo: {
title: string;
yAxisUnit: string;
docPath?: string;
}[];
getEntityQueryPayload: (
node: T,
@@ -207,24 +207,31 @@ function EntityMetrics<T>({
key={entityWidgetInfo[idx].title}
className={styles.entityMetricsCol}
>
<ChartHeader
title={entityWidgetInfo[idx].title}
docPath={entityWidgetInfo[idx].docPath}
metricsExplorerUrl={
queryPayloads[idx] && queryPayloads[idx].graphType !== PANEL_TYPES.TABLE
? getMetricsExplorerUrl({
query: queryPayloads[idx].query,
...(selectedInterval && selectedInterval !== 'custom'
? { relativeTime: selectedInterval }
: {
startTimeMs: timeRange.startTime * 1000,
endTimeMs: timeRange.endTime * 1000,
}),
})
: undefined
}
metricsExplorerTestId={`open-metrics-explorer-${idx}`}
/>
<div className={styles.entityMetricsTitleContainer}>
<span className={styles.entityMetricsTitle}>
{entityWidgetInfo[idx].title}
</span>
{queryPayloads[idx] &&
queryPayloads[idx].graphType !== PANEL_TYPES.TABLE && (
<Tooltip title="Open in Metrics Explorer">
<Link
to={getMetricsExplorerUrl({
query: queryPayloads[idx].query,
...(selectedInterval && selectedInterval !== 'custom'
? { relativeTime: selectedInterval }
: {
startTimeMs: timeRange.startTime * 1000,
endTimeMs: timeRange.endTime * 1000,
}),
})}
className={styles.metricsExplorerLink}
data-testid={`open-metrics-explorer-${idx}`}
>
<Compass size={14} />
</Link>
</Tooltip>
)}
</div>
<div className={styles.entityMetricsCard} ref={graphRef}>
{renderCardContent(query, idx)}
</div>

View File

@@ -87,7 +87,7 @@ describe('EntityTraces - Table Rendering', () => {
expect(badge).toHaveAttribute('data-variant', 'outline');
});
it('should render - when http method is empty', async () => {
it('should render N/A when http method is empty', async () => {
mockQueryRangeV5WithTracesResponse({
customTraces: [{ httpMethod: '', responseStatusCode: '200' }],
});
@@ -96,7 +96,7 @@ describe('EntityTraces - Table Rendering', () => {
renderEntityTraces();
});
await expect(screen.findByText('-')).resolves.toBeInTheDocument();
await expect(screen.findByText('N/A')).resolves.toBeInTheDocument();
expect(screen.queryByTestId('httpMethod')).not.toBeInTheDocument();
});

View File

@@ -4,8 +4,4 @@
.cellText {
color: var(--l2-foreground);
&[data-novalue='true'] {
opacity: 0.6;
}
}

View File

@@ -89,12 +89,8 @@ export const getTraceListColumns = (
if (value === '') {
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography
data-testid={key}
className={styles.cellText}
data-novalue="true"
>
-
<Typography data-testid={key} className={styles.cellText}>
N/A
</Typography>
</BlockLink>
);
@@ -106,9 +102,7 @@ export const getTraceListColumns = (
if (!httpMethod) {
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography className={styles.cellText} data-novalue="true">
-
</Typography>
<Typography className={styles.cellText}>N/A</Typography>
</BlockLink>
);
}
@@ -135,11 +129,8 @@ export const getTraceListColumns = (
if (!isValidCode) {
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography
className={styles.cellText}
data-novalue={numericCode === 0 || !statusCode}
>
{numericCode === 0 || !statusCode ? '-' : statusCode}
<Typography className={styles.cellText}>
{numericCode === 0 || !statusCode ? 'N/A' : statusCode}
</Typography>
</BlockLink>
);

View File

@@ -1,47 +0,0 @@
import { Container } from '@signozhq/icons';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { CustomTab } from '../Base/K8sBaseDetails';
import {
InfraMonitoringEntity,
podUtilizationByPodWidgetInfo,
VIEW_TYPES,
} from '../constants';
import EntityMetrics from './EntityMetrics';
interface CreatePodMetricsTabParams<T> {
getQueryPayload: (
entity: T,
start: number,
end: number,
dotMetricsEnabled: boolean,
) => GetQueryResultsProps[];
category: InfraMonitoringEntity;
queryKey: string;
}
export function createPodMetricsTab<T>({
getQueryPayload,
category,
queryKey,
}: CreatePodMetricsTabParams<T>): CustomTab<T> {
return {
key: VIEW_TYPES.POD_METRICS,
label: 'Pod Metrics',
icon: <Container size={14} />,
render: ({ entity, timeRange, selectedInterval, handleTimeChange }) => (
<EntityMetrics
entity={entity}
selectedInterval={selectedInterval}
timeRange={timeRange}
handleTimeChange={handleTimeChange}
isModalTimeSelection
entityWidgetInfo={podUtilizationByPodWidgetInfo}
getEntityQueryPayload={getQueryPayload}
category={category}
queryKey={queryKey}
/>
),
};
}

View File

@@ -44,7 +44,7 @@ import {
useInfraMonitoringCategory,
useInfraMonitoringGroupBy,
useInfraMonitoringOrderBy,
useInfraMonitoringSelectedItemParams,
useInfraMonitoringSelectedItem,
} from './hooks';
import K8sJobsList from './Jobs/K8sJobsList';
import K8sNamespacesList from './Namespaces/K8sNamespacesList';
@@ -63,7 +63,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
const [selectedCategory, setSelectedCategory] = useInfraMonitoringCategory();
const [, setGroupBy] = useInfraMonitoringGroupBy();
const [, setOrderBy] = useInfraMonitoringOrderBy();
const [, setSelectedItemParams] = useInfraMonitoringSelectedItemParams();
const [, setSelectedItem] = useInfraMonitoringSelectedItem();
const compositeQuery = useGetCompositeQueryParam();
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
@@ -179,7 +179,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
void setSelectedCategory(key as string);
void setOrderBy(null);
void setGroupBy(null);
setSelectedItemParams(null);
void setSelectedItem(null);
redirectWithQueryBuilderData({
...currentQuery,
builder: {

View File

@@ -1,24 +1,18 @@
import { useCallback, useMemo } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useCallback } from 'react';
import { listJobs } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesJobRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
getJobMetricsQueryPayload,
getJobPodMetricsQueryPayload,
jobWidgetInfo,
k8sJobDetailsMetadataConfig,
k8sJobGetEntityName,
@@ -31,7 +25,6 @@ import {
getK8sJobRowKey,
k8sJobsColumnsConfig,
} from './table.config';
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
function K8sJobsList({
controlListPrefix,
@@ -74,12 +67,13 @@ function K8sJobsList({
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:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},
@@ -92,7 +86,7 @@ function K8sJobsList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesJobRecordDTO | null;
error?: APIError | null;
error?: string | null;
}> => {
try {
const response = await listJobs(
@@ -110,30 +104,20 @@ function K8sJobsList({
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:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},
[],
);
const customTabs = useMemo(
() => [
createPodMetricsTab<InframonitoringtypesJobRecordDTO>({
getQueryPayload: getJobPodMetricsQueryPayload,
category: InfraMonitoringEntity.JOBS,
queryKey: 'jobPodMetrics',
}),
],
[],
);
return (
<>
<K8sBaseList<InframonitoringtypesJobRecordDTO, SelectedItemParams>
<K8sBaseList<InframonitoringtypesJobRecordDTO>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.JOBS}
tableColumns={k8sJobsColumnsConfig}
@@ -155,7 +139,6 @@ function K8sJobsList({
entityWidgetInfo={jobWidgetInfo}
getEntityQueryPayload={getJobMetricsQueryPayload}
queryKeyPrefix="job"
customTabs={customTabs}
/>
</>
);

View File

@@ -7,24 +7,13 @@ import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import {
getPodUtilizationByPodQueryPayloads,
INFRA_MONITORING_ATTR_KEYS,
} from '../constants';
import { SelectedItemParams } from '../hooks';
import {
buildEventsExpression,
buildExpressionFromSelectedItemParams,
buildLogsTracesExpression,
} from 'container/InfraMonitoringK8sV2/Base/utils';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
export const k8sJobGetSelectedItemExpression = (
params: SelectedItemParams,
selectedItemId: string,
): string =>
buildExpressionFromSelectedItemParams(
params,
INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME,
);
`${INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME} = ${formatValueForExpression(selectedItemId)}`;
export const k8sJobDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesJobRecordDTO>[] =
[
@@ -47,23 +36,24 @@ export const k8sJobDetailsMetadataConfig: K8sDetailsMetadataConfig<Inframonitori
export const k8sJobInitialEventsExpression = (
item: InframonitoringtypesJobRecordDTO,
): string =>
buildEventsExpression({
objectKind: 'Job',
objectName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] ?? '',
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
): 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 k8sJobInitialLogTracesExpression = (
item: InframonitoringtypesJobRecordDTO,
): string =>
buildLogsTracesExpression({
mainAttributeKey: INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME,
mainAttributeValue: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME],
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
): string => {
const jobName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] ?? '',
);
const namespaceName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
);
return `${INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME} = ${jobName} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${namespaceName}`;
};
export const k8sJobGetEntityName = (
item: InframonitoringtypesJobRecordDTO,
@@ -73,22 +63,18 @@ export const jobWidgetInfo = [
{
title: 'CPU usage',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/jobs/#cpu-usage',
},
{
title: 'Memory Usage',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/jobs/#memory-usage',
},
{
title: 'Network IO',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/jobs/#network-io',
},
{
title: 'Network errors count',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/jobs/#network-errors-count',
},
];
@@ -111,54 +97,10 @@ export const getJobMetricsQueryPayload = (
? 'k8s.pod.network.errors'
: 'k8s_pod_network_errors';
const k8sJobNameKey = dotMetricsEnabled ? 'k8s.job.name' : 'k8s_job_name';
const k8sClusterNameKey = dotMetricsEnabled
? 'k8s.cluster.name'
: 'k8s_cluster_name';
const k8sNamespaceNameKey = dotMetricsEnabled
? 'k8s.namespace.name'
: 'k8s_namespace_name';
const clusterName =
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '';
const namespaceName =
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '';
const filters = [
{
id: 'f1',
key: {
dataType: DataTypes.String,
id: 'k8s_job_name--string--tag--false',
key: k8sJobNameKey,
type: 'tag',
},
op: '=',
value: job.jobName,
},
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'k8s_cluster_name--string--tag--false',
key: k8sClusterNameKey,
type: 'tag',
},
op: '=',
value: clusterName,
},
{
id: 'f3',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value: namespaceName,
},
];
return [
{
selectedTime: 'GLOBAL_TIME',
@@ -178,7 +120,31 @@ export const getJobMetricsQueryPayload = (
disabled: false,
expression: 'A',
filters: {
items: [...filters],
items: [
{
id: '6b59b690',
key: {
dataType: DataTypes.String,
id: 'k8s_job_name--string--tag--false',
key: k8sJobNameKey,
type: 'tag',
},
op: '=',
value: job.jobName,
},
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
],
op: 'AND',
},
functions: [],
@@ -239,7 +205,31 @@ export const getJobMetricsQueryPayload = (
disabled: false,
expression: 'A',
filters: {
items: [...filters],
items: [
{
id: '8c217f4d',
key: {
dataType: DataTypes.String,
id: 'k8s_job_name--string--tag--false',
key: k8sJobNameKey,
type: 'tag',
},
op: '=',
value: job.jobName,
},
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
],
op: 'AND',
},
functions: [],
@@ -300,7 +290,31 @@ export const getJobMetricsQueryPayload = (
disabled: false,
expression: 'A',
filters: {
items: [...filters],
items: [
{
id: '2bbf9d0c',
key: {
dataType: DataTypes.String,
id: 'k8s_job_name--string--tag--false',
key: k8sJobNameKey,
type: 'tag',
},
op: '=',
value: job.jobName,
},
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
],
op: 'AND',
},
functions: [],
@@ -374,7 +388,31 @@ export const getJobMetricsQueryPayload = (
disabled: false,
expression: 'A',
filters: {
items: [...filters],
items: [
{
id: '448e6cf7',
key: {
dataType: DataTypes.String,
id: 'k8s_job_name--string--tag--false',
key: k8sJobNameKey,
type: 'tag',
},
op: '=',
value: job.jobName,
},
{
id: '47b3adae',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
],
op: 'AND',
},
functions: [],
@@ -432,25 +470,3 @@ export const getJobMetricsQueryPayload = (
},
];
};
export const getJobPodMetricsQueryPayload = (
job: InframonitoringtypesJobRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] => {
const k8sJobNameKey = dotMetricsEnabled ? 'k8s.job.name' : 'k8s_job_name';
return getPodUtilizationByPodQueryPayloads(
{
workloadNameKey: k8sJobNameKey,
workloadNameValue: job.jobName ?? '',
clusterName: job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
namespaceName:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
start,
end,
dotMetricsEnabled,
);
};

View File

@@ -6,8 +6,7 @@ import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { SelectedItemParams } from '../hooks';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
@@ -28,13 +27,8 @@ export function getK8sJobRowKey(job: InframonitoringtypesJobRecordDTO): string {
export function getK8sJobItemKey(
job: InframonitoringtypesJobRecordDTO,
): SelectedItemParams {
return {
selectedItem: job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] ?? null,
clusterName: job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? null,
namespaceName:
job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? null,
};
): string {
return job.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_JOB_NAME] || '';
}
export type JobTableColumnConfig =
@@ -81,7 +75,11 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => {
const jobName = value as string;
return <CellValueTooltip value={jobName} />;
return (
<CellValueTooltip value={jobName}>
<TanStackTable.Text>{jobName}</TanStackTable.Text>
</CellValueTooltip>
);
},
},
{
@@ -98,34 +96,40 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
enableResize: true,
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return <CellValueTooltip value={namespaceName} />;
return (
<CellValueTooltip value={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</CellValueTooltip>
);
},
},
{
id: 'pod_counts_by_status',
id: 'pod_counts_by_phase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#pod-counts-by-status">
Pod Status
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#pod-counts-by-phase">
Pod Phases
</ColumnHeader>
),
accessorFn: (row): InframonitoringtypesJobRecordDTO['podCountsByStatus'] =>
row.podCountsByStatus,
accessorFn: (row): InframonitoringtypesJobRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return <GroupedStatusCounts items={getPodStatusItems(podCountsByStatus)} />;
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
);
},
},
{
id: 'completion',
id: 'completion_status',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#completion">
Completions
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#completion-status">
Completion Status
</ColumnHeader>
),
accessorFn: (row): number => row.successfulPods,
@@ -145,7 +149,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
{
value: row.desiredSuccessfulPods,
label: 'Desired',
color: Color.BG_AMBER_500,
color: Color.BG_ROBIN_500,
},
]}
/>

View File

@@ -1,27 +1,19 @@
import { useCallback, useMemo } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useCallback } from 'react';
import { listNamespaces } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesNamespaceRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
getNamespaceMetricsQueryPayload,
getNamespacePodMetricsQueryPayload,
k8sNamespaceDetailsCountsConfig,
k8sNamespaceDetailsMetadataConfig,
k8sNamespaceGetCountsFilterExpression,
k8sNamespaceGetEntityName,
k8sNamespaceGetSelectedItemExpression,
k8sNamespaceInitialEventsExpression,
@@ -33,7 +25,6 @@ import {
getK8sNamespaceRowKey,
k8sNamespacesColumnsConfig,
} from './table.config';
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
function K8sNamespacesList({
controlListPrefix,
@@ -76,12 +67,13 @@ function K8sNamespacesList({
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:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},
@@ -94,7 +86,7 @@ function K8sNamespacesList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesNamespaceRecordDTO | null;
error?: APIError | null;
error?: string | null;
}> => {
try {
const response = await listNamespaces(
@@ -112,30 +104,20 @@ function K8sNamespacesList({
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:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},
[],
);
const customTabs = useMemo(
() => [
createPodMetricsTab<InframonitoringtypesNamespaceRecordDTO>({
getQueryPayload: getNamespacePodMetricsQueryPayload,
category: InfraMonitoringEntity.NAMESPACES,
queryKey: 'namespacePodMetrics',
}),
],
[],
);
return (
<>
<K8sBaseList<InframonitoringtypesNamespaceRecordDTO, SelectedItemParams>
<K8sBaseList<InframonitoringtypesNamespaceRecordDTO>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.NAMESPACES}
tableColumns={k8sNamespacesColumnsConfig}
@@ -154,12 +136,9 @@ function K8sNamespacesList({
getInitialLogTracesExpression={k8sNamespaceInitialLogTracesExpression}
getInitialEventsExpression={k8sNamespaceInitialEventsExpression}
metadataConfig={k8sNamespaceDetailsMetadataConfig}
countsConfig={k8sNamespaceDetailsCountsConfig}
getCountsFilterExpression={k8sNamespaceGetCountsFilterExpression}
entityWidgetInfo={namespaceWidgetInfo}
getEntityQueryPayload={getNamespaceMetricsQueryPayload}
queryKeyPrefix="namespace"
customTabs={customTabs}
/>
</>
);

View File

@@ -6,30 +6,14 @@ import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import {
K8sDetailsCountConfig,
K8sDetailsMetadataConfig,
} from '../Base/K8sBaseDetails';
import {
getPodUtilizationByPodQueryPayloads,
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { SelectedItemParams } from '../hooks';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import {
buildEventsExpression,
buildExpressionFromSelectedItemParams,
buildLogsTracesExpression,
} from 'container/InfraMonitoringK8sV2/Base/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
export const k8sNamespaceGetSelectedItemExpression = (
params: SelectedItemParams,
selectedItemId: string,
): string =>
buildExpressionFromSelectedItemParams(
params,
INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME,
);
`${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${formatValueForExpression(selectedItemId)}`;
export const k8sNamespaceDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesNamespaceRecordDTO>[] =
[
@@ -41,124 +25,64 @@ export const k8sNamespaceDetailsMetadataConfig: K8sDetailsMetadataConfig<Inframo
},
];
export const k8sNamespaceDetailsCountsConfig: K8sDetailsCountConfig<InframonitoringtypesNamespaceRecordDTO>[] =
[
{
label: 'Deployments',
getValue: (p): number => p.counts?.deployments ?? 0,
targetCategory: InfraMonitoringEntity.DEPLOYMENTS,
},
{
label: 'StatefulSets',
getValue: (p): number => p.counts?.statefulSets ?? 0,
targetCategory: InfraMonitoringEntity.STATEFULSETS,
},
{
label: 'DaemonSets',
getValue: (p): number => p.counts?.daemonSets ?? 0,
targetCategory: InfraMonitoringEntity.DAEMONSETS,
},
{
label: 'Jobs',
getValue: (p): number => p.counts?.jobs ?? 0,
targetCategory: InfraMonitoringEntity.JOBS,
},
];
export const k8sNamespaceInitialEventsExpression = (
item: InframonitoringtypesNamespaceRecordDTO,
): string =>
buildEventsExpression({
objectKind: 'Namespace',
objectName: item.namespaceName || '',
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
});
): 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 k8sNamespaceInitialLogTracesExpression = (
item: InframonitoringtypesNamespaceRecordDTO,
): string =>
buildLogsTracesExpression({
mainAttributeKey: INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME,
mainAttributeValue: item.namespaceName,
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
});
): string => {
const name = formatValueForExpression(item.namespaceName || '');
return `${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${name}`;
};
export const k8sNamespaceGetEntityName = (
item: InframonitoringtypesNamespaceRecordDTO,
): string => item.namespaceName || '';
export const k8sNamespaceGetCountsFilterExpression = (
item: InframonitoringtypesNamespaceRecordDTO,
): string => {
const clusterName = item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME];
const clauses: string[] = [];
if (clusterName) {
clauses.push(
`${INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME} = ${formatValueForExpression(clusterName)}`,
);
}
if (item.namespaceName) {
clauses.push(
`${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${formatValueForExpression(item.namespaceName)}`,
);
}
return clauses.join(' AND ');
};
export const namespaceWidgetInfo = [
{
title: 'CPU Usage (cores)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#cpu-usage-cores',
},
{
title: 'Memory Usage (bytes)',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/namespaces/#memory-usage-bytes',
},
{
title: 'Pods CPU (top 10)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#pods-cpu-top-10',
},
{
title: 'Pods Memory (top 10)',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/namespaces/#pods-memory-top-10',
},
{
title: 'Network rate',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#network-rate',
},
{
title: 'Network errors',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#network-errors',
},
{
title: 'StatefulSets (pods)',
title: 'StatefulSets',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#statefulsets',
},
{
title: 'ReplicaSets (pods)',
title: 'ReplicaSets',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#replicasets',
},
{
title: 'DaemonSets (nodes)',
title: 'DaemonSets',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#daemonsets',
},
{
title: 'Deployments (pods)',
title: 'Deployments',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/namespaces/#deployments',
},
];
@@ -255,24 +179,6 @@ export const getNamespaceMetricsQueryPayload = (
'k8s.deployment.name',
'k8s_deployment_name',
);
const k8sClusterNameKey = getKey('k8s.cluster.name', 'k8s_cluster_name');
const clusterName =
namespace.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '';
const filters = [
{
id: 'f1',
key: {
dataType: DataTypes.String,
id: 'k8s_cluster_name--string--tag--false',
key: k8sClusterNameKey,
type: 'tag',
},
op: '=',
value: clusterName,
},
];
return [
{
@@ -305,7 +211,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -345,7 +250,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -385,7 +289,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -425,7 +328,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -499,7 +401,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -539,7 +440,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -579,7 +479,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -619,7 +518,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -659,7 +557,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -699,7 +596,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -773,7 +669,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -854,7 +749,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -935,7 +829,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1022,7 +915,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1109,7 +1001,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1156,7 +1047,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1203,7 +1093,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1284,7 +1173,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1337,7 +1225,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1424,7 +1311,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1471,7 +1357,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1518,7 +1403,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1565,7 +1449,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1646,7 +1529,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1693,7 +1575,6 @@ export const getNamespaceMetricsQueryPayload = (
op: '=',
value: namespace.namespaceName,
},
...filters,
],
op: 'AND',
},
@@ -1753,26 +1634,3 @@ export const getNamespaceMetricsQueryPayload = (
},
];
};
export const getNamespacePodMetricsQueryPayload = (
namespace: InframonitoringtypesNamespaceRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] => {
const k8sNamespaceNameKey = dotMetricsEnabled
? 'k8s.namespace.name'
: 'k8s_namespace_name';
return getPodUtilizationByPodQueryPayloads(
{
workloadNameKey: k8sNamespaceNameKey,
workloadNameValue: namespace.namespaceName ?? '',
clusterName:
namespace.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
},
start,
end,
dotMetricsEnabled,
);
};

View File

@@ -6,7 +6,7 @@ import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
GroupedStatusCounts,
@@ -16,7 +16,6 @@ import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { SelectedItemParams } from '../hooks';
export function getK8sNamespaceRowKey(
namespace: InframonitoringtypesNamespaceRecordDTO,
@@ -30,15 +29,8 @@ export function getK8sNamespaceRowKey(
export function getK8sNamespaceItemKey(
namespace: InframonitoringtypesNamespaceRecordDTO,
): SelectedItemParams {
return {
selectedItem:
namespace.namespaceName ??
namespace.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
null,
clusterName:
namespace.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? null,
};
): string {
return namespace.namespaceName;
}
export type NamespaceTableColumnConfig =
@@ -83,7 +75,11 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return <CellValueTooltip value={namespaceName} />;
return (
<CellValueTooltip value={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</CellValueTooltip>
);
},
},
{
@@ -102,25 +98,25 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
),
},
{
id: 'podCountsByStatus',
id: 'podCountsByPhase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/namespaces#pod-counts-by-status">
Pod Status
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/namespaces#pod-counts-by-phase">
Pod Phases
</ColumnHeader>
),
accessorFn: (
row,
): InframonitoringtypesNamespaceRecordDTO['podCountsByStatus'] =>
row.podCountsByStatus,
): InframonitoringtypesNamespaceRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
cell: ({ row }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodStatusItems(row.podCountsByStatus)} />
<GroupedStatusCounts items={getPodPhaseStatusItems(row.podCountsByPhase)} />
);
},
},

View File

@@ -1,15 +1,11 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listNodes } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesNodeRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
@@ -71,12 +67,13 @@ function K8sNodesList({
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:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},
@@ -89,7 +86,7 @@ function K8sNodesList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesNodeRecordDTO | null;
error?: APIError | null;
error?: string | null;
}> => {
try {
const response = await listNodes(
@@ -107,10 +104,11 @@ function K8sNodesList({
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:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},

View File

@@ -7,19 +7,13 @@ import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import {
buildEventsExpression,
buildLogsTracesExpression,
} from 'container/InfraMonitoringK8sV2/Base/utils';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import { SelectedItemParams } from '../hooks';
export const k8sNodeGetSelectedItemExpression = (
params: SelectedItemParams,
): string =>
`k8s.node.name = ${formatValueForExpression(params.selectedItem ?? '')}`;
selectedItemId: string,
): string => `k8s.node.name = ${formatValueForExpression(selectedItemId)}`;
export const k8sNodeDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesNodeRecordDTO>[] =
[
@@ -34,20 +28,12 @@ export const k8sNodeDetailsMetadataConfig: K8sDetailsMetadataConfig<Inframonitor
export const k8sNodeInitialEventsExpression = (
item: InframonitoringtypesNodeRecordDTO,
): string =>
buildEventsExpression({
objectKind: 'Node',
objectName: item.nodeName || '',
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
});
`${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_KIND} = 'Node' AND ${INFRA_MONITORING_ATTR_KEYS.K8S_OBJECT_NAME} = ${formatValueForExpression(item.nodeName || '')}`;
export const k8sNodeInitialLogTracesExpression = (
item: InframonitoringtypesNodeRecordDTO,
): string =>
buildLogsTracesExpression({
mainAttributeKey: INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME,
mainAttributeValue: item.nodeName,
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
});
`${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 k8sNodeGetEntityName = (
item: InframonitoringtypesNodeRecordDTO,
@@ -57,53 +43,42 @@ export const nodeWidgetInfo = [
{
title: 'CPU Usage (cores)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#cpu-usage-cores',
},
{
title: 'Memory Usage (bytes)',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#memory-usage-bytes',
},
{
title: 'CPU Usage (%)',
yAxisUnit: 'percentunit',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#cpu-usage-',
},
{
title: 'Memory Usage (%)',
yAxisUnit: 'percentunit',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#memory-usage-',
},
{
title: 'Pods by CPU (top 10)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#pods-by-cpu-top-10',
},
{
title: 'Pods by Memory (top 10)',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#pods-by-memory-top-10',
},
{
title: 'Network error count',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#network-error-count',
},
{
title: 'Network IO rate',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#network-io-rate',
},
{
title: 'Filesystem usage (bytes)',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/nodes/#filesystem-usage-bytes',
},
{
title: 'Filesystem usage (%)',
yAxisUnit: 'percentunit',
docPath: '/infrastructure-monitoring/kubernetes/nodes/#filesystem-usage-',
},
];

View File

@@ -7,7 +7,7 @@ import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
CellValueTooltip,
@@ -85,7 +85,11 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => {
const nodeName = value as string;
return <CellValueTooltip value={nodeName} />;
return (
<CellValueTooltip value={nodeName}>
<TanStackTable.Text>{nodeName}</TanStackTable.Text>
</CellValueTooltip>
);
},
},
{
@@ -128,23 +132,23 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
},
},
{
id: 'podCountsByStatus',
id: 'podCountsByPhase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#pod-counts-by-status">
Pod Status
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#pod-counts-by-phase">
Pod Phases
</ColumnHeader>
),
accessorFn: (row): InframonitoringtypesNodeRecordDTO['podCountsByStatus'] =>
row.podCountsByStatus,
accessorFn: (row): InframonitoringtypesNodeRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
cell: ({ row }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodStatusItems(row.podCountsByStatus)} />
<GroupedStatusCounts items={getPodPhaseStatusItems(row.podCountsByPhase)} />
);
},
},

View File

@@ -1,15 +1,11 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listPods } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesPodRecordDTO,
InframonitoringtypesResponseTypeDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
@@ -71,12 +67,13 @@ function K8sPodsList({
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:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},
@@ -89,7 +86,7 @@ function K8sPodsList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesPodRecordDTO | null;
error?: APIError | null;
error?: string | null;
}> => {
try {
const response = await listPods(
@@ -107,10 +104,11 @@ function K8sPodsList({
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:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},

View File

@@ -8,17 +8,11 @@ import { v4 } from 'uuid';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import {
buildEventsExpression,
buildLogsTracesExpression,
} from 'container/InfraMonitoringK8sV2/Base/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import { SelectedItemParams } from '../hooks';
export const k8sPodGetSelectedItemExpression = (
params: SelectedItemParams,
): string =>
`k8s.pod.uid = ${formatValueForExpression(params.selectedItem ?? '')}`;
selectedItemId: string,
): string => `k8s.pod.uid = ${formatValueForExpression(selectedItemId)}`;
export const k8sPodDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesPodRecordDTO>[] =
[
@@ -41,23 +35,21 @@ export const k8sPodDetailsMetadataConfig: K8sDetailsMetadataConfig<Inframonitori
export const k8sPodInitialEventsExpression = (
pod: InframonitoringtypesPodRecordDTO,
): string =>
buildEventsExpression({
objectKind: 'Pod',
objectName: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
clusterName: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
): 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 k8sPodInitialLogTracesExpression = (
pod: InframonitoringtypesPodRecordDTO,
): string =>
buildLogsTracesExpression({
mainAttributeKey: INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME,
mainAttributeValue: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME],
clusterName: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
): string => {
const podName = formatValueForExpression(
pod.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME] || '',
);
return `${INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME} = ${podName}`;
};
export const k8sPodGetEntityName = (
pod: InframonitoringtypesPodRecordDTO,
@@ -67,74 +59,54 @@ export const podWidgetInfo = [
{
title: 'CPU Usage (cores)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/pods/#cpu-usage-cores-1',
},
{
title: 'CPU Request, Limit Utilization',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#cpu-request-limit-utilization',
},
{
title: 'Memory Usage (bytes)',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/pods/#memory-usage-bytes',
},
{
title: 'Memory Request, Limit Utilization',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#memory-request-limit-utilization',
},
{
title: 'Memory by State',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/pods/#memory-by-state',
},
{
title: 'Memory Major Page Faults',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#memory-major-page-faults',
},
{
title: 'CPU Usage by Container (cores)',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#cpu-usage-by-container-cores',
},
{
title: 'CPU Request, Limit Utilization by Container',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#cpu-request-limit-utilization-by-container',
},
{
title: 'Memory Usage by Container (bytes)',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#memory-usage-by-container-bytes',
},
{
title: 'Memory Request, Limit Utilization by Container',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/kubernetes/pods/#memory-request-limit-utilization-by-container',
},
{
title: 'Network rate',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/pods/#network-rate',
},
{
title: 'Network errors',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/pods/#network-errors',
},
{
title: 'File system (bytes)',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/pods/#file-system-bytes',
},
];

View File

@@ -1,9 +1,9 @@
import { Container } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Badge, BadgeColor } from '@signozhq/ui/badge';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import {
InframonitoringtypesPodPhaseDTO,
InframonitoringtypesPodRecordDTO,
InframonitoringtypesPodStatusDTO,
} from 'api/generated/services/sigNoz.schemas';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
@@ -11,11 +11,7 @@ import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import {
formatBytes,
getPodStatusItems,
POD_STATUS_COLORS,
} from '../commonUtils';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
@@ -44,6 +40,15 @@ export function getK8sPodItemKey(
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[] = [
@@ -88,30 +93,34 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => {
const podName = value as string;
return <CellValueTooltip value={podName} />;
return (
<CellValueTooltip value={podName}>
<TanStackTable.Text>{podName}</TanStackTable.Text>
</CellValueTooltip>
);
},
},
{
id: 'podStatus',
id: 'podPhase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#pod-status">
Status
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#pod-phase">
Phase
</ColumnHeader>
),
accessorFn: (row): string => row.podStatus,
width: { min: 160 },
accessorFn: (row): string => row.podPhase,
width: { min: 120 },
enableSort: false,
visibilityBehavior: 'hidden-on-expand',
cell: ({ row }): React.ReactNode => {
if (!row.podStatus) {
if (!row.podPhase) {
return <></>;
}
const color = POD_STATUS_COLORS[row.podStatus] || POD_STATUS_COLORS.unknown;
const color = POD_PHASE_COLORS[row.podPhase] || POD_PHASE_COLORS.unknown;
const label =
row.podStatus === InframonitoringtypesPodStatusDTO.no_data
row.podPhase === InframonitoringtypesPodPhaseDTO.no_data
? 'No Data'
: row.podStatus.charAt(0).toUpperCase() + row.podStatus.slice(1);
: row.podPhase.charAt(0).toUpperCase() + row.podPhase.slice(1);
return (
<Badge color={color} variant="outline">
{label}
@@ -120,34 +129,30 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
},
},
{
id: 'podCountsByStatus',
id: 'podCountsByPhase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#pod-status">
Status
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#pod-phase">
Phases
</ColumnHeader>
),
accessorFn: (row): InframonitoringtypesPodRecordDTO['podCountsByStatus'] =>
row.podCountsByStatus,
accessorFn: (row): InframonitoringtypesPodRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
visibilityBehavior: 'hidden-on-collapse',
cell: ({ row }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts items={getPodStatusItems(row.podCountsByStatus)} />
<GroupedStatusCounts items={getPodPhaseStatusItems(row.podCountsByPhase)} />
);
},
},
{
id: 'podAge',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#age">
Age
</ColumnHeader>
),
header: 'Age',
accessorFn: (row): number => row.podAge,
width: { min: 100 },
enableSort: false,
@@ -163,28 +168,6 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
return <TanStackTable.Text>{formatAge(age)}</TanStackTable.Text>;
},
},
{
id: 'podRestarts',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#restarts">
Restarts
</ColumnHeader>
),
accessorFn: (row): number => row.podRestarts,
width: { min: 100 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const restarts = value as number;
if (restarts === -1) {
return (
<TooltipSimple title="No data">
<Typography.Text>-</Typography.Text>
</TooltipSimple>
);
}
return <TanStackTable.Text>{restarts}</TanStackTable.Text>;
},
},
{
id: 'cpu_request',
header: (): React.ReactNode => (
@@ -333,11 +316,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
},
{
id: 'namespace',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#additional-columns">
Namespace
</ColumnHeader>
),
header: 'Namespace',
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
width: { default: 100 },
@@ -349,11 +328,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
},
{
id: 'node',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#additional-columns">
Node
</ColumnHeader>
),
header: 'Node',
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME] || '',
width: { default: 100 },
@@ -365,11 +340,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
},
{
id: 'cluster',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#additional-columns">
Cluster
</ColumnHeader>
),
header: 'Cluster',
accessorFn: (row): string =>
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] || '',
width: { default: 100 },

View File

@@ -1,24 +1,18 @@
import { useCallback, useMemo } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useCallback } from 'react';
import { listStatefulSets } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesResponseTypeDTO,
InframonitoringtypesStatefulSetRecordDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
getStatefulSetMetricsQueryPayload,
getStatefulSetPodMetricsQueryPayload,
k8sStatefulSetDetailsMetadataConfig,
k8sStatefulSetGetEntityName,
k8sStatefulSetGetSelectedItemExpression,
@@ -31,7 +25,6 @@ import {
getK8sStatefulSetRowKey,
k8sStatefulSetsColumnsConfig,
} from './table.config';
import { createPodMetricsTab } from 'container/InfraMonitoringK8sV2/EntityDetailsUtils/createPodMetricsTab';
function K8sStatefulSetsList({
controlListPrefix,
@@ -74,12 +67,13 @@ function K8sStatefulSetsList({
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:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},
@@ -92,7 +86,7 @@ function K8sStatefulSetsList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesStatefulSetRecordDTO | null;
error?: APIError | null;
error?: string | null;
}> => {
try {
const response = await listStatefulSets(
@@ -110,30 +104,20 @@ function K8sStatefulSetsList({
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:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},
[],
);
const customTabs = useMemo(
() => [
createPodMetricsTab<InframonitoringtypesStatefulSetRecordDTO>({
getQueryPayload: getStatefulSetPodMetricsQueryPayload,
category: InfraMonitoringEntity.STATEFULSETS,
queryKey: 'statefulSetPodMetrics',
}),
],
[],
);
return (
<>
<K8sBaseList<InframonitoringtypesStatefulSetRecordDTO, SelectedItemParams>
<K8sBaseList<InframonitoringtypesStatefulSetRecordDTO>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.STATEFULSETS}
tableColumns={k8sStatefulSetsColumnsConfig}
@@ -155,7 +139,6 @@ function K8sStatefulSetsList({
entityWidgetInfo={statefulSetWidgetInfo}
getEntityQueryPayload={getStatefulSetMetricsQueryPayload}
queryKeyPrefix="statefulSet"
customTabs={customTabs}
/>
</>
);

View File

@@ -7,24 +7,13 @@ import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import {
getPodUtilizationByPodQueryPayloads,
INFRA_MONITORING_ATTR_KEYS,
} from '../constants';
import { SelectedItemParams } from '../hooks';
import {
buildEventsExpression,
buildExpressionFromSelectedItemParams,
buildLogsTracesExpression,
} from 'container/InfraMonitoringK8sV2/Base/utils';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
export const k8sStatefulSetGetSelectedItemExpression = (
params: SelectedItemParams,
selectedItemId: string,
): string =>
buildExpressionFromSelectedItemParams(
params,
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME,
);
`${INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME} = ${formatValueForExpression(selectedItemId)}`;
export const k8sStatefulSetDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesStatefulSetRecordDTO>[] =
[
@@ -33,11 +22,6 @@ export const k8sStatefulSetDetailsMetadataConfig: K8sDetailsMetadataConfig<Infra
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ?? '',
},
{
label: 'Cluster Name',
getValue: (p): string =>
p.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
},
{
label: 'Namespace Name',
getValue: (p): string =>
@@ -47,25 +31,24 @@ export const k8sStatefulSetDetailsMetadataConfig: K8sDetailsMetadataConfig<Infra
export const k8sStatefulSetInitialEventsExpression = (
item: InframonitoringtypesStatefulSetRecordDTO,
): string =>
buildEventsExpression({
objectKind: 'StatefulSet',
objectName:
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ?? '',
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
): 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 k8sStatefulSetInitialLogTracesExpression = (
item: InframonitoringtypesStatefulSetRecordDTO,
): string =>
buildLogsTracesExpression({
mainAttributeKey: INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME,
mainAttributeValue:
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME],
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
): string => {
const statefulSetName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ?? '',
);
const namespaceName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
);
return `${INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME} = ${statefulSetName} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${namespaceName}`;
};
export const k8sStatefulSetGetEntityName = (
item: InframonitoringtypesStatefulSetRecordDTO,
@@ -75,37 +58,26 @@ export const statefulSetWidgetInfo = [
{
title: 'CPU usage, request, limits',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/statefulsets/#cpu-usage-request-limits',
},
{
title: 'CPU request, limit util (%)',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/kubernetes/statefulsets/#cpu-request-limit-utilization-',
},
{
title: 'Memory usage, request, limits',
yAxisUnit: 'bytes',
docPath:
'/infrastructure-monitoring/kubernetes/statefulsets/#memory-usage-request-limits',
},
{
title: 'Memory request, limit util (%)',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/kubernetes/statefulsets/#memory-request-limit-utilization-',
},
{
title: 'Network IO',
yAxisUnit: 'binBps',
docPath: '/infrastructure-monitoring/kubernetes/statefulsets/#network-io',
},
{
title: 'Network errors count',
yAxisUnit: '',
docPath:
'/infrastructure-monitoring/kubernetes/statefulsets/#network-errors-count',
},
];
@@ -121,42 +93,7 @@ export const getStatefulSetMetricsQueryPayload = (
const k8sNamespaceNameKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME
: 'k8s_namespace_name';
const k8sPodNameKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.K8S_POD_NAME
: 'k8s_pod_name';
const k8sClusterNameKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME
: 'k8s_cluster_name';
const clusterName =
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '';
const namespaceName =
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '';
const filters = [
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'k8s_namespace_name--string--tag--false',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value: namespaceName,
},
{
id: 'f3',
key: {
dataType: DataTypes.String,
id: 'k8s_cluster_name--string--tag--false',
key: k8sClusterNameKey,
type: 'tag',
},
op: '=',
value: clusterName,
},
];
const k8sPodNameKey = dotMetricsEnabled ? 'k8s.pod.name' : 'k8s_pod_name';
const k8sPodCpuUtilKey = dotMetricsEnabled
? 'k8s.pod.cpu.usage'
@@ -231,7 +168,19 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
...filters,
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
},
@@ -274,7 +223,19 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
...filters,
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
},
@@ -317,7 +278,19 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
...filters,
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
},
@@ -380,7 +353,19 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
...filters,
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
},
@@ -423,7 +408,19 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
...filters,
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
},
@@ -486,7 +483,19 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
...filters,
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
},
@@ -516,7 +525,7 @@ export const getStatefulSetMetricsQueryPayload = (
filters: {
items: [
{
id: 'f1',
id: 'f3',
key: {
dataType: DataTypes.String,
id: 'pod_name',
@@ -529,7 +538,19 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
...filters,
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
},
@@ -572,7 +593,19 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
...filters,
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
},
@@ -635,7 +668,19 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
...filters,
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
},
@@ -678,7 +723,19 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
...filters,
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
},
@@ -741,7 +798,19 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
...filters,
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
},
@@ -817,7 +886,19 @@ export const getStatefulSetMetricsQueryPayload = (
INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
] ?? '',
},
...filters,
{
id: 'f2',
key: {
dataType: DataTypes.String,
id: 'ns_name',
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ??
'',
},
],
op: 'AND',
},
@@ -862,29 +943,3 @@ export const getStatefulSetMetricsQueryPayload = (
},
];
};
export const getStatefulSetPodMetricsQueryPayload = (
statefulSet: InframonitoringtypesStatefulSetRecordDTO,
start: number,
end: number,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] => {
const k8sStatefulSetNameKey = dotMetricsEnabled
? INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME
: 'k8s_statefulset_name';
return getPodUtilizationByPodQueryPayloads(
{
workloadNameKey: k8sStatefulSetNameKey,
workloadNameValue:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ?? '',
clusterName:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? '',
namespaceName:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? '',
},
start,
end,
dotMetricsEnabled,
);
};

View File

@@ -6,8 +6,7 @@ import { ExpandButtonWrapper } from 'container/InfraMonitoringK8sV2/components';
import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { SelectedItemParams } from '../hooks';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import { formatBytes, getPodPhaseStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
@@ -32,15 +31,10 @@ export function getK8sStatefulSetRowKey(
export function getK8sStatefulSetItemKey(
statefulSet: InframonitoringtypesStatefulSetRecordDTO,
): SelectedItemParams {
return {
selectedItem:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] ?? null,
clusterName:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? null,
namespaceName:
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? null,
};
): string {
return (
statefulSet.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_STATEFULSET_NAME] || ''
);
}
export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesStatefulSetRecordDTO>[] =
@@ -88,7 +82,11 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => {
const statefulsetName = value as string;
return <CellValueTooltip value={statefulsetName} />;
return (
<CellValueTooltip value={statefulsetName}>
<TanStackTable.Text>{statefulsetName}</TanStackTable.Text>
</CellValueTooltip>
);
},
},
{
@@ -105,36 +103,42 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
enableResize: true,
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return <CellValueTooltip value={namespaceName} />;
return (
<CellValueTooltip value={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</CellValueTooltip>
);
},
},
{
id: 'pod_counts_by_status',
id: 'pod_counts_by_phase',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#pod-counts-by-status">
Pod Status
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#pod-counts-by-phase">
Pod Phases
</ColumnHeader>
),
accessorFn: (
row,
): InframonitoringtypesStatefulSetRecordDTO['podCountsByStatus'] =>
row.podCountsByStatus,
): InframonitoringtypesStatefulSetRecordDTO['podCountsByPhase'] =>
row.podCountsByPhase,
width: { min: 250 },
enableSort: false,
enableResize: true,
cell: ({ row }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
const podCountsByPhase = row.podCountsByPhase;
if (!podCountsByPhase) {
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return <GroupedStatusCounts items={getPodStatusItems(podCountsByStatus)} />;
return (
<GroupedStatusCounts items={getPodPhaseStatusItems(podCountsByPhase)} />
);
},
},
{
id: 'pod_replicas',
id: 'pod_status',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#pod-replicas">
Pod Replicas
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#pod-status">
Pod Status
</ColumnHeader>
),
accessorFn: (row): number => row.currentPods,

View File

@@ -1,21 +1,16 @@
import { useCallback } from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { listVolumes } from 'api/generated/services/inframonitoring';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import {
InframonitoringtypesResponseTypeDTO,
InframonitoringtypesVolumeRecordDTO,
Querybuildertypesv5OrderDirectionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InfraMonitoringEvents } from 'constants/events';
import APIError from 'types/api/error';
import K8sBaseDetails, { K8sDetailsFilters } from '../Base/K8sBaseDetails';
import { K8sBaseList } from '../Base/K8sBaseList';
import { K8sBaseFilters } from '../Base/types';
import { InfraMonitoringEntity } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
getVolumeMetricsQueryPayload,
k8sVolumeDetailsMetadataConfig,
@@ -72,12 +67,13 @@ function K8sVolumesList({
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:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},
@@ -90,7 +86,7 @@ function K8sVolumesList({
signal?: AbortSignal,
): Promise<{
data: InframonitoringtypesVolumeRecordDTO | null;
error?: APIError | null;
error?: string | null;
}> => {
try {
const response = await listVolumes(
@@ -108,10 +104,11 @@ function K8sVolumesList({
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:
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) ?? null,
error: errMsg,
};
}
},
@@ -120,7 +117,7 @@ function K8sVolumesList({
return (
<>
<K8sBaseList<InframonitoringtypesVolumeRecordDTO, SelectedItemParams>
<K8sBaseList<InframonitoringtypesVolumeRecordDTO>
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.VOLUMES}
tableColumns={k8sVolumesColumnsConfig}

View File

@@ -6,22 +6,15 @@ import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import { SelectedItemParams } from '../hooks';
import {
buildEventsExpression,
buildExpressionFromSelectedItemParams,
buildLogsTracesExpression,
} from 'container/InfraMonitoringK8sV2/Base/utils';
export const k8sVolumeGetSelectedItemExpression = (
params: SelectedItemParams,
selectedItemId: string,
): string =>
buildExpressionFromSelectedItemParams(
params,
INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME,
);
`${INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME} = ${formatValueForExpression(selectedItemId)}`;
export const k8sVolumeDetailsMetadataConfig: K8sDetailsMetadataConfig<InframonitoringtypesVolumeRecordDTO>[] =
[
@@ -43,23 +36,22 @@ export const k8sVolumeDetailsMetadataConfig: K8sDetailsMetadataConfig<Inframonit
export const k8sVolumeInitialEventsExpression = (
item: InframonitoringtypesVolumeRecordDTO,
): string =>
buildEventsExpression({
objectKind: 'PersistentVolumeClaim',
objectName: item.persistentVolumeClaimName || '',
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
): 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 k8sVolumeInitialLogTracesExpression = (
item: InframonitoringtypesVolumeRecordDTO,
): string =>
buildLogsTracesExpression({
mainAttributeKey: INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME,
mainAttributeValue: item.persistentVolumeClaimName,
clusterName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME],
namespaceName: item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME],
});
): string => {
const pvcName = formatValueForExpression(item.persistentVolumeClaimName || '');
const namespaceName = formatValueForExpression(
item.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
);
return `${INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME} = ${pvcName} AND ${INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME} = ${namespaceName}`;
};
export const k8sVolumeGetEntityName = (
item: InframonitoringtypesVolumeRecordDTO,
@@ -69,27 +61,22 @@ export const volumeWidgetInfo = [
{
title: 'Volume available',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-available',
},
{
title: 'Volume capacity',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-capacity',
},
{
title: 'Volume inodes used',
yAxisUnit: 'short',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-inodes-used',
},
{
title: 'Volume inodes',
yAxisUnit: 'short',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-inodes',
},
{
title: 'Volume inodes free',
yAxisUnit: 'short',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-inodes-free',
},
];

View File

@@ -1,5 +1,6 @@
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
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';
@@ -11,7 +12,6 @@ import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { SelectedItemParams } from '../hooks';
import { HardDrive } from '@signozhq/icons';
export function getK8sVolumeRowKey(
@@ -26,17 +26,8 @@ export function getK8sVolumeRowKey(
export function getK8sVolumeItemKey(
volume: InframonitoringtypesVolumeRecordDTO,
): SelectedItemParams {
return {
selectedItem:
volume.persistentVolumeClaimName ??
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_PERSISTENT_VOLUME_CLAIM_NAME] ??
null,
clusterName:
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_CLUSTER_NAME] ?? null,
namespaceName:
volume.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] ?? null,
};
): string {
return volume.persistentVolumeClaimName;
}
export type VolumeTableColumnConfig =
@@ -81,7 +72,11 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => {
const pvcName = value as string;
return <CellValueTooltip value={pvcName} />;
return (
<CellValueTooltip value={pvcName}>
<TanStackTable.Text>{pvcName}</TanStackTable.Text>
</CellValueTooltip>
);
},
},
{
@@ -97,7 +92,11 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
enableSort: false,
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return <CellValueTooltip value={namespaceName} />;
return (
<CellValueTooltip value={namespaceName}>
<TanStackTable.Text>{namespaceName}</TanStackTable.Text>
</CellValueTooltip>
);
},
},
{

View File

@@ -1,9 +1,5 @@
import { Color } from '@signozhq/design-tokens';
import { BadgeColor } from '@signozhq/ui/badge';
import {
InframonitoringtypesPodCountsByStatusDTO,
InframonitoringtypesPodStatusDTO,
} from 'api/generated/services/sigNoz.schemas';
import { InframonitoringtypesPodCountsByPhaseDTO } from 'api/generated/services/sigNoz.schemas';
import { StatusCountItem } from './components/GroupedStatusCounts';
@@ -68,106 +64,17 @@ export function getStrokeColorForLimitUtilization(value: number): string {
return Color.BG_SAKURA_500;
}
export const POD_STATUS_COLORS: Record<
InframonitoringtypesPodStatusDTO,
BadgeColor
> = {
[InframonitoringtypesPodStatusDTO.running]: 'forest',
[InframonitoringtypesPodStatusDTO.completed]: 'robin',
[InframonitoringtypesPodStatusDTO.pending]: 'amber',
[InframonitoringtypesPodStatusDTO.unknown]: 'vanilla',
[InframonitoringtypesPodStatusDTO.no_data]: 'vanilla',
[InframonitoringtypesPodStatusDTO.failed]: 'cherry',
[InframonitoringtypesPodStatusDTO.crashloopbackoff]: 'cherry',
[InframonitoringtypesPodStatusDTO.imagepullbackoff]: 'cherry',
[InframonitoringtypesPodStatusDTO.errimagepull]: 'cherry',
[InframonitoringtypesPodStatusDTO.createcontainerconfigerror]: 'cherry',
[InframonitoringtypesPodStatusDTO.containercreating]: 'amber',
[InframonitoringtypesPodStatusDTO.oomkilled]: 'cherry',
[InframonitoringtypesPodStatusDTO.error]: 'cherry',
[InframonitoringtypesPodStatusDTO.containercannotrun]: 'cherry',
[InframonitoringtypesPodStatusDTO.evicted]: 'cherry',
[InframonitoringtypesPodStatusDTO.nodeaffinity]: 'cherry',
[InframonitoringtypesPodStatusDTO.nodelost]: 'cherry',
[InframonitoringtypesPodStatusDTO.shutdown]: 'cherry',
[InframonitoringtypesPodStatusDTO.unexpectedadmissionerror]: 'cherry',
};
type PodStatusCategory =
| 'running'
| 'completed'
| 'pending'
| 'unknown'
| 'error';
const POD_STATUS_CATEGORY_MAP: Record<
keyof InframonitoringtypesPodCountsByStatusDTO,
PodStatusCategory
> = {
running: 'running',
completed: 'completed',
pending: 'pending',
unknown: 'unknown',
failed: 'error',
crashLoopBackOff: 'error',
imagePullBackOff: 'error',
errImagePull: 'error',
createContainerConfigError: 'error',
containerCreating: 'error',
oomKilled: 'error',
error: 'error',
containerCannotRun: 'error',
evicted: 'error',
nodeAffinity: 'error',
nodeLost: 'error',
shutdown: 'error',
unexpectedAdmissionError: 'error',
};
type ErrorStatusKey = {
[K in keyof InframonitoringtypesPodCountsByStatusDTO]: (typeof POD_STATUS_CATEGORY_MAP)[K] extends 'error'
? K
: never;
}[keyof InframonitoringtypesPodCountsByStatusDTO];
const ERROR_STATUS_LABELS: Record<ErrorStatusKey, string> = {
failed: 'Failed',
crashLoopBackOff: 'CrashLoopBackOff',
imagePullBackOff: 'ImagePullBackOff',
errImagePull: 'ErrImagePull',
createContainerConfigError: 'CreateContainerConfigError',
containerCreating: 'ContainerCreating',
oomKilled: 'OOMKilled',
error: 'Error',
containerCannotRun: 'ContainerCannotRun',
evicted: 'Evicted',
nodeAffinity: 'NodeAffinity',
nodeLost: 'NodeLost',
shutdown: 'Shutdown',
unexpectedAdmissionError: 'UnexpectedAdmissionError',
};
export function getPodStatusItems(
counts: InframonitoringtypesPodCountsByStatusDTO,
/**
* Builds StatusCountItem[] for GroupedStatusCounts from pod phase counts.
*/
export function getPodPhaseStatusItems(
counts: InframonitoringtypesPodCountsByPhaseDTO,
): StatusCountItem[] {
const errorKeys = Object.keys(ERROR_STATUS_LABELS) as ErrorStatusKey[];
const errorTotal = errorKeys.reduce((sum, key) => sum + counts[key], 0);
const errorBreakdown = errorKeys.map((key) => ({
label: ERROR_STATUS_LABELS[key],
value: counts[key],
}));
return [
{ value: counts.running, label: 'Running', color: Color.BG_FOREST_500 },
{ value: counts.completed, label: 'Completed', color: Color.BG_ROBIN_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 },
{
value: errorTotal,
label: 'Error Status',
color: Color.BG_CHERRY_500,
breakdown: errorBreakdown,
},
];
}

View File

@@ -52,7 +52,3 @@
.divider {
--divider-color: rgba(255, 255, 255, 0.14);
}
.value {
width: fit-content;
}

View File

@@ -1,9 +1,8 @@
import { useCallback, type MouseEvent } from 'react';
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 TanStackTable from 'components/TanStackTableView';
import { useInfraMonitoringCellActionsStore } from './useInfraMonitoringCellActionsStore';
@@ -12,10 +11,12 @@ 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 } =
@@ -93,7 +94,7 @@ export function CellValueTooltip({
className: styles.tooltipContentWrapper,
}}
>
<TanStackTable.Text className={styles.value}>{value}</TanStackTable.Text>
{children}
</TooltipSimple>
);
}

View File

@@ -17,40 +17,9 @@
flex-shrink: 0;
}
.valueWrapper {
min-width: 4ch;
}
.valueWrapperTooltip {
display: block;
width: fit-content;
}
.value {
font-variant-numeric: tabular-nums;
min-width: 4ch;
text-align: left;
cursor: default;
min-width: min-content;
}
.tooltipContent {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 120px;
}
.tooltipHeader {
font-weight: 600;
margin-bottom: 2px;
}
.tooltipRow {
display: flex;
justify-content: space-between;
gap: 12px;
}
.tooltipValue {
font-variant-numeric: tabular-nums;
}

View File

@@ -2,18 +2,11 @@ import { TooltipSimple } from '@signozhq/ui/tooltip';
import styles from './GroupedStatusCounts.module.scss';
import TanStackTable from 'components/TanStackTableView';
import { Typography } from '@signozhq/ui/typography';
export interface StatusBreakdownItem {
label: string;
value: number;
}
export interface StatusCountItem {
value: number;
label: string;
color: string;
breakdown?: StatusBreakdownItem[];
}
interface GroupedStatusCountsProps {
@@ -21,45 +14,6 @@ interface GroupedStatusCountsProps {
showZeroValues?: boolean;
}
function buildTooltipContent(item: StatusCountItem): React.ReactNode {
if (!item.breakdown || item.breakdown.length === 0) {
return (
<Typography.Text>
{item.label}: {item.value}
</Typography.Text>
);
}
const nonZeroBreakdown = item.breakdown.filter((b) => b.value > 0);
if (nonZeroBreakdown.length === 0) {
return (
<div className={styles.tooltipContent}>
<Typography.Text className={styles.tooltipHeader}>
{item.label}
</Typography.Text>
<Typography.Text>No errors</Typography.Text>
</div>
);
}
return (
<div className={styles.tooltipContent}>
<Typography.Text className={styles.tooltipHeader}>
{item.label}
</Typography.Text>
{nonZeroBreakdown.map((b) => (
<div key={b.label} className={styles.tooltipRow}>
<Typography.Text>{b.label}</Typography.Text>
<Typography.Text className={styles.tooltipValue}>
{b.value}
</Typography.Text>
</div>
))}
</div>
);
}
export function GroupedStatusCounts({
items,
showZeroValues = true,
@@ -79,15 +33,13 @@ export function GroupedStatusCounts({
className={styles.separator}
style={{ backgroundColor: item.color }}
/>
<div className={styles.valueWrapper}>
<TooltipSimple title={buildTooltipContent(item)} arrow align="start">
<span className={styles.valueWrapperTooltip}>
<TanStackTable.Text className={styles.value}>
{item.value || '-'}
</TanStackTable.Text>
</span>
</TooltipSimple>
</div>
<TooltipSimple title={`${item.label}: ${item.value}`}>
<span>
<TanStackTable.Text className={styles.value}>
{item.value || '-'}
</TanStackTable.Text>
</span>
</TooltipSimple>
</div>
))}
</div>

View File

@@ -2,12 +2,8 @@ import {
FiltersType,
IQuickFiltersConfig,
} from 'components/QuickFilters/types';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
import { DataSource } from 'types/common/queryBuilder';
// TODO(backend): Find a way to generate this via openapi
export const INFRA_MONITORING_ATTR_KEYS = {
@@ -134,7 +130,6 @@ export enum VIEWS {
CONTAINERS = 'containers',
PROCESSES = 'processes',
EVENTS = 'events',
POD_METRICS = 'pod_metrics',
}
export const VIEW_TYPES = {
@@ -142,7 +137,6 @@ export const VIEW_TYPES = {
LOGS: VIEWS.LOGS,
TRACES: VIEWS.TRACES,
EVENTS: VIEWS.EVENTS,
POD_METRICS: VIEWS.POD_METRICS,
};
export const K8sCategories = {
@@ -903,8 +897,6 @@ export const INFRA_MONITORING_K8S_PARAMS_KEYS = {
PAGE_SIZE: 'pageSize',
EXPANDED: 'expanded',
SELECTED_ITEM: 'selectedItem',
SELECTED_ITEM_CLUSTER_NAME: 'selectedItemClusterName',
SELECTED_ITEM_NAMESPACE_NAME: 'selectedItemNamespaceName',
};
/** Metric namespace prefixes for /fields/keys and /fields/values APIs */
@@ -922,261 +914,3 @@ export const METRIC_NAMESPACE_BY_ENTITY: Record<InfraMonitoringEntity, string> =
[InfraMonitoringEntity.JOBS]: 'k8s.',
[InfraMonitoringEntity.VOLUMES]: 'k8s.volume.',
};
export interface WorkloadFilterContext {
workloadNameKey: string;
workloadNameValue: string;
clusterName: string;
namespaceName?: string;
}
export const podUtilizationByPodWidgetInfo = [
{
title: 'CPU Limit Utilization By Pod Name',
yAxisUnit: 'percentunit',
},
{
title: 'CPU Request Utilization By Pod Name',
yAxisUnit: 'percentunit',
},
{
title: 'Memory Limit Utilization By Pod Name',
yAxisUnit: 'percentunit',
},
{
title: 'Memory Request Utilization By Pod Name',
yAxisUnit: 'percentunit',
},
{
title: 'FileSystem Usage Percentage By Pod Name',
yAxisUnit: 'percentunit',
},
];
export function getPodUtilizationByPodQueryPayloads(
context: WorkloadFilterContext,
start: number,
end: number,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] {
const getKey = (dotKey: string, underscoreKey: string): string =>
dotMetricsEnabled ? dotKey : underscoreKey;
const k8sPodCpuLimitUtilKey = getKey(
'k8s.pod.cpu_limit_utilization',
'k8s_pod_cpu_limit_utilization',
);
const k8sPodCpuRequestUtilKey = getKey(
'k8s.pod.cpu_request_utilization',
'k8s_pod_cpu_request_utilization',
);
const k8sPodMemLimitUtilKey = getKey(
'k8s.pod.memory_limit_utilization',
'k8s_pod_memory_limit_utilization',
);
const k8sPodMemRequestUtilKey = getKey(
'k8s.pod.memory_request_utilization',
'k8s_pod_memory_request_utilization',
);
const k8sPodFsUsageKey = getKey(
'k8s.pod.filesystem.usage',
'k8s_pod_filesystem_usage',
);
const k8sPodFsCapacityKey = getKey(
'k8s.pod.filesystem.capacity',
'k8s_pod_filesystem_capacity',
);
const k8sPodNameKey = getKey('k8s.pod.name', 'k8s_pod_name');
const k8sClusterNameKey = getKey('k8s.cluster.name', 'k8s_cluster_name');
const k8sNamespaceNameKey = getKey('k8s.namespace.name', 'k8s_namespace_name');
const baseFilters = [
{
id: 'workload',
key: {
dataType: DataTypes.String,
id: `${context.workloadNameKey}--string--tag--false`,
key: context.workloadNameKey,
type: 'tag',
},
op: '=',
value: context.workloadNameValue,
},
{
id: 'cluster',
key: {
dataType: DataTypes.String,
id: `${k8sClusterNameKey}--string--tag--false`,
key: k8sClusterNameKey,
type: 'tag',
},
op: '=',
value: context.clusterName,
},
...(context.namespaceName
? [
{
id: 'namespace',
key: {
dataType: DataTypes.String,
id: `${k8sNamespaceNameKey}--string--tag--false`,
key: k8sNamespaceNameKey,
type: 'tag',
},
op: '=',
value: context.namespaceName,
},
]
: []),
];
const podNameGroupBy = [
{
dataType: DataTypes.String,
id: `${k8sPodNameKey}--string--tag--false`,
key: k8sPodNameKey,
type: 'tag',
},
];
const buildSingleMetricQuery = (
metricKey: string,
metricId: string,
): GetQueryResultsProps => ({
selectedTime: 'GLOBAL_TIME',
graphType: PANEL_TYPES.TIME_SERIES,
query: {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: metricId,
key: metricKey,
type: 'Gauge',
},
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
disabled: false,
expression: 'A',
filters: {
items: [...baseFilters],
op: 'AND',
},
functions: [],
groupBy: podNameGroupBy,
having: [],
legend: `{{${k8sPodNameKey}}}`,
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: ReduceOperators.AVG,
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: v4(),
promql: [{ disabled: false, legend: '', name: 'A', query: '' }],
queryType: EQueryType.QUERY_BUILDER,
},
variables: {},
formatForWeb: false,
start,
end,
});
const filesystemUsagePercentQuery: GetQueryResultsProps = {
selectedTime: 'GLOBAL_TIME',
graphType: PANEL_TYPES.TIME_SERIES,
query: {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'fs_usage',
key: k8sPodFsUsageKey,
type: 'Gauge',
},
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
disabled: true,
expression: 'A',
filters: {
items: [...baseFilters],
op: 'AND',
},
functions: [],
groupBy: podNameGroupBy,
having: [],
legend: `{{${k8sPodNameKey}}}`,
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: ReduceOperators.AVG,
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
},
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'fs_capacity',
key: k8sPodFsCapacityKey,
type: 'Gauge',
},
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
disabled: true,
expression: 'B',
filters: {
items: [...baseFilters],
op: 'AND',
},
functions: [],
groupBy: podNameGroupBy,
having: [],
legend: `{{${k8sPodNameKey}}}`,
limit: null,
orderBy: [],
queryName: 'B',
reduceTo: ReduceOperators.AVG,
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
},
],
queryFormulas: [
{
disabled: false,
expression: 'A/B',
legend: `{{${k8sPodNameKey}}}`,
queryName: 'F1',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: v4(),
promql: [{ disabled: false, legend: '', name: 'A', query: '' }],
queryType: EQueryType.QUERY_BUILDER,
},
variables: {},
formatForWeb: false,
start,
end,
};
return [
buildSingleMetricQuery(k8sPodCpuLimitUtilKey, 'cpu_limit_util'),
buildSingleMetricQuery(k8sPodCpuRequestUtilKey, 'cpu_request_util'),
buildSingleMetricQuery(k8sPodMemLimitUtilKey, 'mem_limit_util'),
buildSingleMetricQuery(k8sPodMemRequestUtilKey, 'mem_request_util'),
filesystemUsagePercentQuery,
];
}

View File

@@ -5,10 +5,8 @@ import {
parseAsJson,
parseAsString,
useQueryState,
useQueryStates,
UseQueryStateReturn,
} from 'nuqs';
import { useCallback, useMemo } from 'react';
import {
IBuilderQuery,
TagFilter,
@@ -130,70 +128,16 @@ export const useInfraMonitoringCategory = (): UseQueryStateReturn<
parseAsString.withDefault(K8sCategories.PODS).withOptions(defaultNuqsOptions),
);
export interface SelectedItemParams {
selectedItem: string | null;
clusterName?: string | null;
namespaceName?: string | null;
}
const selectedItemParamsParsers = {
[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM]: parseAsString,
[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM_CLUSTER_NAME]: parseAsString,
[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM_NAMESPACE_NAME]: parseAsString,
export const useInfraMonitoringSelectedItem = (): UseQueryStateReturn<
string,
string | undefined
> => {
return useQueryState(
INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM,
parseAsString,
);
};
export type UseSelectedItemParamsReturn = [
SelectedItemParams,
(params: SelectedItemParams | null) => void,
];
export const useInfraMonitoringSelectedItemParams =
(): UseSelectedItemParamsReturn => {
const [rawParams, setRawParams] = useQueryStates(
selectedItemParamsParsers,
defaultNuqsOptions,
);
const params: SelectedItemParams = useMemo(
() => ({
selectedItem:
rawParams[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM] ?? null,
clusterName:
rawParams[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM_CLUSTER_NAME] ??
null,
namespaceName:
rawParams[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM_NAMESPACE_NAME] ??
null,
}),
[rawParams],
);
const setParams = useCallback(
(newParams: Partial<SelectedItemParams> | null): void => {
if (newParams === null) {
void setRawParams({
[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM]: null,
[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM_CLUSTER_NAME]: null,
[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM_NAMESPACE_NAME]: null,
});
return;
}
void setRawParams({
[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM]:
newParams.selectedItem ?? null,
[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM_CLUSTER_NAME]:
newParams.clusterName ?? null,
[INFRA_MONITORING_K8S_PARAMS_KEYS.SELECTED_ITEM_NAMESPACE_NAME]:
newParams.namespaceName ?? null,
});
},
[setRawParams],
);
return [params, setParams];
};
export const useInfraMonitoringStatusFilter = (): UseQueryStateReturn<
string,
string

View File

@@ -30,7 +30,7 @@ export function makeGroup(overrides: Partial<MapperGroup> = {}): MapperGroup {
export function makeMapper(overrides: Partial<Mapper> = {}): Mapper {
return {
id: 'mapper-1',
groupId: 'group-1',
group_id: 'group-1',
name: 'gen_ai.request.model',
enabled: true,
fieldContext: FieldContext.attribute,
@@ -89,5 +89,5 @@ export const mockGroups: MapperGroup[] = [
];
export const mockMappers: Mapper[] = [
makeMapper({ id: 'mapper-1', groupId: 'group-1' }),
makeMapper({ id: 'mapper-1', group_id: 'group-1' }),
];

View File

@@ -3,13 +3,3 @@
flex-direction: column;
gap: var(--spacing-8);
}
.tabLabel {
display: inline-flex;
align-items: center;
gap: 8px;
}
.tabBadge {
margin: 0;
}

View File

@@ -1,58 +1,30 @@
import { Badge } from '@signozhq/ui/badge';
import { Tabs } from '@signozhq/ui/tabs';
import { useListUnmappedLLMModels } from 'api/generated/services/llmpricingrules';
import { parseAsStringEnum, useQueryState } from 'nuqs';
import { MODEL_COSTS_TAB, TAB_KEY, UNPRICED_MODELS_TAB } from './constants';
import styles from './LLMObservabilityModelPricing.module.scss';
import ModelCostTabPanel from './ModelCostTabPanel';
import UnpricedModelsTab from './UnpricedModelsTab';
import styles from './LLMObservabilityModelPricing.module.scss';
function LLMObservabilityModelPricing(): JSX.Element {
const [activeTab, setActiveTab] = useQueryState(
TAB_KEY,
parseAsStringEnum([MODEL_COSTS_TAB, UNPRICED_MODELS_TAB]).withDefault(
MODEL_COSTS_TAB,
),
);
// Count powers the tab badge; deduped with the tab's own fetch by react-query.
const { data } = useListUnmappedLLMModels();
const unpricedCount = data?.data?.items?.length ?? 0;
return (
<div
className={styles.llmObservabilityModelPricing}
data-testid="llm-observability-model-pricing-page"
>
<Tabs
value={activeTab}
onChange={(key): void => {
void setActiveTab(key as typeof activeTab);
}}
// Model costs is the only enabled tab for now, so default to it. When
// the unpriced-models tab lands in a later PR.
defaultValue="model-costs"
items={[
{
key: MODEL_COSTS_TAB,
key: 'model-costs',
label: 'Model costs',
children: <ModelCostTabPanel />,
},
{
key: UNPRICED_MODELS_TAB,
label: (
<span className={styles.tabLabel}>
Unpriced models
{unpricedCount > 0 && (
<Badge
variant="default"
className={styles.tabBadge}
data-testid="unpriced-models-count"
>
{unpricedCount}
</Badge>
)}
</span>
),
children: <UnpricedModelsTab />,
// Unpriced-models tab lands in a later PR.
key: 'unpriced-models',
label: 'Unpriced models',
disabled: true,
children: null,
},
]}
/>

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