mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-15 10:50:33 +01:00
Compare commits
6 Commits
platform-p
...
feat/expor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5330496257 | ||
|
|
8822364a31 | ||
|
|
b63d1e7ce0 | ||
|
|
f9c4e96656 | ||
|
|
13760e8e2b | ||
|
|
a257184a19 |
@@ -8048,15 +8048,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 +8200,7 @@ components:
|
||||
type: boolean
|
||||
fieldContext:
|
||||
$ref: '#/components/schemas/SpantypesFieldContext'
|
||||
groupId:
|
||||
group_id:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
@@ -8222,7 +8213,7 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- groupId
|
||||
- group_id
|
||||
- name
|
||||
- fieldContext
|
||||
- config
|
||||
@@ -13801,7 +13792,7 @@ paths:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SpantypesGettableSpanMappers'
|
||||
$ref: '#/components/schemas/SpantypesGettableSpanMapperGroups'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
@@ -24464,17 +24455,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- logs:read
|
||||
- traces:read
|
||||
- metrics:read
|
||||
- audit-logs:read
|
||||
- meter-metrics:read
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- logs:read
|
||||
- traces:read
|
||||
- metrics:read
|
||||
- audit-logs:read
|
||||
- meter-metrics:read
|
||||
- VIEWER
|
||||
summary: Query range
|
||||
tags:
|
||||
- querier
|
||||
@@ -24541,17 +24524,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- logs:read
|
||||
- traces:read
|
||||
- metrics:read
|
||||
- audit-logs:read
|
||||
- meter-metrics:read
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- logs:read
|
||||
- traces:read
|
||||
- metrics:read
|
||||
- audit-logs:read
|
||||
- meter-metrics:read
|
||||
- VIEWER
|
||||
summary: Query range preview
|
||||
tags:
|
||||
- querier
|
||||
|
||||
@@ -9258,76 +9258,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 +9504,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 +9576,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 +10916,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> => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.timeseries-export-popover {
|
||||
.export-menu-popover {
|
||||
width: 240px;
|
||||
padding: 0 12px 12px 12px;
|
||||
|
||||
@@ -4,42 +4,43 @@ import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui/popover';
|
||||
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { useClientExport } from 'hooks/useExportData/useClientExport';
|
||||
import {
|
||||
ClientExportData,
|
||||
useClientExport,
|
||||
} from 'hooks/useExportData/useClientExport';
|
||||
import { ExportFormat } from 'lib/exportData/types';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryRangeResponseV5 } from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import './TimeseriesExportMenu.styles.scss';
|
||||
import './ExportMenu.styles.scss';
|
||||
|
||||
interface TimeseriesExportMenuProps {
|
||||
interface ExportMenuProps {
|
||||
dataSource: DataSource;
|
||||
queryResponse: QueryRangeResponseV5;
|
||||
// The queryRange response object the view holds — the hook picks the
|
||||
// serializer (timeseries / table) from what it carries.
|
||||
data: ClientExportData;
|
||||
query?: Query;
|
||||
yAxisUnit?: string;
|
||||
legendMap?: Record<string, string>;
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
// Download menu for in-memory timeseries data (client-side serialization).
|
||||
// Download menu for in-memory query results (client-side serialization).
|
||||
// The raw/list backend export keeps its own menu in DownloadOptionsMenu.
|
||||
export default function TimeseriesExportMenu({
|
||||
export default function ExportMenu({
|
||||
dataSource,
|
||||
queryResponse,
|
||||
data,
|
||||
query,
|
||||
yAxisUnit,
|
||||
legendMap,
|
||||
fileName,
|
||||
}: TimeseriesExportMenuProps): JSX.Element {
|
||||
}: ExportMenuProps): JSX.Element {
|
||||
const [exportFormat, setExportFormat] = useState<string>(ExportFormat.Csv);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||
|
||||
const { isExporting, handleExport: handleClientExport } = useClientExport({
|
||||
response: queryResponse,
|
||||
data,
|
||||
query,
|
||||
yAxisUnit,
|
||||
legendMap,
|
||||
fileName,
|
||||
});
|
||||
|
||||
@@ -57,7 +58,7 @@ export default function TimeseriesExportMenu({
|
||||
color="secondary"
|
||||
size="icon"
|
||||
aria-label="Download"
|
||||
data-testid={`timeseries-export-${dataSource}`}
|
||||
data-testid={`export-menu-${dataSource}`}
|
||||
disabled={isExporting}
|
||||
loading={isExporting}
|
||||
>
|
||||
@@ -65,7 +66,7 @@ export default function TimeseriesExportMenu({
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
</TooltipSimple>
|
||||
<PopoverContent align="end" className="timeseries-export-popover">
|
||||
<PopoverContent align="end" className="export-menu-popover">
|
||||
<div className="export-format">
|
||||
<Typography.Text className="title">FORMAT</Typography.Text>
|
||||
<RadioGroup value={exportFormat} onChange={setExportFormat}>
|
||||
@@ -1,8 +1,8 @@
|
||||
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
|
||||
import { fireEvent, render, screen } from 'tests/test-utils';
|
||||
import { QueryRangeResponseV5 } from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import TimeseriesExportMenu from '../TimeseriesExportMenu';
|
||||
import ExportMenu from '../ExportMenu';
|
||||
|
||||
const mockHandleExport = jest.fn();
|
||||
let mockIsExporting = false;
|
||||
@@ -14,25 +14,26 @@ jest.mock('hooks/useExportData/useClientExport', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const response = {
|
||||
type: 'time_series',
|
||||
data: { results: [] },
|
||||
meta: {},
|
||||
} as unknown as QueryRangeResponseV5;
|
||||
const data = {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: { data: { result: [], resultType: 'time_series' } },
|
||||
} as unknown as MetricQueryRangeSuccessResponse;
|
||||
|
||||
const TEST_ID = `timeseries-export-${DataSource.LOGS}`;
|
||||
const TEST_ID = `export-menu-${DataSource.LOGS}`;
|
||||
|
||||
function renderMenu(): void {
|
||||
render(
|
||||
<TimeseriesExportMenu
|
||||
<ExportMenu
|
||||
dataSource={DataSource.LOGS}
|
||||
queryResponse={response}
|
||||
data={data}
|
||||
fileName="logs-timeseries"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('TimeseriesExportMenu', () => {
|
||||
describe('ExportMenu', () => {
|
||||
beforeEach(() => {
|
||||
mockHandleExport.mockReset();
|
||||
mockIsExporting = false;
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
@@ -82,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,
|
||||
@@ -153,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(
|
||||
() =>
|
||||
@@ -163,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 {
|
||||
@@ -190,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);
|
||||
},
|
||||
@@ -215,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) : '';
|
||||
|
||||
|
||||
@@ -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('');
|
||||
});
|
||||
});
|
||||
@@ -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 ');
|
||||
}
|
||||
|
||||
@@ -9,35 +9,27 @@ 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,
|
||||
buildLogsTracesExpression,
|
||||
} from 'container/InfraMonitoringK8sV2/Base/utils';
|
||||
|
||||
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 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,
|
||||
|
||||
@@ -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,
|
||||
@@ -134,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',
|
||||
@@ -201,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',
|
||||
},
|
||||
@@ -243,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',
|
||||
},
|
||||
@@ -285,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',
|
||||
},
|
||||
@@ -361,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',
|
||||
},
|
||||
@@ -403,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',
|
||||
},
|
||||
@@ -445,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',
|
||||
},
|
||||
@@ -521,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',
|
||||
},
|
||||
@@ -610,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 =
|
||||
@@ -147,31 +139,21 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
</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,
|
||||
@@ -129,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 [
|
||||
{
|
||||
@@ -200,7 +155,6 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -242,7 +196,6 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -284,7 +237,6 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -360,7 +312,6 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -402,7 +353,6 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -444,7 +394,6 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -520,7 +469,6 @@ export const getDeploymentMetricsQueryPayload = (
|
||||
deployment.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_DEPLOYMENT_NAME] ??
|
||||
'',
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -609,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>[] =
|
||||
|
||||
@@ -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,
|
||||
@@ -104,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',
|
||||
@@ -171,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: [],
|
||||
@@ -232,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: [],
|
||||
@@ -293,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: [],
|
||||
@@ -367,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 =
|
||||
|
||||
@@ -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 {
|
||||
getNamespaceMetricsQueryPayload,
|
||||
k8sNamespaceDetailsMetadataConfig,
|
||||
@@ -118,7 +117,7 @@ function K8sNamespacesList({
|
||||
|
||||
return (
|
||||
<>
|
||||
<K8sBaseList<InframonitoringtypesNamespaceRecordDTO, SelectedItemParams>
|
||||
<K8sBaseList<InframonitoringtypesNamespaceRecordDTO>
|
||||
controlListPrefix={controlListPrefix}
|
||||
entity={InfraMonitoringEntity.NAMESPACES}
|
||||
tableColumns={k8sNamespacesColumnsConfig}
|
||||
|
||||
@@ -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 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>[] =
|
||||
[
|
||||
@@ -35,21 +27,17 @@ export const k8sNamespaceDetailsMetadataConfig: K8sDetailsMetadataConfig<Inframo
|
||||
|
||||
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,
|
||||
@@ -191,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 [
|
||||
{
|
||||
@@ -241,7 +211,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -281,7 +250,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -321,7 +289,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -361,7 +328,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -435,7 +401,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -475,7 +440,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -515,7 +479,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -555,7 +518,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -595,7 +557,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -635,7 +596,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -709,7 +669,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -790,7 +749,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -871,7 +829,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -958,7 +915,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1045,7 +1001,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1092,7 +1047,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1139,7 +1093,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1220,7 +1173,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1273,7 +1225,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1360,7 +1311,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1407,7 +1357,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1454,7 +1403,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1501,7 +1449,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1582,7 +1529,6 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
op: '=',
|
||||
value: namespace.namespaceName,
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
@@ -1629,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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
@@ -107,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'
|
||||
@@ -217,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',
|
||||
},
|
||||
@@ -260,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',
|
||||
},
|
||||
@@ -303,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',
|
||||
},
|
||||
@@ -366,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',
|
||||
},
|
||||
@@ -409,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',
|
||||
},
|
||||
@@ -472,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',
|
||||
},
|
||||
@@ -502,7 +525,7 @@ export const getStatefulSetMetricsQueryPayload = (
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: 'f1',
|
||||
id: 'f3',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
id: 'pod_name',
|
||||
@@ -515,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',
|
||||
},
|
||||
@@ -558,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',
|
||||
},
|
||||
@@ -621,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',
|
||||
},
|
||||
@@ -664,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',
|
||||
},
|
||||
@@ -727,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',
|
||||
},
|
||||
@@ -803,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>[] =
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' }),
|
||||
];
|
||||
|
||||
@@ -89,20 +89,21 @@ describe('UnpricedModelsTab (integration)', () => {
|
||||
await screen.findByTestId(`unpriced-model-name-${MODEL}`);
|
||||
await selectRule(user, MODEL, 'rule-openai');
|
||||
|
||||
const trigger = screen.getByTestId(`map-to-select-${MODEL}`);
|
||||
expect(
|
||||
within(screen.getByTestId(`map-to-select-${MODEL}`)).getByText(
|
||||
'openai:gpt-4o ($3.00/$9.00)',
|
||||
),
|
||||
within(trigger).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}`);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
within(trigger).getByText('Select / Create a pricing model'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
within(trigger).queryByText('openai:gpt-4o ($3.00/$9.00)'),
|
||||
).not.toBeInTheDocument(),
|
||||
);
|
||||
expect(
|
||||
within(trigger).getByText('Select / Create a pricing model'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('commits the mapping in one request when confirmed', async () => {
|
||||
|
||||
@@ -182,6 +182,14 @@
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: visible;
|
||||
|
||||
.table-view-container-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.time-series-view-container {
|
||||
|
||||
@@ -18,13 +18,18 @@ import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialFilters, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
initialFilters,
|
||||
initialQueriesMap,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
|
||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import GoToTop from 'container/GoToTop';
|
||||
import LogsExplorerChart from 'container/LogsExplorerChart';
|
||||
import LogsExplorerList from 'container/LogsExplorerList';
|
||||
import ExportMenu from 'components/ExportMenu/ExportMenu';
|
||||
import LogsExplorerTable from 'container/LogsExplorerTable';
|
||||
import {
|
||||
getExportQueryData,
|
||||
@@ -476,6 +481,16 @@ function LogsExplorerViewsContainer({
|
||||
)}
|
||||
{selectedPanelType === PANEL_TYPES.TABLE && !showLiveLogs && (
|
||||
<div className="table-view-container">
|
||||
{data && !isError && (
|
||||
<div className="table-view-container-header">
|
||||
<ExportMenu
|
||||
dataSource={DataSource.LOGS}
|
||||
data={data}
|
||||
query={stagedQuery || initialQueriesMap.metrics}
|
||||
fileName="logs-table"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<LogsExplorerTable
|
||||
data={
|
||||
(data?.payload?.data?.newResult?.data?.result ||
|
||||
|
||||
@@ -48,7 +48,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import uPlot from 'uplot';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import TimeseriesExportMenu from './TimeseriesExportMenu';
|
||||
import ExportMenu from 'components/ExportMenu/ExportMenu';
|
||||
|
||||
import './TimeSeriesView.styles.scss';
|
||||
|
||||
@@ -265,12 +265,11 @@ function TimeSeriesView({
|
||||
)}
|
||||
</div>
|
||||
{showExport && data?.rawV5Response && (
|
||||
<TimeseriesExportMenu
|
||||
<ExportMenu
|
||||
dataSource={dataSource}
|
||||
yAxisUnit={yAxisUnit}
|
||||
queryResponse={data.rawV5Response}
|
||||
data={data}
|
||||
query={currentQuery}
|
||||
legendMap={data.legendMap}
|
||||
fileName={`${dataSource}-timeseries`}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -13,7 +13,7 @@ jest.mock('components/Uplot', () => ({
|
||||
default: (): JSX.Element => <div data-testid="uplot-chart" />,
|
||||
}));
|
||||
|
||||
jest.mock('../TimeseriesExportMenu', () => ({
|
||||
jest.mock('components/ExportMenu/ExportMenu', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div data-testid="timeseries-export-menu" />,
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
.traces-table-view-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Space } from 'antd';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import ExportMenu from 'components/ExportMenu/ExportMenu';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
@@ -20,8 +21,11 @@ import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import './TableView.styles.scss';
|
||||
|
||||
function TableView({
|
||||
setWarning,
|
||||
setIsLoadingQueries,
|
||||
@@ -97,6 +101,16 @@ function TableView({
|
||||
return (
|
||||
<Space.Compact block direction="vertical">
|
||||
{isError && error && <ErrorInPlace error={error as APIError} />}
|
||||
{!isError && data && (
|
||||
<div className="traces-table-view-header">
|
||||
<ExportMenu
|
||||
dataSource={DataSource.TRACES}
|
||||
data={data}
|
||||
query={stagedQuery || initialQueriesMap.traces}
|
||||
fileName="traces-table"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!isError && (
|
||||
<QueryTable
|
||||
query={stagedQuery || initialQueriesMap.traces}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import DownloadOptionsMenu from 'components/DownloadOptionsMenu/DownloadOptionsMenu';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
@@ -153,11 +152,6 @@ function TracesView({
|
||||
</Typography>
|
||||
|
||||
<div className="trace-explorer-controls">
|
||||
<DownloadOptionsMenu
|
||||
dataSource={DataSource.TRACES}
|
||||
panelType={PANEL_TYPES.TRACE}
|
||||
/>
|
||||
|
||||
<TraceExplorerControls
|
||||
isLoading={isLoading}
|
||||
totalCount={responseData?.length || 0}
|
||||
|
||||
@@ -4,7 +4,8 @@ import {
|
||||
getTimestampedFileName,
|
||||
} from 'lib/exportData/downloadFile';
|
||||
import { ExportFormat } from 'lib/exportData/types';
|
||||
import { QueryRangeResponseV5 } from 'types/api/v5/queryRange';
|
||||
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { useClientExport } from '../useClientExport';
|
||||
|
||||
@@ -24,31 +25,87 @@ jest.mock('antd', () => {
|
||||
|
||||
const mockDownloadFile = downloadFile as jest.Mock;
|
||||
|
||||
function timeSeriesResponse(): QueryRangeResponseV5 {
|
||||
const query = {
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
queryName: 'A',
|
||||
dataSource: 'logs',
|
||||
aggregations: [{ expression: 'count()' }],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
},
|
||||
} as unknown as Query;
|
||||
|
||||
function timeSeriesData(): MetricQueryRangeSuccessResponse {
|
||||
return {
|
||||
type: 'time_series',
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
queryName: 'A',
|
||||
aggregations: [
|
||||
{
|
||||
index: 0,
|
||||
alias: '',
|
||||
meta: {},
|
||||
series: [
|
||||
{
|
||||
labels: [{ key: { name: 'service' }, value: 'a' }],
|
||||
values: [{ timestamp: 1000, value: 12 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: { data: { result: [], resultType: 'time_series' } },
|
||||
legendMap: { A: '{{service}}' },
|
||||
rawV5Response: {
|
||||
type: 'time_series',
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
queryName: 'A',
|
||||
aggregations: [
|
||||
{
|
||||
index: 0,
|
||||
alias: '',
|
||||
meta: {},
|
||||
series: [
|
||||
{
|
||||
labels: [{ key: { name: 'service' }, value: 'a' }],
|
||||
values: [{ timestamp: 1000, value: 12 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
meta: {},
|
||||
},
|
||||
meta: {},
|
||||
} as unknown as QueryRangeResponseV5;
|
||||
} as unknown as MetricQueryRangeSuccessResponse;
|
||||
}
|
||||
|
||||
function scalarData(): MetricQueryRangeSuccessResponse {
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: {
|
||||
data: {
|
||||
resultType: 'scalar',
|
||||
result: [
|
||||
{
|
||||
queryName: 'A',
|
||||
legend: '',
|
||||
series: null,
|
||||
list: null,
|
||||
table: {
|
||||
columns: [
|
||||
{
|
||||
name: 'service.name',
|
||||
id: 'service.name',
|
||||
queryName: 'A',
|
||||
isValueColumn: false,
|
||||
},
|
||||
{ name: 'count()', id: 'A', queryName: 'A', isValueColumn: true },
|
||||
],
|
||||
rows: [{ data: { 'service.name': 'frontend', A: 120 } }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} as unknown as MetricQueryRangeSuccessResponse;
|
||||
}
|
||||
|
||||
describe('useClientExport', () => {
|
||||
@@ -64,13 +121,9 @@ describe('useClientExport', () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('exports time_series as CSV to a timestamped <fileName>.csv', () => {
|
||||
it('dispatches timeseries data to the timeseries serializer (csv)', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useClientExport({
|
||||
response: timeSeriesResponse(),
|
||||
fileName: 'chart',
|
||||
legendMap: { A: '{{service}}' },
|
||||
}),
|
||||
useClientExport({ data: timeSeriesData(), query, fileName: 'chart' }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
@@ -83,12 +136,28 @@ describe('useClientExport', () => {
|
||||
expect(name).toBe(getTimestampedFileName('chart', 'csv'));
|
||||
expect(mime).toContain('text/csv');
|
||||
expect(content).toContain('service');
|
||||
expect(content).toContain('a');
|
||||
});
|
||||
|
||||
it('exports as JSONL to a timestamped <fileName>.jsonl with the ndjson mime', () => {
|
||||
it('dispatches scalar (table) data to the table serializer', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useClientExport({ response: timeSeriesResponse() }),
|
||||
useClientExport({ data: scalarData(), query, fileName: 'table' }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.handleExport({ format: ExportFormat.Csv });
|
||||
});
|
||||
|
||||
expect(mockDownloadFile).toHaveBeenCalledTimes(1);
|
||||
const [content, name] = mockDownloadFile.mock.calls[0];
|
||||
expect(name).toBe(getTimestampedFileName('table', 'csv'));
|
||||
expect(content).toContain('service.name');
|
||||
expect(content).toContain('frontend');
|
||||
expect(content).toContain('120');
|
||||
});
|
||||
|
||||
it('exports as JSONL with the ndjson mime', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useClientExport({ data: timeSeriesData(), query }),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
@@ -101,8 +170,8 @@ describe('useClientExport', () => {
|
||||
expect(content).toContain('"series"');
|
||||
});
|
||||
|
||||
it('does nothing when there is no response', () => {
|
||||
const { result } = renderHook(() => useClientExport({}));
|
||||
it('does nothing when there is no data', () => {
|
||||
const { result } = renderHook(() => useClientExport({ query }));
|
||||
|
||||
act(() => {
|
||||
result.current.handleExport({ format: ExportFormat.Csv });
|
||||
@@ -112,13 +181,14 @@ describe('useClientExport', () => {
|
||||
expect(mockMessageError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows an error and does not download for unsupported result types', () => {
|
||||
it('shows an error for unsupported result types', () => {
|
||||
const raw = {
|
||||
type: 'raw',
|
||||
data: { results: [] },
|
||||
meta: {},
|
||||
} as unknown as QueryRangeResponseV5;
|
||||
const { result } = renderHook(() => useClientExport({ response: raw }));
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: { data: { result: [], resultType: '' } },
|
||||
} as unknown as MetricQueryRangeSuccessResponse;
|
||||
const { result } = renderHook(() => useClientExport({ data: raw, query }));
|
||||
|
||||
act(() => {
|
||||
result.current.handleExport({ format: ExportFormat.Csv });
|
||||
|
||||
@@ -4,11 +4,14 @@ import {
|
||||
downloadFile,
|
||||
getTimestampedFileName,
|
||||
} from 'lib/exportData/downloadFile';
|
||||
import { exportScalarData } from 'lib/exportData/exportScalarData';
|
||||
import { exportTimeseriesData } from 'lib/exportData/exportTimeseriesData';
|
||||
import { toCsv } from 'lib/exportData/toCsv';
|
||||
import { toJsonl } from 'lib/exportData/toJsonl';
|
||||
import { ExportFormat, SerializedTable } from 'lib/exportData/types';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryRangeResponseV5, TimeSeriesData } from 'types/api/v5/queryRange';
|
||||
|
||||
@@ -20,33 +23,44 @@ const FORMAT_META: Record<ExportFormat, { mime: string; extension: string }> = {
|
||||
},
|
||||
};
|
||||
|
||||
// Picks the serializer for the response's request type. Narrows the results
|
||||
// union via the response discriminant. scalar lands with #5591; raw/trace are
|
||||
// server-exported, distribution is never emitted.
|
||||
/** The queryRange response object views hold — structural (params left
|
||||
* unconstrained) so both explorer variants assign cleanly. */
|
||||
export type ClientExportData = SuccessResponse<MetricRangePayloadProps> & {
|
||||
rawV5Response?: QueryRangeResponseV5;
|
||||
legendMap?: Record<string, string>;
|
||||
};
|
||||
|
||||
// Picks the serializer from what the queryRange response carries: timeseries
|
||||
// queries surface the raw V5 tree (rawV5Response); table queries carry the
|
||||
// formatForWeb webTables payload (resultType 'scalar'). raw/trace stay
|
||||
// server-exported via useServerExport.
|
||||
function serialize(
|
||||
response: QueryRangeResponseV5,
|
||||
data: ClientExportData,
|
||||
yAxisUnit?: string,
|
||||
legendMap?: Record<string, string>,
|
||||
query?: Query,
|
||||
): SerializedTable {
|
||||
if (response.type === REQUEST_TYPES.TIME_SERIES) {
|
||||
if (data.rawV5Response?.type === REQUEST_TYPES.TIME_SERIES) {
|
||||
return exportTimeseriesData({
|
||||
data: response.data.results as TimeSeriesData[],
|
||||
data: data.rawV5Response.data.results as TimeSeriesData[],
|
||||
yAxisUnit,
|
||||
legendMap,
|
||||
legendMap: data.legendMap,
|
||||
query,
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Export is not supported for "${response.type}" results`);
|
||||
if (data.payload?.data?.resultType === 'scalar' && query) {
|
||||
return exportScalarData({ data, query });
|
||||
}
|
||||
|
||||
throw new Error('Export is not supported for this result type');
|
||||
}
|
||||
|
||||
interface UseClientExportProps {
|
||||
response?: QueryRangeResponseV5;
|
||||
// currently supports only qb v5 responses. Can extend to support future responses.
|
||||
data?: ClientExportData;
|
||||
query?: Query;
|
||||
yAxisUnit?: string;
|
||||
fileName?: string;
|
||||
legendMap?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface ClientExportOptions {
|
||||
@@ -59,23 +73,22 @@ interface UseClientExportReturn {
|
||||
}
|
||||
|
||||
export function useClientExport({
|
||||
response, // currently supports only qb v5 response. Can extend to support future responses.
|
||||
data,
|
||||
query,
|
||||
yAxisUnit,
|
||||
fileName = 'export',
|
||||
legendMap,
|
||||
}: UseClientExportProps): UseClientExportReturn {
|
||||
const [isExporting, setIsExporting] = useState<boolean>(false);
|
||||
|
||||
const handleExport = useCallback(
|
||||
({ format }: ClientExportOptions): void => {
|
||||
if (!response) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsExporting(true);
|
||||
try {
|
||||
const table = serialize(response, yAxisUnit, legendMap, query);
|
||||
const table = serialize(data, yAxisUnit, query);
|
||||
const content =
|
||||
format === ExportFormat.Jsonl ? toJsonl(table) : toCsv(table);
|
||||
const { mime, extension } = FORMAT_META[format];
|
||||
@@ -86,7 +99,7 @@ export function useClientExport({
|
||||
setIsExporting(false);
|
||||
}
|
||||
},
|
||||
[response, query, yAxisUnit, fileName, legendMap],
|
||||
[data, query, yAxisUnit, fileName],
|
||||
);
|
||||
|
||||
return { isExporting, handleExport };
|
||||
|
||||
@@ -19,7 +19,6 @@ interface ExportOptions {
|
||||
|
||||
interface UseExportRawDataProps {
|
||||
dataSource: DataSource;
|
||||
panelType?: PANEL_TYPES;
|
||||
}
|
||||
|
||||
interface UseExportRawDataReturn {
|
||||
@@ -29,7 +28,6 @@ interface UseExportRawDataReturn {
|
||||
|
||||
export function useExportRawData({
|
||||
dataSource,
|
||||
panelType = PANEL_TYPES.LIST,
|
||||
}: UseExportRawDataProps): UseExportRawDataReturn {
|
||||
const [isDownloading, setIsDownloading] = useState<boolean>(false);
|
||||
|
||||
@@ -85,7 +83,7 @@ export function useExportRawData({
|
||||
|
||||
const { queryPayload } = prepareQueryRangePayloadV5({
|
||||
query: exportQuery,
|
||||
graphType: panelType,
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval,
|
||||
});
|
||||
@@ -98,7 +96,7 @@ export function useExportRawData({
|
||||
setIsDownloading(false);
|
||||
}
|
||||
},
|
||||
[stagedQuery, globalSelectedInterval, dataSource, panelType],
|
||||
[stagedQuery, globalSelectedInterval, dataSource],
|
||||
);
|
||||
|
||||
return { isDownloading, handleExportRawData };
|
||||
|
||||
@@ -4,8 +4,6 @@ export function useIntersectionObserver<T extends HTMLElement>(
|
||||
ref: RefObject<T>,
|
||||
options?: IntersectionObserverInit,
|
||||
isObserverOnce?: boolean,
|
||||
/** Defer observation by this many ms to let a transient mount layout settle. */
|
||||
startDelayMs = 0,
|
||||
): boolean {
|
||||
const [isIntersecting, setIntersecting] = useState(false);
|
||||
|
||||
@@ -25,28 +23,16 @@ export function useIntersectionObserver<T extends HTMLElement>(
|
||||
}
|
||||
}, options);
|
||||
|
||||
const startObserving = (): void => {
|
||||
if (currentReference) {
|
||||
observer.observe(currentReference);
|
||||
}
|
||||
};
|
||||
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
if (startDelayMs > 0) {
|
||||
timer = setTimeout(startObserving, startDelayMs);
|
||||
} else {
|
||||
startObserving();
|
||||
if (currentReference) {
|
||||
observer.observe(currentReference);
|
||||
}
|
||||
|
||||
return (): void => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
if (currentReference) {
|
||||
observer.unobserve(currentReference);
|
||||
}
|
||||
};
|
||||
}, [ref, options, isObserverOnce, startDelayMs]);
|
||||
}, [ref, options, isObserverOnce]);
|
||||
|
||||
return isIntersecting;
|
||||
}
|
||||
|
||||
@@ -92,7 +92,6 @@ function Panel({
|
||||
panelId={panelId}
|
||||
data={data}
|
||||
isFetching={isFetching}
|
||||
isVisible={isVisible}
|
||||
isPreviousData={isPreviousData}
|
||||
error={error}
|
||||
refetch={refetch}
|
||||
|
||||
@@ -23,8 +23,6 @@ interface PanelBodyProps {
|
||||
panelId: string;
|
||||
data: PanelQueryData;
|
||||
isFetching: boolean;
|
||||
/** Panel not yet scrolled into view — its fetch is deferred, so show the loader rather than NoData. */
|
||||
isVisible?: boolean;
|
||||
/** Showing a prior page's data while the next loads; forwarded so list renderers can show skeletons. */
|
||||
isPreviousData?: boolean;
|
||||
error: Error | null;
|
||||
@@ -56,7 +54,6 @@ function PanelBody({
|
||||
panelId,
|
||||
data,
|
||||
isFetching,
|
||||
isVisible,
|
||||
isPreviousData,
|
||||
error,
|
||||
refetch,
|
||||
@@ -108,9 +105,9 @@ function PanelBody({
|
||||
);
|
||||
}
|
||||
|
||||
// Full-panel loader on first fetch or while the fetch is deferred (off-screen); a refetch
|
||||
// over existing data keeps the renderer mounted, empty data loads via NoData.
|
||||
if ((isFetching || isVisible === false) && !hasData) {
|
||||
// Full-panel loader only on first fetch; a refetch over existing data keeps the renderer
|
||||
// mounted (e.g. list page change). A refetch over empty data loads via NoData.
|
||||
if (isFetching && !hasData) {
|
||||
return <PanelLoader />;
|
||||
}
|
||||
|
||||
|
||||
@@ -79,21 +79,6 @@ describe('PanelBody', () => {
|
||||
expect(screen.queryByTestId('mock-renderer')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the loader while the fetch is deferred (panel not yet scrolled into view)', () => {
|
||||
render(
|
||||
<PanelBody
|
||||
{...baseProps}
|
||||
panel={runnablePanel()}
|
||||
data={{} as PanelQueryData}
|
||||
isFetching={false}
|
||||
isVisible={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('panel-loading')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('mock-renderer')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps the renderer mounted during a refetch over existing data (e.g. list page change)', () => {
|
||||
render(
|
||||
<PanelBody
|
||||
|
||||
@@ -113,8 +113,8 @@ function ViewPanelModalHeader({
|
||||
/>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={onRefresh}
|
||||
disabled={isFetching}
|
||||
aria-label="Refresh"
|
||||
|
||||
@@ -22,7 +22,6 @@ function SectionGrid({
|
||||
sections,
|
||||
}: SectionGridProps): JSX.Element {
|
||||
const isEditable = useDashboardStore((s) => s.isEditable);
|
||||
|
||||
const rglLayout = useMemo<Layout[]>(
|
||||
() =>
|
||||
items.map((item) => ({
|
||||
|
||||
@@ -10,10 +10,6 @@ const VIEWPORT_OBSERVER_OPTIONS: IntersectionObserverInit = {
|
||||
rootMargin: '200px',
|
||||
};
|
||||
|
||||
// Start observing after RGL's mount unfold settles, so a panel that only
|
||||
// transiently overlaps the viewport during layout doesn't fire a throwaway fetch.
|
||||
const OBSERVER_START_DELAY_MS = 350;
|
||||
|
||||
interface SectionGridItemProps {
|
||||
panel: DashboardtypesPanelDTO;
|
||||
panelId: string;
|
||||
@@ -21,9 +17,9 @@ interface SectionGridItemProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy-loads a single panel: tracks its live viewport intersection and passes it to
|
||||
* the presentational Panel as `isVisible`, so a board of many panels only fetches
|
||||
* (and refetches on time change / auto-refresh) what's on screen.
|
||||
* Lazy-loads a single panel: watches its own viewport intersection (latched) and
|
||||
* passes it to the presentational Panel as `isVisible`, so a board of many panels
|
||||
* only fetches what's on screen.
|
||||
*/
|
||||
function SectionGridItem({
|
||||
panel,
|
||||
@@ -34,10 +30,7 @@ function SectionGridItem({
|
||||
const isVisible = useIntersectionObserver(
|
||||
containerRef,
|
||||
VIEWPORT_OBSERVER_OPTIONS,
|
||||
// Not once: track the live viewport so a time change / auto-refresh only
|
||||
// refetches on-screen panels (off-screen ones stay query-disabled).
|
||||
false,
|
||||
OBSERVER_START_DELAY_MS,
|
||||
true,
|
||||
);
|
||||
useScrollIntoView(panelId, containerRef);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('useScrollIntoView', () => {
|
||||
|
||||
expect(scrollIntoView).toHaveBeenCalledWith({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
block: 'start',
|
||||
});
|
||||
expect(useScrollIntoViewStore.getState().scrollTargetId).toBeNull();
|
||||
});
|
||||
@@ -38,7 +38,7 @@ describe('useScrollIntoView', () => {
|
||||
|
||||
expect(scrollIntoView).toHaveBeenCalledWith({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
block: 'start',
|
||||
});
|
||||
expect(useScrollIntoViewStore.getState().scrollTargetId).toBeNull();
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useScrollIntoViewStore } from '../../../store/useScrollIntoViewStore';
|
||||
export function useScrollIntoView(
|
||||
id: string,
|
||||
ref: RefObject<HTMLElement>,
|
||||
block: ScrollLogicalPosition = 'center',
|
||||
block: ScrollLogicalPosition = 'start',
|
||||
): void {
|
||||
const scrollTargetId = useScrollIntoViewStore((s) => s.scrollTargetId);
|
||||
const setScrollTargetId = useScrollIntoViewStore((s) => s.setScrollTargetId);
|
||||
|
||||
@@ -57,8 +57,5 @@ export function useGetQueryRangeV5({
|
||||
retry: retryUnlessClientError,
|
||||
keepPreviousData,
|
||||
cacheTime,
|
||||
// A resolved window is immutable per key, so a panel scrolled back into view
|
||||
// serves cache instead of refetching; a key change or manual refetch still runs.
|
||||
staleTime: Infinity,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,9 +107,6 @@ export function ContextMenu({
|
||||
}
|
||||
}}
|
||||
trigger="click"
|
||||
// Anchor to body (like the backdrop), not the host container: a modal's
|
||||
// transformed dialog would break `position: fixed` and trap the menu below it.
|
||||
getPopupContainer={(): HTMLElement => document.body}
|
||||
overlayStyle={{
|
||||
position: 'fixed',
|
||||
left: position.left,
|
||||
|
||||
@@ -77,11 +77,6 @@
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
// Body-portaled overlay: stay clickable when a modal sets `body { pointer-events: none }`.
|
||||
.context-menu {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.context-menu .ant-popover-inner {
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
|
||||
2
go.mod
2
go.mod
@@ -83,7 +83,7 @@ require (
|
||||
go.uber.org/zap v1.27.1
|
||||
golang.org/x/crypto v0.52.0
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90
|
||||
golang.org/x/net v0.55.0
|
||||
golang.org/x/net v0.54.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/text v0.37.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1488,8 +1488,8 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su
|
||||
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
||||
@@ -4,26 +4,13 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func telemetryReadScopes() []string {
|
||||
return []string{
|
||||
coretypes.ResourceTelemetryResourceLogs.Scope(coretypes.VerbRead),
|
||||
coretypes.ResourceTelemetryResourceTraces.Scope(coretypes.VerbRead),
|
||||
coretypes.ResourceTelemetryResourceMetrics.Scope(coretypes.VerbRead),
|
||||
coretypes.ResourceTelemetryResourceAuditLogs.Scope(coretypes.VerbRead),
|
||||
coretypes.ResourceTelemetryResourceMeterMetrics.Scope(coretypes.VerbRead),
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *provider) addQuerierRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v5/query_range", handler.New(provider.authzMiddleware.CheckResources(provider.querierHandler.QueryRange, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v5/query_range", handler.New(provider.authzMiddleware.ViewAccess(provider.querierHandler.QueryRange), handler.OpenAPIDef{
|
||||
ID: "QueryRangeV5",
|
||||
Tags: []string{"querier"},
|
||||
Summary: "Query range",
|
||||
@@ -459,17 +446,12 @@ func (provider *provider) addQuerierRoutes(router *mux.Router) error {
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
SecuritySchemes: newScopedSecuritySchemes(telemetryReadScopes()),
|
||||
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: querybuilder.TelemetrySelector,
|
||||
Resources: querybuilder.QueryRangeResources,
|
||||
}))).Methods(http.MethodPost).GetError(); err != nil {
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v5/query_range/preview", handler.New(provider.authzMiddleware.CheckResources(provider.querierHandler.QueryRangePreview, authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v5/query_range/preview", handler.New(provider.authzMiddleware.ViewAccess(provider.querierHandler.QueryRangePreview), handler.OpenAPIDef{
|
||||
ID: "QueryRangePreviewV5",
|
||||
Tags: []string{"querier"},
|
||||
Summary: "Query range preview",
|
||||
@@ -481,13 +463,8 @@ func (provider *provider) addQuerierRoutes(router *mux.Router) error {
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
SecuritySchemes: newScopedSecuritySchemes(telemetryReadScopes()),
|
||||
}, handler.WithResourceDefs(handler.TelemetryResourceDef{
|
||||
Verb: coretypes.VerbRead,
|
||||
Category: coretypes.ActionCategoryDataAccess,
|
||||
Selector: querybuilder.TelemetrySelector,
|
||||
Resources: querybuilder.QueryRangeResources,
|
||||
}))).Methods(http.MethodPost).GetError(); err != nil {
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
|
||||
Description: "Returns all mappers belonging to a mapping group.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(spantypes.GettableSpanMappers),
|
||||
Response: new(spantypes.GettableSpanMapperGroups),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
)
|
||||
import "github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
|
||||
type ResourceDef interface {
|
||||
// resolveRequest is unexported to seal the interface. It returns a slice so a
|
||||
@@ -100,31 +97,3 @@ func (def AttachDetachParentChildResourceDef) resolveRequest(ec coretypes.Extrac
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
type TelemetryResourceDef struct {
|
||||
Verb coretypes.Verb
|
||||
Category coretypes.ActionCategory
|
||||
Selector coretypes.SelectorFunc
|
||||
Resources coretypes.ResourceExtractor
|
||||
}
|
||||
|
||||
func (def TelemetryResourceDef) resolveRequest(ec coretypes.ExtractorContext) []coretypes.ResolvedResource {
|
||||
refs, err := def.Resources(ec)
|
||||
if err != nil {
|
||||
return []coretypes.ResolvedResource{coretypes.NewResolvedResourceWithError(def.Verb, def.Category, err)}
|
||||
}
|
||||
if len(refs) == 0 {
|
||||
return []coretypes.ResolvedResource{coretypes.NewResolvedResourceWithError(
|
||||
def.Verb,
|
||||
def.Category,
|
||||
errors.NewInvalidInputf(errors.CodeInvalidInput, "request resolved to no resources"),
|
||||
)}
|
||||
}
|
||||
|
||||
resolved := make([]coretypes.ResolvedResource, 0, len(refs))
|
||||
for _, ref := range refs {
|
||||
resolved = append(resolved, coretypes.NewResolvedResourceWithID(def.Verb, def.Category, ref.Resource, ref.ID, def.Selector))
|
||||
}
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
||||
@@ -118,10 +118,6 @@ func (middleware *Audit) emitAuditEvent(req *http.Request, writer responseCaptur
|
||||
extractorCtx := coretypes.ExtractorContext{Request: req, ResponseBody: writer.BodyBytes()}
|
||||
|
||||
for _, resource := range resolved {
|
||||
if err := resource.Err(); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
resource.ResolveResponse(extractorCtx)
|
||||
verb, category := resource.Verb(), resource.Category()
|
||||
|
||||
|
||||
@@ -20,21 +20,21 @@ func (c Compiled) IsEmpty() bool {
|
||||
// Compile always returns a non-nil *Compiled. An empty query (or one that
|
||||
// produces no SQL) yields a Compiled with an empty SQL — callers gate on
|
||||
// SQL != "" rather than a nil check.
|
||||
//
|
||||
// A `key OP value` term compiles to a DSL predicate; a bare word is a
|
||||
// case-insensitive substring search over the dashboard name, description, and tag
|
||||
// keys/values. They compose through AND/OR/NOT, so `prod payment` matches both
|
||||
// words (implicit AND) and `prod OR name = 'x'` mixes free text with a filter. A
|
||||
// quoted token matches literally, e.g. `"prod payment"`.
|
||||
func Compile(query string, formatter sqlstore.SQLFormatter) (*Compiled, error) {
|
||||
if len(strings.TrimSpace(query)) == 0 {
|
||||
if len(query) == 0 {
|
||||
return &Compiled{}, nil
|
||||
}
|
||||
|
||||
sql, args, errs := newVisitor(formatter).compile(query)
|
||||
if len(errs) > 0 {
|
||||
queryVisitor := newVisitor(formatter)
|
||||
sql, args, syntaxErrs := queryVisitor.compile(query)
|
||||
|
||||
if len(syntaxErrs) > 0 {
|
||||
return nil, errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardListFilterInvalid,
|
||||
"invalid filter query: %s", strings.Join(errs, "; "))
|
||||
"invalid filter query: %s", strings.Join(syntaxErrs, "; "))
|
||||
}
|
||||
if len(queryVisitor.errors) > 0 {
|
||||
return nil, errors.NewInvalidInputf(dashboardtypes.ErrCodeDashboardListFilterInvalid,
|
||||
"invalid filter query: %s", strings.Join(queryVisitor.errors, "; "))
|
||||
}
|
||||
|
||||
return &Compiled{
|
||||
|
||||
@@ -460,83 +460,6 @@ func TestCompile_ComplexExamples(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompile_FreeText(t *testing.T) {
|
||||
// freeTextSQL is the predicate every free-text query compiles to; only the
|
||||
// bound pattern differs.
|
||||
freeTextSQL := `
|
||||
(
|
||||
lower(COALESCE(json_extract("dashboard"."data", '$.spec.display.name'), '')) LIKE LOWER(?) ESCAPE '\'
|
||||
OR lower(COALESCE(json_extract("dashboard"."data", '$.spec.display.description'), '')) LIKE LOWER(?) ESCAPE '\'
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM tag_relation tr
|
||||
JOIN tag t ON t.id = tr.tag_id
|
||||
WHERE tr.kind = ? AND tr.resource_id = dashboard.id
|
||||
AND (lower(COALESCE(t.key, '')) LIKE LOWER(?) ESCAPE '\' OR lower(COALESCE(t.value, '')) LIKE LOWER(?) ESCAPE '\')
|
||||
))`
|
||||
freeTextArgs := func(pattern string) []any {
|
||||
return []any{pattern, pattern, kindArg, pattern, pattern}
|
||||
}
|
||||
|
||||
runCompileCases(t, []compileCase{
|
||||
{
|
||||
subtestName: "single bare word",
|
||||
dslQueryToCompile: `payment`,
|
||||
expectedSQL: freeTextSQL,
|
||||
expectedArgs: freeTextArgs("%payment%"),
|
||||
},
|
||||
{
|
||||
// consecutive words are implicit-AND per the grammar, so each is its
|
||||
// own term; `"prod payment"` (below) is the way to match the phrase
|
||||
subtestName: "words are separate terms AND'd together",
|
||||
dslQueryToCompile: `prod payment`,
|
||||
expectedSQL: "(" + freeTextSQL + " AND " + freeTextSQL + ")",
|
||||
expectedArgs: append(freeTextArgs("%prod%"), freeTextArgs("%payment%")...),
|
||||
},
|
||||
{
|
||||
subtestName: "a quoted token matches the whole phrase",
|
||||
dslQueryToCompile: `"prod payment"`,
|
||||
expectedSQL: freeTextSQL,
|
||||
expectedArgs: freeTextArgs("%prod payment%"),
|
||||
},
|
||||
{
|
||||
subtestName: "quoting is the escape hatch for a DSL-like literal",
|
||||
dslQueryToCompile: `"team = prod"`,
|
||||
expectedSQL: freeTextSQL,
|
||||
expectedArgs: freeTextArgs("%team = prod%"),
|
||||
},
|
||||
{
|
||||
subtestName: "LIKE wildcards in the term are escaped to match literally",
|
||||
dslQueryToCompile: `"50%"`,
|
||||
expectedSQL: freeTextSQL,
|
||||
expectedArgs: freeTextArgs(`%50\%%`),
|
||||
},
|
||||
{
|
||||
subtestName: "surrounding whitespace is trimmed",
|
||||
dslQueryToCompile: ` payment `,
|
||||
expectedSQL: freeTextSQL,
|
||||
expectedArgs: freeTextArgs("%payment%"),
|
||||
},
|
||||
{
|
||||
subtestName: "free-text term composes with a comparison via AND",
|
||||
dslQueryToCompile: `prod AND name CONTAINS 'signoz'`,
|
||||
expectedSQL: "(" + freeTextSQL + ` AND json_extract("dashboard"."data", '$.spec.display.name') LIKE ? ESCAPE '\')`,
|
||||
expectedArgs: append(freeTextArgs("%prod%"), "%signoz%"),
|
||||
},
|
||||
{
|
||||
subtestName: "free-text words compose with a comparison via OR",
|
||||
dslQueryToCompile: `prod payment OR name = 'x'`,
|
||||
expectedSQL: "((" + freeTextSQL + " AND " + freeTextSQL + `) OR json_extract("dashboard"."data", '$.spec.display.name') = ?)`,
|
||||
expectedArgs: append(append(freeTextArgs("%prod%"), freeTextArgs("%payment%")...), "x"),
|
||||
},
|
||||
{
|
||||
subtestName: "NOT negates a free-text term",
|
||||
dslQueryToCompile: `NOT payment`,
|
||||
expectedSQL: "NOT (" + freeTextSQL + ")",
|
||||
expectedArgs: freeTextArgs("%payment%"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompile_Rejections(t *testing.T) {
|
||||
runCompileCases(t, []compileCase{
|
||||
{
|
||||
|
||||
@@ -32,19 +32,13 @@ func newVisitor(formatter sqlstore.SQLFormatter) *visitor {
|
||||
}
|
||||
}
|
||||
|
||||
// compile builds `?`-placeholder WHERE SQL + args for bun. Each term is either a
|
||||
// `key OP value` comparison or a bare token that becomes a free-text search; the
|
||||
// two compose through the boolean grammar (AND/OR/NOT). Malformed input is
|
||||
// returned as errors.
|
||||
// compile turns the parse tree into `?`-placeholder WHERE SQL + arguments for bun.
|
||||
func (v *visitor) compile(query string) (string, []any, []string) {
|
||||
tree, _, collector := filterquery.Parse(query)
|
||||
if len(collector.Errors) > 0 {
|
||||
return "", nil, collector.Errors
|
||||
}
|
||||
condition, _ := v.visit(tree).(string)
|
||||
if len(v.errors) > 0 {
|
||||
return "", nil, v.errors
|
||||
}
|
||||
if condition == "" {
|
||||
return "", nil, nil
|
||||
}
|
||||
@@ -125,10 +119,10 @@ func (v *visitor) VisitPrimary(ctx *grammar.PrimaryContext) any {
|
||||
if ctx.Comparison() != nil {
|
||||
return v.visit(ctx.Comparison())
|
||||
}
|
||||
// A lone key/value/full-text token is a free-text term, composed with any
|
||||
// comparisons through the boolean grammar. A quoted token matches its contents
|
||||
// literally — the escape hatch for a phrase or a term that looks like DSL.
|
||||
return v.buildFreeTextTerm(trimQuotes(ctx.GetText()))
|
||||
// Bare keys, values, full text, and function calls are not part of the
|
||||
// dashboard list DSL.
|
||||
v.addError("unsupported expression %q — every term must be of the form `key OP value`", ctx.GetText())
|
||||
return ""
|
||||
}
|
||||
|
||||
// VisitComparison dispatches a single `key OP value` term. A key that matches
|
||||
@@ -407,50 +401,6 @@ func buildSubqueryForTagKeyAndValue(subqueryBuilder *sqlbuilder.SelectBuilder, t
|
||||
return buildSubqueryForTagKey(subqueryBuilder, tagKey).Where(valuePredicate)
|
||||
}
|
||||
|
||||
// ─── free-text search ────────────────────────────────────────────────────────
|
||||
|
||||
// buildFreeTextTerm matches value as a case-insensitive substring of the
|
||||
// dashboard name, description, or any tag key/value.
|
||||
func (v *visitor) buildFreeTextTerm(value string) string {
|
||||
nameColumn := string(v.formatter.JSONExtractString("dashboard.data", "$.spec.display.name"))
|
||||
descriptionColumn := string(v.formatter.JSONExtractString("dashboard.data", "$.spec.display.description"))
|
||||
namePredicate := v.buildFreeTextContains(v.selectBuilder, nameColumn, value)
|
||||
descriptionPredicate := v.buildFreeTextContains(v.selectBuilder, descriptionColumn, value)
|
||||
|
||||
subqueryBuilder := sqlbuilder.NewSelectBuilder()
|
||||
keyPredicate := v.buildFreeTextContains(subqueryBuilder, "t.key", value)
|
||||
valuePredicate := v.buildFreeTextContains(subqueryBuilder, "t.value", value)
|
||||
buildSubqueryForFreeTextTag(subqueryBuilder, keyPredicate, valuePredicate)
|
||||
tagPredicate := v.selectBuilder.Exists(subqueryBuilder)
|
||||
|
||||
return v.selectBuilder.Or(namePredicate, descriptionPredicate, tagPredicate)
|
||||
}
|
||||
|
||||
// buildFreeTextContains emits a case-insensitive contains as
|
||||
// LOWER(COALESCE(col, '')) LIKE LOWER(?), identical on SQLite and Postgres.
|
||||
// COALESCE keeps a NULL column (an absent description) false rather than NULL —
|
||||
// otherwise `NOT (…)` goes NULL and drops every description-less dashboard. The
|
||||
// value's % and _ are escaped, and ESCAPE pins backslash as the escape char.
|
||||
func (v *visitor) buildFreeTextContains(builder *sqlbuilder.SelectBuilder, columnExpression, value string) string {
|
||||
lowerColumn := string(v.formatter.LowerExpression("COALESCE(" + columnExpression + ", '')"))
|
||||
pattern := "%" + v.formatter.EscapeLikePattern(value) + "%"
|
||||
return fmt.Sprintf("%s LIKE LOWER(%s) ESCAPE '\\'", lowerColumn, builder.Var(pattern))
|
||||
}
|
||||
|
||||
func buildSubqueryForFreeTextTag(subqueryBuilder *sqlbuilder.SelectBuilder, keyPredicate, valuePredicate string) *sqlbuilder.SelectBuilder {
|
||||
const dashboardTagKind = `"dashboard"`
|
||||
|
||||
return subqueryBuilder.
|
||||
Select("1").
|
||||
From("tag_relation tr").
|
||||
Join("tag t", "t.id = tr.tag_id").
|
||||
Where(
|
||||
subqueryBuilder.Equal("tr.kind", dashboardTagKind),
|
||||
"tr.resource_id = dashboard.id",
|
||||
subqueryBuilder.Or(keyPredicate, valuePredicate),
|
||||
)
|
||||
}
|
||||
|
||||
// ─── value extraction helpers ───────────────────────────────────────────────
|
||||
|
||||
func (v *visitor) addError(format string, arguments ...any) {
|
||||
|
||||
@@ -98,7 +98,7 @@ func (s *store) CreateOrGet(ctx context.Context, tags []*tagtypes.Tag) ([]*tagty
|
||||
BunDBCtx(ctx).
|
||||
NewInsert().
|
||||
Model(&tags).
|
||||
On("CONFLICT (org_id, kind, (LOWER(key)), (LOWER(value))) DO UPDATE").
|
||||
// On("CONFLICT (org_id, kind, (LOWER(key)), (LOWER(value))) DO UPDATE").
|
||||
Set("key = tag.key").
|
||||
Returning("*").
|
||||
Scan(ctx)
|
||||
|
||||
@@ -88,60 +88,62 @@ func TestStore_Create_PopulatesIDsOnFreshInsert(t *testing.T) {
|
||||
assert.Equal(t, preIDB, stored["team\x00blr"].ID)
|
||||
}
|
||||
|
||||
func TestStore_Create_ConflictReturnsExistingRowID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sqlstore := newTestStore(t)
|
||||
s := NewStore(sqlstore)
|
||||
// todo (@namanverma): uncomment once unique index is there.
|
||||
//
|
||||
// func TestStore_Create_ConflictReturnsExistingRowID(t *testing.T) {
|
||||
// ctx := context.Background()
|
||||
// sqlstore := newTestStore(t)
|
||||
// s := NewStore(sqlstore)
|
||||
|
||||
orgID := valuer.GenerateUUID()
|
||||
// orgID := valuer.GenerateUUID()
|
||||
|
||||
// Simulate a concurrent insert: someone else has already inserted "tag:Database".
|
||||
winner := tagtypes.NewTag(orgID, dashboardKind, "tag", "Database")
|
||||
_, err := s.CreateOrGet(ctx, []*tagtypes.Tag{winner})
|
||||
require.NoError(t, err)
|
||||
winnerID := winner.ID
|
||||
// // Simulate a concurrent insert: someone else has already inserted "tag:Database".
|
||||
// winner := tagtypes.NewTag(orgID, dashboardKind, "tag", "Database")
|
||||
// _, err := s.CreateOrGet(ctx, []*tagtypes.Tag{winner})
|
||||
// require.NoError(t, err)
|
||||
// winnerID := winner.ID
|
||||
|
||||
// Now our request runs with a different pre-generated ID for the same
|
||||
// (key, value) — case differs but the functional unique index collapses
|
||||
// them. RETURNING should overwrite our stale ID with winner's ID.
|
||||
loser := tagtypes.NewTag(orgID, dashboardKind, "TAG", "DATABASE")
|
||||
loserPreID := loser.ID
|
||||
require.NotEqual(t, winnerID, loserPreID, "pre-generated IDs must differ for this test to be meaningful")
|
||||
// // Now our request runs with a different pre-generated ID for the same
|
||||
// // (key, value) — case differs but the functional unique index collapses
|
||||
// // them. RETURNING should overwrite our stale ID with winner's ID.
|
||||
// loser := tagtypes.NewTag(orgID, dashboardKind, "TAG", "DATABASE")
|
||||
// loserPreID := loser.ID
|
||||
// require.NotEqual(t, winnerID, loserPreID, "pre-generated IDs must differ for this test to be meaningful")
|
||||
|
||||
got, err := s.CreateOrGet(ctx, []*tagtypes.Tag{loser})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 1)
|
||||
// got, err := s.CreateOrGet(ctx, []*tagtypes.Tag{loser})
|
||||
// require.NoError(t, err)
|
||||
// require.Len(t, got, 1)
|
||||
|
||||
assert.Equal(t, winnerID, got[0].ID, "returned slice should carry the existing row's ID, not our stale one")
|
||||
assert.Equal(t, winnerID, loser.ID, "input slice element is mutated in place")
|
||||
// assert.Equal(t, winnerID, got[0].ID, "returned slice should carry the existing row's ID, not our stale one")
|
||||
// assert.Equal(t, winnerID, loser.ID, "input slice element is mutated in place")
|
||||
|
||||
// And the DB still has exactly one row for that (lower(key), lower(value)) — winner's, with winner's casing.
|
||||
stored := tagsByLowerKeyValue(t, sqlstore.BunDB())
|
||||
require.Len(t, stored, 1)
|
||||
assert.Equal(t, winnerID, stored["tag\x00database"].ID)
|
||||
assert.Equal(t, "tag", stored["tag\x00database"].Key, "winner's casing preserved in key")
|
||||
assert.Equal(t, "Database", stored["tag\x00database"].Value, "winner's casing preserved in value")
|
||||
}
|
||||
// // And the DB still has exactly one row for that (lower(key), lower(value)) — winner's, with winner's casing.
|
||||
// stored := tagsByLowerKeyValue(t, sqlstore.BunDB())
|
||||
// require.Len(t, stored, 1)
|
||||
// assert.Equal(t, winnerID, stored["tag\x00database"].ID)
|
||||
// assert.Equal(t, "tag", stored["tag\x00database"].Key, "winner's casing preserved in key")
|
||||
// assert.Equal(t, "Database", stored["tag\x00database"].Value, "winner's casing preserved in value")
|
||||
// }
|
||||
|
||||
func TestStore_Create_MixedFreshAndConflict(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sqlstore := newTestStore(t)
|
||||
s := NewStore(sqlstore)
|
||||
// func TestStore_Create_MixedFreshAndConflict(t *testing.T) {
|
||||
// ctx := context.Background()
|
||||
// sqlstore := newTestStore(t)
|
||||
// s := NewStore(sqlstore)
|
||||
|
||||
orgID := valuer.GenerateUUID()
|
||||
pre := tagtypes.NewTag(orgID, dashboardKind, "tag", "Database")
|
||||
_, err := s.CreateOrGet(ctx, []*tagtypes.Tag{pre})
|
||||
require.NoError(t, err)
|
||||
preExistingID := pre.ID
|
||||
// orgID := valuer.GenerateUUID()
|
||||
// pre := tagtypes.NewTag(orgID, dashboardKind, "tag", "Database")
|
||||
// _, err := s.CreateOrGet(ctx, []*tagtypes.Tag{pre})
|
||||
// require.NoError(t, err)
|
||||
// preExistingID := pre.ID
|
||||
|
||||
conflict := tagtypes.NewTag(orgID, dashboardKind, "tag", "Database")
|
||||
fresh := tagtypes.NewTag(orgID, dashboardKind, "team", "BLR")
|
||||
freshPreID := fresh.ID
|
||||
// conflict := tagtypes.NewTag(orgID, dashboardKind, "tag", "Database")
|
||||
// fresh := tagtypes.NewTag(orgID, dashboardKind, "team", "BLR")
|
||||
// freshPreID := fresh.ID
|
||||
|
||||
got, err := s.CreateOrGet(ctx, []*tagtypes.Tag{conflict, fresh})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2)
|
||||
// got, err := s.CreateOrGet(ctx, []*tagtypes.Tag{conflict, fresh})
|
||||
// require.NoError(t, err)
|
||||
// require.Len(t, got, 2)
|
||||
|
||||
assert.Equal(t, preExistingID, got[0].ID, "conflicting row's ID overwritten with the existing row's")
|
||||
assert.Equal(t, freshPreID, got[1].ID, "fresh row's pre-generated ID is preserved")
|
||||
}
|
||||
// assert.Equal(t, preExistingID, got[0].ID, "conflicting row's ID overwritten with the existing row's")
|
||||
// assert.Equal(t, freshPreID, got[1].ID, "fresh row's pre-generated ID is preserved")
|
||||
// }
|
||||
|
||||
@@ -357,7 +357,10 @@ func (m *Manager) EditRule(ctx context.Context, ruleStr string, id valuer.UUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parsedRule := ruletypes.PostableRule{}
|
||||
err = json.Unmarshal([]byte(ruleStr), &parsedRule)
|
||||
if err != nil {
|
||||
@@ -369,7 +372,7 @@ func (m *Manager) EditRule(ctx context.Context, ruleStr string, id valuer.UUID)
|
||||
if err := m.validateChannels(ctx, claims.OrgID, &parsedRule); err != nil {
|
||||
return err
|
||||
}
|
||||
existingRule, err := m.ruleStore.GetStoredRule(ctx, orgID, id)
|
||||
existingRule, err := m.ruleStore.GetStoredRule(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -482,14 +485,17 @@ func (m *Manager) DeleteRule(ctx context.Context, idStr string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
_, err = m.ruleStore.GetStoredRule(ctx, orgID, id)
|
||||
_, err = m.ruleStore.GetStoredRule(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.ruleStore.DeleteRule(ctx, orgID, id, func(ctx context.Context) error {
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.ruleStore.DeleteRule(ctx, id, func(ctx context.Context) error {
|
||||
cfg, err := m.alertmanager.GetConfig(ctx, claims.OrgID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -880,14 +886,7 @@ func (m *Manager) ListRuleStates(ctx context.Context) (*ruletypes.GettableRules,
|
||||
}
|
||||
|
||||
func (m *Manager) GetRule(ctx context.Context, id valuer.UUID) (*ruletypes.GettableRule, error) {
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
s, err := m.ruleStore.GetStoredRule(ctx, orgID, id)
|
||||
s, err := m.ruleStore.GetStoredRule(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -952,12 +951,15 @@ func (m *Manager) PatchRule(ctx context.Context, ruleStr string, id valuer.UUID)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
taskName := prepareTaskName(id.StringValue())
|
||||
|
||||
// retrieve rule from DB
|
||||
storedJSON, err := m.ruleStore.GetStoredRule(ctx, orgID, id)
|
||||
storedJSON, err := m.ruleStore.GetStoredRule(ctx, id)
|
||||
if err != nil {
|
||||
m.logger.ErrorContext(ctx, "failed to get stored rule with given id", slog.String("rule.id", id.StringValue()), errors.Attr(err))
|
||||
return nil, err
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
var telemetryGrantKeys = map[string]struct{}{
|
||||
"service.name": {},
|
||||
}
|
||||
|
||||
const telemetryValueSafeBytes = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-"
|
||||
|
||||
func EscapeTelemetryValue(value string) string {
|
||||
var escaped strings.Builder
|
||||
for _, character := range []byte(value) {
|
||||
if strings.IndexByte(telemetryValueSafeBytes, character) >= 0 {
|
||||
escaped.WriteByte(character)
|
||||
continue
|
||||
}
|
||||
escaped.WriteString(fmt.Sprintf("%%%02X", character))
|
||||
}
|
||||
|
||||
return escaped.String()
|
||||
}
|
||||
|
||||
func TelemetrySelector(_ context.Context, resource coretypes.Resource, id string, _ valuer.UUID) ([]coretypes.Selector, error) {
|
||||
values := []string{id}
|
||||
segments := strings.Split(id, "/")
|
||||
for level := len(segments) - 1; level >= 1; level-- {
|
||||
value := strings.Join(segments[:level], "/") + "/" + coretypes.WildCardSelectorString
|
||||
if value == id {
|
||||
continue
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
if id != coretypes.WildCardSelectorString {
|
||||
values = append(values, coretypes.WildCardSelectorString)
|
||||
}
|
||||
|
||||
selectors := make([]coretypes.Selector, 0, len(values))
|
||||
for _, value := range values {
|
||||
selector, err := resource.Type().Selector(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selectors = append(selectors, selector)
|
||||
}
|
||||
|
||||
return selectors, nil
|
||||
}
|
||||
|
||||
func QueryRangeResources(ec coretypes.ExtractorContext) ([]coretypes.ResourceWithID, error) {
|
||||
queries := gjson.GetBytes(ec.RequestBody, "compositeQuery.queries")
|
||||
if !queries.IsArray() || len(queries.Array()) == 0 {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "composite query has no queries")
|
||||
}
|
||||
|
||||
variables, err := queryRangeVariables(ec.RequestBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refs := make([]coretypes.ResourceWithID, 0, len(queries.Array()))
|
||||
seen := make(map[string]struct{})
|
||||
for _, query := range queries.Array() {
|
||||
queryRefs, err := resourcesForQuery(query, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, ref := range queryRefs {
|
||||
key := ref.Resource.Kind().String() + ":" + ref.ID
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
refs = append(refs, ref)
|
||||
}
|
||||
}
|
||||
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
func queryRangeVariables(body []byte) (map[string]qbtypes.VariableItem, error) {
|
||||
variables := make(map[string]qbtypes.VariableItem)
|
||||
|
||||
raw := gjson.GetBytes(body, "variables")
|
||||
if !raw.Exists() {
|
||||
return variables, nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(raw.Raw), &variables); err != nil {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid variables in query range request")
|
||||
}
|
||||
|
||||
return variables, nil
|
||||
}
|
||||
|
||||
func resourcesForQuery(query gjson.Result, variables map[string]qbtypes.VariableItem) ([]coretypes.ResourceWithID, error) {
|
||||
queryType := query.Get("type").String()
|
||||
typeWildcard := queryType + "/" + coretypes.WildCardSelectorString
|
||||
|
||||
switch queryType {
|
||||
case qbtypes.QueryTypeBuilder.StringValue(), qbtypes.QueryTypeSubQuery.StringValue():
|
||||
return resourcesForBuilderQuery(queryType, query.Get("spec"), variables)
|
||||
case qbtypes.QueryTypePromQL.StringValue():
|
||||
return []coretypes.ResourceWithID{{Resource: coretypes.ResourceTelemetryResourceMetrics, ID: typeWildcard}}, nil
|
||||
case qbtypes.QueryTypeClickHouseSQL.StringValue():
|
||||
return []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: typeWildcard},
|
||||
{Resource: coretypes.ResourceTelemetryResourceTraces, ID: typeWildcard},
|
||||
{Resource: coretypes.ResourceTelemetryResourceMetrics, ID: typeWildcard},
|
||||
}, nil
|
||||
case qbtypes.QueryTypeFormula.StringValue(), qbtypes.QueryTypeJoin.StringValue(), qbtypes.QueryTypeTraceOperator.StringValue():
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported query type %q", queryType)
|
||||
}
|
||||
}
|
||||
|
||||
func resourcesForBuilderQuery(queryType string, spec gjson.Result, variables map[string]qbtypes.VariableItem) ([]coretypes.ResourceWithID, error) {
|
||||
resource, err := builderQueryResource(spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ids, err := builderQuerySelectors(queryType, spec.Get("filter.expression").String(), variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refs := make([]coretypes.ResourceWithID, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
refs = append(refs, coretypes.ResourceWithID{Resource: resource, ID: id})
|
||||
}
|
||||
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
func builderQueryResource(spec gjson.Result) (coretypes.Resource, error) {
|
||||
source := spec.Get("source").String()
|
||||
|
||||
switch spec.Get("signal").String() {
|
||||
case telemetrytypes.SignalTraces.StringValue():
|
||||
return coretypes.ResourceTelemetryResourceTraces, nil
|
||||
case telemetrytypes.SignalLogs.StringValue():
|
||||
if source == telemetrytypes.SourceAudit.StringValue() {
|
||||
return coretypes.ResourceTelemetryResourceAuditLogs, nil
|
||||
}
|
||||
return coretypes.ResourceTelemetryResourceLogs, nil
|
||||
case telemetrytypes.SignalMetrics.StringValue():
|
||||
if source == telemetrytypes.SourceMeter.StringValue() {
|
||||
return coretypes.ResourceTelemetryResourceMeterMetrics, nil
|
||||
}
|
||||
return coretypes.ResourceTelemetryResourceMetrics, nil
|
||||
default:
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported signal %q", spec.Get("signal").String())
|
||||
}
|
||||
}
|
||||
|
||||
func builderQuerySelectors(queryType, expression string, variables map[string]qbtypes.VariableItem) ([]string, error) {
|
||||
typeWildcard := queryType + "/" + coretypes.WildCardSelectorString
|
||||
|
||||
if strings.TrimSpace(expression) == "" {
|
||||
return []string{typeWildcard}, nil
|
||||
}
|
||||
|
||||
normalized, err := NormalizeWhereClause(expression, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ids := make([]string, 0)
|
||||
for _, condition := range normalized.Conditions {
|
||||
if !condition.TopLevel {
|
||||
continue
|
||||
}
|
||||
|
||||
key, ok := canonicalTelemetryGrantKey(condition.Key)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if condition.Operator == "=" || condition.Operator == "IN" {
|
||||
for _, value := range condition.Values {
|
||||
ids = append(ids, queryType+"/"+key+"/"+EscapeTelemetryValue(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
return []string{typeWildcard}, nil
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func canonicalTelemetryGrantKey(keyText string) (string, bool) {
|
||||
fieldKey := telemetrytypes.GetFieldKeyFromKeyText(keyText)
|
||||
if fieldKey.FieldContext != telemetrytypes.FieldContextUnspecified && fieldKey.FieldContext != telemetrytypes.FieldContextResource {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if _, ok := telemetryGrantKeys[fieldKey.Name]; !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return fieldKey.Name, true
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func builderQueryBody(signal, filterExpression string) string {
|
||||
return `{"compositeQuery":{"queries":[{"type":"builder_query","spec":{"signal":"` + signal + `","filter":{"expression":"` + filterExpression + `"}}}]}}`
|
||||
}
|
||||
|
||||
func TestQueryRangeResources(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
body string
|
||||
expected []coretypes.ResourceWithID
|
||||
}{
|
||||
{
|
||||
name: "top level service equality",
|
||||
body: builderQueryBody("logs", "service.name = 'checkout' AND status = 500"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/service.name/checkout"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "resource prefixed service key",
|
||||
body: builderQueryBody("traces", "resource.service.name = 'checkout'"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceTraces, ID: "builder_query/service.name/checkout"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "in atom requires every value",
|
||||
body: builderQueryBody("logs", "service.name IN ('b', 'a')"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/service.name/a"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/service.name/b"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple equality atoms each require a grant",
|
||||
body: builderQueryBody("logs", "service.name = 'b' AND service.name = 'a'"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/service.name/a"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/service.name/b"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no filter expression",
|
||||
body: `{"compositeQuery":{"queries":[{"type":"builder_query","spec":{"signal":"logs"}}]}}`,
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/*"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "service atom under or does not qualify",
|
||||
body: builderQueryBody("logs", "service.name = 'a' OR status = 500"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/*"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "negated service atom does not qualify",
|
||||
body: builderQueryBody("logs", "NOT service.name = 'a'"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/*"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "service inequality does not qualify",
|
||||
body: builderQueryBody("logs", "service.name != 'a'"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/*"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unsafe value bytes are escaped",
|
||||
body: builderQueryBody("logs", "service.name = 'check out/2'"),
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/service.name/check%20out%2F2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "audit source maps to audit logs resource",
|
||||
body: `{"compositeQuery":{"queries":[{"type":"builder_query","spec":{"signal":"logs","source":"audit","filter":{"expression":"service.name = 'a'"}}}]}}`,
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceAuditLogs, ID: "builder_query/service.name/a"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "promql is wildcard only",
|
||||
body: `{"compositeQuery":{"queries":[{"type":"promql","spec":{"query":"up"}}]}}`,
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceMetrics, ID: "promql/*"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "clickhouse sql covers all signals",
|
||||
body: `{"compositeQuery":{"queries":[{"type":"clickhouse_sql","spec":{"query":"SELECT 1"}}]}}`,
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "clickhouse_sql/*"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceTraces, ID: "clickhouse_sql/*"},
|
||||
{Resource: coretypes.ResourceTelemetryResourceMetrics, ID: "clickhouse_sql/*"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "formula produces no resources",
|
||||
body: `{"compositeQuery":{"queries":[{"type":"builder_formula","spec":{"expression":"A/B"}}]}}`,
|
||||
expected: []coretypes.ResourceWithID{},
|
||||
},
|
||||
{
|
||||
name: "trace operator rides on its referenced queries",
|
||||
body: `{"compositeQuery":{"queries":[{"type":"builder_query","spec":{"name":"A","signal":"traces","disabled":true,"filter":{"expression":"service.name = 'checkout'"}}},{"type":"builder_query","spec":{"name":"B","signal":"traces","disabled":true,"filter":{"expression":"service.name = 'checkout' AND has_error = true"}}},{"type":"builder_trace_operator","spec":{"name":"T1","expression":"A => B","returnSpansFrom":"A"}}]}}`,
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceTraces, ID: "builder_query/service.name/checkout"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "variable substitution qualifies",
|
||||
body: `{"variables":{"svc":{"value":"checkout"}},"compositeQuery":{"queries":[{"type":"builder_query","spec":{"signal":"logs","filter":{"expression":"service.name = $svc"}}}]}}`,
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/service.name/checkout"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "duplicate queries dedupe",
|
||||
body: `{"compositeQuery":{"queries":[{"type":"builder_query","spec":{"signal":"logs","filter":{"expression":"service.name = 'a'"}}},{"type":"builder_query","spec":{"signal":"logs","filter":{"expression":"service.name='a'"}}}]}}`,
|
||||
expected: []coretypes.ResourceWithID{
|
||||
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: "builder_query/service.name/a"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
refs, err := QueryRangeResources(coretypes.ExtractorContext{RequestBody: []byte(testCase.body)})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testCase.expected, refs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryRangeResourcesErrors(t *testing.T) {
|
||||
bodies := []string{
|
||||
`{"compositeQuery":{"queries":[]}}`,
|
||||
`{}`,
|
||||
builderQueryBody("logs", "service.name = "),
|
||||
`{"compositeQuery":{"queries":[{"type":"builder_query","spec":{"signal":"unknown"}}]}}`,
|
||||
`{"compositeQuery":{"queries":[{"type":"unknown_type"}]}}`,
|
||||
}
|
||||
|
||||
for _, body := range bodies {
|
||||
_, err := QueryRangeResources(coretypes.ExtractorContext{RequestBody: []byte(body)})
|
||||
assert.Error(t, err, "body %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTelemetrySelector(t *testing.T) {
|
||||
orgID := valuer.GenerateUUID()
|
||||
|
||||
selectorValues := func(id string) []string {
|
||||
selectors, err := TelemetrySelector(context.Background(), coretypes.ResourceTelemetryResourceLogs, id, orgID)
|
||||
require.NoError(t, err)
|
||||
values := make([]string, 0, len(selectors))
|
||||
for _, selector := range selectors {
|
||||
values = append(values, selector.String())
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"builder_query/service.name/a", "builder_query/service.name/*", "builder_query/*", "*"}, selectorValues("builder_query/service.name/a"))
|
||||
assert.Equal(t, []string{"builder_query/*", "*"}, selectorValues("builder_query/*"))
|
||||
assert.Equal(t, []string{"promql/*", "*"}, selectorValues("promql/*"))
|
||||
|
||||
_, err := TelemetrySelector(context.Background(), coretypes.ResourceTelemetryResourceLogs, strings.Repeat("a", 256), orgID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -1,558 +0,0 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/filterquery/grammar"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/antlr4-go/antlr/v4"
|
||||
)
|
||||
|
||||
const WhereClauseOperatorFullText = "FULLTEXT"
|
||||
|
||||
type NormalizedWhereClause struct {
|
||||
Expression string
|
||||
Conditions []WhereClauseCondition
|
||||
}
|
||||
|
||||
type WhereClauseCondition struct {
|
||||
Key string
|
||||
Operator string
|
||||
Values []string
|
||||
Negated bool
|
||||
TopLevel bool
|
||||
}
|
||||
|
||||
type joinKind int
|
||||
|
||||
const (
|
||||
joinKindNone joinKind = iota
|
||||
joinKindAnd
|
||||
joinKindOr
|
||||
)
|
||||
|
||||
type normalizedPart struct {
|
||||
text string
|
||||
join joinKind
|
||||
skipped bool
|
||||
}
|
||||
|
||||
type normalizedValue struct {
|
||||
text string
|
||||
raw string
|
||||
}
|
||||
|
||||
type whereClauseNormalizer struct {
|
||||
variables map[string]qbtypes.VariableItem
|
||||
conditions []WhereClauseCondition
|
||||
negated bool
|
||||
orDepth int
|
||||
errors []string
|
||||
}
|
||||
|
||||
func NormalizeWhereClause(expression string, variables map[string]qbtypes.VariableItem) (*NormalizedWhereClause, error) {
|
||||
input := antlr.NewInputStream(expression)
|
||||
lexer := grammar.NewFilterQueryLexer(input)
|
||||
|
||||
lexerErrorListener := NewErrorListener()
|
||||
lexer.RemoveErrorListeners()
|
||||
lexer.AddErrorListener(lexerErrorListener)
|
||||
|
||||
tokens := antlr.NewCommonTokenStream(lexer, 0)
|
||||
parser := grammar.NewFilterQueryParser(tokens)
|
||||
|
||||
parserErrorListener := NewErrorListener()
|
||||
parser.RemoveErrorListeners()
|
||||
parser.AddErrorListener(parserErrorListener)
|
||||
|
||||
tree := parser.Query()
|
||||
|
||||
syntaxErrors := append(lexerErrorListener.SyntaxErrors, parserErrorListener.SyntaxErrors...)
|
||||
if len(syntaxErrors) > 0 {
|
||||
combinedErrors := errors.Newf(
|
||||
errors.TypeInvalidInput,
|
||||
errors.CodeInvalidInput,
|
||||
"Found %d syntax errors while parsing the filter expression.",
|
||||
len(syntaxErrors),
|
||||
)
|
||||
additionals := make([]string, 0, len(syntaxErrors))
|
||||
for _, syntaxError := range syntaxErrors {
|
||||
if syntaxError.Error() != "" {
|
||||
additionals = append(additionals, syntaxError.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil, combinedErrors.WithAdditional(additionals...).WithUrl(searchTroubleshootingGuideURL)
|
||||
}
|
||||
|
||||
visitor := &whereClauseNormalizer{
|
||||
variables: variables,
|
||||
conditions: make([]WhereClauseCondition, 0),
|
||||
}
|
||||
part := visitor.visitQuery(tree)
|
||||
|
||||
if len(visitor.errors) > 0 {
|
||||
combinedErrors := errors.Newf(
|
||||
errors.TypeInvalidInput,
|
||||
errors.CodeInvalidInput,
|
||||
"Found %d errors while parsing the filter expression.",
|
||||
len(visitor.errors),
|
||||
)
|
||||
return nil, combinedErrors.WithAdditional(visitor.errors...).WithUrl(searchTroubleshootingGuideURL)
|
||||
}
|
||||
|
||||
if part.skipped {
|
||||
return &NormalizedWhereClause{Expression: "", Conditions: make([]WhereClauseCondition, 0)}, nil
|
||||
}
|
||||
|
||||
sort.Slice(visitor.conditions, func(i, j int) bool {
|
||||
return visitor.conditions[i].sortKey() < visitor.conditions[j].sortKey()
|
||||
})
|
||||
|
||||
return &NormalizedWhereClause{Expression: part.text, Conditions: visitor.conditions}, nil
|
||||
}
|
||||
|
||||
func (condition WhereClauseCondition) sortKey() string {
|
||||
return condition.Key + "|" + condition.Operator + "|" + strings.Join(condition.Values, ",") + "|" + strconv.FormatBool(condition.Negated) + "|" + strconv.FormatBool(condition.TopLevel)
|
||||
}
|
||||
|
||||
func (visitor *whereClauseNormalizer) visitQuery(ctx grammar.IQueryContext) normalizedPart {
|
||||
if ctx.Expression() == nil {
|
||||
return normalizedPart{skipped: true}
|
||||
}
|
||||
|
||||
return visitor.visitOrExpression(ctx.Expression().OrExpression())
|
||||
}
|
||||
|
||||
func (visitor *whereClauseNormalizer) visitOrExpression(ctx grammar.IOrExpressionContext) normalizedPart {
|
||||
andExpressions := ctx.AllAndExpression()
|
||||
if len(andExpressions) > 1 {
|
||||
visitor.orDepth++
|
||||
defer func() { visitor.orDepth-- }()
|
||||
}
|
||||
|
||||
parts := make([]normalizedPart, 0, len(andExpressions))
|
||||
for _, andExpression := range andExpressions {
|
||||
part := visitor.visitAndExpression(andExpression)
|
||||
if part.skipped {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, part)
|
||||
}
|
||||
|
||||
if len(parts) == 0 {
|
||||
return normalizedPart{skipped: true}
|
||||
}
|
||||
|
||||
parts = sortAndDedupeNormalizedParts(parts)
|
||||
if len(parts) == 1 {
|
||||
return parts[0]
|
||||
}
|
||||
|
||||
texts := make([]string, len(parts))
|
||||
for index, part := range parts {
|
||||
texts[index] = part.text
|
||||
}
|
||||
|
||||
return normalizedPart{text: strings.Join(texts, " OR "), join: joinKindOr}
|
||||
}
|
||||
|
||||
func (visitor *whereClauseNormalizer) visitAndExpression(ctx grammar.IAndExpressionContext) normalizedPart {
|
||||
unaryExpressions := ctx.AllUnaryExpression()
|
||||
parts := make([]normalizedPart, 0, len(unaryExpressions))
|
||||
for _, unaryExpression := range unaryExpressions {
|
||||
part := visitor.visitUnaryExpression(unaryExpression)
|
||||
if part.skipped {
|
||||
continue
|
||||
}
|
||||
if part.join == joinKindOr {
|
||||
part = normalizedPart{text: "(" + part.text + ")", join: joinKindNone}
|
||||
}
|
||||
parts = append(parts, part)
|
||||
}
|
||||
|
||||
if len(parts) == 0 {
|
||||
return normalizedPart{skipped: true}
|
||||
}
|
||||
|
||||
parts = sortAndDedupeNormalizedParts(parts)
|
||||
if len(parts) == 1 {
|
||||
return parts[0]
|
||||
}
|
||||
|
||||
texts := make([]string, len(parts))
|
||||
for index, part := range parts {
|
||||
texts[index] = part.text
|
||||
}
|
||||
|
||||
return normalizedPart{text: strings.Join(texts, " AND "), join: joinKindAnd}
|
||||
}
|
||||
|
||||
func (visitor *whereClauseNormalizer) visitUnaryExpression(ctx grammar.IUnaryExpressionContext) normalizedPart {
|
||||
negated := ctx.NOT() != nil
|
||||
if negated {
|
||||
visitor.negated = !visitor.negated
|
||||
}
|
||||
|
||||
part := visitor.visitPrimary(ctx.Primary())
|
||||
|
||||
if negated {
|
||||
visitor.negated = !visitor.negated
|
||||
|
||||
if part.skipped {
|
||||
return part
|
||||
}
|
||||
if part.join != joinKindNone {
|
||||
return normalizedPart{text: "NOT (" + part.text + ")", join: joinKindNone}
|
||||
}
|
||||
return normalizedPart{text: "NOT " + part.text, join: joinKindNone}
|
||||
}
|
||||
|
||||
return part
|
||||
}
|
||||
|
||||
func (visitor *whereClauseNormalizer) visitPrimary(ctx grammar.IPrimaryContext) normalizedPart {
|
||||
if ctx.OrExpression() != nil {
|
||||
return visitor.visitOrExpression(ctx.OrExpression())
|
||||
}
|
||||
if ctx.Comparison() != nil {
|
||||
return visitor.visitComparison(ctx.Comparison())
|
||||
}
|
||||
if ctx.FunctionCall() != nil {
|
||||
return normalizedPart{text: visitor.visitFunctionCall(ctx.FunctionCall())}
|
||||
}
|
||||
if ctx.FullText() != nil {
|
||||
return normalizedPart{text: visitor.visitFullText(ctx.FullText())}
|
||||
}
|
||||
if ctx.Key() != nil {
|
||||
return normalizedPart{text: visitor.fullTextTerm(ctx.Key().GetText())}
|
||||
}
|
||||
if ctx.Value() != nil {
|
||||
value := visitor.normalizeValue(ctx.Value())
|
||||
return normalizedPart{text: visitor.fullTextTerm(value.raw)}
|
||||
}
|
||||
|
||||
return normalizedPart{skipped: true}
|
||||
}
|
||||
|
||||
func (visitor *whereClauseNormalizer) visitComparison(ctx grammar.IComparisonContext) normalizedPart {
|
||||
key := normalizeKeyText(ctx.Key().GetText())
|
||||
|
||||
if ctx.EXISTS() != nil {
|
||||
operator := "EXISTS"
|
||||
if ctx.NOT() != nil {
|
||||
operator = "NOT EXISTS"
|
||||
}
|
||||
visitor.appendCondition(key, operator, nil)
|
||||
return normalizedPart{text: key + " " + operator}
|
||||
}
|
||||
|
||||
if ctx.InClause() != nil {
|
||||
return visitor.visitInComparison(key, "IN", visitor.visitInValues(ctx.InClause().ValueList(), ctx.InClause().Value()))
|
||||
}
|
||||
|
||||
if ctx.NotInClause() != nil {
|
||||
return visitor.visitInComparison(key, "NOT IN", visitor.visitInValues(ctx.NotInClause().ValueList(), ctx.NotInClause().Value()))
|
||||
}
|
||||
|
||||
if ctx.BETWEEN() != nil {
|
||||
operator := "BETWEEN"
|
||||
if ctx.NOT() != nil {
|
||||
operator = "NOT BETWEEN"
|
||||
}
|
||||
|
||||
values := ctx.AllValue()
|
||||
low := visitor.normalizeValue(values[0])
|
||||
high := visitor.normalizeValue(values[1])
|
||||
visitor.appendCondition(key, operator, []string{low.raw, high.raw})
|
||||
return normalizedPart{text: key + " " + operator + " " + low.text + " AND " + high.text}
|
||||
}
|
||||
|
||||
operator := ""
|
||||
switch {
|
||||
case ctx.EQUALS() != nil:
|
||||
operator = "="
|
||||
case ctx.NOT_EQUALS() != nil, ctx.NEQ() != nil:
|
||||
operator = "!="
|
||||
case ctx.LT() != nil:
|
||||
operator = "<"
|
||||
case ctx.LE() != nil:
|
||||
operator = "<="
|
||||
case ctx.GT() != nil:
|
||||
operator = ">"
|
||||
case ctx.GE() != nil:
|
||||
operator = ">="
|
||||
case ctx.LIKE() != nil:
|
||||
operator = "LIKE"
|
||||
case ctx.ILIKE() != nil:
|
||||
operator = "ILIKE"
|
||||
case ctx.REGEXP() != nil:
|
||||
operator = "REGEXP"
|
||||
case ctx.CONTAINS() != nil:
|
||||
operator = "CONTAINS"
|
||||
}
|
||||
if ctx.NOT() != nil {
|
||||
operator = "NOT " + operator
|
||||
}
|
||||
|
||||
value, skipped := visitor.substituteScalarVariable(visitor.normalizeValue(ctx.AllValue()[0]))
|
||||
if skipped {
|
||||
return normalizedPart{skipped: true}
|
||||
}
|
||||
|
||||
visitor.appendCondition(key, operator, []string{value.raw})
|
||||
return normalizedPart{text: key + " " + operator + " " + value.text}
|
||||
}
|
||||
|
||||
func (visitor *whereClauseNormalizer) visitInComparison(key, operator string, values []normalizedValue) normalizedPart {
|
||||
values, skipped := visitor.substituteListVariable(values)
|
||||
if skipped {
|
||||
return normalizedPart{skipped: true}
|
||||
}
|
||||
|
||||
sort.Slice(values, func(i, j int) bool { return values[i].text < values[j].text })
|
||||
|
||||
texts := make([]string, 0, len(values))
|
||||
raws := make([]string, 0, len(values))
|
||||
for index, value := range values {
|
||||
if index > 0 && value.text == values[index-1].text {
|
||||
continue
|
||||
}
|
||||
texts = append(texts, value.text)
|
||||
raws = append(raws, value.raw)
|
||||
}
|
||||
|
||||
visitor.appendCondition(key, operator, raws)
|
||||
return normalizedPart{text: key + " " + operator + " (" + strings.Join(texts, ", ") + ")"}
|
||||
}
|
||||
|
||||
func (visitor *whereClauseNormalizer) visitInValues(valueList grammar.IValueListContext, value grammar.IValueContext) []normalizedValue {
|
||||
values := make([]normalizedValue, 0)
|
||||
if valueList != nil {
|
||||
for _, valueCtx := range valueList.AllValue() {
|
||||
values = append(values, visitor.normalizeValue(valueCtx))
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
return append(values, visitor.normalizeValue(value))
|
||||
}
|
||||
|
||||
func (visitor *whereClauseNormalizer) visitFunctionCall(ctx grammar.IFunctionCallContext) string {
|
||||
functionName := ""
|
||||
switch {
|
||||
case ctx.HAS() != nil:
|
||||
functionName = "has"
|
||||
case ctx.HASANY() != nil:
|
||||
functionName = "hasAny"
|
||||
case ctx.HASALL() != nil:
|
||||
functionName = "hasAll"
|
||||
case ctx.HASTOKEN() != nil:
|
||||
functionName = "hasToken"
|
||||
}
|
||||
|
||||
key := ""
|
||||
texts := make([]string, 0)
|
||||
raws := make([]string, 0)
|
||||
for index, param := range ctx.FunctionParamList().AllFunctionParam() {
|
||||
switch {
|
||||
case param.Key() != nil:
|
||||
keyText := normalizeKeyText(param.Key().GetText())
|
||||
if index == 0 {
|
||||
key = keyText
|
||||
} else {
|
||||
raws = append(raws, keyText)
|
||||
}
|
||||
texts = append(texts, keyText)
|
||||
case param.Value() != nil:
|
||||
value := visitor.normalizeValue(param.Value())
|
||||
texts = append(texts, value.text)
|
||||
raws = append(raws, value.raw)
|
||||
case param.Array() != nil:
|
||||
arrayText, arrayRaws := visitor.visitArray(param.Array())
|
||||
texts = append(texts, arrayText)
|
||||
raws = append(raws, arrayRaws...)
|
||||
}
|
||||
}
|
||||
|
||||
visitor.appendCondition(key, functionName, raws)
|
||||
return functionName + "(" + strings.Join(texts, ", ") + ")"
|
||||
}
|
||||
|
||||
func (visitor *whereClauseNormalizer) visitArray(ctx grammar.IArrayContext) (string, []string) {
|
||||
texts := make([]string, 0)
|
||||
raws := make([]string, 0)
|
||||
for _, valueCtx := range ctx.ValueList().AllValue() {
|
||||
value := visitor.normalizeValue(valueCtx)
|
||||
texts = append(texts, value.text)
|
||||
raws = append(raws, value.raw)
|
||||
}
|
||||
|
||||
return "[" + strings.Join(texts, ", ") + "]", raws
|
||||
}
|
||||
|
||||
func (visitor *whereClauseNormalizer) visitFullText(ctx grammar.IFullTextContext) string {
|
||||
if ctx.QUOTED_TEXT() != nil {
|
||||
return visitor.fullTextTerm(trimQuotes(ctx.QUOTED_TEXT().GetText()))
|
||||
}
|
||||
|
||||
return visitor.fullTextTerm(ctx.FREETEXT().GetText())
|
||||
}
|
||||
|
||||
func (visitor *whereClauseNormalizer) fullTextTerm(term string) string {
|
||||
visitor.appendCondition("", WhereClauseOperatorFullText, []string{term})
|
||||
return quoteValue(term)
|
||||
}
|
||||
|
||||
func (visitor *whereClauseNormalizer) normalizeValue(ctx grammar.IValueContext) normalizedValue {
|
||||
switch {
|
||||
case ctx.QUOTED_TEXT() != nil:
|
||||
raw := trimQuotes(ctx.QUOTED_TEXT().GetText())
|
||||
return normalizedValue{text: quoteValue(raw), raw: raw}
|
||||
case ctx.NUMBER() != nil:
|
||||
text := ctx.NUMBER().GetText()
|
||||
return normalizedValue{text: text, raw: text}
|
||||
case ctx.BOOL() != nil:
|
||||
text := strings.ToLower(ctx.BOOL().GetText())
|
||||
return normalizedValue{text: text, raw: text}
|
||||
default:
|
||||
raw := ctx.KEY().GetText()
|
||||
if strings.HasPrefix(raw, "$") {
|
||||
return normalizedValue{text: raw, raw: raw}
|
||||
}
|
||||
return normalizedValue{text: quoteValue(raw), raw: raw}
|
||||
}
|
||||
}
|
||||
|
||||
func (visitor *whereClauseNormalizer) appendCondition(key, operator string, values []string) {
|
||||
if values == nil {
|
||||
values = make([]string, 0)
|
||||
}
|
||||
|
||||
visitor.conditions = append(visitor.conditions, WhereClauseCondition{
|
||||
Key: key,
|
||||
Operator: operator,
|
||||
Values: values,
|
||||
Negated: visitor.negated,
|
||||
TopLevel: visitor.orDepth == 0 && !visitor.negated,
|
||||
})
|
||||
}
|
||||
|
||||
func (visitor *whereClauseNormalizer) substituteScalarVariable(value normalizedValue) (normalizedValue, bool) {
|
||||
variableItem, ok := visitor.resolveVariable(value.raw)
|
||||
if !ok {
|
||||
return value, false
|
||||
}
|
||||
|
||||
if skipped := visitor.errIfSkippedOrEmpty(variableItem, value.raw); skipped {
|
||||
return normalizedValue{}, true
|
||||
}
|
||||
|
||||
switch variableValues := variableItem.Value.(type) {
|
||||
case []any:
|
||||
return formatVariableValue(variableValues[0]), false
|
||||
case any:
|
||||
return formatVariableValue(variableValues), false
|
||||
}
|
||||
|
||||
return value, false
|
||||
}
|
||||
|
||||
func (visitor *whereClauseNormalizer) substituteListVariable(values []normalizedValue) ([]normalizedValue, bool) {
|
||||
if len(values) != 1 {
|
||||
return values, false
|
||||
}
|
||||
|
||||
variableItem, ok := visitor.resolveVariable(values[0].raw)
|
||||
if !ok {
|
||||
return values, false
|
||||
}
|
||||
|
||||
if skipped := visitor.errIfSkippedOrEmpty(variableItem, values[0].raw); skipped {
|
||||
return nil, true
|
||||
}
|
||||
|
||||
switch variableValues := variableItem.Value.(type) {
|
||||
case []any:
|
||||
substituted := make([]normalizedValue, 0, len(variableValues))
|
||||
for _, variableValue := range variableValues {
|
||||
substituted = append(substituted, formatVariableValue(variableValue))
|
||||
}
|
||||
return substituted, false
|
||||
case any:
|
||||
return []normalizedValue{formatVariableValue(variableValues)}, false
|
||||
}
|
||||
|
||||
return values, false
|
||||
}
|
||||
|
||||
func (visitor *whereClauseNormalizer) errIfSkippedOrEmpty(variableItem qbtypes.VariableItem, raw string) bool {
|
||||
if variableItem.Type == qbtypes.DynamicVariableType {
|
||||
if allValue, ok := variableItem.Value.(string); ok && allValue == "__all__" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if variableValues, ok := variableItem.Value.([]any); ok && len(variableValues) == 0 {
|
||||
visitor.errors = append(visitor.errors, fmt.Sprintf("malformed request payload: variable `%s` used in expression has an empty list value", strings.TrimPrefix(raw, "$")))
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (visitor *whereClauseNormalizer) resolveVariable(raw string) (qbtypes.VariableItem, bool) {
|
||||
if len(visitor.variables) == 0 {
|
||||
return qbtypes.VariableItem{}, false
|
||||
}
|
||||
|
||||
variableItem, ok := visitor.variables[raw]
|
||||
if !ok && len(raw) > 0 {
|
||||
variableItem, ok = visitor.variables[raw[1:]]
|
||||
}
|
||||
|
||||
return variableItem, ok
|
||||
}
|
||||
|
||||
func formatVariableValue(value any) normalizedValue {
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
return normalizedValue{text: quoteValue(typed), raw: typed}
|
||||
case bool:
|
||||
text := strconv.FormatBool(typed)
|
||||
return normalizedValue{text: text, raw: text}
|
||||
default:
|
||||
text := fmt.Sprintf("%v", typed)
|
||||
return normalizedValue{text: text, raw: text}
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeKeyText(keyText string) string {
|
||||
fieldKey := telemetrytypes.GetFieldKeyFromKeyText(keyText)
|
||||
return telemetrytypes.TelemetryFieldKeyToText(&fieldKey)
|
||||
}
|
||||
|
||||
func quoteValue(value string) string {
|
||||
escaped := strings.ReplaceAll(value, `\`, `\\`)
|
||||
escaped = strings.ReplaceAll(escaped, `'`, `\'`)
|
||||
return "'" + escaped + "'"
|
||||
}
|
||||
|
||||
func sortAndDedupeNormalizedParts(parts []normalizedPart) []normalizedPart {
|
||||
sort.Slice(parts, func(i, j int) bool { return parts[i].text < parts[j].text })
|
||||
|
||||
deduped := parts[:0]
|
||||
for index, part := range parts {
|
||||
if index > 0 && part.text == parts[index-1].text {
|
||||
continue
|
||||
}
|
||||
deduped = append(deduped, part)
|
||||
}
|
||||
|
||||
return deduped
|
||||
}
|
||||
@@ -1,421 +0,0 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNormalizeWhereClauseEquivalenceClasses(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
expressions []string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "spacing and keyword case",
|
||||
expressions: []string{
|
||||
"service.name = 'frontend' AND status = 200",
|
||||
"service.name='frontend' and status=200",
|
||||
"service.name = 'frontend' AND status = 200",
|
||||
"service.name = frontend AND status = 200",
|
||||
},
|
||||
expected: "service.name = 'frontend' AND status = 200",
|
||||
},
|
||||
{
|
||||
name: "operand order",
|
||||
expressions: []string{
|
||||
"a = 1 AND b = 2",
|
||||
"b = 2 AND a = 1",
|
||||
},
|
||||
expected: "a = 1 AND b = 2",
|
||||
},
|
||||
{
|
||||
name: "implicit and explicit AND",
|
||||
expressions: []string{
|
||||
"a = 1 b = 2",
|
||||
"a = 1 AND b = 2",
|
||||
},
|
||||
expected: "a = 1 AND b = 2",
|
||||
},
|
||||
{
|
||||
name: "quote styles",
|
||||
expressions: []string{
|
||||
`a = "frontend"`,
|
||||
"a = 'frontend'",
|
||||
},
|
||||
expected: "a = 'frontend'",
|
||||
},
|
||||
{
|
||||
name: "redundant parentheses",
|
||||
expressions: []string{
|
||||
"(a = 1)",
|
||||
"a = 1",
|
||||
"((a = 1))",
|
||||
},
|
||||
expected: "a = 1",
|
||||
},
|
||||
{
|
||||
name: "in clause forms and value order",
|
||||
expressions: []string{
|
||||
"a IN (1, 2)",
|
||||
"a IN [2, 1]",
|
||||
"a in (2, 1, 1)",
|
||||
},
|
||||
expected: "a IN (1, 2)",
|
||||
},
|
||||
{
|
||||
name: "single value in",
|
||||
expressions: []string{
|
||||
"a IN 1",
|
||||
"a IN (1)",
|
||||
"a IN [1]",
|
||||
},
|
||||
expected: "a IN (1)",
|
||||
},
|
||||
{
|
||||
name: "operator aliases",
|
||||
expressions: []string{
|
||||
"a == 1",
|
||||
"a = 1",
|
||||
},
|
||||
expected: "a = 1",
|
||||
},
|
||||
{
|
||||
name: "not equals aliases",
|
||||
expressions: []string{
|
||||
"a <> 1",
|
||||
"a != 1",
|
||||
},
|
||||
expected: "a != 1",
|
||||
},
|
||||
{
|
||||
name: "duplicate siblings",
|
||||
expressions: []string{
|
||||
"a = 1 AND a = 1",
|
||||
"a = 1",
|
||||
},
|
||||
expected: "a = 1",
|
||||
},
|
||||
{
|
||||
name: "grouped or under and",
|
||||
expressions: []string{
|
||||
"a = 1 AND (b = 2 OR c = 3)",
|
||||
"(c = 3 OR b = 2) AND a = 1",
|
||||
},
|
||||
expected: "(b = 2 OR c = 3) AND a = 1",
|
||||
},
|
||||
{
|
||||
name: "exists spellings",
|
||||
expressions: []string{
|
||||
"service.name EXISTS",
|
||||
"service.name exists",
|
||||
"service.name EXIST",
|
||||
},
|
||||
expected: "service.name EXISTS",
|
||||
},
|
||||
{
|
||||
name: "contains spellings",
|
||||
expressions: []string{
|
||||
"body CONTAINS 'error'",
|
||||
"body contain 'error'",
|
||||
},
|
||||
expected: "body CONTAINS 'error'",
|
||||
},
|
||||
{
|
||||
name: "full text term forms",
|
||||
expressions: []string{
|
||||
`"panic"`,
|
||||
"'panic'",
|
||||
"panic",
|
||||
},
|
||||
expected: "'panic'",
|
||||
},
|
||||
{
|
||||
name: "not without parens",
|
||||
expressions: []string{
|
||||
"NOT a = 1",
|
||||
"not (a = 1)",
|
||||
},
|
||||
expected: "NOT a = 1",
|
||||
},
|
||||
{
|
||||
name: "not over grouped or",
|
||||
expressions: []string{
|
||||
"NOT (b = 2 OR a = 1)",
|
||||
"not (a = 1 or b = 2)",
|
||||
},
|
||||
expected: "NOT (a = 1 OR b = 2)",
|
||||
},
|
||||
{
|
||||
name: "function name case",
|
||||
expressions: []string{
|
||||
"HAS(tags, 'x')",
|
||||
"has(tags, 'x')",
|
||||
},
|
||||
expected: "has(tags, 'x')",
|
||||
},
|
||||
{
|
||||
name: "boolean case",
|
||||
expressions: []string{
|
||||
"a = TRUE",
|
||||
"a = true",
|
||||
},
|
||||
expected: "a = true",
|
||||
},
|
||||
{
|
||||
name: "between",
|
||||
expressions: []string{
|
||||
"duration BETWEEN 1 AND 10",
|
||||
"duration between 1 and 10",
|
||||
},
|
||||
expected: "duration BETWEEN 1 AND 10",
|
||||
},
|
||||
{
|
||||
name: "not in",
|
||||
expressions: []string{
|
||||
"a NOT IN (2, 1)",
|
||||
"a not in [1, 2]",
|
||||
},
|
||||
expected: "a NOT IN (1, 2)",
|
||||
},
|
||||
{
|
||||
name: "key with datatype annotation",
|
||||
expressions: []string{
|
||||
"resource.service.name:string = 'x'",
|
||||
},
|
||||
expected: "resource.service.name:string = 'x'",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
for _, expression := range testCase.expressions {
|
||||
canonical, err := NormalizeWhereClause(expression, nil)
|
||||
require.NoError(t, err, "expression %q", expression)
|
||||
assert.Equal(t, testCase.expected, canonical.Expression, "expression %q", expression)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeWhereClauseNonEquivalence(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
left string
|
||||
right string
|
||||
}{
|
||||
{name: "different values", left: "a = 1", right: "a = 2"},
|
||||
{name: "different keys", left: "a = 1", right: "b = 1"},
|
||||
{name: "different operators", left: "a = 1", right: "a != 1"},
|
||||
{name: "no semantic rewrite of not", left: "NOT a = 1", right: "a != 1"},
|
||||
{name: "number literals as authored", left: "a = 1.0", right: "a = 1"},
|
||||
{name: "between bounds are ordered", left: "a BETWEEN 1 AND 10", right: "a BETWEEN 10 AND 1"},
|
||||
{name: "function params are ordered", left: "has(tags, 'x')", right: "has('x', tags)"},
|
||||
{name: "and vs or", left: "a = 1 AND b = 2", right: "a = 1 OR b = 2"},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
left, err := NormalizeWhereClause(testCase.left, nil)
|
||||
require.NoError(t, err)
|
||||
right, err := NormalizeWhereClause(testCase.right, nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, left.Expression, right.Expression)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeWhereClauseAtoms(t *testing.T) {
|
||||
canonical, err := NormalizeWhereClause("NOT (a = 1 OR b IN ('y', 'x')) AND service.name EXISTS AND hasAny(tags, ['p', 'q']) AND \"panic\"", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := []WhereClauseCondition{
|
||||
{Key: "a", Operator: "=", Values: []string{"1"}, Negated: true},
|
||||
{Key: "b", Operator: "IN", Values: []string{"x", "y"}, Negated: true},
|
||||
{Key: "service.name", Operator: "EXISTS", Values: []string{}, Negated: false, TopLevel: true},
|
||||
{Key: "tags", Operator: "hasAny", Values: []string{"p", "q"}, Negated: false, TopLevel: true},
|
||||
{Key: "", Operator: WhereClauseOperatorFullText, Values: []string{"panic"}, Negated: false, TopLevel: true},
|
||||
}
|
||||
assert.ElementsMatch(t, expected, canonical.Conditions)
|
||||
}
|
||||
|
||||
func TestNormalizeWhereClauseTopLevel(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
expression string
|
||||
expected map[string]bool
|
||||
}{
|
||||
{
|
||||
name: "and siblings are top level",
|
||||
expression: "service.name = 'a' AND status = 500",
|
||||
expected: map[string]bool{"service.name": true, "status": true},
|
||||
},
|
||||
{
|
||||
name: "or branches are not top level",
|
||||
expression: "service.name = 'a' OR status = 500",
|
||||
expected: map[string]bool{"service.name": false, "status": false},
|
||||
},
|
||||
{
|
||||
name: "and sibling stays top level next to a grouped or",
|
||||
expression: "service.name = 'a' AND (x = 1 OR y = 2)",
|
||||
expected: map[string]bool{"service.name": true, "x": false, "y": false},
|
||||
},
|
||||
{
|
||||
name: "parenthesized pure and group stays top level",
|
||||
expression: "(service.name = 'a' AND b = 2) AND c = 3",
|
||||
expected: map[string]bool{"service.name": true, "b": true, "c": true},
|
||||
},
|
||||
{
|
||||
name: "negated condition is not top level",
|
||||
expression: "NOT service.name = 'a' AND status = 500",
|
||||
expected: map[string]bool{"service.name": false, "status": true},
|
||||
},
|
||||
{
|
||||
name: "double negation restores top level",
|
||||
expression: "NOT (NOT (service.name = 'a'))",
|
||||
expected: map[string]bool{"service.name": true},
|
||||
},
|
||||
{
|
||||
name: "in condition under and is top level",
|
||||
expression: "service.name IN ('a', 'b') AND x = 1",
|
||||
expected: map[string]bool{"service.name": true, "x": true},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
normalized, err := NormalizeWhereClause(testCase.expression, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
actual := make(map[string]bool)
|
||||
for _, condition := range normalized.Conditions {
|
||||
actual[condition.Key] = condition.TopLevel
|
||||
}
|
||||
assert.Equal(t, testCase.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeWhereClauseEscaping(t *testing.T) {
|
||||
canonical, err := NormalizeWhereClause(`a = "it's fine"`, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `a = 'it\'s fine'`, canonical.Expression)
|
||||
require.Len(t, canonical.Conditions, 1)
|
||||
assert.Equal(t, []string{"it's fine"}, canonical.Conditions[0].Values)
|
||||
|
||||
equivalent, err := NormalizeWhereClause(canonical.Expression, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, canonical.Expression, equivalent.Expression)
|
||||
assert.Equal(t, canonical.Conditions, equivalent.Conditions)
|
||||
}
|
||||
|
||||
func TestNormalizeWhereClauseSyntaxError(t *testing.T) {
|
||||
_, err := NormalizeWhereClause("a = ", nil)
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = NormalizeWhereClause("AND a = 1", nil)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNormalizeWhereClauseIdempotence(t *testing.T) {
|
||||
expressions := []string{
|
||||
"service.name='frontend' and (status = 500 or status=502) not retired k8s.pod.name exists",
|
||||
"a IN [3, 1, 2] AND hasAll(tags, ['a', 'b']) AND body CONTAINS 'x'",
|
||||
"duration BETWEEN 1 AND 10 OR duration > 100",
|
||||
`msg = 'with \'escapes\' and "quotes"'`,
|
||||
}
|
||||
|
||||
for _, expression := range expressions {
|
||||
first, err := NormalizeWhereClause(expression, nil)
|
||||
require.NoError(t, err, "expression %q", expression)
|
||||
|
||||
second, err := NormalizeWhereClause(first.Expression, nil)
|
||||
require.NoError(t, err, "canonical output %q must re-parse", first.Expression)
|
||||
assert.Equal(t, first.Expression, second.Expression, "canonicalization must be idempotent for %q", expression)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeWhereClauseVariables(t *testing.T) {
|
||||
variables := map[string]qbtypes.VariableItem{
|
||||
"service": {Value: "frontend"},
|
||||
"statuses": {Value: []any{float64(502), float64(500)}},
|
||||
"env": {Type: qbtypes.DynamicVariableType, Value: "__all__"},
|
||||
"limit": {Value: float64(100)},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
expression string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "scalar substitution",
|
||||
expression: "service.name = $service",
|
||||
expected: "service.name = 'frontend'",
|
||||
},
|
||||
{
|
||||
name: "array substitution in IN is sorted",
|
||||
expression: "status IN $statuses",
|
||||
expected: "status IN (500, 502)",
|
||||
},
|
||||
{
|
||||
name: "numeric substitution",
|
||||
expression: "duration > $limit",
|
||||
expected: "duration > 100",
|
||||
},
|
||||
{
|
||||
name: "dynamic all prunes the condition",
|
||||
expression: "a = 1 AND deployment.environment IN $env",
|
||||
expected: "a = 1",
|
||||
},
|
||||
{
|
||||
name: "unknown variable stays a token",
|
||||
expression: "service.name = $unknown",
|
||||
expected: "service.name = $unknown",
|
||||
},
|
||||
{
|
||||
name: "substituted forms hash-equal to concrete forms",
|
||||
expression: "status IN (502, 500) AND service.name = 'frontend'",
|
||||
expected: "service.name = 'frontend' AND status IN (500, 502)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
normalized, err := NormalizeWhereClause(testCase.expression, variables)
|
||||
require.NoError(t, err, "expression %q", testCase.expression)
|
||||
assert.Equal(t, testCase.expected, normalized.Expression)
|
||||
})
|
||||
}
|
||||
|
||||
substituted, err := NormalizeWhereClause("service.name = $service AND status IN $statuses", variables)
|
||||
require.NoError(t, err)
|
||||
concrete, err := NormalizeWhereClause("status IN (500, 502) AND service.name = 'frontend'", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, concrete.Expression, substituted.Expression)
|
||||
assert.Equal(t, concrete.Conditions, substituted.Conditions)
|
||||
}
|
||||
|
||||
func TestNormalizeWhereClauseVariablesFullyPruned(t *testing.T) {
|
||||
variables := map[string]qbtypes.VariableItem{
|
||||
"env": {Type: qbtypes.DynamicVariableType, Value: "__all__"},
|
||||
}
|
||||
|
||||
normalized, err := NormalizeWhereClause("deployment.environment IN $env", variables)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", normalized.Expression)
|
||||
assert.Empty(t, normalized.Conditions)
|
||||
}
|
||||
|
||||
func TestNormalizeWhereClauseVariablesEmptyList(t *testing.T) {
|
||||
variables := map[string]qbtypes.VariableItem{
|
||||
"statuses": {Value: []any{}},
|
||||
}
|
||||
|
||||
_, err := NormalizeWhereClause("status IN $statuses", variables)
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -50,13 +50,13 @@ func (m *MockSQLRuleStore) EditRule(ctx context.Context, rule *ruletypes.Storabl
|
||||
}
|
||||
|
||||
// DeleteRule implements ruletypes.RuleStore - delegates to underlying ruleStore to trigger SQL.
|
||||
func (m *MockSQLRuleStore) DeleteRule(ctx context.Context, orgID valuer.UUID, id valuer.UUID, fn func(context.Context) error) error {
|
||||
return m.ruleStore.DeleteRule(ctx, orgID, id, fn)
|
||||
func (m *MockSQLRuleStore) DeleteRule(ctx context.Context, id valuer.UUID, fn func(context.Context) error) error {
|
||||
return m.ruleStore.DeleteRule(ctx, id, fn)
|
||||
}
|
||||
|
||||
// GetStoredRule implements ruletypes.RuleStore - delegates to underlying ruleStore to trigger SQL.
|
||||
func (m *MockSQLRuleStore) GetStoredRule(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*ruletypes.StorableRule, error) {
|
||||
return m.ruleStore.GetStoredRule(ctx, orgID, id)
|
||||
func (m *MockSQLRuleStore) GetStoredRule(ctx context.Context, id valuer.UUID) (*ruletypes.StorableRule, error) {
|
||||
return m.ruleStore.GetStoredRule(ctx, id)
|
||||
}
|
||||
|
||||
// GetStoredRules implements ruletypes.RuleStore - delegates to underlying ruleStore to trigger SQL.
|
||||
@@ -82,14 +82,14 @@ func (m *MockSQLRuleStore) ExpectCreateRule(rule *ruletypes.StorableRule) {
|
||||
|
||||
// ExpectEditRule sets up SQL expectations for EditRule operation.
|
||||
func (m *MockSQLRuleStore) ExpectEditRule(rule *ruletypes.StorableRule) {
|
||||
expectedPattern := `UPDATE "rule".+` + rule.UpdatedBy + `.+` + rule.OrgID + `.+WHERE \(org_id = '` + rule.OrgID + `'\) AND \(id = '` + rule.ID.StringValue() + `'\)`
|
||||
expectedPattern := `UPDATE "rule".+` + rule.UpdatedBy + `.+` + rule.OrgID + `.+WHERE \(id = '` + rule.ID.StringValue() + `'\)`
|
||||
m.mock.ExpectExec(expectedPattern).
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
}
|
||||
|
||||
// ExpectDeleteRule sets up SQL expectations for DeleteRule operation.
|
||||
func (m *MockSQLRuleStore) ExpectDeleteRule(ruleID valuer.UUID) {
|
||||
expectedPattern := `DELETE FROM "rule".+WHERE \(org_id = '.+'\) AND \(id = '` + ruleID.StringValue() + `'\)`
|
||||
expectedPattern := `DELETE FROM "rule".+WHERE \(id = '` + ruleID.StringValue() + `'\)`
|
||||
m.mock.ExpectExec(expectedPattern).
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
}
|
||||
@@ -98,7 +98,7 @@ func (m *MockSQLRuleStore) ExpectDeleteRule(ruleID valuer.UUID) {
|
||||
func (m *MockSQLRuleStore) ExpectGetStoredRule(ruleID valuer.UUID, rule *ruletypes.StorableRule) {
|
||||
rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "created_by", "updated_by", "deleted", "data", "org_id"}).
|
||||
AddRow(rule.ID, rule.CreatedAt, rule.UpdatedAt, rule.CreatedBy, rule.UpdatedBy, rule.Deleted, rule.Data, rule.OrgID)
|
||||
expectedPattern := `SELECT (.+) FROM "rule".+WHERE \(org_id = '.+'\) AND \(id = '` + ruleID.StringValue() + `'\)`
|
||||
expectedPattern := `SELECT (.+) FROM "rule".+WHERE \(id = '` + ruleID.StringValue() + `'\)`
|
||||
m.mock.ExpectQuery(expectedPattern).
|
||||
WillReturnRows(rows)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@ func (r *rule) EditRule(ctx context.Context, storedRule *ruletypes.StorableRule,
|
||||
BunDBCtx(ctx).
|
||||
NewUpdate().
|
||||
Model(storedRule).
|
||||
Where("org_id = ?", storedRule.OrgID).
|
||||
Where("id = ?", storedRule.ID.StringValue()).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
@@ -68,13 +67,12 @@ func (r *rule) EditRule(ctx context.Context, storedRule *ruletypes.StorableRule,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *rule) DeleteRule(ctx context.Context, orgID valuer.UUID, id valuer.UUID, cb func(context.Context) error) error {
|
||||
func (r *rule) DeleteRule(ctx context.Context, id valuer.UUID, cb func(context.Context) error) error {
|
||||
if err := r.sqlstore.RunInTxCtx(ctx, nil, func(ctx context.Context) error {
|
||||
_, err := r.sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewDelete().
|
||||
Model(new(ruletypes.StorableRule)).
|
||||
Where("org_id = ?", orgID.StringValue()).
|
||||
Where("id = ?", id.StringValue()).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
@@ -104,13 +102,12 @@ func (r *rule) GetStoredRules(ctx context.Context, orgID string) ([]*ruletypes.S
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func (r *rule) GetStoredRule(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*ruletypes.StorableRule, error) {
|
||||
func (r *rule) GetStoredRule(ctx context.Context, id valuer.UUID) (*ruletypes.StorableRule, error) {
|
||||
rule := new(ruletypes.StorableRule)
|
||||
err := r.sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(rule).
|
||||
Where("org_id = ?", orgID.StringValue()).
|
||||
Where("id = ?", id.StringValue()).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -219,7 +219,6 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewAddMetricReductionRulesFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewRemoveOrganizationTuplesFactory(sqlstore),
|
||||
sqlmigration.NewAddRoleTransactionGroupsFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewAddTagUniqueIndexFactory(sqlstore, sqlschema),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type addTagUniqueIndex struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
sqlschema sqlschema.SQLSchema
|
||||
}
|
||||
|
||||
func NewAddTagUniqueIndexFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_tag_unique_index"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &addTagUniqueIndex{
|
||||
sqlstore: sqlstore,
|
||||
sqlschema: sqlschema,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *addTagUniqueIndex) Register(migrations *migrate.Migrations) error {
|
||||
return migrations.Register(migration.Up, migration.Down)
|
||||
}
|
||||
|
||||
func (migration *addTagUniqueIndex) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
sqls := migration.sqlschema.Operator().CreateIndex(
|
||||
&sqlschema.UniqueIndexWithExpressions{
|
||||
TableName: "tag",
|
||||
Expressions: []string{"org_id", "kind", "LOWER(key)", "LOWER(value)"},
|
||||
},
|
||||
)
|
||||
|
||||
for _, sql := range sqls {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (migration *addTagUniqueIndex) Down(_ context.Context, _ *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -55,13 +55,6 @@ func OneID(extractor ResourceIDExtractor) ResourceIDsExtractor {
|
||||
}}
|
||||
}
|
||||
|
||||
type ResourceWithID struct {
|
||||
Resource Resource
|
||||
ID string
|
||||
}
|
||||
|
||||
type ResourceExtractor func(ExtractorContext) ([]ResourceWithID, error)
|
||||
|
||||
func PathParam(name string) ResourceIDExtractor {
|
||||
return ResourceIDExtractor{Phase: PhaseRequest, Fn: func(ec ExtractorContext) (string, error) {
|
||||
if ec.Request == nil {
|
||||
|
||||
@@ -66,7 +66,7 @@ func MustNewObjectFromString(input string) *Object {
|
||||
return &Object{Resource: resource, Selector: typed.MustSelector(orgParts[1])}
|
||||
}
|
||||
|
||||
parts := strings.SplitN(input, "/", 4)
|
||||
parts := strings.Split(input, "/")
|
||||
if len(parts) != 4 {
|
||||
panic(errors.Newf(errors.TypeInternal, errors.CodeInternal, "invalid input format: %s", input))
|
||||
}
|
||||
|
||||
@@ -23,5 +23,5 @@ var (
|
||||
TypeRole = Type{valuer.NewString("role"), regexp.MustCompile(`^([a-z-]{1,50}|\*)$`), []Verb{VerbAssignee, VerbCreate, VerbList, VerbRead, VerbUpdate, VerbDelete, VerbAttach, VerbDetach}}
|
||||
TypeOrganization = Type{valuer.NewString("organization"), regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`), []Verb{VerbRead, VerbUpdate}}
|
||||
TypeMetaResource = Type{valuer.NewString("metaresource"), regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`), []Verb{VerbCreate, VerbList, VerbRead, VerbUpdate, VerbDelete, VerbAttach, VerbDetach}}
|
||||
TypeTelemetryResource = Type{valuer.NewString("telemetryresource"), regexp.MustCompile(`^(\*|[a-z_]{1,32}(/(\*|[A-Za-z0-9._%-]{1,128})){0,2})$`), []Verb{VerbRead}}
|
||||
TypeTelemetryResource = Type{valuer.NewString("telemetryresource"), regexp.MustCompile(`^\*$`), []Verb{VerbRead}}
|
||||
)
|
||||
|
||||
@@ -30,19 +30,6 @@ func NewResolvedResource(
|
||||
return resolved
|
||||
}
|
||||
|
||||
func NewResolvedResourceWithID(verb Verb, category ActionCategory, resource Resource, id string, selector SelectorFunc) ResolvedResource {
|
||||
resolved := &resolvedResource{verb: verb, category: category, resource: resource, selector: selector}
|
||||
if id != "" {
|
||||
resolved.ids = []string{id}
|
||||
}
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
||||
func NewResolvedResourceWithError(verb Verb, category ActionCategory, err error) ResolvedResource {
|
||||
return &resolvedResource{verb: verb, category: category, err: err}
|
||||
}
|
||||
|
||||
func (resolved *resolvedResource) fill(phase ExtractPhase, ec ExtractorContext) {
|
||||
if !resolved.idExtractor.IsPhase(phase) {
|
||||
return
|
||||
|
||||
@@ -76,9 +76,9 @@ type PostableRule struct {
|
||||
}
|
||||
|
||||
type NotificationSettings struct {
|
||||
GroupBy []string `json:"groupBy,omitempty"`
|
||||
Renotify *Renotify `json:"renotify,omitempty"`
|
||||
UsePolicy bool `json:"usePolicy,omitempty"`
|
||||
GroupBy []string `json:"groupBy,omitempty"`
|
||||
Renotify Renotify `json:"renotify,omitzero"`
|
||||
UsePolicy bool `json:"usePolicy,omitempty"`
|
||||
// NewGroupEvalDelay is the grace period for new series to be excluded from alerts evaluation
|
||||
NewGroupEvalDelay valuer.TextDuration `json:"newGroupEvalDelay,omitzero"`
|
||||
}
|
||||
@@ -92,7 +92,7 @@ type Renotify struct {
|
||||
func (ns *NotificationSettings) GetAlertManagerNotificationConfig() alertmanagertypes.NotificationConfig {
|
||||
var renotifyInterval time.Duration
|
||||
var noDataRenotifyInterval time.Duration
|
||||
if ns.Renotify != nil && ns.Renotify.Enabled {
|
||||
if ns.Renotify.Enabled {
|
||||
if slices.Contains(ns.Renotify.AlertStates, StateNoData) {
|
||||
noDataRenotifyInterval = ns.Renotify.ReNotifyInterval.Duration()
|
||||
}
|
||||
@@ -204,12 +204,10 @@ func (ns *NotificationSettings) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
|
||||
// Validate states after unmarshaling
|
||||
if ns.Renotify != nil {
|
||||
for _, state := range ns.Renotify.AlertStates {
|
||||
if state != StateFiring && state != StateNoData {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid alert state: %s", state)
|
||||
for _, state := range ns.Renotify.AlertStates {
|
||||
if state != StateFiring && state != StateNoData {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid alert state: %s", state)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -222,11 +220,6 @@ func (r *PostableRule) processRuleDefaults() {
|
||||
r.SchemaVersion = DefaultSchemaVersion
|
||||
}
|
||||
|
||||
// TODO(srikanthccv): remove as this is now a legacy field
|
||||
if r.Version == "" {
|
||||
r.Version = "v5"
|
||||
}
|
||||
|
||||
// v2alpha1 uses the Evaluation envelope for window/frequency;
|
||||
// only default top-level fields for v1.
|
||||
if r.SchemaVersion != SchemaVersionV2Alpha1 {
|
||||
@@ -278,7 +271,7 @@ func (r *PostableRule) processRuleDefaults() {
|
||||
r.RuleCondition.Thresholds = &thresholdData
|
||||
r.Evaluation = &EvaluationEnvelope{RollingEvaluation, RollingWindow{EvalWindow: r.EvalWindow, Frequency: r.Frequency}}
|
||||
r.NotificationSettings = &NotificationSettings{
|
||||
Renotify: &Renotify{
|
||||
Renotify: Renotify{
|
||||
Enabled: true,
|
||||
ReNotifyInterval: valuer.MustParseTextDuration("4h"),
|
||||
AlertStates: []AlertState{StateFiring},
|
||||
@@ -564,7 +557,7 @@ func (r *PostableRule) validateV2Alpha1() []error {
|
||||
errs = append(errs, errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"notificationSettings: field is required for schemaVersion %q", SchemaVersionV2Alpha1))
|
||||
} else {
|
||||
if r.NotificationSettings.Renotify != nil && r.NotificationSettings.Renotify.Enabled && !r.NotificationSettings.Renotify.ReNotifyInterval.IsPositive() {
|
||||
if r.NotificationSettings.Renotify.Enabled && !r.NotificationSettings.Renotify.ReNotifyInterval.IsPositive() {
|
||||
errs = append(errs, errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"notificationSettings.renotify.interval: must be a positive duration when renotify is enabled"))
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
@@ -1328,27 +1327,3 @@ func TestAnomalyNegationEval(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionDefaultsToV5(t *testing.T) {
|
||||
content := `{
|
||||
"alert": "cpu high",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"ruleType": "threshold_rule",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": {
|
||||
"compositeQuery": {
|
||||
"queries": [{"type": "promql", "spec": {"name": "A", "query": "up"}}],
|
||||
"panelType": "graph",
|
||||
"queryType": "promql"
|
||||
},
|
||||
"thresholds": {"kind": "basic", "spec": [{"name": "critical", "target": 90, "matchType": "1", "op": "1"}]}
|
||||
},
|
||||
"evaluation": {"kind": "rolling", "spec": {"evalWindow": "5m", "frequency": "1m"}},
|
||||
"notificationSettings": {"usePolicy": false}
|
||||
}`
|
||||
|
||||
rule := PostableRule{}
|
||||
require.NoError(t, json.Unmarshal([]byte(content), &rule))
|
||||
assert.Equal(t, "v5", rule.Version)
|
||||
assert.NoError(t, rule.Validate())
|
||||
}
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
package ruletypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func readBody(t *testing.T, stored string) map[string]json.RawMessage {
|
||||
t.Helper()
|
||||
g := GettableRule{}
|
||||
require.NoError(t, json.Unmarshal([]byte(stored), &g))
|
||||
out, err := json.Marshal(NewRule(&g))
|
||||
require.NoError(t, err)
|
||||
var body map[string]json.RawMessage
|
||||
require.NoError(t, json.Unmarshal(out, &body))
|
||||
return body
|
||||
}
|
||||
|
||||
func TestV2RoundTripRolling(t *testing.T) {
|
||||
thresholds := `{
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{"name": "critical", "target": 90.5, "targetUnit": "%", "recoveryTarget": 80.5, "matchType": "at_least_once", "op": "above", "channels": ["slack-critical"]},
|
||||
{"name": "warning", "target": 75, "targetUnit": "%", "matchType": "at_least_once", "op": "above", "channels": ["slack-warnings", "email-oncall"]}
|
||||
]
|
||||
}`
|
||||
thresholdsEchoed := `{
|
||||
"kind": "basic",
|
||||
"spec": [
|
||||
{"name": "critical", "target": 90.5, "targetUnit": "%", "recoveryTarget": 80.5, "matchType": "at_least_once", "op": "above", "channels": ["slack-critical"]},
|
||||
{"name": "warning", "target": 75, "targetUnit": "%", "recoveryTarget": null, "matchType": "at_least_once", "op": "above", "channels": ["slack-warnings", "email-oncall"]}
|
||||
]
|
||||
}`
|
||||
evaluation := `{"kind": "rolling", "spec": {"evalWindow": "90m", "frequency": "90s"}}`
|
||||
notificationSettings := `{
|
||||
"groupBy": ["service.name", "deployment.environment"],
|
||||
"renotify": {"enabled": true, "interval": "45m", "alertStates": ["firing", "nodata"]},
|
||||
"usePolicy": true,
|
||||
"newGroupEvalDelay": "10m"
|
||||
}`
|
||||
labels := `{"team": "infra", "severity": "critical"}`
|
||||
annotations := `{"summary": "CPU above {{$threshold}}", "description": "value {{$value}}"}`
|
||||
|
||||
stored := `{
|
||||
"alert": "cpu high",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"description": "watches cpu",
|
||||
"ruleType": "promql_rule",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"version": "v5",
|
||||
"disabled": true,
|
||||
"labels": ` + labels + `,
|
||||
"annotations": ` + annotations + `,
|
||||
"condition": {
|
||||
"compositeQuery": {
|
||||
"queries": [{"type": "promql", "spec": {"name": "A", "query": "avg(cpu_usage)"}}],
|
||||
"panelType": "graph",
|
||||
"queryType": "promql",
|
||||
"unit": "percent"
|
||||
},
|
||||
"selectedQueryName": "A",
|
||||
"alertOnAbsent": true,
|
||||
"absentFor": 10,
|
||||
"requireMinPoints": true,
|
||||
"requiredNumPoints": 4,
|
||||
"thresholds": ` + thresholds + `
|
||||
},
|
||||
"evaluation": ` + evaluation + `,
|
||||
"notificationSettings": ` + notificationSettings + `
|
||||
}`
|
||||
|
||||
body := readBody(t, stored)
|
||||
|
||||
assert.JSONEq(t, `"cpu high"`, string(body["alert"]))
|
||||
assert.JSONEq(t, `"METRIC_BASED_ALERT"`, string(body["alertType"]))
|
||||
assert.JSONEq(t, `"watches cpu"`, string(body["description"]))
|
||||
assert.JSONEq(t, `"promql_rule"`, string(body["ruleType"]))
|
||||
assert.JSONEq(t, `"v2alpha1"`, string(body["schemaVersion"]))
|
||||
assert.JSONEq(t, `true`, string(body["disabled"]))
|
||||
assert.JSONEq(t, labels, string(body["labels"]))
|
||||
assert.JSONEq(t, annotations, string(body["annotations"]))
|
||||
assert.JSONEq(t, evaluation, string(body["evaluation"]))
|
||||
assert.JSONEq(t, notificationSettings, string(body["notificationSettings"]))
|
||||
|
||||
var condition map[string]json.RawMessage
|
||||
require.NoError(t, json.Unmarshal(body["condition"], &condition))
|
||||
assert.JSONEq(t, thresholdsEchoed, string(condition["thresholds"]))
|
||||
assert.JSONEq(t, `"A"`, string(condition["selectedQueryName"]))
|
||||
assert.JSONEq(t, `true`, string(condition["alertOnAbsent"]))
|
||||
assert.JSONEq(t, `10`, string(condition["absentFor"]))
|
||||
assert.JSONEq(t, `true`, string(condition["requireMinPoints"]))
|
||||
assert.JSONEq(t, `4`, string(condition["requiredNumPoints"]))
|
||||
}
|
||||
|
||||
func TestV2RoundTripCumulative(t *testing.T) {
|
||||
evaluation := `{
|
||||
"kind": "cumulative",
|
||||
"spec": {
|
||||
"schedule": {"type": "daily", "minute": 30, "hour": 9},
|
||||
"frequency": "5m",
|
||||
"timezone": "America/New_York"
|
||||
}
|
||||
}`
|
||||
|
||||
stored := `{
|
||||
"alert": "daily budget",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"ruleType": "threshold_rule",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": {
|
||||
"compositeQuery": {
|
||||
"queries": [{"type": "promql", "spec": {"name": "A", "query": "sum(cost)"}}],
|
||||
"panelType": "graph",
|
||||
"queryType": "promql"
|
||||
},
|
||||
"thresholds": {"kind": "basic", "spec": [{"name": "critical", "target": 100, "matchType": "at_least_once", "op": "above"}]}
|
||||
},
|
||||
"evaluation": ` + evaluation + `,
|
||||
"notificationSettings": {"usePolicy": false}
|
||||
}`
|
||||
|
||||
body := readBody(t, stored)
|
||||
assert.JSONEq(t, evaluation, string(body["evaluation"]))
|
||||
}
|
||||
|
||||
func TestV2MinimalReadShape(t *testing.T) {
|
||||
stored := `{
|
||||
"alert": "minimal",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"ruleType": "threshold_rule",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": {
|
||||
"compositeQuery": {
|
||||
"queries": [{"type": "promql", "spec": {"name": "A", "query": "up"}}],
|
||||
"panelType": "graph",
|
||||
"queryType": "promql"
|
||||
},
|
||||
"thresholds": {"kind": "basic", "spec": [{"name": "critical", "target": 90, "matchType": "at_least_once", "op": "above"}]}
|
||||
},
|
||||
"evaluation": {"kind": "rolling", "spec": {"evalWindow": "5m", "frequency": "1m"}},
|
||||
"notificationSettings": {"usePolicy": false}
|
||||
}`
|
||||
|
||||
body := readBody(t, stored)
|
||||
|
||||
for _, field := range []string{"labels", "annotations", "description", "preferredChannels", "evalWindow", "frequency"} {
|
||||
assert.NotContains(t, body, field)
|
||||
}
|
||||
|
||||
var ns map[string]json.RawMessage
|
||||
require.NoError(t, json.Unmarshal(body["notificationSettings"], &ns))
|
||||
for _, field := range []string{"renotify", "groupBy", "newGroupEvalDelay", "usePolicy"} {
|
||||
assert.NotContains(t, ns, field, "notificationSettings.%s", field)
|
||||
}
|
||||
|
||||
assert.JSONEq(t, `false`, string(body["disabled"]))
|
||||
assert.JSONEq(t, `"v5"`, string(body["version"]))
|
||||
|
||||
var condition struct {
|
||||
Thresholds struct {
|
||||
Spec []map[string]json.RawMessage `json:"spec"`
|
||||
} `json:"thresholds"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(body["condition"], &condition))
|
||||
require.Len(t, condition.Thresholds.Spec, 1)
|
||||
spec := condition.Thresholds.Spec[0]
|
||||
assert.JSONEq(t, `""`, string(spec["targetUnit"]))
|
||||
assert.JSONEq(t, `null`, string(spec["channels"]))
|
||||
assert.JSONEq(t, `null`, string(spec["recoveryTarget"]))
|
||||
}
|
||||
|
||||
func TestRenotifyRoundTrip(t *testing.T) {
|
||||
base := `{
|
||||
"alert": "cpu high",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"ruleType": "threshold_rule",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": {
|
||||
"compositeQuery": {
|
||||
"queries": [{"type": "promql", "spec": {"name": "A", "query": "up"}}],
|
||||
"panelType": "graph",
|
||||
"queryType": "promql"
|
||||
},
|
||||
"thresholds": {"kind": "basic", "spec": [{"name": "critical", "target": 90, "matchType": "at_least_once", "op": "above"}]}
|
||||
},
|
||||
"evaluation": {"kind": "rolling", "spec": {"evalWindow": "5m", "frequency": "1m"}},
|
||||
"notificationSettings": %s
|
||||
}`
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
settings string
|
||||
wantRenotify string
|
||||
}{
|
||||
{
|
||||
name: "absent renotify stays absent",
|
||||
settings: `{"usePolicy": false}`,
|
||||
},
|
||||
{
|
||||
name: "explicitly disabled renotify is echoed",
|
||||
settings: `{"renotify": {"enabled": false}}`,
|
||||
wantRenotify: `{"enabled": false}`,
|
||||
},
|
||||
{
|
||||
name: "enabled renotify with states is echoed",
|
||||
settings: `{"renotify": {"enabled": true, "interval": "30m", "alertStates": ["firing"]}}`,
|
||||
wantRenotify: `{"enabled": true, "interval": "30m", "alertStates": ["firing"]}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
body := readBody(t, strings.Replace(base, "%s", tc.settings, 1))
|
||||
var ns map[string]json.RawMessage
|
||||
require.NoError(t, json.Unmarshal(body["notificationSettings"], &ns))
|
||||
if tc.wantRenotify == "" {
|
||||
assert.NotContains(t, ns, "renotify")
|
||||
} else {
|
||||
assert.JSONEq(t, tc.wantRenotify, string(ns["renotify"]))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchMergePreservesUnpatchedFields(t *testing.T) {
|
||||
stored := `{
|
||||
"alert": "cpu high",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"ruleType": "threshold_rule",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": {
|
||||
"compositeQuery": {
|
||||
"queries": [{"type": "promql", "spec": {"name": "A", "query": "up"}}],
|
||||
"panelType": "graph",
|
||||
"queryType": "promql"
|
||||
},
|
||||
"thresholds": {"kind": "basic", "spec": [{"name": "critical", "target": 90, "matchType": "at_least_once", "op": "above"}]}
|
||||
},
|
||||
"evaluation": {"kind": "rolling", "spec": {"evalWindow": "5m", "frequency": "1m"}},
|
||||
"notificationSettings": {"renotify": {"enabled": true, "interval": "30m", "alertStates": ["firing"]}}
|
||||
}`
|
||||
|
||||
rule := PostableRule{}
|
||||
require.NoError(t, json.Unmarshal([]byte(stored), &rule))
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"disabled": true}`), &rule))
|
||||
require.NoError(t, rule.Validate())
|
||||
|
||||
assert.True(t, rule.Disabled)
|
||||
assert.NotNil(t, rule.RuleCondition.Thresholds)
|
||||
assert.NotNil(t, rule.Evaluation)
|
||||
require.NotNil(t, rule.NotificationSettings)
|
||||
require.NotNil(t, rule.NotificationSettings.Renotify)
|
||||
assert.True(t, rule.NotificationSettings.Renotify.Enabled)
|
||||
}
|
||||
@@ -56,8 +56,8 @@ type RuleAlert struct {
|
||||
type RuleStore interface {
|
||||
CreateRule(context.Context, *StorableRule, func(context.Context, valuer.UUID) error) (valuer.UUID, error)
|
||||
EditRule(context.Context, *StorableRule, func(context.Context) error) error
|
||||
DeleteRule(context.Context, valuer.UUID, valuer.UUID, func(context.Context) error) error
|
||||
DeleteRule(context.Context, valuer.UUID, func(context.Context) error) error
|
||||
GetStoredRules(context.Context, string) ([]*StorableRule, error)
|
||||
GetStoredRule(context.Context, valuer.UUID, valuer.UUID) (*StorableRule, error)
|
||||
GetStoredRule(context.Context, valuer.UUID) (*StorableRule, error)
|
||||
GetStoredRulesByMetricName(context.Context, string, string) ([]RuleAlert, error)
|
||||
}
|
||||
|
||||
@@ -169,10 +169,12 @@ func TestValidate_PostableRule_Common(t *testing.T) {
|
||||
errSubstr: "alert",
|
||||
},
|
||||
|
||||
// only "v5" is allowed; missing/empty defaults to "v5"
|
||||
// only "v5" is allowed
|
||||
{
|
||||
name: "missing version defaults to v5",
|
||||
json: removeField(validV1Builder(), "version"),
|
||||
name: "missing version",
|
||||
json: removeField(validV1Builder(), "version"),
|
||||
wantErr: true,
|
||||
errSubstr: "version",
|
||||
},
|
||||
{
|
||||
name: "wrong version v4",
|
||||
@@ -187,8 +189,10 @@ func TestValidate_PostableRule_Common(t *testing.T) {
|
||||
errSubstr: "version",
|
||||
},
|
||||
{
|
||||
name: "empty version defaults to v5",
|
||||
json: patchJSON(validV1Builder(), `{"version": ""}`),
|
||||
name: "empty version",
|
||||
json: patchJSON(validV1Builder(), `{"version": ""}`),
|
||||
wantErr: true,
|
||||
errSubstr: "version",
|
||||
},
|
||||
|
||||
// alert type, capital case to avoid breaking changes
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user