mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-15 19:00:34 +01:00
Compare commits
2 Commits
main
...
anomaly-v2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6356b3b8c | ||
|
|
90512748fb |
@@ -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
|
||||
@@ -13801,7 +13742,7 @@ paths:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SpantypesGettableSpanMappers'
|
||||
$ref: '#/components/schemas/SpantypesGettableSpanMapperGroups'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
|
||||
@@ -5669,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;
|
||||
};
|
||||
@@ -5846,10 +5813,6 @@ export interface InframonitoringtypesClusterRecordDTO {
|
||||
* @type string
|
||||
*/
|
||||
clusterName: string;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
counts: InframonitoringtypesClusterRecordDTOCounts;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
@@ -6405,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;
|
||||
};
|
||||
@@ -6439,10 +6379,6 @@ export type InframonitoringtypesNamespaceRecordDTOMeta =
|
||||
InframonitoringtypesNamespaceRecordDTOMetaAnyOf | null;
|
||||
|
||||
export interface InframonitoringtypesNamespaceRecordDTO {
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
counts: InframonitoringtypesNamespaceRecordDTOCounts;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
@@ -9258,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',
|
||||
@@ -9574,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;
|
||||
/**
|
||||
@@ -9622,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;
|
||||
/**
|
||||
@@ -10923,7 +10852,7 @@ export type ListSpanMappersPathParameters = {
|
||||
groupId: string;
|
||||
};
|
||||
export type ListSpanMappers200 = {
|
||||
data: SpantypesGettableSpanMappersDTO;
|
||||
data: SpantypesGettableSpanMapperGroupsDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?: (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -188,7 +188,7 @@ function AnomalyThreshold({
|
||||
}}
|
||||
options={ANOMALY_SEASONALITY_OPTIONS}
|
||||
/>
|
||||
{notificationSettings.routingPolicies ? (
|
||||
{!notificationSettings.routingPolicies ? (
|
||||
<>
|
||||
<Typography.Text
|
||||
data-testid="seasonality-text"
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -52,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';
|
||||
@@ -63,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;
|
||||
|
||||
@@ -76,8 +71,6 @@ 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;
|
||||
@@ -88,7 +81,7 @@ export interface K8sBaseDetailsProps<T> {
|
||||
category: InfraMonitoringEntity;
|
||||
eventCategory: string;
|
||||
// Data fetching configuration
|
||||
getSelectedItemExpression: (params: SelectedItemParams) => string;
|
||||
getSelectedItemExpression: (selectedItem: string) => string;
|
||||
fetchEntityData: (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
@@ -98,12 +91,9 @@ export interface K8sBaseDetailsProps<T> {
|
||||
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,
|
||||
@@ -146,8 +136,6 @@ export default function K8sBaseDetails<T>({
|
||||
getInitialLogTracesExpression,
|
||||
getInitialEventsExpression,
|
||||
metadataConfig,
|
||||
countsConfig,
|
||||
getCountsFilterExpression,
|
||||
entityWidgetInfo,
|
||||
getEntityQueryPayload,
|
||||
queryKeyPrefix,
|
||||
@@ -164,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(
|
||||
() =>
|
||||
@@ -174,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 {
|
||||
@@ -201,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);
|
||||
},
|
||||
@@ -226,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) : '';
|
||||
|
||||
@@ -490,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 && (
|
||||
|
||||
@@ -10,6 +10,7 @@ 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';
|
||||
@@ -20,10 +21,8 @@ import {
|
||||
InfraMonitoringEntity,
|
||||
} from '../constants';
|
||||
import {
|
||||
SelectedItemParams,
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringOrderBy,
|
||||
useInfraMonitoringSelectedItemParams,
|
||||
useInfraMonitoringStatusFilter,
|
||||
} from '../hooks';
|
||||
import { useInfraMonitoringLineClamp } from '../components';
|
||||
@@ -50,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;
|
||||
@@ -73,8 +69,8 @@ export type K8sBaseListProps<
|
||||
}>;
|
||||
/** 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,
|
||||
@@ -82,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,
|
||||
@@ -96,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);
|
||||
@@ -232,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, {
|
||||
@@ -251,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, {
|
||||
@@ -313,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}
|
||||
@@ -386,7 +347,7 @@ export function K8sBaseList<
|
||||
{showEmptyState ? (
|
||||
<div className={styles.emptyStateContainer}>{emptyTableMessage}</div>
|
||||
) : (
|
||||
<TanStackTable<T, TItemKey>
|
||||
<TanStackTable<T>
|
||||
data={pageData}
|
||||
columns={tableColumns}
|
||||
columnStorageKey={columnStorageKey}
|
||||
|
||||
@@ -22,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';
|
||||
|
||||
@@ -35,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 */
|
||||
@@ -60,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,
|
||||
@@ -72,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();
|
||||
@@ -176,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 => {
|
||||
@@ -248,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}
|
||||
|
||||
@@ -22,7 +22,6 @@ import { openInNewTab } from 'utils/navigation';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
|
||||
import { InfraMonitoringEntity } from '../../constants';
|
||||
import { SelectedItemParams } from '../../hooks';
|
||||
|
||||
window.ResizeObserver =
|
||||
window.ResizeObserver ||
|
||||
@@ -166,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;
|
||||
}) {
|
||||
@@ -200,7 +196,7 @@ function renderComponent<
|
||||
value={{ viewportHeight: 800, itemHeight: 50 }}
|
||||
>
|
||||
<TooltipProvider>
|
||||
<K8sBaseList<T, TItemKey> {...props} />
|
||||
<K8sBaseList {...props} />
|
||||
</TooltipProvider>
|
||||
</VirtuosoMockContext.Provider>
|
||||
</NuqsTestingAdapter>
|
||||
@@ -945,113 +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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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('');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 ');
|
||||
}
|
||||
|
||||
@@ -14,9 +14,7 @@ import { InfraMonitoringEntity } from '../constants';
|
||||
import {
|
||||
clusterWidgetInfo,
|
||||
getClusterMetricsQueryPayload,
|
||||
k8sClusterDetailsCountsConfig,
|
||||
k8sClusterDetailsMetadataConfig,
|
||||
k8sClusterGetCountsFilterExpression,
|
||||
k8sClusterGetEntityName,
|
||||
k8sClusterGetSelectedItemExpression,
|
||||
k8sClusterInitialEventsExpression,
|
||||
@@ -138,8 +136,6 @@ function K8sClustersList({
|
||||
getInitialLogTracesExpression={k8sClusterInitialLogTracesExpression}
|
||||
getInitialEventsExpression={k8sClusterInitialEventsExpression}
|
||||
metadataConfig={k8sClusterDetailsMetadataConfig}
|
||||
countsConfig={k8sClusterDetailsCountsConfig}
|
||||
getCountsFilterExpression={k8sClusterGetCountsFilterExpression}
|
||||
entityWidgetInfo={clusterWidgetInfo}
|
||||
getEntityQueryPayload={getClusterMetricsQueryPayload}
|
||||
queryKeyPrefix="cluster"
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ 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,
|
||||
@@ -112,7 +111,7 @@ function K8sDaemonSetsList({
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<InframonitoringtypesDaemonSetRecordDTO, SelectedItemParams>
|
||||
<K8sBaseList<InframonitoringtypesDaemonSetRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
tableColumns={k8sDaemonSetsColumnsConfig}
|
||||
|
||||
@@ -7,21 +7,13 @@ import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
import {
|
||||
buildEventsExpression,
|
||||
buildExpressionFromSelectedItemParams,
|
||||
buildLogsTracesExpression,
|
||||
} from 'container/InfraMonitoringK8sV2/Base/utils';
|
||||
|
||||
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>[] =
|
||||
[
|
||||
@@ -45,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,
|
||||
@@ -71,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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -141,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',
|
||||
@@ -208,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',
|
||||
},
|
||||
@@ -250,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',
|
||||
},
|
||||
@@ -292,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',
|
||||
},
|
||||
@@ -368,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',
|
||||
},
|
||||
@@ -410,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',
|
||||
},
|
||||
@@ -452,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',
|
||||
},
|
||||
@@ -528,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',
|
||||
},
|
||||
@@ -617,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',
|
||||
},
|
||||
|
||||
@@ -6,7 +6,6 @@ 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, getPodPhaseStatusItems } from '../commonUtils';
|
||||
import {
|
||||
CellValueTooltip,
|
||||
@@ -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 =
|
||||
@@ -140,38 +132,28 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -330,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 => (
|
||||
@@ -402,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>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -11,7 +11,6 @@ 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,
|
||||
@@ -118,7 +117,7 @@ function K8sDeploymentsList({
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<InframonitoringtypesDeploymentRecordDTO, SelectedItemParams>
|
||||
<K8sBaseList<InframonitoringtypesDeploymentRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
tableColumns={k8sDeploymentsColumnsConfig}
|
||||
|
||||
@@ -7,21 +7,13 @@ import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
import {
|
||||
buildEventsExpression,
|
||||
buildExpressionFromSelectedItemParams,
|
||||
buildLogsTracesExpression,
|
||||
} from 'container/InfraMonitoringK8sV2/Base/utils';
|
||||
|
||||
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>[] =
|
||||
[
|
||||
@@ -44,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,
|
||||
@@ -71,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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -136,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 [
|
||||
{
|
||||
@@ -207,7 +155,6 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -249,7 +196,6 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -291,7 +237,6 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -367,7 +312,6 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -409,7 +353,6 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -451,7 +394,6 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -527,7 +469,6 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -616,7 +557,6 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
|
||||
@@ -6,7 +6,6 @@ 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, getPodPhaseStatusItems } from '../commonUtils';
|
||||
import {
|
||||
CellValueTooltip,
|
||||
@@ -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>[] =
|
||||
@@ -133,10 +125,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
},
|
||||
},
|
||||
{
|
||||
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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -11,7 +11,6 @@ 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,
|
||||
jobWidgetInfo,
|
||||
@@ -118,7 +117,7 @@ function K8sJobsList({
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<InframonitoringtypesJobRecordDTO, SelectedItemParams>
|
||||
<K8sBaseList<InframonitoringtypesJobRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.JOBS}
|
||||
tableColumns={k8sJobsColumnsConfig}
|
||||
|
||||
@@ -7,21 +7,13 @@ import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
import {
|
||||
buildEventsExpression,
|
||||
buildExpressionFromSelectedItemParams,
|
||||
buildLogsTracesExpression,
|
||||
} from 'container/InfraMonitoringK8sV2/Base/utils';
|
||||
|
||||
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>[] =
|
||||
[
|
||||
@@ -44,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,
|
||||
@@ -70,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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -108,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',
|
||||
@@ -175,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: [],
|
||||
@@ -236,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: [],
|
||||
@@ -297,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: [],
|
||||
@@ -371,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: [],
|
||||
|
||||
@@ -6,7 +6,6 @@ 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, getPodPhaseStatusItems } from '../commonUtils';
|
||||
import {
|
||||
CellValueTooltip,
|
||||
@@ -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 =
|
||||
@@ -132,10 +126,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
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,
|
||||
@@ -155,7 +149,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
|
||||
{
|
||||
value: row.desiredSuccessfulPods,
|
||||
label: 'Desired',
|
||||
color: Color.BG_AMBER_500,
|
||||
color: Color.BG_ROBIN_500,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -11,12 +11,9 @@ 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,
|
||||
k8sNamespaceDetailsCountsConfig,
|
||||
k8sNamespaceDetailsMetadataConfig,
|
||||
k8sNamespaceGetCountsFilterExpression,
|
||||
k8sNamespaceGetEntityName,
|
||||
k8sNamespaceGetSelectedItemExpression,
|
||||
k8sNamespaceInitialEventsExpression,
|
||||
@@ -120,7 +117,7 @@ function K8sNamespacesList({
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<InframonitoringtypesNamespaceRecordDTO, SelectedItemParams>
|
||||
<K8sBaseList<InframonitoringtypesNamespaceRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.NAMESPACES}
|
||||
tableColumns={k8sNamespacesColumnsConfig}
|
||||
@@ -139,8 +136,6 @@ function K8sNamespacesList({
|
||||
getInitialLogTracesExpression={k8sNamespaceInitialLogTracesExpression}
|
||||
getInitialEventsExpression={k8sNamespaceInitialEventsExpression}
|
||||
metadataConfig={k8sNamespaceDetailsMetadataConfig}
|
||||
countsConfig={k8sNamespaceDetailsCountsConfig}
|
||||
getCountsFilterExpression={k8sNamespaceGetCountsFilterExpression}
|
||||
entityWidgetInfo={namespaceWidgetInfo}
|
||||
getEntityQueryPayload={getNamespaceMetricsQueryPayload}
|
||||
queryKeyPrefix="namespace"
|
||||
|
||||
@@ -6,29 +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 {
|
||||
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>[] =
|
||||
[
|
||||
@@ -40,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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -254,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 [
|
||||
{
|
||||
@@ -304,7 +211,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -344,7 +250,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -384,7 +289,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -424,7 +328,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -498,7 +401,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -538,7 +440,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -578,7 +479,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -618,7 +518,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -658,7 +557,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -698,7 +596,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -772,7 +669,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -853,7 +749,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -934,7 +829,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1021,7 +915,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1108,7 +1001,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1155,7 +1047,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1202,7 +1093,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1283,7 +1173,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1336,7 +1225,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1423,7 +1311,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1470,7 +1357,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1517,7 +1403,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1564,7 +1449,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1645,7 +1529,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1692,7 +1575,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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-',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -152,11 +152,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
|
||||
},
|
||||
{
|
||||
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,
|
||||
@@ -320,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 },
|
||||
@@ -336,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 },
|
||||
@@ -352,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 },
|
||||
|
||||
@@ -11,7 +11,6 @@ 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,
|
||||
k8sStatefulSetDetailsMetadataConfig,
|
||||
@@ -118,7 +117,7 @@ function K8sStatefulSetsList({
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<InframonitoringtypesStatefulSetRecordDTO, SelectedItemParams>
|
||||
<K8sBaseList<InframonitoringtypesStatefulSetRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.STATEFULSETS}
|
||||
tableColumns={k8sStatefulSetsColumnsConfig}
|
||||
|
||||
@@ -7,21 +7,13 @@ import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { K8sDetailsMetadataConfig } from '../Base/K8sBaseDetails';
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
import {
|
||||
buildEventsExpression,
|
||||
buildExpressionFromSelectedItemParams,
|
||||
buildLogsTracesExpression,
|
||||
} from 'container/InfraMonitoringK8sV2/Base/utils';
|
||||
|
||||
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>[] =
|
||||
[
|
||||
@@ -30,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 =>
|
||||
@@ -44,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,
|
||||
@@ -72,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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -118,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'
|
||||
@@ -228,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',
|
||||
},
|
||||
@@ -271,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',
|
||||
},
|
||||
@@ -314,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',
|
||||
},
|
||||
@@ -377,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',
|
||||
},
|
||||
@@ -420,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',
|
||||
},
|
||||
@@ -483,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',
|
||||
},
|
||||
@@ -513,7 +525,7 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'f1',
|
||||
id: 'f3',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'pod_name',
|
||||
@@ -526,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',
|
||||
},
|
||||
@@ -569,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',
|
||||
},
|
||||
@@ -632,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',
|
||||
},
|
||||
@@ -675,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',
|
||||
},
|
||||
@@ -738,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',
|
||||
},
|
||||
@@ -814,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',
|
||||
},
|
||||
|
||||
@@ -6,7 +6,6 @@ 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, getPodPhaseStatusItems } from '../commonUtils';
|
||||
import {
|
||||
CellValueTooltip,
|
||||
@@ -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>[] =
|
||||
@@ -141,10 +135,10 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
|
||||
},
|
||||
},
|
||||
{
|
||||
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,
|
||||
|
||||
@@ -11,7 +11,6 @@ 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,
|
||||
@@ -118,7 +117,7 @@ function K8sVolumesList({
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<InframonitoringtypesVolumeRecordDTO, SelectedItemParams>
|
||||
<K8sBaseList<InframonitoringtypesVolumeRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.VOLUMES}
|
||||
tableColumns={k8sVolumesColumnsConfig}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -897,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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' }),
|
||||
];
|
||||
|
||||
@@ -3,13 +3,3 @@
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.tabLabel {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tabBadge {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { toast } from '@signozhq/ui/sonner';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import {
|
||||
getListLLMPricingRulesQueryKey,
|
||||
getListUnmappedLLMModelsQueryKey,
|
||||
useCreateOrUpdateLLMPricingRules,
|
||||
} from 'api/generated/services/llmpricingrules';
|
||||
|
||||
@@ -11,16 +10,9 @@ import {
|
||||
EMPTY_DRAFT,
|
||||
TOAST_MODEL_COST_ADDED,
|
||||
TOAST_MODEL_COST_UPDATED,
|
||||
} from 'container/LLMObservability/Settings/ModelPricing/constants';
|
||||
import type {
|
||||
DrawerDraft,
|
||||
DrawerMode,
|
||||
PricingRule,
|
||||
} from 'container/LLMObservability/Settings/ModelPricing/types';
|
||||
import {
|
||||
buildRulePayload,
|
||||
draftFromRule,
|
||||
} from 'container/LLMObservability/Settings/ModelPricing/utils';
|
||||
} from '../../../../constants';
|
||||
import type { DrawerDraft, DrawerMode, PricingRule } from '../../../../types';
|
||||
import { buildRulePayload, draftFromRule } from '../../../../utils';
|
||||
|
||||
interface UseModelCostDrawerResult {
|
||||
isOpen: boolean;
|
||||
@@ -46,26 +38,17 @@ export function useModelCostDrawer(): UseModelCostDrawerResult {
|
||||
const { mutateAsync: createOrUpdate, isLoading: isSaving } =
|
||||
useCreateOrUpdateLLMPricingRules();
|
||||
|
||||
// Adding pricing can also resolve a model that was showing up as unpriced, so
|
||||
// refresh the unmapped list (and its tab-badge count) alongside the rules list.
|
||||
const invalidateList = useCallback(async (): Promise<void> => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getListLLMPricingRulesQueryKey(),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getListUnmappedLLMModelsQueryKey(),
|
||||
}),
|
||||
]);
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: getListLLMPricingRulesQueryKey(),
|
||||
});
|
||||
}, [queryClient]);
|
||||
|
||||
// prefillModelName seeds the billing model ID when adding from an unpriced
|
||||
// model row, so the user only has to fill in pricing.
|
||||
const openForAdd = useCallback((prefillModelName?: string): void => {
|
||||
const openForAdd = useCallback((): void => {
|
||||
setMode('add');
|
||||
setInitialDraft({
|
||||
...EMPTY_DRAFT,
|
||||
modelName: prefillModelName ?? '',
|
||||
modelName: '',
|
||||
patterns: [],
|
||||
});
|
||||
setSelectedRuleId(null);
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
.drawerSurface {
|
||||
padding: var(--spacing-7);
|
||||
border-radius: 6px;
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
.unpricedModelsTab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.banner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-5);
|
||||
padding: var(--spacing-6) var(--spacing-8);
|
||||
border-radius: var(--radius-2);
|
||||
border: 1px solid color-mix(in srgb, var(--bg-amber-400) 30%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-amber-400) 8%, transparent);
|
||||
color: var(--bg-amber-300, var(--bg-amber-400));
|
||||
}
|
||||
|
||||
.bannerIcon {
|
||||
flex-shrink: 0;
|
||||
margin-top: var(--spacing-1);
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: var(--spacing-6) var(--spacing-8);
|
||||
border-radius: var(--radius-2);
|
||||
background: color-mix(in srgb, var(--bg-cherry-400) 8%, transparent);
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { TriangleAlert } from '@signozhq/icons';
|
||||
import { useListUnmappedLLMModels } from 'api/generated/services/llmpricingrules';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import styles from './UnpricedModelsTab.module.scss';
|
||||
import ModelCostDrawer, {
|
||||
useModelCostDrawer,
|
||||
} from '../ModelCostTabPanel/components/ModelCostDrawer';
|
||||
import type { PricingRule, UnpricedModel } from '../types';
|
||||
import MapConfirmDialog from './components/MapConfirmDialog';
|
||||
import type { UnpricedColumnsConfig } from './components/UnpricedModelsTable/TableConfig';
|
||||
import UnpricedModelsTable from './components/UnpricedModelsTable';
|
||||
import { useUnpricedModelMapping } from './hooks/useUnpricedModelMapping';
|
||||
import { usePendingMappingStore } from './usePendingMappingStore';
|
||||
|
||||
function UnpricedModelsTab(): JSX.Element {
|
||||
const { data, isLoading, isError } = useListUnmappedLLMModels();
|
||||
|
||||
const { user } = useAppContext();
|
||||
const [canManagePricing] = useComponentPermission(
|
||||
['manage_llm_pricing'],
|
||||
user.role,
|
||||
);
|
||||
|
||||
const models: UnpricedModel[] = useMemo(() => data?.data?.items || [], [data]);
|
||||
|
||||
// Picking a billing model stages a single mapping for confirmation; the
|
||||
// mapping only commits once the user confirms in the dialog. Kept in a store
|
||||
// (not local state) so the row's memoized select trigger can mirror the pick —
|
||||
// see usePendingMappingStore.
|
||||
const pendingMapping = usePendingMappingStore((state) => state.pending);
|
||||
const setPending = usePendingMappingStore((state) => state.setPending);
|
||||
const clearPending = usePendingMappingStore((state) => state.clearPending);
|
||||
const { mapModel, isSaving } = useUnpricedModelMapping();
|
||||
|
||||
// Reset any staged mapping when leaving the tab so a stale pick doesn't reopen
|
||||
// the dialog on remount (the store outlives this component).
|
||||
useEffect(() => (): void => clearPending(), [clearPending]);
|
||||
|
||||
// Reuses the "Model costs" add/edit drawer to define brand-new pricing for a
|
||||
// model that has no matching billing model to map onto. Saving resolves the
|
||||
// model, so it drops off this tab (the drawer invalidates the unmapped list).
|
||||
const drawer = useModelCostDrawer();
|
||||
|
||||
const onRequestMap = useCallback(
|
||||
(model: UnpricedModel, rule: PricingRule): void => {
|
||||
setPending({ model, rule });
|
||||
},
|
||||
[setPending],
|
||||
);
|
||||
|
||||
const onConfirmMap = useCallback(async (): Promise<void> => {
|
||||
if (!pendingMapping) {
|
||||
return;
|
||||
}
|
||||
const didSave = await mapModel(pendingMapping);
|
||||
if (didSave) {
|
||||
clearPending();
|
||||
}
|
||||
}, [mapModel, pendingMapping, clearPending]);
|
||||
|
||||
// openForAdd is stable (useCallback with no deps); drawer itself is a fresh
|
||||
// object each render, so depend on the method, not the object.
|
||||
const { openForAdd } = drawer;
|
||||
const onCreateNew = useCallback(
|
||||
(modelName: string): void => openForAdd(modelName),
|
||||
[openForAdd],
|
||||
);
|
||||
const columnsConfig = useMemo<UnpricedColumnsConfig>(
|
||||
() => ({
|
||||
canManage: canManagePricing,
|
||||
onRequestMap,
|
||||
onCreateNew,
|
||||
}),
|
||||
[canManagePricing, onRequestMap, onCreateNew],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.unpricedModelsTab}>
|
||||
<div className={styles.banner}>
|
||||
<TriangleAlert size="sm" className={styles.bannerIcon} />
|
||||
<Typography.Text as="span" size="small" color="warning">
|
||||
Models detected in traces without pricing. Map each to a billing model or
|
||||
create pricing so estimated cost can be computed.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
{isError && (
|
||||
<div className={styles.error}>
|
||||
<Typography.Text as="p" size="small" color="danger" role="alert">
|
||||
Failed to load unpriced models. Please try again.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<UnpricedModelsTable
|
||||
models={models}
|
||||
isLoading={isLoading}
|
||||
columnsConfig={columnsConfig}
|
||||
/>
|
||||
|
||||
{pendingMapping && (
|
||||
<MapConfirmDialog
|
||||
open
|
||||
model={pendingMapping.model}
|
||||
rule={pendingMapping.rule}
|
||||
isSaving={isSaving}
|
||||
onConfirm={onConfirmMap}
|
||||
onCancel={clearPending}
|
||||
/>
|
||||
)}
|
||||
|
||||
{drawer.isOpen && (
|
||||
<ModelCostDrawer
|
||||
isOpen={drawer.isOpen}
|
||||
mode={drawer.mode}
|
||||
initialDraft={drawer.initialDraft}
|
||||
onClose={drawer.close}
|
||||
onSave={drawer.save}
|
||||
isSaving={drawer.isSaving}
|
||||
saveError={drawer.saveError}
|
||||
canManage={canManagePricing}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UnpricedModelsTab;
|
||||
@@ -1,180 +0,0 @@
|
||||
import type { LlmpricingruletypesUpdatableLLMPricingRulesDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
LLM_PRICING_ENDPOINT,
|
||||
LLM_UNMAPPED_ENDPOINT,
|
||||
makeListResponse,
|
||||
makeUnmappedResponse,
|
||||
mockRules,
|
||||
} from 'container/LLMObservability/Settings/ModelPricing/__tests__/fixtures';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
|
||||
|
||||
import UnpricedModelsTab from '../UnpricedModelsTab';
|
||||
|
||||
const toastSuccess = jest.fn();
|
||||
const toastError = jest.fn();
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
toast: {
|
||||
success: (...args: unknown[]): void => toastSuccess(...args),
|
||||
error: (...args: unknown[]): void => toastError(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
const MODEL = 'gpt-4o-mini-2024-07-18';
|
||||
|
||||
function setup(): void {
|
||||
server.use(
|
||||
rest.get(LLM_UNMAPPED_ENDPOINT, (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(makeUnmappedResponse())),
|
||||
),
|
||||
rest.get(LLM_PRICING_ENDPOINT, (_req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(makeListResponse(mockRules))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Picks a billing model in a row's dropdown: open the combobox, then click the
|
||||
// option. The dropdown fetches its options from the rules list (mocked above).
|
||||
async function selectRule(
|
||||
user: ReturnType<typeof userEvent.setup>,
|
||||
modelName: string,
|
||||
ruleId: string,
|
||||
): Promise<void> {
|
||||
await user.click(screen.getByTestId(`map-to-select-${modelName}`));
|
||||
await user.click(await screen.findByTestId(`map-to-option-${ruleId}`));
|
||||
}
|
||||
|
||||
describe('UnpricedModelsTab (integration)', () => {
|
||||
beforeEach(() => {
|
||||
window.history.pushState(null, '', '/');
|
||||
setup();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
toastSuccess.mockClear();
|
||||
toastError.mockClear();
|
||||
});
|
||||
|
||||
it('opens the confirm dialog with the target rule pricing when a model is picked', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<UnpricedModelsTab />);
|
||||
|
||||
await screen.findByTestId(`unpriced-model-name-${MODEL}`);
|
||||
|
||||
// Picking a rule stages the mapping in the confirm dialog.
|
||||
await selectRule(user, MODEL, 'rule-openai');
|
||||
|
||||
const confirmItem = await screen.findByTestId(
|
||||
`unpriced-map-confirm-item-${MODEL}`,
|
||||
);
|
||||
expect(confirmItem).toBeInTheDocument();
|
||||
// Shows the target billing model + its pricing so the user can eyeball it.
|
||||
expect(screen.getByText('openai:gpt-4o')).toBeInTheDocument();
|
||||
expect(screen.getByText('$3.00')).toBeInTheDocument();
|
||||
expect(screen.getByText('$9.00')).toBeInTheDocument();
|
||||
// While the dialog is open, the row's trigger mirrors the staged pick
|
||||
// instead of the placeholder.
|
||||
const trigger = screen.getByTestId(`map-to-select-${MODEL}`);
|
||||
expect(
|
||||
within(trigger).getByText('openai:gpt-4o ($3.00/$9.00)'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('reverts the row trigger to the placeholder when the mapping is cancelled', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<UnpricedModelsTab />);
|
||||
|
||||
await screen.findByTestId(`unpriced-model-name-${MODEL}`);
|
||||
await selectRule(user, MODEL, 'rule-openai');
|
||||
|
||||
expect(
|
||||
within(screen.getByTestId(`map-to-select-${MODEL}`)).getByText(
|
||||
'openai:gpt-4o ($3.00/$9.00)',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await user.click(await screen.findByTestId('unpriced-map-cancel-btn'));
|
||||
|
||||
await waitFor(() => {
|
||||
const trigger = screen.getByTestId(`map-to-select-${MODEL}`);
|
||||
expect(
|
||||
within(trigger).getByText('Select / Create a pricing model'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('commits the mapping in one request when confirmed', async () => {
|
||||
const sent: LlmpricingruletypesUpdatableLLMPricingRulesDTO[] = [];
|
||||
server.use(
|
||||
rest.put(LLM_PRICING_ENDPOINT, async (req, res, ctx) => {
|
||||
sent.push(await req.json());
|
||||
return res(ctx.status(200), ctx.json({ status: 'success' }));
|
||||
}),
|
||||
);
|
||||
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<UnpricedModelsTab />);
|
||||
|
||||
await screen.findByTestId(`unpriced-model-name-${MODEL}`);
|
||||
await selectRule(user, MODEL, 'rule-openai');
|
||||
|
||||
await user.click(await screen.findByTestId('unpriced-map-confirm-btn'));
|
||||
|
||||
await waitFor(() => expect(sent).toHaveLength(1));
|
||||
expect(sent[0].rules).toHaveLength(1);
|
||||
expect(sent[0].rules?.[0].modelPattern).toContain(MODEL);
|
||||
await waitFor(() =>
|
||||
expect(toastSuccess).toHaveBeenCalledWith('Mapped model'),
|
||||
);
|
||||
// Dialog closes on success.
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.queryByTestId(`unpriced-map-confirm-item-${MODEL}`),
|
||||
).not.toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('cancels the mapping without committing', async () => {
|
||||
const sent: LlmpricingruletypesUpdatableLLMPricingRulesDTO[] = [];
|
||||
server.use(
|
||||
rest.put(LLM_PRICING_ENDPOINT, async (req, res, ctx) => {
|
||||
sent.push(await req.json());
|
||||
return res(ctx.status(200), ctx.json({ status: 'success' }));
|
||||
}),
|
||||
);
|
||||
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<UnpricedModelsTab />);
|
||||
|
||||
await screen.findByTestId(`unpriced-model-name-${MODEL}`);
|
||||
await selectRule(user, MODEL, 'rule-openai');
|
||||
|
||||
await user.click(await screen.findByTestId('unpriced-map-cancel-btn'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.queryByTestId(`unpriced-map-confirm-item-${MODEL}`),
|
||||
).not.toBeInTheDocument(),
|
||||
);
|
||||
expect(sent).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('opens the add-cost drawer prefilled when creating pricing for a model', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<UnpricedModelsTab />);
|
||||
|
||||
await screen.findByTestId(`unpriced-model-name-${MODEL}`);
|
||||
|
||||
// Open the row's dropdown and take the "Create pricing for …" escape hatch
|
||||
// instead of mapping onto an existing billing model.
|
||||
await user.click(screen.getByTestId(`map-to-select-${MODEL}`));
|
||||
await user.click(await screen.findByTestId(`map-to-create-${MODEL}`));
|
||||
|
||||
// The shared add-cost drawer opens with the model name prefilled.
|
||||
const drawerTitle = await screen.findByText('Add model cost');
|
||||
expect(drawerTitle).toBeInTheDocument();
|
||||
expect(screen.getByTestId('drawer-model-id-input')).toHaveValue(MODEL);
|
||||
});
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-6);
|
||||
}
|
||||
|
||||
.mapping {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
flex-shrink: 0;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.pricing {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-3);
|
||||
padding: var(--spacing-6);
|
||||
border-radius: var(--radius-2);
|
||||
border: 1px solid var(--l2-border);
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
.pricingRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-6);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { ArrowRight, Check, Link2, X } from '@signozhq/icons';
|
||||
import { startCase } from 'lodash-es';
|
||||
|
||||
import styles from './MapConfirmDialog.module.scss';
|
||||
import type {
|
||||
PricingRule,
|
||||
UnpricedModel,
|
||||
} from 'container/LLMObservability/Settings/ModelPricing/types';
|
||||
import {
|
||||
formatPricePerMillion,
|
||||
getCanonicalId,
|
||||
getExtraBuckets,
|
||||
} from 'container/LLMObservability/Settings/ModelPricing/utils';
|
||||
|
||||
interface MapConfirmDialogProps {
|
||||
open: boolean;
|
||||
model: UnpricedModel;
|
||||
rule: PricingRule;
|
||||
isSaving: boolean;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
// Per-row confirm step before mapping an unpriced model onto an existing billing
|
||||
// model. Mapping appends the span's model name as a match pattern on the chosen
|
||||
// rule, so the model inherits that rule's pricing — shown here so the user can
|
||||
// eyeball the rates before committing. Dismissible (outside click / close), since
|
||||
// mapping is reversible (re-map or edit the rule's patterns afterwards).
|
||||
function MapConfirmDialog({
|
||||
open,
|
||||
model,
|
||||
rule,
|
||||
isSaving,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: MapConfirmDialogProps): JSX.Element {
|
||||
const extraBuckets = getExtraBuckets(rule);
|
||||
|
||||
const pricingRows = [
|
||||
{ key: 'input', label: 'Input / 1M', value: rule.pricing?.input },
|
||||
{ key: 'output', label: 'Output / 1M', value: rule.pricing?.output },
|
||||
...extraBuckets.map((bucket) => ({
|
||||
key: bucket.key,
|
||||
label: startCase(bucket.key),
|
||||
value: bucket.pricePerMillion,
|
||||
})),
|
||||
];
|
||||
|
||||
const footer = (
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={onCancel}
|
||||
disabled={isSaving}
|
||||
prefix={<X size={12} />}
|
||||
testId="unpriced-map-cancel-btn"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
loading={isSaving}
|
||||
onClick={onConfirm}
|
||||
prefix={<Check size={12} />}
|
||||
testId="unpriced-map-confirm-btn"
|
||||
>
|
||||
Map model
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<DialogWrapper
|
||||
open={open}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
onCancel();
|
||||
}
|
||||
}}
|
||||
width="base"
|
||||
title="Map to billing model"
|
||||
titleIcon={<Link2 size={16} />}
|
||||
footer={footer}
|
||||
testId="unpriced-map-confirm-dialog"
|
||||
>
|
||||
<div className={styles.body}>
|
||||
<Typography.Text as="p" size="small" color="muted">
|
||||
Spans from this model will be priced using the selected billing model.
|
||||
</Typography.Text>
|
||||
|
||||
<div className={styles.mapping}>
|
||||
<Typography.Text
|
||||
weight="semibold"
|
||||
testId={`unpriced-map-confirm-item-${model.modelName}`}
|
||||
>
|
||||
{model.modelName}
|
||||
</Typography.Text>
|
||||
<ArrowRight size={14} className={styles.arrow} />
|
||||
<Typography.Text weight="semibold">{getCanonicalId(rule)}</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className={styles.pricing}>
|
||||
{pricingRows.map((row) => (
|
||||
<div className={styles.pricingRow} key={row.key}>
|
||||
<Typography.Text as="span" size="small" color="muted">
|
||||
{row.label}
|
||||
</Typography.Text>
|
||||
<Typography.Text as="span" size="small" weight="semibold">
|
||||
{formatPricePerMillion(row.value)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</DialogWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default MapConfirmDialog;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './MapConfirmDialog';
|
||||
@@ -1,27 +0,0 @@
|
||||
.mapToCell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-1);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mapToSelect {
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.mapToDropdown {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.skeletonList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-3);
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
}
|
||||
|
||||
.skeletonRow {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxCommand,
|
||||
ComboboxContent,
|
||||
ComboboxCreateItem,
|
||||
ComboboxEmpty,
|
||||
ComboboxInput,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
ComboboxSeparator,
|
||||
ComboboxTrigger,
|
||||
} from '@signozhq/ui/combobox';
|
||||
import { Plus } from '@signozhq/icons';
|
||||
import { Skeleton } from 'antd';
|
||||
|
||||
import styles from './MapToBillingModelSelect.module.scss';
|
||||
import { RULE_OPTIONS_LIMIT } from 'container/LLMObservability/Settings/ModelPricing/constants';
|
||||
import type { PricingRule } from 'container/LLMObservability/Settings/ModelPricing/types';
|
||||
import { getRuleOptionLabel } from 'container/LLMObservability/Settings/ModelPricing/utils';
|
||||
import { usePendingMappingLabel } from 'container/LLMObservability/Settings/ModelPricing/UnpricedModelsTab/usePendingMappingStore';
|
||||
import { useMapToBillingModelSearch } from './useMapToBillingModelSearch';
|
||||
|
||||
// One placeholder row per fetched option, so the skeleton height matches the
|
||||
// loaded list. Stable keys derived from the fetch limit.
|
||||
const SKELETON_ROW_KEYS = Array.from(
|
||||
{ length: RULE_OPTIONS_LIMIT },
|
||||
(_, index) => `skeleton-${index}`,
|
||||
);
|
||||
|
||||
interface MapToBillingModelSelectProps {
|
||||
modelName: string;
|
||||
disabled: boolean;
|
||||
onSelect: (rule: PricingRule) => void;
|
||||
onCreateNew: () => void;
|
||||
}
|
||||
|
||||
// Searchable, server-paged dropdown for picking the billing model an unpriced
|
||||
// model maps onto. Only RULE_OPTIONS_LIMIT rules are fetched at a time; typing
|
||||
// narrows the set via the rules API rather than client-side filtering, so cmdk's
|
||||
// own filter is disabled (shouldFilter={false}). The dropdown is a pure picker —
|
||||
// choosing a rule hands it up to the confirm dialog rather than persisting a
|
||||
// selection here. The trigger only mirrors the staged pick (read from the
|
||||
// pending-mapping store) while that dialog is open, reverting on confirm/cancel.
|
||||
function MapToBillingModelSelect({
|
||||
modelName,
|
||||
disabled,
|
||||
onSelect,
|
||||
onCreateNew,
|
||||
}: MapToBillingModelSelectProps): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { searchText, setSearchText, rules, rulesById, isFetching } =
|
||||
useMapToBillingModelSearch(open);
|
||||
const selectedLabel = usePendingMappingLabel(modelName);
|
||||
|
||||
const handleSelect = (ruleId: string): void => {
|
||||
const rule = rulesById.get(ruleId);
|
||||
if (rule) {
|
||||
onSelect(rule);
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleCreateNew = (): void => {
|
||||
setOpen(false);
|
||||
onCreateNew();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.mapToCell}>
|
||||
<Combobox open={open} onOpenChange={setOpen}>
|
||||
<ComboboxTrigger
|
||||
className={styles.mapToSelect}
|
||||
disabled={disabled}
|
||||
placeholder="Select / Create a pricing model"
|
||||
value={selectedLabel}
|
||||
testId={`map-to-select-${modelName}`}
|
||||
/>
|
||||
<ComboboxContent className={styles.mapToDropdown}>
|
||||
<ComboboxCommand shouldFilter={false}>
|
||||
<ComboboxInput
|
||||
value={searchText}
|
||||
onValueChange={setSearchText}
|
||||
placeholder="Search billing models"
|
||||
testId={`map-to-search-${modelName}`}
|
||||
/>
|
||||
<ComboboxList>
|
||||
{rules.map((rule) => (
|
||||
<ComboboxItem
|
||||
key={rule.id}
|
||||
value={rule.id}
|
||||
onSelect={(): void => handleSelect(rule.id)}
|
||||
data-testid={`map-to-option-${rule.id}`}
|
||||
>
|
||||
{getRuleOptionLabel(rule)}
|
||||
</ComboboxItem>
|
||||
))}
|
||||
{isFetching && (
|
||||
<div
|
||||
className={styles.skeletonList}
|
||||
data-testid={`map-to-loading-${modelName}`}
|
||||
>
|
||||
{SKELETON_ROW_KEYS.map((key) => (
|
||||
<Skeleton.Input
|
||||
key={key}
|
||||
active
|
||||
block
|
||||
size="small"
|
||||
className={styles.skeletonRow}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!isFetching && rules.length === 0 && (
|
||||
<ComboboxEmpty>No billing models found</ComboboxEmpty>
|
||||
)}
|
||||
</ComboboxList>
|
||||
{/* Kept outside ComboboxList so it stays pinned as a footer while the
|
||||
options scroll. Escape hatch when no existing billing model fits:
|
||||
define this model's own pricing rather than mapping onto another. */}
|
||||
<ComboboxSeparator alwaysRender />
|
||||
<ComboboxCreateItem
|
||||
inputValue={modelName}
|
||||
value={`create-pricing-${modelName}`}
|
||||
prefix={<Plus size={14} />}
|
||||
onSelect={handleCreateNew}
|
||||
testId={`map-to-create-${modelName}`}
|
||||
>
|
||||
Create pricing for "{modelName}"
|
||||
</ComboboxCreateItem>
|
||||
</ComboboxCommand>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MapToBillingModelSelect;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './MapToBillingModelSelect';
|
||||
@@ -1,44 +0,0 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useListLLMPricingRules } from 'api/generated/services/llmpricingrules';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
|
||||
import {
|
||||
RULE_OPTIONS_LIMIT,
|
||||
SEARCH_DEBOUNCE_MS,
|
||||
} from 'container/LLMObservability/Settings/ModelPricing/constants';
|
||||
import type { PricingRule } from 'container/LLMObservability/Settings/ModelPricing/types';
|
||||
|
||||
interface UseMapToBillingModelSearch {
|
||||
searchText: string;
|
||||
setSearchText: (value: string) => void;
|
||||
rules: PricingRule[];
|
||||
// Fetched rules keyed by id, so a pick can resolve to its full rule object.
|
||||
rulesById: Map<string, PricingRule>;
|
||||
isFetching: boolean;
|
||||
}
|
||||
|
||||
// Server-side search for the per-row "Map to billing model" dropdown. Only
|
||||
// RULE_OPTIONS_LIMIT rules are fetched at a time; typing narrows the set via the
|
||||
// rules API's `q` param instead of filtering client-side. The fetch is gated on
|
||||
// `enabled` (the dropdown's open state) so closed rows don't fetch, and
|
||||
// react-query dedupes identical query keys so rows sharing a term hit the network
|
||||
// once.
|
||||
export function useMapToBillingModelSearch(
|
||||
enabled: boolean,
|
||||
): UseMapToBillingModelSearch {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const debouncedSearch = useDebounce(searchText, SEARCH_DEBOUNCE_MS);
|
||||
|
||||
const { data, isFetching } = useListLLMPricingRules(
|
||||
{ offset: 0, limit: RULE_OPTIONS_LIMIT, q: debouncedSearch || undefined },
|
||||
{ query: { enabled } },
|
||||
);
|
||||
|
||||
const rules = useMemo<PricingRule[]>(() => data?.data?.items || [], [data]);
|
||||
const rulesById = useMemo(
|
||||
() => new Map(rules.map((rule) => [rule.id, rule])),
|
||||
[rules],
|
||||
);
|
||||
|
||||
return { searchText, setSearchText, rules, rulesById, isFetching };
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export { getUnpricedModelsColumns } from './table.config';
|
||||
export type { UnpricedColumnsConfig } from './table.config';
|
||||
@@ -1,88 +0,0 @@
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView';
|
||||
|
||||
import MapToBillingModelSelect from 'container/LLMObservability/Settings/ModelPricing/UnpricedModelsTab/components/MapToBillingModelSelect';
|
||||
import type {
|
||||
PricingRule,
|
||||
UnpricedModel,
|
||||
} from 'container/LLMObservability/Settings/ModelPricing/types';
|
||||
import { formatSpanCount } from 'container/LLMObservability/Settings/ModelPricing/utils';
|
||||
import styles from './tableConfig.module.scss';
|
||||
|
||||
export interface UnpricedColumnsConfig {
|
||||
canManage: boolean;
|
||||
// Picking a billing model requests a mapping — the tab opens a confirm dialog
|
||||
// and commits that single mapping there (no batch/selection state here).
|
||||
onRequestMap: (model: UnpricedModel, rule: PricingRule) => void;
|
||||
// Opens the add-cost drawer prefilled with the row's model name.
|
||||
onCreateNew: (modelName: string) => void;
|
||||
}
|
||||
|
||||
// Column definitions for the unpriced-models TanStackTable. Sorting is off — the
|
||||
// unmapped-models endpoint returns the full set in one shot with no ordering knob.
|
||||
// Each row's action is self-contained: pick a billing model to map onto (confirmed
|
||||
// in a dialog) or create pricing for the model, so there's no bulk-save column.
|
||||
export function getUnpricedModelsColumns({
|
||||
canManage,
|
||||
onRequestMap,
|
||||
onCreateNew,
|
||||
}: UnpricedColumnsConfig): TableColumnDef<UnpricedModel>[] {
|
||||
return [
|
||||
{
|
||||
id: 'model',
|
||||
header: 'Model (from spans)',
|
||||
accessorFn: (row): string => row.modelName,
|
||||
width: { min: 240, default: '100%' },
|
||||
enableMove: false,
|
||||
enableRemove: false,
|
||||
cell: ({ row }): JSX.Element => (
|
||||
<Typography.Text
|
||||
weight="semibold"
|
||||
truncate={1}
|
||||
testId={`unpriced-model-name-${row.modelName}`}
|
||||
>
|
||||
{row.modelName}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'provider',
|
||||
header: 'Provider',
|
||||
width: { min: 140 },
|
||||
enableMove: false,
|
||||
cell: ({ row }): string => row.provider || 'Unknown',
|
||||
},
|
||||
{
|
||||
id: 'spans',
|
||||
header: 'Spans',
|
||||
width: { min: 100 },
|
||||
enableMove: false,
|
||||
cell: ({ row }): JSX.Element => (
|
||||
<Badge
|
||||
color="cherry"
|
||||
variant="outline"
|
||||
className={styles.spansBadge}
|
||||
data-testid={`unpriced-spans-${row.modelName}`}
|
||||
>
|
||||
{formatSpanCount(row.spanCount)}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'mapTo',
|
||||
header: 'Map to billing model',
|
||||
width: { min: 280, default: '100%' },
|
||||
enableMove: false,
|
||||
enableRemove: false,
|
||||
cell: ({ row }): JSX.Element => (
|
||||
<MapToBillingModelSelect
|
||||
modelName={row.modelName}
|
||||
disabled={!canManage}
|
||||
onSelect={(rule): void => onRequestMap(row, rule)}
|
||||
onCreateNew={(): void => onCreateNew(row.modelName)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
.spansBadge {
|
||||
margin: 0;
|
||||
font-family: var(--code-font-family, monospace);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Capped scroll viewport — mirrors ModelCostsTable so the unpriced tab doesn't
|
||||
// shift on load, but uses max-height so a short list stays compact instead of
|
||||
// reserving the full viewport. The extra offset over the model-costs table
|
||||
// accounts for the banner toolbar above.
|
||||
.unpricedModelsTable {
|
||||
--tanstack-table-row-height: 48px;
|
||||
max-height: calc(100vh - 220px);
|
||||
overflow-y: auto;
|
||||
|
||||
:global(table) tbody tr {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.unpricedModelsEmpty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
color: var(--l3-foreground);
|
||||
font-size: var(--periscope-font-size-base);
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
|
||||
import { SKELETON_ROW_COUNT } from 'container/LLMObservability/Settings/ModelPricing/constants';
|
||||
import type { UnpricedModel } from 'container/LLMObservability/Settings/ModelPricing/types';
|
||||
import styles from './UnpricedModelsTable.module.scss';
|
||||
import {
|
||||
getUnpricedModelsColumns,
|
||||
type UnpricedColumnsConfig,
|
||||
} from './TableConfig';
|
||||
|
||||
interface UnpricedModelsTableProps {
|
||||
models: UnpricedModel[];
|
||||
isLoading: boolean;
|
||||
columnsConfig: UnpricedColumnsConfig;
|
||||
}
|
||||
|
||||
// The unmapped-models endpoint returns the full set in one response, so there's
|
||||
// no pagination here — just a content-height list. Virtual scroll is disabled
|
||||
// because the set is small and bounded.
|
||||
function UnpricedModelsTable({
|
||||
models,
|
||||
isLoading,
|
||||
columnsConfig,
|
||||
}: UnpricedModelsTableProps): JSX.Element {
|
||||
const columns = useMemo(
|
||||
() => getUnpricedModelsColumns(columnsConfig),
|
||||
[columnsConfig],
|
||||
);
|
||||
|
||||
if (!isLoading && models.length === 0) {
|
||||
return (
|
||||
<div
|
||||
className={styles.unpricedModelsEmpty}
|
||||
data-testid="unpriced-models-empty"
|
||||
>
|
||||
All models in your traces are priced.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TanStackTable<UnpricedModel>
|
||||
className={styles.unpricedModelsTable}
|
||||
data={models}
|
||||
columns={columns}
|
||||
isLoading={isLoading}
|
||||
skeletonRowCount={SKELETON_ROW_COUNT}
|
||||
getRowKey={(row): string => row.modelName}
|
||||
disableVirtualScroll
|
||||
testId="unpriced-models-table"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default UnpricedModelsTable;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './UnpricedModelsTable';
|
||||
@@ -1,70 +0,0 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import {
|
||||
getListLLMPricingRulesQueryKey,
|
||||
getListUnmappedLLMModelsQueryKey,
|
||||
useCreateOrUpdateLLMPricingRules,
|
||||
} from 'api/generated/services/llmpricingrules';
|
||||
|
||||
import type {
|
||||
PricingRule,
|
||||
UnpricedModel,
|
||||
} from 'container/LLMObservability/Settings/ModelPricing/types';
|
||||
import { buildPatternMappingPayload } from 'container/LLMObservability/Settings/ModelPricing/utils';
|
||||
|
||||
// A single row's choice: map this unpriced model onto this billing rule.
|
||||
export interface UnpricedModelMapping {
|
||||
model: UnpricedModel;
|
||||
rule: PricingRule;
|
||||
}
|
||||
|
||||
interface UseUnpricedModelMappingResult {
|
||||
// Commits a single mapping in one request. Resolves true on success so the
|
||||
// caller can close the confirm dialog and clear its staged pick.
|
||||
mapModel: (mapping: UnpricedModelMapping) => Promise<boolean>;
|
||||
// True while the save is in flight, so the confirm button can spin.
|
||||
isSaving: boolean;
|
||||
}
|
||||
|
||||
// Maps an unpriced model onto an existing pricing rule. There's no dedicated
|
||||
// "edit" endpoint — mapping reuses CreateOrUpdate (PUT), appending the model name
|
||||
// as a match pattern on the chosen rule so the model inherits its pricing. Both
|
||||
// the unmapped list and the rules list are invalidated on success so the mapped
|
||||
// model drops out of this tab immediately.
|
||||
export function useUnpricedModelMapping(): UseUnpricedModelMappingResult {
|
||||
const queryClient = useQueryClient();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const { mutateAsync: createOrUpdate } = useCreateOrUpdateLLMPricingRules();
|
||||
|
||||
const mapModel = useCallback(
|
||||
async ({ model, rule }: UnpricedModelMapping): Promise<boolean> => {
|
||||
const payload = buildPatternMappingPayload(rule, model.modelName);
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await createOrUpdate({ data: { rules: [payload] } });
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getListUnmappedLLMModelsQueryKey(),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getListLLMPricingRulesQueryKey(),
|
||||
}),
|
||||
]);
|
||||
toast.success('Mapped model');
|
||||
return true;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Mapping failed';
|
||||
toast.error(message);
|
||||
return false;
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
},
|
||||
[createOrUpdate, queryClient],
|
||||
);
|
||||
|
||||
return { mapModel, isSaving };
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './UnpricedModelsTab';
|
||||
@@ -1,39 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
import type { PricingRule, UnpricedModel } from '../types';
|
||||
import { getRuleOptionLabel } from '../utils';
|
||||
|
||||
// A model + the billing rule it's about to be mapped onto, held while the confirm
|
||||
// dialog is open.
|
||||
export interface PendingMapping {
|
||||
model: UnpricedModel;
|
||||
rule: PricingRule;
|
||||
}
|
||||
|
||||
interface PendingMappingState {
|
||||
pending: PendingMapping | null;
|
||||
setPending: (mapping: PendingMapping) => void;
|
||||
clearPending: () => void;
|
||||
}
|
||||
|
||||
// The staged mapping lives in a store (not component state/props) because the
|
||||
// row's select trigger, which mirrors the staged pick, sits inside a memoized
|
||||
// TanStackTable cell whose value is cached per row — a prop/context change would
|
||||
// not re-render it, but a store subscription does. Picking a billing model stages
|
||||
// a single mapping for confirmation; it only commits once confirmed in the dialog.
|
||||
export const usePendingMappingStore = create<PendingMappingState>((set) => ({
|
||||
pending: null,
|
||||
setPending: (mapping): void => set({ pending: mapping }),
|
||||
clearPending: (): void => set({ pending: null }),
|
||||
}));
|
||||
|
||||
// Label to show on the given row's select trigger while its mapping is staged, or
|
||||
// undefined when nothing is staged for that row (falls back to the placeholder).
|
||||
export const usePendingMappingLabel = (modelName: string): string | undefined =>
|
||||
usePendingMappingStore((state) => {
|
||||
const { pending } = state;
|
||||
if (!pending || pending.model.modelName !== modelName) {
|
||||
return undefined;
|
||||
}
|
||||
return getRuleOptionLabel(pending.rule);
|
||||
});
|
||||
@@ -2,19 +2,14 @@ import {
|
||||
LlmpricingruletypesLLMPricingRuleCacheModeDTO as CacheModeDTO,
|
||||
LlmpricingruletypesLLMPricingRuleUnitDTO as UnitDTO,
|
||||
type ListLLMPricingRules200,
|
||||
type ListUnmappedLLMModels200,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import type { PricingRule, UnpricedModel } from '../types';
|
||||
import type { PricingRule } from '../types';
|
||||
|
||||
// Endpoint glob used by MSW handlers. The generated client hits a relative
|
||||
// `/api/v1/llm_pricing_rules`, so the `*` prefix matches regardless of base URL.
|
||||
export const LLM_PRICING_ENDPOINT = '*/api/v1/llm_pricing_rules';
|
||||
export const LLM_PRICING_RULE_ENDPOINT = '*/api/v1/llm_pricing_rules/:id';
|
||||
// Distinct path (extra segment), so it needs its own handler — the list glob
|
||||
// above does not match it.
|
||||
export const LLM_UNMAPPED_ENDPOINT =
|
||||
'*/api/v1/llm_pricing_rules/unmapped_models';
|
||||
|
||||
// Builds a valid pricing rule, with overrides merged shallowly. Pricing is
|
||||
// replaced wholesale when provided so callers can shape cache buckets freely.
|
||||
@@ -65,26 +60,6 @@ export const mockRules: PricingRule[] = [
|
||||
}),
|
||||
];
|
||||
|
||||
// Unpriced models seen in traces with no matching pricing rule.
|
||||
export const mockUnpricedModels: UnpricedModel[] = [
|
||||
{ modelName: 'gpt-4o-mini-2024-07-18', provider: 'openai', spanCount: 18400 },
|
||||
{
|
||||
modelName: 'claude-3-7-sonnet-20250219',
|
||||
provider: 'anthropic',
|
||||
spanCount: 9200,
|
||||
},
|
||||
];
|
||||
|
||||
// Wraps unpriced models in the envelope the unmapped-models query reads.
|
||||
export function makeUnmappedResponse(
|
||||
items: UnpricedModel[] = mockUnpricedModels,
|
||||
): ListUnmappedLLMModels200 {
|
||||
return {
|
||||
status: 'success',
|
||||
data: { items },
|
||||
};
|
||||
}
|
||||
|
||||
// Wraps items in the list response envelope the list query reads
|
||||
// (`data.data.items` / `data.data.total`).
|
||||
export function makeListResponse(
|
||||
|
||||
@@ -34,13 +34,6 @@ export const SOURCE_FILTER_TO_IS_OVERRIDE: Record<
|
||||
// loaded page renders — otherwise the table height jumps on load.
|
||||
export const SKELETON_ROW_COUNT = PAGE_SIZE;
|
||||
|
||||
export const RULE_OPTIONS_LIMIT = 10;
|
||||
|
||||
// URL-backed key for the active tab on the model-pricing page.
|
||||
export const TAB_KEY = 'tab';
|
||||
export const MODEL_COSTS_TAB = 'model-costs';
|
||||
export const UNPRICED_MODELS_TAB = 'unpriced-models';
|
||||
|
||||
export const PROVIDER_OPTIONS = [
|
||||
{ value: 'OpenAI', label: 'OpenAI' },
|
||||
{ value: 'Anthropic', label: 'Anthropic' },
|
||||
@@ -69,7 +62,6 @@ export const EMPTY_DRAFT: DrawerDraft = {
|
||||
provider: 'OpenAI',
|
||||
patterns: [],
|
||||
isOverride: true,
|
||||
enabled: true,
|
||||
pricing: {
|
||||
input: null,
|
||||
output: null,
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import {
|
||||
LlmpricingruletypesLLMPricingRuleCacheModeDTO as CacheModeDTO,
|
||||
type LlmpricingruletypesLLMPricingRuleDTO,
|
||||
type LlmpricingruletypesUnmappedModelDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
export type PricingRule = LlmpricingruletypesLLMPricingRuleDTO;
|
||||
|
||||
// A model seen in trace data (gen_ai.request.model) that no pricing rule matches.
|
||||
export type UnpricedModel = LlmpricingruletypesUnmappedModelDTO;
|
||||
|
||||
export interface ExtraBucket {
|
||||
key: string;
|
||||
pricePerMillion: number;
|
||||
@@ -33,7 +29,6 @@ export interface DrawerDraft {
|
||||
provider: string;
|
||||
patterns: string[];
|
||||
isOverride: boolean;
|
||||
enabled: boolean;
|
||||
pricing: {
|
||||
input: number | null;
|
||||
output: number | null;
|
||||
|
||||
@@ -86,7 +86,6 @@ export const draftFromRule = (rule: PricingRule): DrawerDraft => ({
|
||||
provider: rule.provider,
|
||||
patterns: rule.modelPattern || [],
|
||||
isOverride: !!rule.isOverride,
|
||||
enabled: rule.enabled,
|
||||
pricing: {
|
||||
input: rule.pricing?.input ?? 0,
|
||||
output: rule.pricing?.output ?? 0,
|
||||
@@ -130,7 +129,7 @@ export const buildRulePayload = (
|
||||
provider: draft.provider.trim(),
|
||||
modelPattern: draft.patterns,
|
||||
isOverride: draft.isOverride,
|
||||
enabled: draft.enabled,
|
||||
enabled: true,
|
||||
unit: UnitDTO.per_million_tokens,
|
||||
pricing: buildPricingPayload(draft),
|
||||
});
|
||||
@@ -162,38 +161,3 @@ export const validatePricing = (
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const spanCountFormatter = new Intl.NumberFormat('en', {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1,
|
||||
});
|
||||
|
||||
export const formatSpanCount = (count: number): string =>
|
||||
spanCountFormatter.format(count);
|
||||
|
||||
// Label for the "Map to billing model" dropdown, e.g. "openai:gpt-4o ($15.00/$60.00)".
|
||||
export const getRuleOptionLabel = (rule: PricingRule): string =>
|
||||
`${getCanonicalId(rule)} (${formatPricePerMillion(
|
||||
rule.pricing?.input,
|
||||
)}/${formatPricePerMillion(rule.pricing?.output)})`;
|
||||
|
||||
export const buildPatternMappingPayload = (
|
||||
rule: PricingRule,
|
||||
modelName: string,
|
||||
): LlmpricingruletypesUpdatableLLMPricingRuleDTO => {
|
||||
const existing = rule.modelPattern ?? [];
|
||||
const modelPattern = existing.includes(modelName)
|
||||
? existing
|
||||
: [...existing, modelName];
|
||||
return {
|
||||
id: rule.id,
|
||||
sourceId: rule.sourceId,
|
||||
modelName: rule.modelName,
|
||||
provider: rule.provider,
|
||||
modelPattern,
|
||||
isOverride: rule.isOverride,
|
||||
enabled: rule.enabled,
|
||||
unit: rule.unit,
|
||||
pricing: rule.pricing,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2872,72 +2872,17 @@ export const nodeWidgetInfo = [
|
||||
];
|
||||
|
||||
export const hostWidgetInfo = [
|
||||
{
|
||||
title: 'CPU Usage',
|
||||
yAxisUnit: 'percentunit',
|
||||
docPath: '/infrastructure-monitoring/host-monitoring/#cpu-usage',
|
||||
},
|
||||
{
|
||||
title: 'Memory Usage',
|
||||
yAxisUnit: 'bytes',
|
||||
docPath: '/infrastructure-monitoring/host-monitoring/#memory-usage',
|
||||
},
|
||||
{
|
||||
title: 'System Load Average',
|
||||
yAxisUnit: '',
|
||||
docPath: '/infrastructure-monitoring/host-monitoring/#system-load-average',
|
||||
},
|
||||
{
|
||||
title: 'Network usage (bytes)',
|
||||
yAxisUnit: 'bytes',
|
||||
docPath: '/infrastructure-monitoring/host-monitoring/#network-usage-bytes',
|
||||
},
|
||||
{
|
||||
title: 'Network usage (packet/s)',
|
||||
yAxisUnit: 'pps',
|
||||
docPath: '/infrastructure-monitoring/host-monitoring/#network-usage-packetss',
|
||||
},
|
||||
{
|
||||
title: 'Network errors',
|
||||
yAxisUnit: 'short',
|
||||
docPath: '/infrastructure-monitoring/host-monitoring/#network-errors',
|
||||
},
|
||||
{
|
||||
title: 'Network drops',
|
||||
yAxisUnit: 'short',
|
||||
docPath: '/infrastructure-monitoring/host-monitoring/#network-drops',
|
||||
},
|
||||
{
|
||||
title: 'Network connections',
|
||||
yAxisUnit: 'short',
|
||||
docPath: '/infrastructure-monitoring/host-monitoring/#network-connections',
|
||||
},
|
||||
{
|
||||
title: 'System disk io (bytes transferred)',
|
||||
yAxisUnit: 'bytes',
|
||||
docPath: '/infrastructure-monitoring/host-monitoring/#system-disk-io-bytes',
|
||||
},
|
||||
{
|
||||
title: 'System disk operations/s',
|
||||
yAxisUnit: 'short',
|
||||
docPath:
|
||||
'/infrastructure-monitoring/host-monitoring/#system-disk-operationss',
|
||||
},
|
||||
{
|
||||
title: 'Queue size',
|
||||
yAxisUnit: 'short',
|
||||
docPath: '/infrastructure-monitoring/host-monitoring/#queue-size',
|
||||
},
|
||||
{
|
||||
title: 'System disk operation time/s',
|
||||
yAxisUnit: 's',
|
||||
docPath:
|
||||
'/infrastructure-monitoring/host-monitoring/#system-disk-operation-times',
|
||||
},
|
||||
{
|
||||
title: 'Disk Usage (%) by mountpoint',
|
||||
yAxisUnit: 'percentunit',
|
||||
docPath:
|
||||
'/infrastructure-monitoring/host-monitoring/#disk-usage--by-mountpoint',
|
||||
},
|
||||
{ title: 'CPU Usage', yAxisUnit: 'percentunit' },
|
||||
{ title: 'Memory Usage', yAxisUnit: 'bytes' },
|
||||
{ title: 'System Load Average', yAxisUnit: '' },
|
||||
{ title: 'Network usage (bytes)', yAxisUnit: 'bytes' },
|
||||
{ title: 'Network usage (packet/s)', yAxisUnit: 'pps' },
|
||||
{ title: 'Network errors', yAxisUnit: 'short' },
|
||||
{ title: 'Network drops', yAxisUnit: 'short' },
|
||||
{ title: 'Network connections', yAxisUnit: 'short' },
|
||||
{ title: 'System disk io (bytes transferred)', yAxisUnit: 'bytes' },
|
||||
{ title: 'System disk operations/s', yAxisUnit: 'short' },
|
||||
{ title: 'Queue size', yAxisUnit: 'short' },
|
||||
{ title: 'System disk operation time/s', yAxisUnit: 's' },
|
||||
{ title: 'Disk Usage (%) by mountpoint', yAxisUnit: 'percentunit' },
|
||||
];
|
||||
|
||||
@@ -191,6 +191,14 @@
|
||||
min-height: 0;
|
||||
overflow-y: visible;
|
||||
|
||||
.time-series-view-container-header {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.time-series-view {
|
||||
flex-shrink: 0;
|
||||
height: 65vh;
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
getListQuery,
|
||||
getQueryByPanelType,
|
||||
} from 'container/LogsExplorerViews/explorerUtils';
|
||||
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsFilter';
|
||||
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||
@@ -460,17 +461,18 @@ function LogsExplorerViewsContainer({
|
||||
)}
|
||||
{selectedPanelType === PANEL_TYPES.TIME_SERIES && !showLiveLogs && (
|
||||
<div className="time-series-view-container">
|
||||
<div className="time-series-view-container-header">
|
||||
<BuilderUnitsFilter onChange={onUnitChange} yAxisUnit={yAxisUnit} />
|
||||
</div>
|
||||
<TimeSeriesView
|
||||
isLoading={isLoading || isFetching}
|
||||
data={data}
|
||||
isError={isError}
|
||||
error={error as APIError}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onYAxisUnitChange={onUnitChange}
|
||||
isFilterApplied={!isEmpty(listQuery?.filters?.items)}
|
||||
dataSource={DataSource.LOGS}
|
||||
setWarning={setWarning}
|
||||
allowExport
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user