Compare commits

..

2 Commits

Author SHA1 Message Date
Abhi Kumar
f5c48c1b66 test(dashboard-v2): e2e for panel actions, controls, drilldown and the View modal
Covers what a reader does to a panel on the dashboard, without opening the
editor: the actions menu (clone, delete, move to section, download, Create
Alerts), the header search and column resize on Table/List, List pagination,
the drilldown context menu, the View modal, and grid drag/resize.

The drilldown and View modal specs are the load-bearing ones. Drilldown
asserts the aggregate menu's navigation targets and its variable set/create
paths, which reach into the dashboard spec. The View modal specs assert the
two-way handoff with the editor: both directions carry live unsaved state, so
they check the state arrives AND that nothing was persisted on the way.

Adds a test handle to the column resize grip (ResizableHeader takes the column
key) so a resized column can be identified; no behaviour change.

The editor locators (helpers/panel-editor-v2.ts) land here because the View
modal handoff is their first consumer; the editor specs that follow reuse them.

Specs: actions menu, table and list controls, drilldown, View modal, grid
layout (38 tests).
2026-08-05 18:36:10 +05:30
Abhi Kumar
b475efceee test(dashboard-v2): e2e harness and panel rendering specs
Stands up the shared harness for the V2 dashboard E2E suite and lands the
four panel specs that exercise it end to end.

The harness:
- a `dashboards` fixture that seeds a dashboard over POST /api/v2/dashboards
  and opens it, so specs never build state through the UI
- typed spec builders (helpers/dashboard-v2-spec.ts) for V5 panel specs, so a
  test declares the panel it needs instead of hand-writing envelope JSON
- deterministic query_range mocks, keyed by panel kind, for the assertions
  that need known series rather than whatever the stack happens to hold
- uPlot readers that assert chart state (hidden series, axis bounds, scales)
  instead of pixels

Two supporting changes outside the suite:
- UPlotChart hangs its live uPlot instance off the chart container. A canvas
  exposes nothing assertable from the outside, so this is what lets a test
  read real chart state.
- the V2 specs are removed from playwright.config testIgnore; the V1
  dashboard specs stay ignored until they are ported.

Also tolerates the sidenav_pinned unique-constraint 500 in the auth fixture:
parallel workers log in as the same user and race on that row, and the loser
of the race already has the state it asked for.

Specs: panel rendering, panel states, zoom, legend and tooltip (25 tests).
2026-08-05 18:35:59 +05:30
176 changed files with 6031 additions and 4841 deletions

View File

@@ -6,10 +6,6 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import useUpdatedQuery from 'container/GridCardLayout/useResolveQuery';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import {
applySerializedParams,
serialize,
} from 'lib/compositeQuery/serializer';
import { useNotifications } from 'hooks/useNotifications';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { AppState } from 'store/reducers';
@@ -128,13 +124,15 @@ export function useNavigateToExplorer(): (
});
}
applySerializedParams(serialize(preparedQuery), urlParams);
const JSONCompositeQuery = encodeURIComponent(JSON.stringify(preparedQuery));
const basePath =
dataSource === DataSource.TRACES
? ROUTES.TRACES_EXPLORER
: ROUTES.LOGS_EXPLORER;
const newExplorerPath = `${basePath}?${urlParams.toString()}`;
const newExplorerPath = `${basePath}?${urlParams.toString()}&${
QueryParams.compositeQuery
}=${JSONCompositeQuery}`;
window.open(withBasePath(newExplorerPath), sameTab ? '_self' : '_blank');
},

View File

@@ -6,7 +6,6 @@ import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { VIEWS } from './constants';
import { MouseEventHandler } from 'react';
export type LogDetailProps = {
log: ILog | null;
@@ -17,8 +16,6 @@ export type LogDetailProps = {
logs?: ILog[];
onNavigateLog?: (log: ILog) => void;
onScrollToLog?: (logId: string) => void;
handleOpenInExplorer?: MouseEventHandler;
getContainer?: DrawerProps['getContainer'];
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
Pick<DrawerProps, 'onClose'>;

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useCopyToClipboard } from 'react-use';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useCopyToClipboard, useLocation } from 'react-use';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button } from '@signozhq/ui/button';
import { Drawer, Tooltip } from 'antd';
@@ -13,6 +14,9 @@ import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySear
import { convertExpressionToFilters } from 'components/QueryBuilderV2/utils';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
import InfraMetrics from 'container/LogDetailedView/InfraMetrics/InfraMetrics';
import Overview from 'container/LogDetailedView/Overview';
@@ -27,6 +31,8 @@ import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import createQueryParams from 'lib/createQueryParams';
import { cloneDeep } from 'lodash-es';
import {
ArrowDown,
@@ -44,9 +50,12 @@ import {
} from '@signozhq/icons';
import { JsonView } from 'periscope/components/JsonView';
import { useAppContext } from 'providers/App/App';
import { AppState } from 'store/reducers';
import { ILogBody } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed } from 'utils/app';
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
@@ -66,8 +75,6 @@ function LogDetailInner({
logs,
onNavigateLog,
onScrollToLog,
handleOpenInExplorer,
getContainer,
}: LogDetailInnerProps): JSX.Element {
const initialContextQuery = useInitialQuery(log);
const [contextQuery, setContextQuery] = useState<Query | undefined>(
@@ -84,7 +91,7 @@ function LogDetailInner({
const [filters, setFilters] = useState<TagFilter | null>(null);
const [isEdit, setIsEdit] = useState<boolean>(false);
const { stagedQuery } = useQueryBuilder();
const { stagedQuery, updateAllQueriesOperators } = useQueryBuilder();
// Handle clicks outside to close drawer, except on explicitly ignored regions
useEffect(() => {
@@ -179,6 +186,11 @@ function LogDetailInner({
});
const isDarkMode = useIsDarkMode();
const location = useLocation();
const { safeNavigate } = useSafeNavigate();
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { notifications } = useNotifications();
@@ -234,6 +246,25 @@ function LogDetailInner({
});
};
// Go to logs explorer page with the log data
const handleOpenInExplorer = (e?: React.MouseEvent): void => {
const queryParams = {
[QueryParams.activeLogId]: `"${log?.id}"`,
[QueryParams.startTime]: minTime?.toString() || '',
[QueryParams.endTime]: maxTime?.toString() || '',
[QueryParams.compositeQuery]: JSON.stringify(
updateAllQueriesOperators(
initialQueriesMap[DataSource.LOGS],
PANEL_TYPES.LIST,
DataSource.LOGS,
),
),
};
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`, {
newTab: !!e && isModifierKeyPressed(e),
});
};
const handleQueryExpressionChange = useCallback(
(value: string, queryIndex: number) => {
// update the query at the given index
@@ -302,6 +333,13 @@ function LogDetailInner({
}
};
// Only show when opened from infra monitoring page
const showOpenInExplorerBtn = useMemo(
() => location.pathname?.includes('/infrastructure-monitoring'),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const logType = log?.attributes_string?.log_level || LogType.INFO;
const currentLogIndex = logs ? logs.findIndex((l) => l.id === log.id) : -1;
const isPrevDisabled =
@@ -336,7 +374,6 @@ function LogDetailInner({
width="60%"
mask={false}
maskClosable={false}
getContainer={getContainer}
title={
<div className="log-detail-drawer__title" data-log-detail-ignore="true">
<div className="log-detail-drawer__title-left">
@@ -374,7 +411,7 @@ function LogDetailInner({
/>
</Tooltip>
</div>
{handleOpenInExplorer && (
{showOpenInExplorerBtn && (
<div>
<Button
variant="outlined"

View File

@@ -5,7 +5,6 @@ import { Skeleton } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import {
IQuickFiltersConfig,
QuickFilterChangeEventData,
QuickFiltersSource,
} from 'components/QuickFilters/types';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
@@ -29,12 +28,11 @@ interface ICheckboxProps {
filter: IQuickFiltersConfig;
source: QuickFiltersSource;
onFilterChange?: (query: Query) => void;
onQuickFilterChange?: (data: QuickFilterChangeEventData) => void;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
const { source, filter, onFilterChange, onQuickFilterChange } = props;
const { source, filter, onFilterChange } = props;
const [searchText, setSearchText] = useState<string>('');
const activeQueryIndex = useActiveQueryIndex(source);
@@ -63,7 +61,6 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
attributeValues,
activeQueryIndex,
onFilterChange,
onQuickFilterChange,
});
const setSearchTextDebounced = useDebouncedFn((...args) => {

View File

@@ -1,6 +1,5 @@
import {
IQuickFiltersConfig,
QuickFilterChangeEventData,
QuickFiltersSource,
} from 'components/QuickFilters/types';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@@ -20,7 +19,6 @@ interface UseCheckboxFilterActionsProps {
attributeValues: string[];
activeQueryIndex: number;
onFilterChange?: ((query: Query) => void) | null;
onQuickFilterChange?: (data: QuickFilterChangeEventData) => void;
}
interface UseCheckboxFilterActionsReturn {
@@ -44,7 +42,6 @@ function useCheckboxFilterActions({
attributeValues,
activeQueryIndex,
onFilterChange,
onQuickFilterChange,
}: UseCheckboxFilterActionsProps): UseCheckboxFilterActionsReturn {
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
@@ -63,34 +60,20 @@ function useCheckboxFilterActions({
previousState?: CheckedState,
sectionType?: SectionType,
): void => {
const updatedQuery = applyCheckboxToggle({
currentQuery,
activeQueryIndex,
filter,
source,
attributeValues,
value,
checked,
isOnlyOrAllClicked,
previousState,
sectionType,
});
dispatch(updatedQuery);
if (onQuickFilterChange) {
const queryData = updatedQuery.builder.queryData[activeQueryIndex];
const expression = queryData?.filter?.expression || '';
const filterItemKeys = (queryData?.filters?.items || [])
.map((item) => item.key?.key)
.filter((key): key is string => !!key);
onQuickFilterChange({
filterKey: filter.attributeKey.key,
expression,
filterItemKeys,
});
}
dispatch(
applyCheckboxToggle({
currentQuery,
activeQueryIndex,
filter,
source,
attributeValues,
value,
checked,
isOnlyOrAllClicked,
previousState,
sectionType,
}),
);
};
const onClear = (): void => {

View File

@@ -5,7 +5,6 @@ import { Typography } from '@signozhq/ui/typography';
import { LoaderCircle } from '@signozhq/icons';
import {
IQuickFiltersConfig,
QuickFilterChangeEventData,
QuickFilterCheckboxUseFieldApis,
QuickFiltersSource,
} from 'components/QuickFilters/types';
@@ -34,15 +33,13 @@ interface CheckboxFilterV2Props {
filter: IQuickFiltersConfig;
source: QuickFiltersSource;
onFilterChange?: (query: Query) => void;
onQuickFilterChange?: (data: QuickFilterChangeEventData) => void;
useFieldApis: QuickFilterCheckboxUseFieldApis;
}
export default function CheckboxFilterV2(
props: CheckboxFilterV2Props,
): JSX.Element {
const { source, filter, onFilterChange, onQuickFilterChange, useFieldApis } =
props;
const { source, filter, onFilterChange, useFieldApis } = props;
const [searchText, setSearchText] = useState<string>('');
const [userToggleState, setUserToggleState] = useState<boolean | null>(null);
@@ -106,7 +103,6 @@ export default function CheckboxFilterV2(
attributeValues,
activeQueryIndex,
onFilterChange,
onQuickFilterChange,
});
const setSearchTextDebounced = useDebouncedFn((...args) => {

View File

@@ -49,7 +49,6 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
handleFilterVisibilityChange,
source,
onFilterChange,
onQuickFilterChange,
signal,
showFilterCollapse = true,
showQueryName = true,
@@ -306,7 +305,6 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
source={source}
filter={filter}
onFilterChange={onFilterChange}
onQuickFilterChange={onQuickFilterChange}
useFieldApis={useFieldApis}
/>
) : (
@@ -315,7 +313,6 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
source={source}
filter={filter}
onFilterChange={onFilterChange}
onQuickFilterChange={onQuickFilterChange}
/>
);
case FiltersType.DURATION:
@@ -336,7 +333,6 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
source={source}
filter={filter}
onFilterChange={onFilterChange}
onQuickFilterChange={onQuickFilterChange}
useFieldApis={useFieldApis}
/>
) : (
@@ -345,7 +341,6 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
source={source}
filter={filter}
onFilterChange={onFilterChange}
onQuickFilterChange={onQuickFilterChange}
/>
);
}

View File

@@ -42,18 +42,11 @@ export interface IQuickFiltersConfig {
defaultOpen: boolean;
}
export interface QuickFilterChangeEventData {
filterKey: string;
expression: string;
filterItemKeys: string[];
}
export interface IQuickFiltersProps {
config: IQuickFiltersConfig[];
handleFilterVisibilityChange: () => void;
source: QuickFiltersSource;
onFilterChange?: (query: Query) => void;
onQuickFilterChange?: (data: QuickFilterChangeEventData) => void;
signal?: SignalType;
className?: string;
showFilterCollapse?: boolean;

View File

@@ -1,12 +1,8 @@
import { type ReactNode, useLayoutEffect, useMemo } from 'react';
import { useLayoutEffect, type ReactNode } from 'react';
import { chromePerformanceTanstackTableEndHover } from './perfDevtools';
import { useIsRowHovered } from './TanStackTableStateContext';
import {
TooltipContentProps,
TooltipSimple,
TooltipSimpleProps,
} from '@signozhq/ui/tooltip';
import { TooltipSimple, TooltipSimpleProps } from '@signozhq/ui/tooltip';
export type HoverTooltipProps = Omit<TooltipSimpleProps, 'open'> & {
rowId: string;
@@ -26,28 +22,9 @@ export function TanStackHoverTooltip({
}
}, [isHovered, rowId]);
const tooltipContentProps = useMemo(
() =>
({
onPointerDownOutside: (e): void => {
e.preventDefault();
e.stopPropagation();
},
}) satisfies TooltipContentProps,
[],
);
if (!isHovered) {
return <>{children}</>;
}
return (
<TooltipSimple
delayDuration={700}
tooltipContentProps={tooltipContentProps}
{...tooltipProps}
>
{children}
</TooltipSimple>
);
return <TooltipSimple {...tooltipProps}>{children}</TooltipSimple>;
}

View File

@@ -51,7 +51,6 @@ import { useColumnHandlers } from './useColumnHandlers';
import { useColumnState } from './useColumnState';
import { useEffectiveData } from './useEffectiveData';
import { useFlatItems } from './useFlatItems';
import { useResetScroll } from './useResetScroll';
import { useRowKeyData } from './useRowKeyData';
import { useTableParams } from './useTableParams';
import { buildPageSizeItems, buildTanstackColumnDef } from './utils';
@@ -111,7 +110,6 @@ function TanStackTableInner<TData, TItemKey = string>(
suffixPaginationContent,
enableAlternatingRowColors,
disableVirtualScroll,
resetScrollKey,
}: TanStackTableProps<TData, TItemKey>,
forwardedRef: React.ForwardedRef<TanStackTableHandle>,
): JSX.Element {
@@ -326,8 +324,6 @@ function TanStackTableInner<TData, TItemKey = string>(
});
}, [flatIndexForActiveRow]);
useResetScroll(virtuosoRef, resetScrollKey);
const { sensors, columnIds, handleDragEnd } = useColumnDnd({
columns: effectiveColumns,
onColumnOrderChange: handleColumnOrderChange,

View File

@@ -217,55 +217,6 @@ describe('useColumnState', () => {
});
});
describe('storageKey changes', () => {
it('does not auto-show hidden columns when switching between different storageKeys', () => {
// Regression test: when switching Pods→Nodes→Pods, hidden columns were
// incorrectly shown because prevColumnIdsRef wasn't reset on storageKey change.
// The autoShowNewlyAddedColumns effect would see pod columns as "new" (not in
// node columns) and unhide them.
const KEY_PODS = 'k8s-pods';
const KEY_NODES = 'k8s-nodes';
const podColumns = [
col('podName'),
col('podStatus'),
col('cpu_request', { defaultVisibility: false }),
col('memory_request', { defaultVisibility: false }),
];
const nodeColumns = [
col('nodeName'),
col('nodeStatus'),
col('cpu'),
col('memory'),
];
// Initialize both tables
act(() => {
useColumnStore.getState().initializeFromDefaults(KEY_PODS, podColumns);
useColumnStore.getState().initializeFromDefaults(KEY_NODES, nodeColumns);
});
// Start with pods - cpu_request and memory_request should be hidden
const { result, rerender } = renderHook(
({ storageKey, columns }) => useColumnState({ storageKey, columns }),
{ initialProps: { storageKey: KEY_PODS, columns: podColumns } },
);
expect(result.current.hiddenColumnIds).toContain('cpu_request');
expect(result.current.hiddenColumnIds).toContain('memory_request');
// Switch to nodes
rerender({ storageKey: KEY_NODES, columns: nodeColumns });
// Switch back to pods - hidden columns should STILL be hidden
rerender({ storageKey: KEY_PODS, columns: podColumns });
expect(result.current.hiddenColumnIds).toContain('cpu_request');
expect(result.current.hiddenColumnIds).toContain('memory_request');
});
});
describe('actions', () => {
it('hideColumn hides a column', () => {
const columns = [col('a'), col('b')];

View File

@@ -1,103 +0,0 @@
import { RefObject } from 'react';
import { renderHook } from '@testing-library/react';
import type { TableVirtuosoHandle } from 'react-virtuoso';
import { useResetScroll } from '../useResetScroll';
function createMockRef(scrollTo: jest.Mock): RefObject<TableVirtuosoHandle> {
return {
current: {
scrollToIndex: jest.fn(),
scrollIntoView: jest.fn(),
scrollTo,
scrollBy: jest.fn(),
},
};
}
describe('useResetScroll', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('does not call scrollTo on initial mount', () => {
const scrollTo = jest.fn();
const ref = createMockRef(scrollTo);
renderHook(() => useResetScroll(ref, 'initial-key'));
expect(scrollTo).not.toHaveBeenCalled();
});
it('calls scrollTo when key changes', () => {
const scrollTo = jest.fn();
const ref = createMockRef(scrollTo);
const { rerender } = renderHook(
({ resetKey }) => useResetScroll(ref, resetKey),
{ initialProps: { resetKey: 'key-1' } },
);
expect(scrollTo).not.toHaveBeenCalled();
rerender({ resetKey: 'key-2' });
expect(scrollTo).toHaveBeenCalledWith({ left: 0 });
});
it('calls scrollTo once per key change', () => {
const scrollTo = jest.fn();
const ref = createMockRef(scrollTo);
const { rerender } = renderHook(
({ resetKey }) => useResetScroll(ref, resetKey),
{ initialProps: { resetKey: 'key-1' } },
);
rerender({ resetKey: 'key-2' });
rerender({ resetKey: 'key-3' });
rerender({ resetKey: 'key-4' });
expect(scrollTo).toHaveBeenCalledTimes(3);
});
it('does not call scrollTo when key unchanged', () => {
const scrollTo = jest.fn();
const ref = createMockRef(scrollTo);
const { rerender } = renderHook(
({ resetKey }) => useResetScroll(ref, resetKey),
{ initialProps: { resetKey: 'same-key' } },
);
rerender({ resetKey: 'same-key' });
rerender({ resetKey: 'same-key' });
expect(scrollTo).not.toHaveBeenCalled();
});
it('handles null ref.current gracefully', () => {
const ref: RefObject<TableVirtuosoHandle | null> = { current: null };
const { rerender } = renderHook(
({ resetKey }) => useResetScroll(ref, resetKey),
{ initialProps: { resetKey: 'key-1' } },
);
expect(() => rerender({ resetKey: 'key-2' })).not.toThrow();
});
it('calls scrollTo when changing from undefined to defined', () => {
const scrollTo = jest.fn();
const ref = createMockRef(scrollTo);
const { rerender } = renderHook(
({ resetKey }) => useResetScroll(ref, resetKey),
{ initialProps: { resetKey: undefined as string | undefined } },
);
rerender({ resetKey: 'new-key' });
expect(scrollTo).toHaveBeenCalledWith({ left: 0 });
});
});

View File

@@ -215,16 +215,6 @@ export * from './useTableParams';
* />
* ```
*
* @example Reset scroll on context change — use `resetScrollKey` to scroll back to start
* when the data context changes (e.g., switching between categories or tabs).
* ```tsx
* <TanStackTable
* data={data}
* columns={columns}
* resetScrollKey={selectedCategory}
* />
* ```
*
* @example useTableParams — manages pagination state with URL sync and persistence
*
* The `useTableParams` hook handles page, limit, orderBy, and expanded state. It can sync

View File

@@ -213,8 +213,6 @@ export type TanStackTableProps<TData, TItemKey = string> = {
enableAlternatingRowColors?: boolean;
/** Disable virtual scrolling and render all rows at once. Cannot be used with onEndReached. */
disableVirtualScroll?: boolean;
/** When this value changes, the table's scroll resets to the start. Useful for resetting scroll when the data context changes (e.g., switching categories). */
resetScrollKey?: string;
};
export type TanStackTableHandle = TableVirtuosoHandle & {

View File

@@ -67,13 +67,6 @@ export function useColumnState<TData>({
const columnSizing = useStoreSizing(storageKey ?? '');
const prevColumnIdsRef = useRef<Set<string> | null>(null);
const prevStorageKeyRef = useRef<string | undefined>(storageKey);
// Reset prevColumnIdsRef when storageKey changes to avoid cross-table interference
if (prevStorageKeyRef.current !== storageKey) {
prevColumnIdsRef.current = null;
prevStorageKeyRef.current = storageKey;
}
useEffect(
function autoShowNewlyAddedColumns(): void {

View File

@@ -1,18 +0,0 @@
import { RefObject, useEffect, useRef } from 'react';
import type { TableVirtuosoHandle } from 'react-virtuoso';
export function useResetScroll(
scrollRef: RefObject<TableVirtuosoHandle | null>,
resetKey: string | undefined,
): void {
const prevKeyRef = useRef(resetKey);
useEffect(() => {
if (prevKeyRef.current !== resetKey) {
prevKeyRef.current = resetKey;
scrollRef.current?.scrollTo({
left: 0,
});
}
}, [resetKey, scrollRef]);
}

View File

@@ -1,9 +1,3 @@
import logEvent from 'api/common/logEvent';
import type { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import { getNavigationReferrer } from 'lib/navigation';
import { extractQueryPairs } from 'utils/queryContextUtils';
import { isCustomTimeRange } from 'store/globalTime';
export enum Events {
UPDATE_GRAPH_VISIBILITY_STATE = 'UPDATE_GRAPH_VISIBILITY_STATE',
UPDATE_GRAPH_MANAGER_TABLE = 'UPDATE_GRAPH_MANAGER_TABLE',
@@ -45,155 +39,3 @@ export enum InfraMonitoringEvents {
StatefulSet = 'statefulSet',
Volumes = 'volumes',
}
export function logInfraFilterCustomizedEvent(
entityType: InfraMonitoringEntity,
source: 'quick_filter' | 'search' | 'host_status_toggle',
expression: string,
extraKeys?: string[],
): void {
const expressionKeys = extractQueryPairs(expression?.trim() || '').map(
(pair) => pair.key,
);
if (extraKeys) {
extraKeys.forEach((key) => expressionKeys.push(key));
}
if (expressionKeys.length === 0) {
return;
}
void logEvent('infra_filter_customized', {
entity_type: entityType,
source,
expression_keys: [...new Set(expressionKeys)],
});
}
export function logInfraMonitoringListViewedEvent(
entity: InfraMonitoringEntity,
): void {
const referrer = getNavigationReferrer();
void logEvent('infra_list_viewed', {
entity,
referrer,
});
}
export function logInfraTimeRangeCustomizedEvent(
entityType: InfraMonitoringEntity,
rangeLabel: string,
): void {
void logEvent('infra_time_range_customized', {
entity_type: entityType,
range_label: isCustomTimeRange(rangeLabel) ? 'custom' : rangeLabel,
});
}
export function logInfraColumnCustomizedEvent(
entityType: InfraMonitoringEntity,
columnsList: string[],
fontSize: string,
maxLinesPerRow: number,
source: 'list' | 'expanded',
): void {
void logEvent('infra_column_customized', {
entity_type: entityType,
columns_list: columnsList,
font_size: fontSize,
max_lines_per_row: maxLinesPerRow,
source,
});
}
export function logInfraColumnSortedEvent(
entityType: InfraMonitoringEntity,
columnKey: string,
direction: 'asc' | 'desc',
source: 'list' | 'expanded',
): void {
void logEvent('infra_column_sorted', {
entity_type: entityType,
column_key: columnKey,
direction,
source,
});
}
export function logInfraDrawerTimeRangeCustomizedEvent(
entityType: InfraMonitoringEntity,
rangeLabel: string,
): void {
void logEvent('infra_drawer_time_range_customized', {
entity_type: entityType,
range_label: isCustomTimeRange(rangeLabel) ? 'custom' : rangeLabel,
});
}
export function logInfraDrawerFilterCustomizedEvent(
entityType: InfraMonitoringEntity,
tab: 'metrics' | 'logs' | 'traces' | 'events' | 'pod_metrics',
expression: string,
filterSource: 'search' | 'logs',
): void {
const expressionKeys = extractQueryPairs(expression?.trim() || '').map(
(pair) => pair.key,
);
if (expressionKeys.length === 0) {
return;
}
void logEvent('infra_drawer_filter_customized', {
entity_type: entityType,
tab,
expression_keys: [...new Set(expressionKeys)],
filter_source: filterSource,
});
}
export function logInfraGroupByCustomizedEvent(
entityType: InfraMonitoringEntity,
groupByKeysList: string[],
): void {
void logEvent('infra_group_by_customized', {
entity_type: entityType,
group_by_keys_list: groupByKeysList,
});
}
export function logInfraDrawerTabViewedEvent(
entityType: InfraMonitoringEntity,
tab: string,
isDefaultTab: boolean,
): void {
void logEvent('infra_drawer_tab_viewed', {
entity_type: entityType,
tab,
is_default_tab: isDefaultTab,
});
}
export function logInfraExplorerNavigatedEvent(params: {
entityType: InfraMonitoringEntity;
destination:
| 'metrics_explorer'
| 'logs_explorer'
| 'traces_explorer'
| 'k8s_list';
source: 'chart_compass_icon' | 'tab_cta_button' | 'stats_card';
tab: string;
sourceKey: string | null;
drawerDurationMsAtNavigation: number | null;
}): void {
void logEvent('infra_explorer_navigated', {
entity_type: params.entityType,
destination: params.destination,
source: params.source,
tab: params.tab,
source_key: params.sourceKey,
drawer_duration_ms_at_navigation: params.drawerDurationMsAtNavigation,
});
}

View File

@@ -18,6 +18,7 @@ export enum QueryParams {
q = 'q',
activeLogId = 'activeLogId',
timeRange = 'timeRange',
compositeQuery = 'compositeQuery',
panelTypes = 'panelTypes',
pageSize = 'pageSize',
viewMode = 'viewMode',

View File

@@ -3,7 +3,3 @@ export const DASHBOARD_CACHE_TIME = 30_000;
export const DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED = 0;
export const FIELD_API_CACHE_TIME = 60_000;
// intentionally lower since I only want to prevent refresh in background
// after click in the row
export const INFRA_MONITORING_DETAILS_CACHE_TIME = 1_000;

View File

@@ -1,7 +1,5 @@
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { serialize } from 'lib/compositeQuery/serializer';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { getAutoContexts } from '../getAutoContexts';
@@ -53,8 +51,8 @@ describe('getAutoContexts', () => {
it('includes the query in alert edit context', () => {
const ruleId = 'rule-edit';
const query = { queryType: 'builder', builder: { queryData: [] } };
const serializedParams = serialize(query as unknown as Query);
const search = `?${QueryParams.ruleId}=${ruleId}&${serializedParams.toString()}`;
const compositeQuery = encodeURIComponent(JSON.stringify(query));
const search = `?${QueryParams.ruleId}=${ruleId}&${QueryParams.compositeQuery}=${compositeQuery}`;
const contexts = getAutoContexts(ROUTES.EDIT_ALERTS, search);
@@ -74,8 +72,8 @@ describe('getAutoContexts', () => {
it('includes the query in alert new context (no ruleId)', () => {
const query = { queryType: 'builder', builder: { queryData: [] } };
const serializedParams = serialize(query as unknown as Query);
const search = `?${serializedParams.toString()}`;
const compositeQuery = encodeURIComponent(JSON.stringify(query));
const search = `?${QueryParams.compositeQuery}=${compositeQuery}`;
const contexts = getAutoContexts(ROUTES.ALERTS_NEW, search);
@@ -241,24 +239,4 @@ describe('getAutoContexts', () => {
),
).toStrictEqual([]);
});
it('decodes the serialized composite query into metadata.query', () => {
const query = { builder: { queryData: [] } } as unknown as Query;
const search = `?${serialize(query).toString()}`;
const [context] = getAutoContexts(ROUTES.LOGS_EXPLORER, search);
expect(context.metadata?.query).toStrictEqual(query);
});
it('omits metadata.query when no serialized query is in the URL', () => {
// Detection no longer gates on the `compositeQuery` key — it routes
// through `deserialize`/the adapter list — so non-query params (time
// range, etc.) must not be mistaken for a query.
const search = `?${QueryParams.startTime}=1700000000000&${QueryParams.endTime}=1700003600000`;
const [context] = getAutoContexts(ROUTES.LOGS_EXPLORER, search);
expect(context.metadata).not.toHaveProperty('query');
});
});

View File

@@ -24,7 +24,7 @@ import {
undoExecution,
} from 'api/ai-assistant/chat';
import ROUTES from 'constants/routes';
import { serialize } from 'lib/compositeQuery/serializer';
import { QueryParams } from 'constants/query';
import { openInNewTab } from 'utils/navigation';
import {
ArchiveRestore,
@@ -363,8 +363,8 @@ function applyFilter(action: MessageActionDTO, deps: ApplyFilterDeps): void {
}
// eslint-disable-next-line no-console
console.log('[apply_filter] off-page → history.push', base);
const params = serialize(normalized);
deps.history.push(`${base}?${params.toString()}`);
const encoded = encodeURIComponent(JSON.stringify(normalized));
deps.history.push(`${base}?${QueryParams.compositeQuery}=${encoded}`);
}
/** Picks the right rollback API call for a given action kind. */

View File

@@ -8,7 +8,6 @@ import { getViewById } from 'api/saveView/getViewById';
import ROUTES from 'constants/routes';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { deserialize } from 'lib/compositeQuery/serializer';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { AllViewsProps, ViewProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder';
@@ -219,9 +218,7 @@ describe('buildExplorerNavigationUrl', () => {
);
expect(url).toContain(ROUTES.LOGS_EXPLORER);
const params = new URLSearchParams(new URL(url, 'http://x').search);
expect(deserialize(params)).not.toBeNull();
expect(url).toContain(`${QueryParams.compositeQuery}=`);
expect(url).toContain(`${QueryParams.viewKey}=`);
});
});

View File

@@ -2,10 +2,6 @@ import { getAllViews } from 'api/saveView/getAllViews';
import { getViewById } from 'api/saveView/getViewById';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
applySerializedParams,
serialize,
} from 'lib/compositeQuery/serializer';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { SOURCEPAGE_VS_ROUTES } from 'pages/SaveView/constants';
import { ViewProps } from 'types/api/saveViews/types';
@@ -79,7 +75,10 @@ export function buildExplorerNavigationUrl(
searchParams: Record<string, unknown>,
): string {
const params = new URLSearchParams();
applySerializedParams(serialize(query), params);
params.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(query)),
);
Object.entries(searchParams).forEach(([key, value]) => {
params.set(key, JSON.stringify(value));
});

View File

@@ -1,7 +1,6 @@
import type { MessageContext } from 'api/ai-assistant/chat';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { deserialize } from 'lib/compositeQuery/serializer';
import { AlertListTabs } from 'pages/AlertList/types';
import { NEW_PANEL_ID } from 'pages/DashboardPageV2/DashboardContainer/PanelEditor/newPanelRoute';
import { matchPath } from 'react-router-dom';
@@ -346,9 +345,15 @@ function collectSharedMetadata(
out.timeRange = { start: startTime, end: endTime };
}
const decodedQuery = deserialize(params);
if (decodedQuery) {
out.query = decodedQuery;
// Query Builder state — URL-encoded JSON written by `QueryBuilderProvider`.
const compositeQueryRaw = params.get(QueryParams.compositeQuery);
if (compositeQueryRaw) {
try {
out.query = JSON.parse(decodeURIComponent(compositeQueryRaw));
} catch {
// Malformed JSON in the URL — drop silently rather than throw
// inside a context-collection helper.
}
}
// Saved view selectors (logs / traces explorer) and dashboard variables.

View File

@@ -27,7 +27,6 @@ import {
} from 'hooks/useResourceAttribute/utils';
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
import useUrlQuery from 'hooks/useUrlQuery';
import { deserialize, serializeToParams } from 'lib/compositeQuery/serializer';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { isUndefined } from 'lodash-es';
@@ -62,6 +61,7 @@ type QueryParams = {
pageSize: number;
exceptionType?: string;
serviceName?: string;
compositeQuery?: string;
};
function AllErrors(): JSX.Element {
@@ -78,6 +78,7 @@ function AllErrors(): JSX.Element {
getUpdatedPageSize,
getUpdatedExceptionType,
getUpdatedServiceName,
getUpdatedCompositeQuery,
} = useMemo(
() => ({
updatedOrder: getOrder(params.get(urlKey.order)),
@@ -86,6 +87,7 @@ function AllErrors(): JSX.Element {
getUpdatedPageSize: getUpdatePageSize(params.get(urlKey.pageSize)),
getUpdatedExceptionType: getFilterString(params.get(urlKey.exceptionType)),
getUpdatedServiceName: getFilterString(params.get(urlKey.serviceName)),
getUpdatedCompositeQuery: getFilterString(params.get(urlKey.compositeQuery)),
}),
[params],
);
@@ -211,6 +213,7 @@ function AllErrors(): JSX.Element {
offset: getUpdatedOffset,
orderParam: getUpdatedParams,
pageSize: getUpdatedPageSize,
compositeQuery: getUpdatedCompositeQuery,
};
if (exceptionFilterValue && exceptionFilterValue !== 'undefined') {
@@ -221,13 +224,7 @@ function AllErrors(): JSX.Element {
queryParams.serviceName = serviceFilterValue;
}
// Carry the active query across the filter change so the trace context survives.
history.replace(
`${pathname}?${createQueryParams({
...queryParams,
...(compositeData ? serializeToParams(compositeData) : {}),
})}`,
);
history.replace(`${pathname}?${createQueryParams(queryParams)}`);
confirm();
},
[
@@ -236,7 +233,7 @@ function AllErrors(): JSX.Element {
getUpdatedPageSize,
getUpdatedParams,
getUpdatedServiceName,
compositeData,
getUpdatedCompositeQuery,
pathname,
updatedOrder,
],
@@ -441,9 +438,7 @@ function AllErrors(): JSX.Element {
serviceName: getFilterString(params.get(urlKey.serviceName)),
exceptionType: getFilterString(params.get(urlKey.exceptionType)),
});
// Re-serialize from the live URL rather than forwarding the raw param, so
// every key the adapter owns is carried over.
const compositeQuery = deserialize(params);
const compositeQuery = params.get(urlKey.compositeQuery) || '';
history.replace(
`${pathname}?${createQueryParams({
order: updatedOrder,
@@ -452,7 +447,7 @@ function AllErrors(): JSX.Element {
pageSize,
exceptionType,
serviceName,
...(compositeQuery ? serializeToParams(compositeQuery) : {}),
compositeQuery,
})}`,
);
}

View File

@@ -46,11 +46,32 @@ export const MOCK_USE_QUERIES_DATA = [
},
];
// Deliberately double-encoded: this is the legacy shape older bookmarks/shared
// links still carry, so it also covers the serializer's legacy decode fallback.
export const INIT_URL_WITH_COMMON_QUERY =
'/exceptions?compositeQuery=%257B%2522queryType%2522%253A%2522builder%2522%252C%2522builder%2522%253A%257B%2522queryData%2522%253A%255B%257B%2522dataSource%2522%253A%2522traces%2522%252C%2522queryName%2522%253A%2522A%2522%252C%2522aggregateOperator%2522%253A%2522noop%2522%252C%2522aggregateAttribute%2522%253A%257B%2522id%2522%253A%2522----resource--false%2522%252C%2522dataType%2522%253A%2522%2522%252C%2522key%2522%253A%2522%2522%252C%2522isColumn%2522%253Afalse%252C%2522type%2522%253A%2522resource%2522%252C%2522isJSON%2522%253Afalse%257D%252C%2522timeAggregation%2522%253A%2522rate%2522%252C%2522spaceAggregation%2522%253A%2522sum%2522%252C%2522functions%2522%253A%255B%255D%252C%2522filters%2522%253A%257B%2522items%2522%253A%255B%257B%2522id%2522%253A%2522db118ac7-9313-4adb-963f-f31b5b32c496%2522%252C%2522op%2522%253A%2522in%2522%252C%2522key%2522%253A%257B%2522key%2522%253A%2522deployment.environment%2522%252C%2522dataType%2522%253A%2522string%2522%252C%2522type%2522%253A%2522resource%2522%252C%2522isColumn%2522%253Afalse%252C%2522isJSON%2522%253Afalse%257D%252C%2522value%2522%253A%2522mq-kafka%2522%257D%255D%252C%2522op%2522%253A%2522AND%2522%257D%252C%2522expression%2522%253A%2522A%2522%252C%2522disabled%2522%253Afalse%252C%2522stepInterval%2522%253A60%252C%2522having%2522%253A%255B%255D%252C%2522limit%2522%253Anull%252C%2522orderBy%2522%253A%255B%255D%252C%2522groupBy%2522%253A%255B%255D%252C%2522legend%2522%253A%2522%2522%252C%2522reduceTo%2522%253A%2522avg%2522%257D%255D%252C%2522queryFormulas%2522%253A%255B%255D%257D%252C%2522promql%2522%253A%255B%257B%2522name%2522%253A%2522A%2522%252C%2522query%2522%253A%2522%2522%252C%2522legend%2522%253A%2522%2522%252C%2522disabled%2522%253Afalse%257D%255D%252C%2522clickhouse_sql%2522%253A%255B%257B%2522name%2522%253A%2522A%2522%252C%2522legend%2522%253A%2522%2522%252C%2522disabled%2522%253Afalse%252C%2522query%2522%253A%2522%2522%257D%255D%252C%2522id%2522%253A%2522dd576d04-0822-476d-b0c2-807a7af2e5e7%2522%257D';
export const extractCompositeQueryObject = (
url: string,
): Record<string, unknown> | null => {
try {
const urlObj = new URL(`http://dummy-base${url}`); // Add dummy base to parse relative URL
const encodedParam = urlObj.searchParams.get('compositeQuery');
if (!encodedParam) {
return null;
}
// Decode twice
const firstDecode = decodeURIComponent(encodedParam);
const secondDecode = decodeURIComponent(firstDecode);
// Parse JSON
return JSON.parse(secondDecode);
} catch (err) {
console.error('Failed to extract compositeQuery:', err);
return null;
}
};
export const TAG_FROM_QUERY = [
{
BoolValues: [],

View File

@@ -11,8 +11,6 @@ import {
export const isOrder = (order: string | null): order is Order =>
!!(order === 'ascending' || order === 'descending');
// The serialized query is deliberately absent: its param keys are owned by the
// compositeQuery serializer (see lib/compositeQuery), not spelled out here.
export const urlKey = {
order: 'order',
offset: 'offset',
@@ -20,6 +18,7 @@ export const urlKey = {
pageSize: 'pageSize',
exceptionType: 'exceptionType',
serviceName: 'serviceName',
compositeQuery: 'compositeQuery',
};
export const isOrderParams = (orderBy: string | null): orderBy is OrderBy =>

View File

@@ -2,8 +2,8 @@ import { memo } from 'react';
import { Card, Modal } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES, PANEL_TYPES_INITIAL_QUERY } from 'constants/queryBuilder';
import { serializeToParams } from 'lib/compositeQuery/serializer';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
@@ -28,7 +28,9 @@ function PanelTypeSelectionModal(): JSX.Element {
const queryParams = {
graphType: name,
widgetId: id,
...serializeToParams(PANEL_TYPES_INITIAL_QUERY[name]),
[QueryParams.compositeQuery]: JSON.stringify(
PANEL_TYPES_INITIAL_QUERY[name],
),
};
history.push(

View File

@@ -63,8 +63,6 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import useErrorNotification from 'hooks/useErrorNotification';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import { serializeToParams } from 'lib/compositeQuery/serializer';
import createQueryParams from 'lib/createQueryParams';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { cloneDeep, isEqual, omit } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
@@ -176,7 +174,7 @@ function ExplorerOptions({
const handleConditionalQueryModification = useCallback(
// eslint-disable-next-line sonarjs/cognitive-complexity
(defaultQuery: Query | null): Record<string, string> => {
(defaultQuery: Query | null): string => {
const queryToUse = defaultQuery || query;
if (!queryToUse) {
throw new Error('No query provided');
@@ -186,7 +184,7 @@ function ExplorerOptions({
StringOperators.NOOP &&
sourcepage !== DataSource.LOGS
) {
return serializeToParams(queryToUse);
return JSON.stringify(queryToUse);
}
// Convert NOOP to COUNT for alerts and strip orderBy for logs
@@ -210,7 +208,14 @@ function ExplorerOptions({
);
}
return serializeToParams(modifiedQuery);
try {
return JSON.stringify(modifiedQuery);
} catch (err) {
throw new Error(
'Failed to stringify modified query: ' +
(err instanceof Error ? err.message : String(err)),
);
}
},
[panelType, query, sourcepage],
);
@@ -233,9 +238,13 @@ function ExplorerOptions({
});
}
const serializedParams = handleConditionalQueryModification(defaultQuery);
const stringifiedQuery = handleConditionalQueryModification(defaultQuery);
history.push(`${ROUTES.ALERTS_NEW}?${createQueryParams(serializedParams)}`);
history.push(
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
stringifiedQuery,
)}`,
);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[handleConditionalQueryModification, history],

View File

@@ -3,7 +3,6 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import { MOCK_QUERY } from 'container/QueryTable/Drilldown/__tests__/mockTableData';
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { serialize } from 'lib/compositeQuery/serializer';
import { rest, server } from 'mocks-server/server';
import {
defaultFeatureFlags,
@@ -381,9 +380,9 @@ describe('ExplorerOptionWrapper', () => {
await waitFor(() => {
expect(mockSafeNavigate).toHaveBeenCalledTimes(1);
expect(mockSafeNavigate).toHaveBeenCalledWith(
`/dashboard/${TEST_DASHBOARD_ID}/new?graphType=${panelTypeParam}&widgetId=${widgetId}&${serialize(
query,
).toString()}`,
`/dashboard/${TEST_DASHBOARD_ID}/new?graphType=${panelTypeParam}&widgetId=${widgetId}&compositeQuery=${encodeURIComponent(
JSON.stringify(query),
)}`,
);
});

View File

@@ -34,7 +34,6 @@ import useGetYAxisUnit from 'hooks/useGetYAxisUnit';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { clearSerializedParams } from 'lib/compositeQuery/serializer';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEmpty, isEqual } from 'lodash-es';
@@ -385,7 +384,7 @@ function FormAlertRules({
const onCancelHandler = useCallback(
(e?: React.MouseEvent) => {
clearSerializedParams(urlQuery);
urlQuery.delete(QueryParams.compositeQuery);
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
@@ -611,7 +610,7 @@ function FormAlertRules({
`${ruleId}`,
]);
clearSerializedParams(urlQuery);
urlQuery.delete(QueryParams.compositeQuery);
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);

View File

@@ -23,10 +23,6 @@ import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import {
clearSerializedParams,
serializeToParams,
} from 'lib/compositeQuery/serializer';
import createQueryParams from 'lib/createQueryParams';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import {
@@ -217,7 +213,9 @@ function WidgetGraphComponent({
[QueryParams.graphType]: clonedWidget?.panelTypes,
[QueryParams.widgetId]: uuid,
...(clonedWidget?.query && {
...serializeToParams(clonedWidget.query),
[QueryParams.compositeQuery]: encodeURIComponent(
JSON.stringify(clonedWidget.query),
),
}),
};
safeNavigate(`${pathname}/new?${createQueryParams(queryParams)}`);
@@ -258,7 +256,7 @@ function WidgetGraphComponent({
const onToggleModelHandler = (): void => {
const existingSearchParams = new URLSearchParams(search);
existingSearchParams.delete(QueryParams.expandedWidgetId);
clearSerializedParams(existingSearchParams);
existingSearchParams.delete(QueryParams.compositeQuery);
existingSearchParams.delete(QueryParams.graphType);
const updatedQueryParams = Object.fromEntries(existingSearchParams.entries());
if (queryResponse.data?.payload) {

View File

@@ -29,10 +29,6 @@ import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
import useComponentPermission from 'hooks/useComponentPermission';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import {
applySerializedParams,
serialize,
} from 'lib/compositeQuery/serializer';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { isEmpty } from 'lodash-es';
import { unparse } from 'papaparse';
@@ -90,7 +86,10 @@ function WidgetHeader({
const widgetId = widget.id;
urlQuery.set(QueryParams.widgetId, widgetId);
urlQuery.set(QueryParams.graphType, widget.panelTypes);
applySerializedParams(serialize(widget.query), urlQuery);
urlQuery.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(widget.query)),
);
const generatedUrl = buildAbsolutePath({
relativePath: 'new',
urlQueryString: urlQuery.toString(),

View File

@@ -7,10 +7,6 @@ import { useListRules } from 'api/generated/services/rules';
import type { RuletypesRuleDTO } from 'api/generated/services/sigNoz.schemas';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import {
applySerializedParams,
serialize,
} from 'lib/compositeQuery/serializer';
import history from 'lib/history';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { ArrowRight, ArrowUpRight, Plus } from '@signozhq/icons';
@@ -138,7 +134,10 @@ export default function AlertRules({
const compositeQuery = mapQueryDataFromApi(
toCompositeMetricQuery(record.condition.compositeQuery),
);
applySerializedParams(serialize(compositeQuery), params);
params.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(compositeQuery)),
);
const panelType = record.condition.compositeQuery.panelType;
if (panelType) {

View File

@@ -13,15 +13,8 @@ import {
import { AxiosError } from 'axios';
import APIError from 'types/api/error';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import {
QuickFilterChangeEventData,
QuickFiltersSource,
} from 'components/QuickFilters/types';
import {
InfraMonitoringEvents,
logInfraFilterCustomizedEvent,
logInfraMonitoringListViewedEvent,
} from 'constants/events';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { InfraMonitoringEvents } from 'constants/events';
import { initialQueriesMap } from 'constants/queryBuilder';
import K8sBaseDetails, {
K8sDetailsFilters,
@@ -193,19 +186,6 @@ function Hosts(): JSX.Element {
hostInitialLogTracesExpression(host),
[],
);
const handleQuickFilterChange = useCallback(
(data: QuickFilterChangeEventData): void => {
logInfraFilterCustomizedEvent(
InfraMonitoringEntity.HOSTS,
'quick_filter',
data.expression,
data.filterItemKeys,
);
},
[],
);
const controlListPrefix = !showFilters ? (
<div className={styles.quickFiltersToggleContainer}>
<Button
@@ -219,10 +199,6 @@ function Hosts(): JSX.Element {
</div>
) : undefined;
useEffect(() => {
logInfraMonitoringListViewedEvent(InfraMonitoringEntity.HOSTS);
}, []);
return (
<>
<div className={styles.infraMonitoringContainer}>
@@ -251,7 +227,6 @@ function Hosts(): JSX.Element {
startUnixMilli,
endUnixMilli,
}}
onQuickFilterChange={handleQuickFilterChange}
/>
</>
</OverlayScrollbar>

View File

@@ -78,7 +78,7 @@
display: flex;
flex-direction: column;
align-items: stretch;
min-width: 800px;
min-width: 595px;
> * {
min-width: 0;

View File

@@ -1,12 +1,9 @@
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
import { logInfraFilterCustomizedEvent } from 'constants/events';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import {
StatusFilterValue,
useInfraMonitoringPageListing,
useInfraMonitoringStatusFilter,
} from 'container/InfraMonitoringK8sV2/hooks';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import styles from './StatusFilter.module.scss';
@@ -22,21 +19,11 @@ const statusOptions: Array<{
function StatusFilter(): JSX.Element {
const [statusFilter, setStatusFilter] = useInfraMonitoringStatusFilter();
const [, setCurrentPage] = useInfraMonitoringPageListing();
const { currentQuery } = useQueryBuilder();
const handleChange = (value: string): void => {
if (value !== undefined) {
void setStatusFilter(value === 'all' ? '' : (value as StatusFilterValue));
void setCurrentPage(1);
const expression =
currentQuery.builder.queryData[0]?.filter?.expression || '';
logInfraFilterCustomizedEvent(
InfraMonitoringEntity.HOSTS,
'host_status_toggle',
expression,
value === 'all' ? [] : ['host_status'],
);
}
};

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { Color } from '@signozhq/design-tokens';
import { Badge } from '@signozhq/ui/badge';
import { Progress } from '@signozhq/ui/progress';
import { Typography } from '@signozhq/ui/typography';
import {
InframonitoringtypesHostRecordDTO,
InframonitoringtypesHostStatusDTO,
@@ -9,7 +10,6 @@ import {
import { K8sDetailsMetadataConfig } from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
import { TextNoData } from 'container/InfraMonitoringK8sV2/components';
import { SelectedItemParams } from 'container/InfraMonitoringK8sV2/hooks';
import {
getHostQueryPayload,
@@ -70,7 +70,7 @@ export const hostDetailsMetadataConfig: HostDetailMetadataConfigType[] = [
{value}
</Badge>
) : (
<TextNoData type="typography" />
<Typography.Text>-</Typography.Text>
),
},
{

View File

@@ -175,7 +175,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
accessorFn: (row): number => row.cpu,
width: { min: 220 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpu = value as number;
return (
<div className={styles.progressContainer}>
@@ -183,7 +183,6 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
value={cpu}
entity={InfraMonitoringEntity.HOSTS}
attribute="CPU metric"
rowId={rowId}
>
<EntityProgressBar value={cpu} type="cpu" />
</ValidateColumnValueWrapper>
@@ -198,13 +197,14 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
tooltip="Excluding cache memory."
docPath="/infrastructure-monitoring/host-monitoring#memory-usage"
>
Memory Usage (WSS)
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.memory,
width: { min: 200 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memory = value as number;
return (
<div className={styles.progressContainer}>
@@ -212,7 +212,6 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
value={memory}
entity={InfraMonitoringEntity.HOSTS}
attribute="memory metric"
rowId={rowId}
>
<EntityProgressBar value={memory} type="memory" />
</ValidateColumnValueWrapper>
@@ -220,33 +219,6 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
);
},
},
{
id: 'diskUsage',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#disk-usage">
Disk Usage
</ColumnHeader>
),
accessorFn: (row): number => row.diskUsage,
width: { min: 200 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
const diskUsage = value as number;
return (
<div className={styles.progressContainer}>
<ValidateColumnValueWrapper
value={diskUsage}
entity={InfraMonitoringEntity.HOSTS}
attribute="disk usage metric"
rowId={rowId}
>
<EntityProgressBar value={diskUsage} type="disk" />
</ValidateColumnValueWrapper>
</div>
);
},
},
{
id: 'wait',
header: (): React.ReactNode => (
@@ -257,7 +229,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
accessorFn: (row): number => row.wait,
width: { min: 120 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const wait = value as number;
return (
@@ -265,7 +237,6 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
value={wait}
entity={InfraMonitoringEntity.HOSTS}
attribute="IOWait metric"
rowId={rowId}
>
<TanStackTable.Text>
{`${Number((wait * 100).toFixed(1))}%`}
@@ -278,13 +249,14 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
id: 'load15',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#load-avg">
Load Avg (15min)
Load Avg
<br /> (15min)
</ColumnHeader>
),
accessorFn: (row): number => row.load15,
width: { min: 160 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const load15 = Number(value);
return (
@@ -292,11 +264,36 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
value={load15}
entity={InfraMonitoringEntity.HOSTS}
attribute="load average metric"
rowId={rowId}
>
<TanStackTable.Text>{load15.toFixed(2)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'diskUsage',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#disk-usage">
Disk Usage
</ColumnHeader>
),
accessorFn: (row): number => row.diskUsage,
width: { min: 200 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const diskUsage = value as number;
return (
<div className={styles.progressContainer}>
<ValidateColumnValueWrapper
value={diskUsage}
entity={InfraMonitoringEntity.HOSTS}
attribute="disk usage metric"
>
<EntityProgressBar value={diskUsage} type="disk" />
</ValidateColumnValueWrapper>
</div>
);
},
},
];

View File

@@ -7,7 +7,7 @@ import {
IQuickFiltersConfig,
} from 'components/QuickFilters/types';
import { TriangleAlert } from '@signozhq/icons';
import TanStackTable from 'components/TanStackTableView';
import { CellValueTooltip } from 'container/InfraMonitoringK8sV2/components';
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
@@ -21,7 +21,7 @@ export function HostnameCell({
}): React.ReactElement {
const isEmpty = !hostName || !hostName.trim();
if (!isEmpty) {
return <TanStackTable.Text>{hostName}</TanStackTable.Text>;
return <CellValueTooltip value={hostName} />;
}
return (
<>

View File

@@ -28,10 +28,6 @@ import {
Time,
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import {
applySerializedParams,
serialize,
} from 'lib/compositeQuery/serializer';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import GetMinMax from 'lib/getMinMax';
import {
@@ -407,7 +403,7 @@ export default function K8sBaseDetails<T>({
},
};
applySerializedParams(serialize(compositeQuery as any), urlQuery);
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
@@ -432,7 +428,7 @@ export default function K8sBaseDetails<T>({
},
};
applySerializedParams(serialize(compositeQuery as any), urlQuery);
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useRef } from 'react';
import { useCallback, useMemo, useRef } from 'react';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { Card } from 'antd';
import logEvent from 'api/common/logEvent';
@@ -21,9 +21,6 @@ import {
getUserExpressionFromCombined,
} from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
import { InfraMonitoringEvents } from 'constants/events';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8s/constants';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { FontSize } from 'container/OptionsMenu/types';
@@ -36,15 +33,9 @@ import {
import { getOldLogsOperatorFromNew } from 'hooks/logs/useActiveLog';
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
import useScrollToLog from 'hooks/logs/useScrollToLog';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { serializeToParams } from 'lib/compositeQuery/serializer';
import createQueryParams from 'lib/createQueryParams';
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
import { ILog } from 'types/api/logs/log';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { isModifierKeyPressed } from 'utils/app';
import { validateQuery } from 'utils/queryValidationUtils';
import EntityEmptyState from '../EntityEmptyState/EntityEmptyState';
@@ -143,7 +134,7 @@ function EntityLogsContent({
if (validation.isValid) {
querySearchOnRun(newUserExpression);
void logEvent(InfraMonitoringEvents.FilterApplied, {
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category,
@@ -171,45 +162,6 @@ function EntityLogsContent({
virtuosoRef,
});
const { updateAllQueriesOperators } = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
const handleOpenInExplorer = useCallback(
(e: React.MouseEvent<Element, MouseEvent>, log: ILog) => {
const baseQuery = updateAllQueriesOperators(
initialQueriesMap[DataSource.LOGS],
PANEL_TYPES.LIST,
DataSource.LOGS,
);
const queryParams = {
[QueryParams.activeLogId]: `"${log?.id}"`,
[QueryParams.startTime]: timeRange.startTime.toString(),
[QueryParams.endTime]: timeRange.endTime.toString(),
...serializeToParams({
...baseQuery,
builder: {
...baseQuery.builder,
queryData: baseQuery.builder.queryData.map((item) => ({
...item,
filter: { expression },
})),
},
} satisfies Query),
};
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`, {
newTab: !!e && isModifierKeyPressed(e),
});
},
[
timeRange.startTime,
timeRange.endTime,
expression,
safeNavigate,
updateAllQueriesOperators,
],
);
const getItemContent = useCallback(
(_: number, logToRender: ILog): JSX.Element => {
return (
@@ -335,7 +287,6 @@ function EntityLogsContent({
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onScrollToLog={handleScrollToLog}
handleOpenInExplorer={(e): void => handleOpenInExplorer(e, activeLog)}
/>
)}
</div>

View File

@@ -2,7 +2,6 @@
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
text-wrap: auto;
}
.infoIcon {

View File

@@ -8,6 +8,7 @@ const DOCS_BASE_URL = `${process.env.DOCS_BASE_URL}/docs`;
interface ColumnHeaderProps {
children?: React.ReactNode;
title?: string;
docPath?: string;
tooltip?: string;
className?: string;
@@ -15,6 +16,7 @@ interface ColumnHeaderProps {
function ColumnHeader({
children,
title,
docPath,
tooltip,
className,
@@ -24,6 +26,16 @@ function ColumnHeader({
return children;
}
if (title) {
const parts = title.split('\n');
return parts.map((part, index) => (
<div key={`${part}-${index}`}>
{part}
{index < parts.length - 1 && <br />}
</div>
));
}
return null;
};

View File

@@ -1,17 +1,15 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import { useCopyToClipboard } from 'react-use';
import { Copy, X } from '@signozhq/icons';
import { Color, Spacing } from '@signozhq/design-tokens';
import { X } from '@signozhq/icons';
import { Divider } from '@signozhq/ui/divider';
import { Button } from '@signozhq/ui/button';
import { DrawerWrapper, DrawerWrapperProps } from '@signozhq/ui/drawer';
import { toast } from '@signozhq/ui/sonner';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import { Drawer } from 'antd';
import logEvent from 'api/common/logEvent';
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import APIError from 'types/api/error';
import { InfraMonitoringEvents } from 'constants/events';
import { useIsDarkMode } from 'hooks/useDarkMode';
import {
GlobalTimeProvider,
NANO_SECOND_MULTIPLIER,
@@ -24,10 +22,8 @@ import LoadingContainer from '../LoadingContainer';
import K8sBaseDetailsContent from './K8sBaseDetailsContent';
import { K8sBaseDetailsProps } from './types';
import { useDrawerLifecycleStore } from './useDrawerLifecycleStore';
import styles from '../EntityDetailsUtils/entityDetails.module.scss';
import { INFRA_MONITORING_DETAILS_CACHE_TIME } from 'constants/queryCacheTime';
import '../EntityDetailsUtils/entityDetails.styles.scss';
export type {
CustomTab,
@@ -38,23 +34,6 @@ export type {
K8sDetailsMetadataConfig,
} from './types';
// TODO(H4ad): Improve this on component level
const DRAWER_TRANSITION = { duration: 0.3, ease: [0.25, 0.1, 0.25, 1] };
const DRAWER_MOTION_PROPS = {
onOpenAutoFocus: (e: Event): void => e.preventDefault(),
initial: { opacity: 0, transform: 'translateX(100%)' },
animate: {
opacity: 1,
transform: 'translateX(0%)',
transition: DRAWER_TRANSITION,
},
exit: {
opacity: 0,
transform: 'translateX(100%)',
transition: DRAWER_TRANSITION,
},
} as unknown as DrawerWrapperProps['drawerContentProps'];
export default function K8sBaseDetails<T>({
category,
eventCategory,
@@ -79,6 +58,8 @@ export default function K8sBaseDetails<T>({
(s) => s.getAutoRefreshQueryKey,
);
const isDarkMode = useIsDarkMode();
const [selectedItemParams, setSelectedItemParams] =
useInfraMonitoringSelectedItemParams();
const selectedItem = selectedItemParams.selectedItem;
@@ -120,8 +101,6 @@ export default function K8sBaseDetails<T>({
return fetchEntityData({ filter: { expression }, start, end }, signal);
},
cacheTime: INFRA_MONITORING_DETAILS_CACHE_TIME,
staleTime: INFRA_MONITORING_DETAILS_CACHE_TIME,
enabled: !!selectedItem,
});
@@ -142,39 +121,10 @@ export default function K8sBaseDetails<T>({
return getInitialEventsExpression(entity);
}, [entity, getInitialEventsExpression]);
const markDrawerOpened = useDrawerLifecycleStore((s) => s.markOpened);
const markDrawerClosed = useDrawerLifecycleStore((s) => s.markClosed);
useEffect(() => {
if (selectedItem) {
markDrawerOpened();
} else {
markDrawerClosed();
}
}, [selectedItem, markDrawerOpened, markDrawerClosed]);
const handleClose = useCallback((): void => {
setSelectedItemParams(null);
}, [setSelectedItemParams]);
const handleOpenChange = useCallback(
(open: boolean): void => {
if (!open) {
handleClose();
}
},
[handleClose],
);
const [, copyToClipboard] = useCopyToClipboard();
const handleCopyId = useCallback((): void => {
if (selectedItem) {
copyToClipboard(selectedItem);
toast.success('ID copied to clipboard', { position: 'bottom-left' });
}
}, [copyToClipboard, selectedItem]);
const entityName = entity ? getEntityName(entity) : '';
useEffect(() => {
@@ -187,49 +137,31 @@ export default function K8sBaseDetails<T>({
}
}, [entity, eventCategory]);
// TODO(H4ad): Improve this on component level
// DrawerWrapper types `title` as string but renders any ReactNode.
const drawerTitle = (
<>
<Button
variant="ghost"
size="sm"
color="secondary"
onClick={handleClose}
data-testid="close-drawer-button"
className={styles.closeButton}
prefix={<X />}
/>
<Divider type="vertical" />
<Typography.Text className={styles.title}>
{entityName ||
((isEntityError || hasResponseError) && 'Failed to load entity details') ||
(isEntityLoading && 'Loading...') ||
'-'}
</Typography.Text>
<TooltipSimple title="Copy ID">
<Button
variant="ghost"
size="sm"
color="secondary"
onClick={handleCopyId}
data-testid="copy-id-button"
>
<Copy size={14} />
</Button>
</TooltipSimple>
</>
) as unknown as string;
return (
<DrawerWrapper
<Drawer
width="70%"
title={
<>
<Divider type="vertical" />
<Typography.Text className="title">
{entityName ||
((isEntityError || hasResponseError) &&
'Failed to load entity details') ||
(isEntityLoading && 'Loading...') ||
'-'}
</Typography.Text>
</>
}
placement="right"
onClose={handleClose}
open={!!selectedItem}
onOpenChange={handleOpenChange}
direction="right"
title={drawerTitle}
showCloseButton={false}
drawerContentProps={DRAWER_MOTION_PROPS}
className={styles.entityDetailDrawer}
style={{
overscrollBehavior: 'contain',
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
}}
className="entity-detail-drawer"
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
>
{(isEntityLoading || !selectedItem) && <LoadingContainer />}
@@ -278,6 +210,6 @@ export default function K8sBaseDetails<T>({
/>
</GlobalTimeProvider>
)}
</DrawerWrapper>
</Drawer>
);
}

View File

@@ -1,4 +1,4 @@
import { Fragment, useEffect, useMemo, useRef } from 'react';
import { Fragment, useEffect, useMemo } from 'react';
import {
BarChart,
ChevronsLeftRight,
@@ -6,29 +6,19 @@ import {
DraftingCompass,
ScrollText,
} from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import { Button, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent';
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
import {
InfraMonitoringEvents,
logInfraDrawerTabViewedEvent,
logInfraExplorerNavigatedEvent,
} from 'constants/events';
import { InfraMonitoringEvents } from 'constants/events';
import { QueryParams } from 'constants/query';
import {
initialQueryBuilderFormValuesMap,
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import {
applySerializedParams,
serialize,
} from 'lib/compositeQuery/serializer';
import { parseAsString, useQueryState } from 'nuqs';
import { EQueryType } from 'types/common/dashboard';
import {
LogsAggregatorOperator,
TracesAggregatorOperator,
@@ -52,9 +42,6 @@ import {
import { EntityCountsSection } from './components/EntityCountsSection/EntityCountsSection';
import { K8sBaseDetailsContentProps } from './types';
import { getDrawerDurationMs } from './useDrawerLifecycleStore';
import styles from '../EntityDetailsUtils/entityDetails.module.scss';
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function K8sBaseDetailsContent<T>({
@@ -120,17 +107,6 @@ export default function K8sBaseDetailsContent<T>({
const effectiveView = hideDetailViewTabs ? VIEW_TYPES.METRICS : selectedView;
// Wait until the view is a valid tab so we don't log a pending value before
// the validation effect above corrects an out-of-scope query param
const hasLoggedDefaultTab = useRef(false);
useEffect(() => {
if (hasLoggedDefaultTab.current || !validTabs.includes(effectiveView)) {
return;
}
hasLoggedDefaultTab.current = true;
logInfraDrawerTabViewedEvent(category, effectiveView, true);
}, [validTabs, effectiveView, category]);
const [, setLogFiltersParam] = useInfraMonitoringLogFilters();
const [, setTracesFiltersParam] = useInfraMonitoringTracesFilters();
const [, setEventsFiltersParam] = useInfraMonitoringEventsFilters();
@@ -157,7 +133,6 @@ export default function K8sBaseDetailsContent<T>({
category: eventCategory,
view: value,
});
logInfraDrawerTabViewedEvent(category, value, false);
};
const handleExplorePagesRedirect = (): void => {
@@ -179,16 +154,6 @@ export default function K8sBaseDetailsContent<T>({
view: selectedView,
});
logInfraExplorerNavigatedEvent({
entityType: category,
destination:
selectedView === VIEW_TYPES.LOGS ? 'logs_explorer' : 'traces_explorer',
source: 'tab_cta_button',
tab: selectedView,
sourceKey: null,
drawerDurationMsAtNavigation: getDrawerDurationMs(),
});
if (selectedView === VIEW_TYPES.LOGS) {
const fullExpression = combineInitialAndUserExpression(
logsAndTracesInitialExpression,
@@ -197,7 +162,7 @@ export default function K8sBaseDetailsContent<T>({
const compositeQuery = {
...initialQueryState,
queryType: EQueryType.QUERY_BUILDER,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
@@ -211,7 +176,7 @@ export default function K8sBaseDetailsContent<T>({
},
};
applySerializedParams(serialize(compositeQuery), urlQuery);
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
@@ -222,7 +187,7 @@ export default function K8sBaseDetailsContent<T>({
const compositeQuery = {
...initialQueryState,
queryType: EQueryType.QUERY_BUILDER,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
@@ -236,7 +201,7 @@ export default function K8sBaseDetailsContent<T>({
},
};
applySerializedParams(serialize(compositeQuery), urlQuery);
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
@@ -244,39 +209,34 @@ export default function K8sBaseDetailsContent<T>({
return (
<>
<div className={styles.entityDetailsEntity}>
<div className={styles.entityDetailsGrid}>
<div className={styles.labelsRow}>
<div className="entity-detail-drawer__entity">
<div className="entity-details-grid">
<div className="labels-row">
{metadataConfig.map((config) => (
<Typography.Text
key={config.label}
color="muted"
size="small"
weight="medium"
className={styles.entityDetailsMetadataLabel}
className="entity-details-metadata-label"
>
{config.label}
</Typography.Text>
))}
</div>
<div className={styles.valuesRow}>
<div className="values-row">
{metadataConfig.map((config) => {
const value = config.getValue(entity);
if (config.render) {
return config.render(value, entity);
}
const displayValue = String(value);
return (
<Typography.Text
key={config.label}
size="small"
weight="medium"
className={styles.entityDetailsMetadataValue}
className="entity-details-metadata-value"
>
{displayValue}
{config.render ? (
config.render(value, entity)
) : (
<Tooltip title={displayValue}>{displayValue}</Tooltip>
)}
</Typography.Text>
);
})}
@@ -293,17 +253,15 @@ export default function K8sBaseDetailsContent<T>({
selectedItem={selectedItem}
filterExpression={getCountsFilterExpression(entity)}
closeDrawer={handleClose}
entityType={category}
activeTab={selectedView}
/>
)}
</div>
{!hideDetailViewTabs && (
<div className={styles.viewsTabsContainer}>
<div className="views-tabs-container">
<ToggleGroupSimple
type="single"
className={styles.viewsTabs}
className="views-tabs"
onChange={handleTabChange}
value={selectedView}
items={[
@@ -312,7 +270,7 @@ export default function K8sBaseDetailsContent<T>({
{
value: VIEW_TYPES.METRICS,
label: (
<div className={styles.viewTitle}>
<div className="view-title">
<BarChart size={14} />
Metrics
</div>
@@ -325,7 +283,7 @@ export default function K8sBaseDetailsContent<T>({
{
value: VIEW_TYPES.LOGS,
label: (
<div className={styles.viewTitle}>
<div className="view-title">
<ScrollText size={14} />
Logs
</div>
@@ -338,7 +296,7 @@ export default function K8sBaseDetailsContent<T>({
{
value: VIEW_TYPES.TRACES,
label: (
<div className={styles.viewTitle}>
<div className="view-title">
<DraftingCompass size={14} />
Traces
</div>
@@ -351,7 +309,7 @@ export default function K8sBaseDetailsContent<T>({
{
value: VIEW_TYPES.EVENTS,
label: (
<div className={styles.viewTitle}>
<div className="view-title">
<ChevronsLeftRight size={14} />
Events
</div>
@@ -362,7 +320,7 @@ export default function K8sBaseDetailsContent<T>({
...(customTabs?.map((tab) => ({
value: tab.key,
label: (
<div className={styles.viewTitle}>
<div className="view-title">
{tab.icon}
{tab.label}
</div>
@@ -372,30 +330,22 @@ export default function K8sBaseDetailsContent<T>({
/>
{selectedView === VIEW_TYPES.LOGS && (
<TooltipSimple title="Go to Logs Explorer" side="left" arrow>
<Tooltip title="Go to Logs Explorer" placement="left">
<Button
variant="ghost"
size="icon"
color="secondary"
className={styles.compassButton}
icon={<Compass size={18} />}
className="compass-button"
onClick={handleExplorePagesRedirect}
>
<Compass size={18} />
</Button>
</TooltipSimple>
/>
</Tooltip>
)}
{selectedView === VIEW_TYPES.TRACES && (
<TooltipSimple title="Go to Traces Explorer" side="left" arrow>
<Tooltip title="Go to Traces Explorer" placement="left">
<Button
variant="ghost"
size="icon"
color="secondary"
className={styles.compassButton}
icon={<Compass size={18} />}
className="compass-button"
onClick={handleExplorePagesRedirect}
>
<Compass size={18} />
</Button>
</TooltipSimple>
/>
</Tooltip>
)}
</div>
)}
@@ -408,7 +358,6 @@ export default function K8sBaseDetailsContent<T>({
getEntityQueryPayload={getEntityQueryPayload}
category={category}
queryKey={`${queryKeyPrefix}Metrics`}
view={VIEW_TYPES.METRICS}
/>
)}
{effectiveView === VIEW_TYPES.LOGS && (

View File

@@ -1,19 +1,14 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useQuery, useQueryClient } from 'react-query';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import TanStackTable, {
SortState,
TableColumnDef,
useCalculatedPageSize,
useHiddenColumnIds,
useTableParams,
} from 'components/TanStackTableView';
import {
InfraMonitoringEvents,
logInfraColumnSortedEvent,
logInfraTimeRangeCustomizedEvent,
} from 'constants/events';
import { InfraMonitoringEvents } from 'constants/events';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGlobalTimeStore } from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
@@ -32,16 +27,11 @@ import {
useInfraMonitoringSelectedItemParams,
useInfraMonitoringStatusFilter,
} from '../hooks';
import {
useInfraMonitoringFontSize,
useInfraMonitoringLineClamp,
} from './useInfraMonitoringTablePreferencesStore';
import { useInfraMonitoringLineClamp } from '../components';
import { K8sEmptyState } from './K8sEmptyState';
import { K8sExpandedRow } from './K8sExpandedRow';
import K8sOptionsSidePanel from './K8sOptionsSidePanel';
import K8sHeader from './K8sHeader';
import { K8sPaginationWarning } from './K8sPaginationWarning';
import K8sTableToolbar from './K8sTableToolbar';
import { K8sBaseFilters } from './types';
import { getGroupedByMeta } from './utils';
import { K8sInstrumentationChecksCallout } from './components/K8sInstrumentationChecksCallout/K8sInstrumentationChecksCallout';
@@ -114,7 +104,6 @@ export function K8sBaseList<
const { currentQuery } = useQueryBuilder();
const expression = currentQuery.builder.queryData[0]?.filter?.expression || '';
const lineClamp = useInfraMonitoringLineClamp();
const fontSize = useInfraMonitoringFontSize();
const [groupBy] = useInfraMonitoringGroupBy();
const [orderBy] = useInfraMonitoringOrderBy();
const [statusFilter] = useInfraMonitoringStatusFilter();
@@ -124,7 +113,6 @@ export function K8sBaseList<
const columnStorageKey = `k8s-${entity}-columns`;
const hiddenColumnIds = useHiddenColumnIds(columnStorageKey);
const [isOptionsDrawerOpen, setIsOptionsDrawerOpen] = useState(false);
const { containerRef, calculatedPageSize } = useCalculatedPageSize({
rowHeight: 42,
@@ -229,14 +217,6 @@ export function K8sBaseList<
void queryClient.cancelQueries({ queryKey });
}, [queryClient, queryKey]);
const handleOpenOptionsDrawer = useCallback((): void => {
setIsOptionsDrawerOpen(true);
}, []);
const handleCloseOptionsDrawer = useCallback((): void => {
setIsOptionsDrawerOpen(false);
}, []);
const pageData = data?.data ?? [];
const totalCount = data?.total || 0;
const hasFilters = !!expression?.trim();
@@ -255,14 +235,6 @@ export function K8sBaseList<
});
}, [eventCategory, totalCount]);
const prevSelectedTimeRef = useRef(selectedTime);
useEffect(() => {
if (prevSelectedTimeRef.current !== selectedTime) {
logInfraTimeRangeCustomizedEvent(entity, selectedTime);
prevSelectedTimeRef.current = selectedTime;
}
}, [selectedTime, entity]);
const handleRowClick = useCallback(
(record: T, itemKey: TItemKey): void => {
if (groupBy.length === 0) {
@@ -374,7 +346,6 @@ export function K8sBaseList<
extraQueryKeyParts={extraQueryKeyParts}
getRowKey={getRowKey}
getItemKey={getItemKey}
detailsQueryKeyPrefix={detailsQueryKeyPrefix}
/>
),
[
@@ -384,7 +355,6 @@ export function K8sBaseList<
getItemKey,
expandedRowColumns,
extraQueryKeyParts,
detailsQueryKeyPrefix,
],
);
@@ -393,15 +363,6 @@ export function K8sBaseList<
[isGroupedByAttribute],
);
const handleSort = useCallback(
(sort: SortState | null): void => {
if (sort) {
logInfraColumnSortedEvent(entity, sort.columnName, sort.order, 'list');
}
},
[entity],
);
const showTableLoadingState = isLoading;
const emptyTableMessage: React.ReactNode = renderEmptyState?.({
@@ -431,21 +392,17 @@ export function K8sBaseList<
<>
<K8sHeader
controlListPrefix={controlListPrefix}
leftFilters={leftFilters}
entity={entity}
showAutoRefresh={!selectedItem}
columns={tableColumns}
columnStorageKey={columnStorageKey}
isFetching={isFetching}
cancelQuery={cancelQuery}
/>
<div ref={containerRef} className={styles.tableContainer}>
<K8sInstrumentationChecksCallout entity={entity} />
<K8sTableToolbar
entity={entity}
eventCategory={eventCategory}
leftFilters={leftFilters}
onOpenOptionsDrawer={handleOpenOptionsDrawer}
/>
{isError && (
<Typography>
{data?.error?.toString() || 'Something went wrong'}
@@ -466,7 +423,6 @@ export function K8sBaseList<
getGroupKey={getGroupKeyFn}
onRowClick={handleRowClick}
onRowClickNewTab={handleRowClickNewTab}
onSort={handleSort}
renderExpandedRow={isGroupedByAttribute ? renderExpandedRow : undefined}
getRowCanExpand={isGroupedByAttribute ? getRowCanExpand : undefined}
className={cx(styles.k8SListTable)}
@@ -484,21 +440,11 @@ export function K8sBaseList<
onLimitChange: setLimit,
}}
plainTextCellLineClamp={lineClamp}
cellTypographySize={fontSize}
prefixPaginationContent={paginationWarningContent}
paginationClassname={styles.paginationContainer}
resetScrollKey={entity}
/>
)}
</div>
<K8sOptionsSidePanel
open={isOptionsDrawerOpen}
columns={tableColumns}
storageKey={columnStorageKey}
entity={entity}
onClose={handleCloseOptionsDrawer}
/>
</>
);
}

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useQuery, useQueryClient } from 'react-query';
import { useQuery } from 'react-query';
import { useLocation } from 'react-router-dom';
import { Querybuildertypesv5QueryWarnDataDTO } from 'api/generated/services/sigNoz.schemas';
import { Button } from '@signozhq/ui/button';
@@ -10,12 +10,9 @@ import TanStackTable, {
TableColumnDef,
TanStackTableStateProvider,
} from 'components/TanStackTableView';
import { QueryParams } from 'constants/query';
import { CornerDownRight } from '@signozhq/icons';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import {
applySerializedParams,
serialize,
} from 'lib/compositeQuery/serializer';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { v4 as uuid } from 'uuid';
@@ -24,7 +21,6 @@ import { useGlobalTimeStore } from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
import { logInfraColumnSortedEvent } from 'constants/events';
import { InfraMonitoringEntity } from '../constants';
import {
SelectedItemParams,
@@ -34,8 +30,6 @@ import {
useInfraMonitoringSelectedItemParams,
} from '../hooks';
import { K8sBaseFilters } from './types';
import { useLogEventForColumnCustomized } from './useLogEventForColumnCustomized';
import { useInfraMonitoringFontSize } from './useInfraMonitoringTablePreferencesStore';
import styles from './K8sExpandedRow.module.scss';
import { buildExpressionFromGroupMeta } from './utils';
@@ -68,14 +62,9 @@ export type K8sExpandedRowProps<T, TItemKey = string> = {
getRowKey?: (record: T) => string;
/** Function to get the item key used for selection. Defaults to getRowKey if not provided. */
getItemKey?: (record: T) => TItemKey;
/** Query key prefix for pre-caching detail data on row click */
detailsQueryKeyPrefix?: string;
};
export function K8sExpandedRow<
T,
TItemKey extends string | SelectedItemParams = string,
>({
export function K8sExpandedRow<T, TItemKey = string>({
rowKey,
groupMeta,
entity,
@@ -84,9 +73,7 @@ export function K8sExpandedRow<
extraQueryKeyParts = [],
getRowKey,
getItemKey,
detailsQueryKeyPrefix,
}: K8sExpandedRowProps<T, TItemKey>): JSX.Element {
const fontSize = useInfraMonitoringFontSize();
const [, setGroupBy] = useInfraMonitoringGroupBy();
const [, setCurrentPage] = useInfraMonitoringPageListing();
const { currentQuery } = useQueryBuilder();
@@ -97,7 +84,6 @@ export function K8sExpandedRow<
const { safeNavigate } = useSafeNavigate();
const urlQuery = useUrlQuery();
const location = useLocation();
const queryClient = useQueryClient();
const orderByParamKey = useMemo(
() => `orderBy_${rowKey.replace(/[^a-zA-Z0-9]/g, '_')}`,
@@ -119,13 +105,6 @@ export function K8sExpandedRow<
const storageKey = `k8s-${entity}-columns-expanded`;
useLogEventForColumnCustomized({
entity,
source: 'expanded',
storageKey,
columns: tableColumns,
});
const expressionForRecord = useMemo(
() => buildExpressionFromGroupMeta(parentExpression || '', groupMeta),
[parentExpression, groupMeta],
@@ -198,45 +177,18 @@ export function K8sExpandedRow<
const expandedData = data?.data ?? [];
const handleRowClick = useCallback(
(row: T, itemKey: TItemKey): void => {
const params: SelectedItemParams =
typeof itemKey === 'object' && itemKey !== null
? itemKey
: {
selectedItem: itemKey,
clusterName: null,
namespaceName: null,
};
if (detailsQueryKeyPrefix) {
const detailQueryKey = getAutoRefreshQueryKey(
selectedTime,
`${detailsQueryKeyPrefix}EntityDetails`,
params.selectedItem,
params.clusterName,
params.namespaceName,
);
queryClient.setQueryData(detailQueryKey, { data: row });
}
setSelectedItemParams(params);
},
[
setSelectedItemParams,
detailsQueryKeyPrefix,
getAutoRefreshQueryKey,
selectedTime,
queryClient,
],
);
const handleSort = useCallback(
(sort: SortState | null): void => {
if (sort) {
logInfraColumnSortedEvent(entity, sort.columnName, sort.order, 'expanded');
(_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,
});
}
},
[entity],
[setSelectedItemParams],
);
const handleViewAllClick = (): void => {
@@ -262,7 +214,10 @@ export function K8sExpandedRow<
};
const newUrlQuery = new URLSearchParams(urlQuery.toString());
applySerializedParams(serialize(updatedQuery), newUrlQuery);
newUrlQuery.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(updatedQuery)),
);
safeNavigate(`${location.pathname}?${newUrlQuery.toString()}`);
};
@@ -302,7 +257,6 @@ export function K8sExpandedRow<
getRowKey={getRowKey}
getItemKey={getItemKey}
onRowClick={handleRowClick}
onSort={handleSort}
enableQueryParams={{
orderBy: orderByParamKey,
}}
@@ -310,7 +264,7 @@ export function K8sExpandedRow<
className: styles.expandedTable,
}}
disableVirtualScroll
cellTypographySize={fontSize}
cellTypographySize="medium"
/>
</TanStackTableStateProvider>
{!isLoading && expandedData.length > 0 && (

View File

@@ -0,0 +1,62 @@
.drawer {
--dialog-description-padding: 0px 0px var(--spacing-8) 0px;
}
.columnItem {
display: flex;
align-items: center;
}
.columnsTitle {
color: var(--l2-foreground);
font-size: var(--periscope-font-size-small, 11px);
font-weight: var(--periscope-font-weight-medium, 500);
text-transform: uppercase;
padding: var(--spacing-4) var(--spacing-8);
border-bottom: 1px solid var(--l2-border);
&:not(:first-of-type) {
border-top: 1px solid var(--l2-border);
}
}
.columnsList {
display: flex;
flex-direction: column;
[data-slot='icon'] {
display: none;
}
[data-slot='entity-group-header'] {
padding-left: 0;
}
[data-slot='column-header'] {
display: flex;
width: auto;
span {
padding: 0;
text-align: left;
}
br {
display: none;
}
}
}
.columnItem {
justify-content: flex-start !important;
width: 100%;
> span {
width: auto !important;
}
}
.horizontalDivider {
border-top: 1px solid var(--l1-border);
}

View File

@@ -0,0 +1,143 @@
import { ReactNode, useMemo } from 'react';
import { Button } from '@signozhq/ui/button';
import { DrawerWrapper } from '@signozhq/ui/drawer';
import {
hideColumn,
showColumn,
TableColumnDef,
useHiddenColumnIds,
} from 'components/TanStackTableView';
import styles from './K8sFiltersSidePanel.module.scss';
type ColumnPickerItem = {
id: string;
label: ReactNode;
canBeHidden: boolean;
visibilityBehavior:
| 'hidden-on-expand'
| 'hidden-on-collapse'
| 'always-visible';
};
function renderHeader(header: string | (() => ReactNode)): ReactNode {
return typeof header === 'function' ? header() : header;
}
function toColumnPickerItems<T>(
columns: TableColumnDef<T>[],
): ColumnPickerItem[] {
return columns.map((col) => ({
id: col.id,
label: renderHeader(col.header),
canBeHidden: col.canBeHidden !== false && col.enableRemove !== false,
visibilityBehavior: col.visibilityBehavior ?? 'always-visible',
}));
}
function K8sFiltersSidePanel<TData>({
open,
onClose,
columns,
storageKey,
}: {
open: boolean;
onClose: () => void;
columns: TableColumnDef<TData>[];
storageKey: string;
}): JSX.Element {
const columnPickerItems = useMemo(
() => toColumnPickerItems(columns),
[columns],
);
const hiddenColumnIds = useHiddenColumnIds(storageKey);
const addedColumns = useMemo(
() =>
columnPickerItems.filter(
(column) =>
!hiddenColumnIds.includes(column.id) &&
column.visibilityBehavior !== 'hidden-on-collapse',
),
[columnPickerItems, hiddenColumnIds],
);
const hiddenColumns = useMemo(
() =>
columnPickerItems.filter((column) => hiddenColumnIds.includes(column.id)),
[columnPickerItems, hiddenColumnIds],
);
const handleRemoveColumn = (columnId: string): void => {
hideColumn(storageKey, columnId);
};
const handleAddColumn = (columnId: string): void => {
showColumn(storageKey, columnId);
};
const drawerContent = (
<>
<div className={styles.columnsTitle}>Added Columns (Click to remove)</div>
<div className={styles.columnsList}>
{addedColumns.map((column) => (
<div className={styles.columnItem} key={column.id}>
<Button
variant="ghost"
color="none"
className={styles.columnItem}
disabled={!column.canBeHidden}
data-testid={`remove-column-${column.id}`}
onClick={(): void => handleRemoveColumn(column.id)}
>
{column.label}
</Button>
</div>
))}
</div>
<div className={styles.horizontalDivider} />
<div className={styles.columnsTitle}>Other Columns (Click to add)</div>
<div className={styles.columnsList}>
{hiddenColumns.map((column) => (
<div className={styles.columnItem} key={column.id}>
<Button
variant="ghost"
color="none"
className={styles.columnItem}
data-can-be-added="true"
data-testid={`add-column-${column.id}`}
onClick={(): void => handleAddColumn(column.id)}
tabIndex={0}
>
{column.label}
</Button>
</div>
))}
</div>
</>
);
return (
<DrawerWrapper
open={open}
onOpenChange={(isOpen): void => {
if (!isOpen) {
onClose();
}
}}
title="Columns"
direction="right"
showCloseButton
showOverlay={false}
className={styles.drawer}
>
{drawerContent}
</DrawerWrapper>
);
}
export default K8sFiltersSidePanel;

View File

@@ -5,10 +5,25 @@
align-items: stretch;
gap: var(--spacing-4);
width: 100%;
:global(.ant-select-selector) {
border-radius: 2px;
border: 1px solid var(--l2-border) !important;
background-color: var(--l2-background) !important;
input {
font-size: 12px;
}
:global([data-slot='badge'] .ant-typography) {
font-size: 12px;
}
}
}
.k8SListControlsRow {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-4);
width: 100%;
align-items: center;
@@ -19,6 +34,27 @@
width: 100%;
}
.k8SFiltersGroupByRow {
display: flex;
flex: 1 1 auto;
align-items: center;
justify-content: flex-end;
gap: var(--spacing-4);
min-width: 0;
@media (max-width: 1600px) {
flex: 1 1 100%;
order: 9;
}
}
.k8SAttributeSearchContainer {
flex: 1 1 240px;
max-width: 400px;
display: flex;
align-items: center;
}
.k8SRunButton {
flex: 0 0 auto;
}
@@ -28,12 +64,17 @@
}
.k8SDateTimeSelection {
margin-left: auto;
flex: 0 0 auto;
display: flex;
justify-content: flex-end;
--date-time-selector-border-color: var(--l2-border);
@media (max-width: 1200px) {
flex: 1 1 100%;
order: 10;
}
:global(.timeSelection-input) {
height: 32px;
}
@@ -54,3 +95,42 @@
}
}
}
.groupByLabel {
min-width: max-content;
font-size: var(--periscope-font-size-base, 13px);
font-weight: var(--periscope-font-weight-regular, 400);
line-height: 18px;
letter-spacing: -0.07px;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--l2-border);
border-right: none;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
display: flex;
height: 32px;
padding: var(--spacing-3) var(--spacing-3) var(--spacing-3) var(--spacing-4);
justify-content: center;
align-items: center;
gap: var(--spacing-2);
}
.groupBySelect {
width: 100%;
:global(.ant-select-selector) {
border-left: none;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
}
.k8SListControlsRight {
min-width: 240px;
display: flex;
align-items: center;
gap: var(--spacing-2);
}

View File

@@ -1,22 +1,29 @@
import React, { useCallback, useMemo, useRef } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { Select } from 'antd';
import logEvent from 'api/common/logEvent';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
import { useGetFieldsKeys } from 'api/generated/services/fields';
import {
InfraMonitoringEvents,
logInfraFilterCustomizedEvent,
} from 'constants/events';
TelemetrytypesFieldContextDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { TableColumnDef } from 'components/TanStackTableView';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
import { InfraMonitoringEvents } from 'constants/events';
import { QueryParams } from 'constants/query';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
applySerializedParams,
serialize,
} from 'lib/compositeQuery/serializer';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { SlidersHorizontal } from '@signozhq/icons';
import { v4 as uuid } from 'uuid';
import { useGlobalTimeQueryInvalidate } from 'store/globalTime';
import {
useGlobalTimeQueryInvalidate,
useGlobalTimeStore,
} from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
import { DataSource } from 'types/common/queryBuilder';
import {
@@ -24,25 +31,37 @@ import {
InfraMonitoringEntity,
METRIC_NAMESPACE_BY_ENTITY,
} from '../constants';
import { useInfraMonitoringPageListing } from '../hooks';
import {
useInfraMonitoringGroupBy,
useInfraMonitoringPageListing,
} from '../hooks';
import K8sFiltersSidePanel from './K8sFiltersSidePanel';
import styles from './K8sHeader.module.scss';
import { TooltipSimple } from '@signozhq/ui/tooltip';
interface K8sHeaderProps {
interface K8sHeaderProps<TData> {
controlListPrefix?: React.ReactNode;
leftFilters?: React.ReactNode;
entity: InfraMonitoringEntity;
showAutoRefresh: boolean;
columns: TableColumnDef<TData>[];
columnStorageKey: string;
isFetching?: boolean;
cancelQuery: () => void;
}
function K8sHeader({
function K8sHeader<TData>({
controlListPrefix,
leftFilters,
entity,
showAutoRefresh,
columns,
columnStorageKey,
isFetching = false,
cancelQuery,
}: K8sHeaderProps): JSX.Element {
}: K8sHeaderProps<TData>): JSX.Element {
const [isFiltersSidePanelOpen, setIsFiltersSidePanelOpen] = useState(false);
// null = user never touched the search box; fall back to the current
// query expression on Run so an untouched search doesn't wipe filters
const stagedExpressionRef = useRef<string | null>(null);
@@ -88,7 +107,10 @@ function K8sHeader({
// Use window.location.search to get fresh URL params (avoids stale hook state)
const newUrlQuery = new URLSearchParams(window.location.search);
applySerializedParams(serialize(updatedQuery), newUrlQuery);
newUrlQuery.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(updatedQuery)),
);
safeNavigate(`${location.pathname}?${newUrlQuery.toString()}`);
void invalidateQueries();
@@ -99,8 +121,6 @@ function K8sHeader({
page: InfraMonitoringEvents.ListPage,
category: entity,
});
logInfraFilterCustomizedEvent(entity, 'search', finalExpression);
}
},
[
@@ -125,11 +145,101 @@ function K8sHeader({
cancelQuery();
}, [cancelQuery]);
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
const { minTime, maxTime } = getMinMaxTime();
const startUnixMilli = Math.floor(minTime / NANO_SECOND_MULTIPLIER);
const endUnixMilli = Math.floor(maxTime / NANO_SECOND_MULTIPLIER);
const { data: groupByFiltersData, isLoading: isLoadingGroupByFilters } =
useGetFieldsKeys(
{
signal: TelemetrytypesSignalDTO.metrics,
metricNamespace: METRIC_NAMESPACE_BY_ENTITY[entity],
limit: 100,
startUnixMilli,
endUnixMilli,
fieldContext: TelemetrytypesFieldContextDTO.resource,
// the search text is intentionally not included
// searchText: expression,
},
{
query: {
queryKey: ['getFieldsKeys', entity, startUnixMilli, endUnixMilli],
},
},
);
const flatFieldKeys = useMemo(() => {
const keys = groupByFiltersData?.data?.keys;
if (!keys) {
return [];
}
const allKeys = Object.values(keys).flat();
const seen = new Set<string>();
return allKeys.filter((field) => {
if (seen.has(field.name)) {
return false;
}
seen.add(field.name);
return true;
});
}, [groupByFiltersData]);
const groupByOptions = useMemo(
() =>
flatFieldKeys.map((field) => ({
value: field.name,
label: field.name,
})),
[flatFieldKeys],
);
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
const handleGroupByChange = useCallback(
(value: string[]) => {
void setCurrentPage(1);
void setGroupBy(value);
void logEvent(InfraMonitoringEvents.GroupByChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: InfraMonitoringEvents.Pod,
});
},
[setCurrentPage, setGroupBy],
);
const onClickOutside = useCallback(() => {
setIsFiltersSidePanelOpen(false);
}, []);
return (
<div className={styles.k8SListControls}>
<div className={styles.k8SListControlsRow}>
{controlListPrefix}
<div className={styles.k8SFiltersGroupByRow}>
{leftFilters}
<div className={styles.k8SAttributeSearchContainer}>
<div className={styles.groupByLabel}>Group by</div>
<Select
className={styles.groupBySelect}
loading={isLoadingGroupByFilters}
mode="multiple"
value={groupBy}
allowClear
maxTagCount="responsive"
placeholder="Search for attribute"
options={groupByOptions}
onChange={handleGroupByChange}
/>
</div>
</div>
<div className={styles.k8SDateTimeSelection}>
<DateTimeSelectionV2
showAutoRefresh={showAutoRefresh}
@@ -145,6 +255,20 @@ function K8sHeader({
handleCancelQuery={handleCancelQuery}
className={styles.k8SRunButton}
/>
<TooltipSimple title="Click to add more columns to this table">
<Button
type="button"
variant="outlined"
size="icon"
color="secondary"
data-testid="k8s-list-filters-button"
onClick={(): void => setIsFiltersSidePanelOpen(true)}
className={styles.k8SFiltersButton}
>
<SlidersHorizontal size={14} />
</Button>
</TooltipSimple>
</div>
<div className={styles.k8SQbSearchContainer}>
@@ -159,6 +283,13 @@ function K8sHeader({
metricNamespace={METRIC_NAMESPACE_BY_ENTITY[entity]}
/>
</div>
<K8sFiltersSidePanel
open={isFiltersSidePanelOpen}
columns={columns}
storageKey={columnStorageKey}
onClose={onClickOutside}
/>
</div>
);
}

View File

@@ -1,105 +0,0 @@
.drawer {
// for some reason, the scrollbar is overriding this
// for now, I will keep forcing but needs to investigate a global level
// why all drawers are being override with l1-background
background-color: var(--l2-background) !important;
border-color: var(--l2-border) !important;
--dialog-description-padding: 0px 0px var(--spacing-8) 0px;
[data-slot='drawer-description'] {
overflow-y: auto;
}
}
.sectionTitle {
padding: var(--spacing-4) var(--spacing-8);
border-bottom: 1px solid var(--l2-border);
&:not(:first-of-type) {
border-top: 1px solid var(--l2-border);
}
}
.sectionTitleText {
text-transform: uppercase;
}
.fontSizeContainer {
display: flex;
flex-direction: column;
padding: var(--spacing-4) 0;
}
.fontSizeOption {
width: 100%;
--button-display: flex;
--button-justify-content: space-between;
--button-align-items: center;
--button-padding: var(--spacing-4) var(--spacing-8);
}
.fontSizeLabel {
text-transform: capitalize;
}
.checkIcon {
color: var(--accent-foreground);
}
.lineClampContainer {
padding: 12px;
--input-prefix-padding: var(--spacing-3);
--input-suffix-padding: var(--spacing-3);
}
.columnsList {
display: flex;
flex-direction: column;
[data-slot='icon'] {
display: none;
}
[data-slot='entity-group-header'] {
padding-left: 0;
}
[data-slot='column-header'] {
display: flex;
width: auto;
span {
padding: 0;
text-align: left;
}
br {
display: none;
}
}
}
.columnItem {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-2) var(--spacing-8);
> span {
width: auto !important;
}
&:first-child {
padding-top: var(--spacing-8);
}
}
.columnLabel {
flex: 1;
text-align: left;
}
.horizontalDivider {
border-top: 1px solid var(--l1-border);
}

View File

@@ -1,256 +0,0 @@
import { ChangeEvent, ReactNode, useCallback, useMemo } from 'react';
import { Button } from '@signozhq/ui/button';
import { DrawerWrapper } from '@signozhq/ui/drawer';
import { Input } from '@signozhq/ui/input';
import { Switch } from '@signozhq/ui/switch';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Check, Minus, Plus } from '@signozhq/icons';
import {
hideColumn,
showColumn,
TableColumnDef,
useColumnOrder,
useHiddenColumnIds,
} from 'components/TanStackTableView';
import { InfraMonitoringEntity } from '../constants';
import {
FontSize,
useInfraMonitoringTablePreferencesStore,
} from './useInfraMonitoringTablePreferencesStore';
import { useLogEventForColumnCustomized } from './useLogEventForColumnCustomized';
import { sortByColumnOrder } from './utils';
import styles from './K8sOptionsSidePanel.module.scss';
import { Typography } from '@signozhq/ui/typography';
type ColumnPickerItem = {
id: string;
label: ReactNode;
canBeHidden: boolean;
visibilityBehavior:
| 'hidden-on-expand'
| 'hidden-on-collapse'
| 'always-visible';
};
function renderHeader(header: string | (() => ReactNode)): ReactNode {
return typeof header === 'function' ? header() : header;
}
function toColumnPickerItems<T>(
columns: TableColumnDef<T>[],
): ColumnPickerItem[] {
return columns.map((col) => ({
id: col.id,
label: renderHeader(col.header),
canBeHidden: col.canBeHidden !== false && col.enableRemove !== false,
visibilityBehavior: col.visibilityBehavior ?? 'always-visible',
}));
}
const FONT_SIZE_OPTIONS: { value: FontSize; label: string }[] = [
{ value: 'small', label: 'Small' },
{ value: 'medium', label: 'Medium' },
{ value: 'large', label: 'Large' },
];
function K8sOptionsSidePanel<TData>({
open,
onClose,
columns,
storageKey,
entity,
}: {
open: boolean;
onClose: () => void;
columns: TableColumnDef<TData>[];
storageKey: string;
entity: InfraMonitoringEntity;
}): JSX.Element {
const columnPickerItems = useMemo(
() => toColumnPickerItems(columns),
[columns],
);
const hiddenColumnIds = useHiddenColumnIds(storageKey);
const columnOrder = useColumnOrder(storageKey);
const lineClamp = useInfraMonitoringTablePreferencesStore((s) => s.lineClamp);
const fontSize = useInfraMonitoringTablePreferencesStore((s) => s.fontSize);
const increaseLineClamp = useInfraMonitoringTablePreferencesStore(
(s) => s.increaseLineClamp,
);
const decreaseLineClamp = useInfraMonitoringTablePreferencesStore(
(s) => s.decreaseLineClamp,
);
const setLineClamp = useInfraMonitoringTablePreferencesStore(
(s) => s.setLineClamp,
);
const setFontSize = useInfraMonitoringTablePreferencesStore(
(s) => s.setFontSize,
);
const visibleColumnItems = useMemo(
() =>
columnPickerItems.filter(
(column) => column.visibilityBehavior !== 'hidden-on-collapse',
),
[columnPickerItems],
);
const orderedVisibleColumnItems = useMemo(
() => sortByColumnOrder(visibleColumnItems, (col) => col.id, columnOrder),
[visibleColumnItems, columnOrder],
);
useLogEventForColumnCustomized({
entity,
source: 'list',
storageKey,
columns,
});
const handleToggleColumn = (columnId: string, checked: boolean): void => {
if (checked) {
showColumn(storageKey, columnId);
} else {
hideColumn(storageKey, columnId);
}
};
const handleLineClampChange = useCallback(
(value: ChangeEvent<HTMLInputElement>): void => {
const valueAsNumber = value.target.valueAsNumber;
if (value && Number.isInteger(valueAsNumber)) {
setLineClamp(valueAsNumber);
}
},
[setLineClamp],
);
const drawerContent = (
<>
<div className={styles.sectionTitle}>
<Typography.Text size="sm" className={styles.sectionTitleText}>
Font Size
</Typography.Text>
</div>
<div className={styles.fontSizeContainer}>
{FONT_SIZE_OPTIONS.map((option) => (
<Button
key={option.value}
variant="ghost"
color="none"
className={styles.fontSizeOption}
data-testid={`font-size-${option.value}`}
onClick={(): void => setFontSize(option.value)}
>
{option.label}
{fontSize === option.value && (
<Check size={14} className={styles.checkIcon} />
)}
</Button>
))}
</div>
<div className={styles.horizontalDivider} />
<div className={styles.sectionTitle}>
<Typography.Text size="sm" className={styles.sectionTitleText}>
Max lines per row
</Typography.Text>
</div>
<div className={styles.lineClampContainer}>
<Input
min={1}
max={10}
value={lineClamp}
onChange={handleLineClampChange}
data-testid="line-clamp-input"
type="number"
prefix={
<Button
variant="solid"
color="primary"
size="sm"
className={styles.lineClampButton}
data-testid="line-clamp-decrease"
onClick={decreaseLineClamp}
prefix={<Minus />}
disabled={lineClamp <= 1}
/>
}
suffix={
<Button
variant="solid"
color="primary"
size="sm"
className={styles.lineClampButton}
data-testid="line-clamp-increase"
onClick={increaseLineClamp}
prefix={<Plus />}
disabled={lineClamp >= 10}
/>
}
/>
</div>
<div className={styles.horizontalDivider} />
<div className={styles.sectionTitle}>
<Typography.Text size="sm" className={styles.sectionTitleText}>
Columns
</Typography.Text>
</div>
<div className={styles.columnsList}>
{orderedVisibleColumnItems.map((column) => {
const isVisible = !hiddenColumnIds.includes(column.id);
const switchElement = (
<Switch
value={isVisible}
disabled={!column.canBeHidden}
data-testid={`toggle-column-${column.id}`}
onChange={(checked): void => handleToggleColumn(column.id, checked)}
/>
);
return (
<div className={styles.columnItem} key={column.id}>
<Typography.Text size="sm" className={styles.columnLabel}>
{column.label}
</Typography.Text>
{column.canBeHidden ? (
switchElement
) : (
<TooltipSimple title="Required column cannot be hidden" arrow>
{switchElement}
</TooltipSimple>
)}
</div>
);
})}
</div>
</>
);
return (
<DrawerWrapper
open={open}
onOpenChange={(isOpen): void => {
if (!isOpen) {
onClose();
}
}}
title="Options"
direction="right"
width="narrow"
showCloseButton
showOverlay={false}
className={styles.drawer}
>
{drawerContent}
</DrawerWrapper>
);
}
export default K8sOptionsSidePanel;

View File

@@ -1,65 +0,0 @@
.toolbar {
display: flex;
align-items: center;
gap: var(--spacing-3);
padding: 0px var(--spacing-4) var(--spacing-4) var(--spacing-4);
background: var(--l1-background);
border-bottom: 1px solid var(--l1-border);
}
.groupByContainer {
display: flex;
align-items: center;
min-width: 300px;
max-width: 400px;
:global(.ant-select-selector) {
border-radius: 2px;
border: 1px solid var(--l2-border) !important;
background-color: var(--l2-background) !important;
input {
font-size: 12px;
}
:global([data-slot='badge'] .ant-typography) {
font-size: 12px;
}
}
}
.groupByLabel {
min-width: max-content;
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
border: 1px solid var(--l2-border);
border-right: none;
display: flex;
height: 32px;
padding: var(--spacing-3) var(--spacing-3) var(--spacing-3) var(--spacing-4);
justify-content: center;
align-items: center;
gap: var(--spacing-2);
}
.groupBySelect {
width: 100%;
:global(.ant-select-selector) {
border-left: none;
border-top-left-radius: 0px !important;
border-bottom-left-radius: 0px !important;
}
}
.spacer {
flex: 1;
}
.toolbarButton {
flex-shrink: 0;
--button-padding: 0px;
}

View File

@@ -1,112 +0,0 @@
import { useCallback } from 'react';
import { Button } from '@signozhq/ui/button';
import { Select } from 'antd';
import { Download, SlidersVertical } from '@signozhq/icons';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import logEvent from 'api/common/logEvent';
import {
InfraMonitoringEvents,
logInfraGroupByCustomizedEvent,
} from 'constants/events';
import { InfraMonitoringEntity } from '../constants';
import {
useInfraMonitoringGroupBy,
useInfraMonitoringPageListing,
} from '../hooks';
import { useInfraMonitoringGroupByData } from './useInfraMonitoringGroupByData';
import styles from './K8sTableToolbar.module.scss';
interface K8sTableToolbarProps {
entity: InfraMonitoringEntity;
eventCategory: InfraMonitoringEvents;
leftFilters?: React.ReactNode;
onOpenOptionsDrawer: () => void;
onDownload?: () => void;
}
function K8sTableToolbar({
entity,
eventCategory,
leftFilters,
onOpenOptionsDrawer,
onDownload,
}: K8sTableToolbarProps): JSX.Element {
const { groupByOptions, isLoading: isLoadingGroupByFilters } =
useInfraMonitoringGroupByData(entity);
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
const [, setCurrentPage] = useInfraMonitoringPageListing();
const handleGroupByChange = useCallback(
(value: string[]) => {
void setCurrentPage(1);
void setGroupBy(value);
void logEvent(InfraMonitoringEvents.GroupByChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: eventCategory,
});
logInfraGroupByCustomizedEvent(entity, value);
},
[entity, eventCategory, setCurrentPage, setGroupBy],
);
return (
<div className={styles.toolbar}>
<div className={styles.groupByContainer}>
<div className={styles.groupByLabel}>Group by</div>
<Select
className={styles.groupBySelect}
loading={isLoadingGroupByFilters}
mode="multiple"
value={groupBy}
allowClear
maxTagCount="responsive"
placeholder="Search for attribute"
options={groupByOptions}
onChange={handleGroupByChange}
/>
</div>
<div className={styles.spacer} />
{leftFilters}
{onDownload && (
<TooltipSimple title="Download">
<Button
type="button"
variant="ghost"
size="icon"
color="secondary"
data-testid="k8s-table-download-button"
onClick={onDownload}
className={styles.toolbarButton}
>
<Download size={14} />
</Button>
</TooltipSimple>
)}
<TooltipSimple title="Options">
<Button
type="button"
variant="ghost"
size="icon"
color="secondary"
data-testid="k8s-table-options-button"
onClick={onOpenOptionsDrawer}
className={styles.toolbarButton}
>
<SlidersVertical size={14} />
</Button>
</TooltipSimple>
</div>
);
}
export default K8sTableToolbar;

View File

@@ -1,187 +0,0 @@
/* eslint-disable no-restricted-syntax */
import { act, renderHook } from '@testing-library/react';
import { TableColumnDef, useColumnStore } from 'components/TanStackTableView';
import { logInfraColumnCustomizedEvent } from 'constants/events';
import { InfraMonitoringEntity } from '../../constants';
import { useInfraMonitoringTablePreferencesStore } from '../useInfraMonitoringTablePreferencesStore';
import { useLogEventForColumnCustomized } from '../useLogEventForColumnCustomized';
jest.mock('constants/events', () => ({
logInfraColumnCustomizedEvent: jest.fn(),
}));
const mockLogEvent = logInfraColumnCustomizedEvent as jest.Mock;
type TestRow = { id: string; name: string };
const col = (id: string): TableColumnDef<TestRow> => ({
id,
header: id,
cell: (): null => null,
});
const columns = [col('a'), col('b'), col('c')];
describe('useLogEventForColumnCustomized', () => {
beforeEach(() => {
useColumnStore.setState({ tables: {} });
useInfraMonitoringTablePreferencesStore.setState({
lineClamp: 1,
fontSize: 'medium',
});
localStorage.clear();
mockLogEvent.mockClear();
});
it('does not emit on mount', () => {
const storageKey = 'test-no-emit-on-mount';
act(() => {
useColumnStore.getState().initializeFromDefaults(storageKey, columns);
});
renderHook(() =>
useLogEventForColumnCustomized({
entity: InfraMonitoringEntity.PODS,
source: 'list',
storageKey,
columns,
}),
);
expect(mockLogEvent).not.toHaveBeenCalled();
});
it('emits when a column is hidden after mount', () => {
const storageKey = 'test-emit-on-hide';
act(() => {
useColumnStore.getState().initializeFromDefaults(storageKey, columns);
});
renderHook(() =>
useLogEventForColumnCustomized({
entity: InfraMonitoringEntity.PODS,
source: 'list',
storageKey,
columns,
}),
);
act(() => {
useColumnStore.getState().hideColumn(storageKey, 'b');
});
expect(mockLogEvent).toHaveBeenCalledTimes(1);
expect(mockLogEvent).toHaveBeenCalledWith(
InfraMonitoringEntity.PODS,
['a', 'c'],
'medium',
1,
'list',
);
});
it('emits when font size changes after mount', () => {
const storageKey = 'test-emit-on-font-size';
act(() => {
useColumnStore.getState().initializeFromDefaults(storageKey, columns);
});
renderHook(() =>
useLogEventForColumnCustomized({
entity: InfraMonitoringEntity.PODS,
source: 'list',
storageKey,
columns,
}),
);
act(() => {
useInfraMonitoringTablePreferencesStore.getState().setFontSize('small');
});
expect(mockLogEvent).toHaveBeenCalledTimes(1);
});
// Regression: K8sDynamicList keeps K8sBaseList (and the options side panel)
// mounted across category switches, so this hook survives a Pods -> Nodes
// switch with every input changed. The switch itself is not a customization
// and must not emit.
it('does not emit when the storage key changes (category switch)', () => {
const podsKey = 'test-switch-pods';
const nodesKey = 'test-switch-nodes';
const nodeColumns = [col('x'), col('y')];
act(() => {
useColumnStore.getState().initializeFromDefaults(podsKey, columns);
useColumnStore.getState().initializeFromDefaults(nodesKey, nodeColumns);
});
const { rerender } = renderHook(
(props) => useLogEventForColumnCustomized(props),
{
initialProps: {
entity: InfraMonitoringEntity.PODS,
source: 'list' as const,
storageKey: podsKey,
columns,
},
},
);
rerender({
entity: InfraMonitoringEntity.NODES,
source: 'list' as const,
storageKey: nodesKey,
columns: nodeColumns,
});
expect(mockLogEvent).not.toHaveBeenCalled();
// A real customization after the switch still emits, for the new entity
act(() => {
useColumnStore.getState().hideColumn(nodesKey, 'y');
});
expect(mockLogEvent).toHaveBeenCalledTimes(1);
expect(mockLogEvent).toHaveBeenCalledWith(
InfraMonitoringEntity.NODES,
['x'],
'medium',
1,
'list',
);
});
// Regression: every expanded group row mounts its own instance sharing the
// `k8s-<entity>-columns-expanded` storage key; one customization must not
// emit once per row.
it('emits once when multiple instances share a storage key', () => {
const storageKey = 'test-shared-key';
act(() => {
useColumnStore.getState().initializeFromDefaults(storageKey, columns);
});
const params = {
entity: InfraMonitoringEntity.PODS,
source: 'expanded' as const,
storageKey,
columns,
};
renderHook(() => useLogEventForColumnCustomized(params));
renderHook(() => useLogEventForColumnCustomized(params));
renderHook(() => useLogEventForColumnCustomized(params));
act(() => {
useColumnStore.getState().hideColumn(storageKey, 'a');
});
expect(mockLogEvent).toHaveBeenCalledTimes(1);
// A subsequent distinct customization emits again, exactly once
act(() => {
useColumnStore.getState().hideColumn(storageKey, 'b');
});
expect(mockLogEvent).toHaveBeenCalledTimes(2);
});
});

View File

@@ -1,16 +1,10 @@
import { Tooltip } from 'antd';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import { Compass } from '@signozhq/icons';
import { TextNoData } from '../../../components/TextNoData';
import { logInfraExplorerNavigatedEvent } from 'constants/events';
import { QueryParams } from 'constants/query';
import { initialQueriesMap } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import {
applySerializedParams,
serialize,
} from 'lib/compositeQuery/serializer';
import { Link } from 'react-router-dom';
import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
@@ -19,7 +13,6 @@ import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
InfraMonitoringEntity,
} from '../../../constants';
import { getDrawerDurationMs } from '../../useDrawerLifecycleStore';
import styles from './EntityCountsSection.module.scss';
export interface EntityCountConfig<T> {
@@ -34,8 +27,6 @@ interface EntityCountsSectionProps<T> {
selectedItem: string;
filterExpression: string;
closeDrawer: () => void;
entityType: InfraMonitoringEntity;
activeTab: string;
}
export function EntityCountsSection<T>({
@@ -44,21 +35,7 @@ export function EntityCountsSection<T>({
selectedItem,
filterExpression,
closeDrawer,
entityType,
activeTab,
}: EntityCountsSectionProps<T>): JSX.Element {
const handleCardNavigate = (cardLabel: string): void => {
logInfraExplorerNavigatedEvent({
entityType,
destination: 'k8s_list',
source: 'stats_card',
tab: activeTab,
sourceKey: cardLabel,
drawerDurationMsAtNavigation: getDrawerDurationMs(),
});
closeDrawer();
};
const buildNavigationUrl = (targetCategory: InfraMonitoringEntity): string => {
const defaultQuery = initialQueriesMap[DataSource.METRICS];
@@ -77,7 +54,10 @@ export function EntityCountsSection<T>({
const urlParams = new URLSearchParams();
urlParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.CATEGORY, targetCategory);
applySerializedParams(serialize(compositeQuery), urlParams);
urlParams.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(compositeQuery)),
);
const currentSearchParams = new URLSearchParams(window.location.search);
const detailRelativeTime = currentSearchParams.get(
@@ -136,26 +116,17 @@ export function EntityCountsSection<T>({
>
{config.label}
</Typography.Text>
{config.getValue(entity) ? (
<Typography.Text
className={styles.countValue}
size="xl"
weight="semibold"
>
{config.getValue(entity)}
</Typography.Text>
) : (
<TextNoData type="typography" className={styles.countValue} />
)}
<Typography.Text className={styles.countValue} size="xl" weight="semibold">
{config.getValue(entity) || '-'}
</Typography.Text>
<Link
to={buildNavigationUrl(config.targetCategory)}
onClick={(): void => handleCardNavigate(config.label)}
onClick={closeDrawer}
data-testid={`navigate-${config.label.toLowerCase().replace(/\s+/g, '-')}`}
>
<TooltipSimple
<Tooltip
title={`View ${config.label.toLowerCase()} of '${selectedItem}'`}
side="top"
arrow
placement="top"
>
<Button
size="icon"
@@ -164,7 +135,7 @@ export function EntityCountsSection<T>({
className={styles.navigateButton}
prefix={<Compass size={14} />}
/>
</TooltipSimple>
</Tooltip>
</Link>
</div>
))}

View File

@@ -1,23 +0,0 @@
import { create } from 'zustand';
export interface IDrawerLifecycleStore {
openedAt: number | null;
markOpened: () => void;
markClosed: () => void;
getDrawerDurationMs: () => number | null;
}
export const useDrawerLifecycleStore = create<IDrawerLifecycleStore>()(
(set, get) => ({
openedAt: null,
markOpened: (): void => set({ openedAt: Date.now() }),
markClosed: (): void => set({ openedAt: null }),
getDrawerDurationMs: (): number | null => {
const { openedAt } = get();
return openedAt == null ? null : Date.now() - openedAt;
},
}),
);
export const getDrawerDurationMs = (): number | null =>
useDrawerLifecycleStore.getState().getDrawerDurationMs();

View File

@@ -1,73 +0,0 @@
import { useMemo } from 'react';
import { useGetFieldsKeys } from 'api/generated/services/fields';
import {
TelemetrytypesFieldContextDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { useGlobalTimeStore } from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
import {
InfraMonitoringEntity,
METRIC_NAMESPACE_BY_ENTITY,
} from '../constants';
export interface GroupByOption {
value: string;
label: string;
}
export interface UseInfraMonitoringGroupByDataReturn {
groupByOptions: GroupByOption[];
isLoading: boolean;
}
export function useInfraMonitoringGroupByData(
entity: InfraMonitoringEntity,
): UseInfraMonitoringGroupByDataReturn {
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
const { minTime, maxTime } = getMinMaxTime();
const startUnixMilli = Math.floor(minTime / NANO_SECOND_MULTIPLIER);
const endUnixMilli = Math.floor(maxTime / NANO_SECOND_MULTIPLIER);
const { data: groupByFiltersData, isLoading } = useGetFieldsKeys(
{
signal: TelemetrytypesSignalDTO.metrics,
metricNamespace: METRIC_NAMESPACE_BY_ENTITY[entity],
limit: 100,
startUnixMilli,
endUnixMilli,
fieldContext: TelemetrytypesFieldContextDTO.resource,
},
{
query: {
queryKey: ['getFieldsKeys', entity, startUnixMilli, endUnixMilli],
},
},
);
const groupByOptions = useMemo(() => {
const keys = groupByFiltersData?.data?.keys;
if (!keys) {
return [];
}
const allKeys = Object.values(keys).flat();
const seen = new Set<string>();
return allKeys
.filter((field) => {
if (seen.has(field.name)) {
return false;
}
seen.add(field.name);
return true;
})
.map((field) => ({
value: field.name,
label: field.name,
}));
}, [groupByFiltersData]);
return { groupByOptions, isLoading };
}

View File

@@ -1,58 +0,0 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { CellTypographySize } from 'components/TanStackTableView';
export type FontSize = CellTypographySize;
export interface IInfraMonitoringTablePreferencesStore {
lineClamp: number;
fontSize: FontSize;
increaseLineClamp: () => void;
decreaseLineClamp: () => void;
setLineClamp: (value: number) => void;
setFontSize: (value: FontSize) => void;
}
const MIN_LINE_CLAMP = 1;
const MAX_LINE_CLAMP = 10;
const DEFAULT_LINE_CLAMP = 1;
const DEFAULT_FONT_SIZE: FontSize = 'medium';
export const useInfraMonitoringTablePreferencesStore =
create<IInfraMonitoringTablePreferencesStore>()(
persist(
(set, get) => ({
lineClamp: DEFAULT_LINE_CLAMP,
fontSize: DEFAULT_FONT_SIZE,
increaseLineClamp: (): void => {
const current = get().lineClamp;
if (current < MAX_LINE_CLAMP) {
set({ lineClamp: current + 1 });
}
},
decreaseLineClamp: (): void => {
const current = get().lineClamp;
if (current > MIN_LINE_CLAMP) {
set({ lineClamp: current - 1 });
}
},
setLineClamp: (value: number): void => {
if (value >= MIN_LINE_CLAMP && value <= MAX_LINE_CLAMP) {
set({ lineClamp: value });
}
},
setFontSize: (value: FontSize): void => {
set({ fontSize: value });
},
}),
{
name: '@signoz/infra-monitoring-table-preferences',
},
),
);
export const useInfraMonitoringLineClamp = (): number =>
useInfraMonitoringTablePreferencesStore((s) => s.lineClamp);
export const useInfraMonitoringFontSize = (): FontSize =>
useInfraMonitoringTablePreferencesStore((s) => s.fontSize);

View File

@@ -1,89 +0,0 @@
import { useEffect, useMemo, useRef } from 'react';
import {
TableColumnDef,
useColumnOrder,
useHiddenColumnIds,
} from 'components/TanStackTableView';
import { logInfraColumnCustomizedEvent } from 'constants/events';
import { InfraMonitoringEntity } from '../constants';
import {
useInfraMonitoringFontSize,
useInfraMonitoringLineClamp,
} from './useInfraMonitoringTablePreferencesStore';
import { sortByColumnOrder } from './utils';
interface UseEmitColumnCustomizedParams<TData> {
entity: InfraMonitoringEntity;
source: 'list' | 'expanded';
storageKey: string;
columns: TableColumnDef<TData>[];
}
// Multiple instances of this hook can share a storageKey (every expanded group
// row mounts one), so the last emitted payload is tracked per key to collapse
// their simultaneous effect runs into a single event.
const lastEmittedPayloadByKey = new Map<string, string>();
export function useLogEventForColumnCustomized<TData>({
entity,
source,
storageKey,
columns,
}: UseEmitColumnCustomizedParams<TData>): void {
const hiddenColumnIds = useHiddenColumnIds(storageKey);
const columnOrder = useColumnOrder(storageKey);
const fontSize = useInfraMonitoringFontSize();
const lineClamp = useInfraMonitoringLineClamp();
const hiddenBehavior =
source === 'expanded' ? 'hidden-on-expand' : 'hidden-on-collapse';
const orderedVisibleColumnIds = useMemo(() => {
const visibleColumns = columns.filter(
(col) =>
(col.visibilityBehavior ?? 'always-visible') !== hiddenBehavior &&
!hiddenColumnIds.includes(col.id),
);
return sortByColumnOrder(visibleColumns, (col) => col.id, columnOrder).map(
(col) => col.id,
);
}, [columns, hiddenColumnIds, columnOrder, hiddenBehavior]);
const isFirstRender = useRef(true);
const prevStorageKeyRef = useRef(storageKey);
// Re-arm the first-render skip when the table identity changes (the hook
// survives category switches because K8sDynamicList keeps K8sBaseList
// mounted), so the switch itself is not reported as a customization
if (prevStorageKeyRef.current !== storageKey) {
prevStorageKeyRef.current = storageKey;
isFirstRender.current = true;
}
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
const dedupeKey = `${storageKey}:${source}`;
const payload = JSON.stringify([
entity,
orderedVisibleColumnIds,
fontSize,
lineClamp,
]);
if (lastEmittedPayloadByKey.get(dedupeKey) === payload) {
return;
}
lastEmittedPayloadByKey.set(dedupeKey, payload);
logInfraColumnCustomizedEvent(
entity,
orderedVisibleColumnIds,
fontSize,
lineClamp,
source,
);
}, [entity, source, storageKey, orderedVisibleColumnIds, fontSize, lineClamp]);
}

View File

@@ -9,22 +9,6 @@ import {
import { SelectedItemParams } from 'container/InfraMonitoringK8sV2/hooks';
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
export function sortByColumnOrder<T>(
items: T[],
getId: (item: T) => string,
columnOrder: string[],
): T[] {
if (columnOrder.length === 0) {
return items;
}
const orderIndex = new Map(columnOrder.map((id, index) => [id, index]));
return [...items].sort(
(a, b) =>
(orderIndex.get(getId(a)) ?? Number.MAX_SAFE_INTEGER) -
(orderIndex.get(getId(b)) ?? Number.MAX_SAFE_INTEGER),
);
}
export function getGroupedByMeta<
T extends { meta?: Record<string, string> | null },
>(itemData: T, groupBy: string[]): Record<string, string> {

View File

@@ -10,8 +10,8 @@ import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import {
CellValueTooltip,
GroupedStatusCounts,
TextNoData,
ValidateColumnValueWrapper,
} from '../components';
import {
@@ -75,9 +75,10 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
enableMove: false,
pin: 'left',
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => (
<TanStackTable.Text>{value}</TanStackTable.Text>
),
cell: ({ value }): React.ReactNode => {
const clusterName = value as string;
return <CellValueTooltip value={clusterName} />;
},
},
{
id: 'nodeCountsByReadiness',
@@ -94,7 +95,7 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
enableSort: false,
cell: ({ row, rowId }): React.ReactNode => {
if (!row.nodeCountsByReadiness) {
return <TextNoData type="tanstack" />;
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
@@ -132,7 +133,7 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
cell: ({ row, rowId }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TextNoData type="tanstack" />;
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts
@@ -146,17 +147,17 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
id: 'cpu',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#cpu-usage-cores">
CPU Usage (cores)
CPU Usage
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.clusterCPU,
width: { min: 160 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={cpu}
entity={InfraMonitoringEntity.CLUSTERS}
attribute="CPU metric"
@@ -170,17 +171,17 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
id: 'cpu_allocatable',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#cpu-alloc-cores">
CPU Allocatable (cores)
CPU Allocatable
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.clusterCPUAllocatable,
width: { min: 200 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpuAllocatable = Number(value);
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={cpuAllocatable}
entity={InfraMonitoringEntity.CLUSTERS}
attribute="CPU allocatable metric"
@@ -194,17 +195,17 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#memory-usage-wss">
Memory Usage (WSS)
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.clusterMemory,
width: { min: 180 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memory = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={memory}
entity={InfraMonitoringEntity.CLUSTERS}
attribute="memory metric"
@@ -218,17 +219,17 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
id: 'memory_allocatable',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#memory-allocatable">
Memory Allocatable
Memory
<br /> Allocatable
</ColumnHeader>
),
accessorFn: (row): number => row.clusterMemoryAllocatable,
width: { min: 180 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memoryAllocatable = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={memoryAllocatable}
entity={InfraMonitoringEntity.CLUSTERS}
attribute="memory allocatable metric"

View File

@@ -9,9 +9,9 @@ import K8sGroupCell from '../Base/K8sGroupCell';
import { SelectedItemParams } from '../hooks';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
GroupedStatusCounts,
TextNoData,
ValidateColumnValueWrapper,
} from '../components';
import {
@@ -85,9 +85,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
enableMove: false,
pin: 'left',
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => (
<TanStackTable.Text>{value}</TanStackTable.Text>
),
cell: ({ value }): React.ReactNode => {
const daemonsetName = value as string;
return <CellValueTooltip value={daemonsetName} />;
},
},
{
id: 'namespaceName',
@@ -101,9 +102,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
width: { min: 160 },
enableSort: false,
enableResize: true,
cell: ({ value }): React.ReactNode => (
<TanStackTable.Text>{value}</TanStackTable.Text>
),
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return <CellValueTooltip value={namespaceName} />;
},
},
{
id: 'pod_counts_by_status',
@@ -122,7 +124,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
cell: ({ row, rowId }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TextNoData type="tanstack" />;
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts
@@ -175,7 +177,8 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
id: 'cpu_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-req-usage-">
CPU Request Usage (%)
CPU Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.daemonSetCPURequest,
@@ -183,11 +186,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpuRequest = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={cpuRequest}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="CPU Request"
@@ -201,18 +203,18 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
id: 'cpu_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-limit-usage-">
CPU Limit Usage (%)
CPU Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.daemonSetCPULimit,
width: { min: 160 },
enableSort: true,
enableResize: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpuLimit = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={cpuLimit}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="CPU Limit"
@@ -226,7 +228,8 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
id: 'cpu',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-usage-cores">
CPU Usage (cores)
CPU Usage
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.daemonSetCPU,
@@ -234,11 +237,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={cpu}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="CPU metric"
@@ -252,7 +254,8 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
id: 'memory_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-req-usage-">
Memory Request Usage (%)
Memory Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.daemonSetMemoryRequest,
@@ -260,11 +263,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memoryRequest = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={memoryRequest}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="Memory Request"
@@ -278,18 +280,18 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
id: 'memory_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-limit-usage-">
Memory Limit Usage (%)
Memory Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.daemonSetMemoryLimit,
width: { min: 190 },
enableSort: true,
enableResize: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memoryLimit = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={memoryLimit}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="Memory Limit"
@@ -303,18 +305,18 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-usage-wss">
Memory Usage (WSS)
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.daemonSetMemory,
width: { min: 180 },
enableSort: true,
enableResize: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memory = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={memory}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="memory metric"
@@ -335,11 +337,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
width: { min: 140 },
enableSort: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const readyNodes = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={readyNodes}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="ready node"
@@ -360,11 +361,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
width: { min: 140 },
enableSort: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const currentNodes = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={currentNodes}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="current node"
@@ -385,11 +385,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
width: { min: 140 },
enableSort: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const desiredNodes = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={desiredNodes}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="desired node"
@@ -410,11 +409,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
width: { min: 140 },
enableSort: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const misscheduledNodes = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={misscheduledNodes}
entity={InfraMonitoringEntity.DAEMONSETS}
attribute="misscheduled node"

View File

@@ -9,9 +9,9 @@ import K8sGroupCell from '../Base/K8sGroupCell';
import { SelectedItemParams } from '../hooks';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
GroupedStatusCounts,
TextNoData,
ValidateColumnValueWrapper,
} from '../components';
import {
@@ -86,9 +86,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
enableMove: false,
pin: 'left',
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => (
<TanStackTable.Text>{value}</TanStackTable.Text>
),
cell: ({ value }): React.ReactNode => {
const deploymentName = value as string;
return <CellValueTooltip value={deploymentName} />;
},
},
{
id: 'namespaceName',
@@ -120,7 +121,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
cell: ({ row, rowId }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TextNoData type="tanstack" />;
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts
@@ -163,7 +164,8 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
id: 'cpu_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-req-usage-">
CPU Request Usage (%)
CPU Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.deploymentCPURequest,
@@ -171,11 +173,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpuRequest = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={cpuRequest}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="CPU Request"
@@ -189,18 +190,18 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
id: 'cpu_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-limit-usage-">
CPU Limit Usage (%)
CPU Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.deploymentCPULimit,
width: { min: 210 },
enableSort: true,
enableResize: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpuLimit = Number(value);
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={cpuLimit}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="CPU Limit"
@@ -214,18 +215,18 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
id: 'cpu',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-usage-cores">
CPU Usage (cores)
CPU Usage
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.deploymentCPU,
width: { min: 160 },
enableSort: true,
enableResize: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={cpu}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="CPU metric"
@@ -239,7 +240,8 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
id: 'memory_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-req-usage-">
Memory Request Usage (%)
Memory Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.deploymentMemoryRequest,
@@ -247,11 +249,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memoryRequest = Number(value);
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={memoryRequest}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="Memory Request"
@@ -265,18 +266,18 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
id: 'memory_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-limit-usage-">
Memory Limit Usage (%)
Memory Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.deploymentMemoryLimit,
width: { min: 210 },
enableSort: true,
enableResize: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memoryLimit = Number(value);
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={memoryLimit}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="Memory Limit"
@@ -290,18 +291,18 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-usage-wss">
Memory Usage (WSS)
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.deploymentMemory,
width: { min: 180 },
enableSort: true,
enableResize: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memory = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={memory}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="memory metric"
@@ -322,11 +323,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
width: { min: 120 },
enableSort: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const availablePods = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={availablePods}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="available pod"
@@ -347,11 +347,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
width: { min: 120 },
enableSort: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const desiredPods = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={desiredPods}
entity={InfraMonitoringEntity.DEPLOYMENTS}
attribute="desired pod"

View File

@@ -1,5 +0,0 @@
.container {
display: flex;
align-items: center;
gap: 8px;
}

View File

@@ -3,10 +3,7 @@ import { Undo } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import logEvent from 'api/common/logEvent';
import {
InfraMonitoringEvents,
logInfraDrawerTimeRangeCustomizedEvent,
} from 'constants/events';
import { InfraMonitoringEvents } from 'constants/events';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
@@ -16,8 +13,6 @@ import {
import { useEntityDetailsTime } from './useEntityDetailsTime';
import styles from './EntityDateTimeSelector.module.scss';
interface EntityDateTimeSelectorProps {
eventEntity: string;
category: InfraMonitoringEntity;
@@ -46,14 +41,13 @@ function EntityDateTimeSelector({
view,
interval,
});
logInfraDrawerTimeRangeCustomizedEvent(category, interval);
handleTimeChange(interval, dateTimeRange);
},
[category, view, eventEntity, handleTimeChange],
);
return (
<div className={styles.container}>
<div className="entity-date-time-selector">
{hasTimeChanged && (
<TooltipSimple title="Reset to list time" side="bottom">
<Button

View File

@@ -16,10 +16,7 @@ import {
combineInitialAndUserExpression,
getUserExpressionFromCombined,
} from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
import {
InfraMonitoringEvents,
logInfraDrawerFilterCustomizedEvent,
} from 'constants/events';
import { InfraMonitoringEvents } from 'constants/events';
import Controls from 'container/Controls';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import LoadingContainer from 'container/InfraMonitoringK8sV2/LoadingContainer';
@@ -127,13 +124,6 @@ function EntityEventsContent({
view: InfraMonitoringEvents.EventsView,
});
logInfraDrawerFilterCustomizedEvent(
category,
'events',
newUserExpression || '',
'search',
);
refetch();
}
},

View File

@@ -73,7 +73,7 @@
.listContainer {
flex: 1;
height: calc(100vh - 330px) !important;
height: calc(100vh - 312px) !important;
display: flex;
height: 100%;

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useRef } from 'react';
import { useCallback, useMemo, useRef } from 'react';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { Card } from 'antd';
import logEvent from 'api/common/logEvent';
@@ -20,10 +20,7 @@ import {
combineInitialAndUserExpression,
getUserExpressionFromCombined,
} from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
import {
InfraMonitoringEvents,
logInfraDrawerFilterCustomizedEvent,
} from 'constants/events';
import { InfraMonitoringEvents } from 'constants/events';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { FontSize } from 'container/OptionsMenu/types';
@@ -45,15 +42,6 @@ import { K8S_ENTITY_LOGS_EXPRESSION_KEY, useInfiniteEntityLogs } from './hooks';
import { getEntityLogsQueryPayload } from './utils';
import styles from './EntityLogs.module.scss';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { serializeToParams } from 'lib/compositeQuery/serializer';
import createQueryParams from 'lib/createQueryParams';
import { isModifierKeyPressed } from 'utils/app';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
interface Props {
eventEntity: string;
@@ -69,9 +57,6 @@ function EntityLogsContent({
}: Omit<Props, 'initialExpression'>): JSX.Element {
const { timeRange } = useEntityDetailsTime();
const virtuosoRef = useRef<VirtuosoHandle>(null);
const logDetailContainerRef = useRef<HTMLDivElement>(null);
const { safeNavigate } = useSafeNavigate();
const expression = useExpression();
const inputExpression = useInputExpression();
@@ -100,10 +85,8 @@ function EntityLogsContent({
: partExpression;
querySearchOnRun(newUser);
logInfraDrawerFilterCustomizedEvent(category, 'logs', newUser, 'logs');
},
[userExpression, querySearchOnRun, handleCloseLogDetail, category],
[userExpression, querySearchOnRun, handleCloseLogDetail],
);
const {
@@ -144,13 +127,6 @@ function EntityLogsContent({
view: InfraMonitoringEvents.LogsView,
});
logInfraDrawerFilterCustomizedEvent(
category,
'logs',
newUserExpression || '',
'search',
);
refetch();
}
},
@@ -172,44 +148,6 @@ function EntityLogsContent({
virtuosoRef,
});
const { updateAllQueriesOperators } = useQueryBuilder();
const handleOpenInExplorer = useCallback(
(e: React.MouseEvent<Element, MouseEvent>, log: ILog) => {
const baseQuery = updateAllQueriesOperators(
initialQueriesMap[DataSource.LOGS],
PANEL_TYPES.LIST,
DataSource.LOGS,
);
const queryParams = {
[QueryParams.activeLogId]: `"${log?.id}"`,
[QueryParams.startTime]: timeRange.startTime.toString(),
[QueryParams.endTime]: timeRange.endTime.toString(),
...serializeToParams({
...baseQuery,
builder: {
...baseQuery.builder,
queryData: baseQuery.builder.queryData.map((item) => ({
...item,
filter: { expression },
})),
},
} satisfies Query),
};
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`, {
newTab: !!e && isModifierKeyPressed(e),
});
},
[
timeRange.startTime,
timeRange.endTime,
expression,
safeNavigate,
updateAllQueriesOperators,
],
);
const getItemContent = useCallback(
(_: number, logToRender: ILog): JSX.Element => {
return (
@@ -319,7 +257,6 @@ function EntityLogsContent({
{renderContent}
</div>
)}
<div ref={logDetailContainerRef} data-log-detail-ignore="true" />
{selectedTab && activeLog && (
<LogDetail
log={activeLog}
@@ -330,10 +267,6 @@ function EntityLogsContent({
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onScrollToLog={handleScrollToLog}
handleOpenInExplorer={(e) => handleOpenInExplorer(e, activeLog)}
getContainer={(): HTMLElement =>
logDetailContainerRef.current || document.body
}
/>
)}
</div>

View File

@@ -2,7 +2,6 @@
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
width: 100%;
}
.infoIcon {
@@ -21,7 +20,6 @@
align-items: center;
color: var(--l2-foreground);
transition: opacity 0.2s;
margin-left: auto;
&:hover {
color: var(--l3-foreground);

View File

@@ -1,6 +1,6 @@
import { Link } from 'react-router-dom';
import { Compass, Info } from '@signozhq/icons';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Tooltip } from 'antd';
import styles from './ChartHeader.module.scss';
@@ -12,7 +12,6 @@ interface ChartHeaderProps {
tooltip?: string;
metricsExplorerUrl?: string;
metricsExplorerTestId?: string;
onExploreClick?: () => void;
}
function ChartHeader({
@@ -21,13 +20,12 @@ function ChartHeader({
tooltip,
metricsExplorerUrl,
metricsExplorerTestId = 'open-metrics-explorer',
onExploreClick,
}: ChartHeaderProps): JSX.Element {
const renderInfoIcon = (): React.ReactNode => {
if (docPath) {
const tooltipTitle = tooltip || 'Not sure what this represents?';
return (
<TooltipSimple
<Tooltip
arrow
title={
<>
@@ -46,17 +44,17 @@ function ChartHeader({
<span className={styles.infoIcon} data-testid="chart-header-info-icon">
<Info size="md" />
</span>
</TooltipSimple>
</Tooltip>
);
}
if (tooltip) {
return (
<TooltipSimple title={tooltip} arrow>
<Tooltip title={tooltip}>
<span className={styles.infoIcon} data-testid="chart-header-info-icon">
<Info size="md" />
</span>
</TooltipSimple>
</Tooltip>
);
}
@@ -68,16 +66,15 @@ function ChartHeader({
<span className={styles.chartHeaderLabel}>{title}</span>
{renderInfoIcon()}
{metricsExplorerUrl && (
<TooltipSimple title="Go to Metrics Explorer" arrow>
<Tooltip title="Open in Metrics Explorer">
<Link
to={metricsExplorerUrl}
className={styles.metricsExplorerLink}
data-testid={metricsExplorerTestId}
onClick={onExploreClick}
>
<Compass size={14} />
</Link>
</TooltipSimple>
</Tooltip>
)}
</div>
);

View File

@@ -2,17 +2,11 @@ import { useCallback, useMemo, useRef } from 'react';
import { UseQueryResult } from 'react-query';
import { Skeleton } from 'antd';
import cx from 'classnames';
import {
InfraMonitoringEvents,
logInfraExplorerNavigatedEvent,
} from 'constants/events';
import { InfraMonitoringEvents } from 'constants/events';
import { PANEL_TYPES } from 'constants/queryBuilder';
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import {
InfraMonitoringEntity,
VIEW_TYPES,
} from 'container/InfraMonitoringK8sV2/constants';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
@@ -23,8 +17,6 @@ import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { getMetricsExplorerUrl } from 'utils/explorerUtils';
import { getDrawerDurationMs } from 'container/InfraMonitoringK8sV2/Base/useDrawerLifecycleStore';
import { buildEntityMetricsChartConfig } from './configBuilder';
import ChartHeader from './ChartHeader';
@@ -51,7 +43,6 @@ interface EntityMetricsProps<T> {
) => GetQueryResultsProps[];
queryKey: string;
category: InfraMonitoringEntity;
view?: string;
}
function EntityMetrics<T>({
@@ -61,7 +52,6 @@ function EntityMetrics<T>({
getEntityQueryPayload,
queryKey,
category,
view = VIEW_TYPES.METRICS,
}: EntityMetricsProps<T>): JSX.Element {
const { timeRange, selectedInterval, handleTimeChange } =
useEntityDetailsTime();
@@ -213,16 +203,6 @@ function EntityMetrics<T>({
: undefined
}
metricsExplorerTestId={`open-metrics-explorer-${idx}`}
onExploreClick={(): void =>
logInfraExplorerNavigatedEvent({
entityType: category,
destination: 'metrics_explorer',
source: 'chart_compass_icon',
tab: view,
sourceKey: entityWidgetInfo[idx].title,
drawerDurationMsAtNavigation: getDrawerDurationMs(),
})
}
/>
<div className={styles.entityMetricsCard} ref={graphRef}>
{renderCardContent(query, idx)}

View File

@@ -1,6 +1,5 @@
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { TooltipProvider } from '@signozhq/ui/tooltip';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import * as appContextHooks from 'providers/App/App';
import { LicenseEvent } from 'types/api/licensesV3/getActive';
@@ -301,16 +300,14 @@ const renderEntityMetrics = (overrides = {}): any => {
return render(
<MemoryRouter>
<TooltipProvider>
<EntityMetrics
entity={defaultProps.entity}
eventEntity="test"
entityWidgetInfo={defaultProps.entityWidgetInfo}
getEntityQueryPayload={defaultProps.getEntityQueryPayload}
queryKey={defaultProps.queryKey}
category={defaultProps.category}
/>
</TooltipProvider>
<EntityMetrics
entity={defaultProps.entity}
eventEntity="test"
entityWidgetInfo={defaultProps.entityWidgetInfo}
getEntityQueryPayload={defaultProps.getEntityQueryPayload}
queryKey={defaultProps.queryKey}
category={defaultProps.category}
/>
</MemoryRouter>,
);
};

View File

@@ -16,10 +16,7 @@ import {
getUserExpressionFromCombined,
} from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
import { ResizeTable } from 'components/ResizeTable';
import {
InfraMonitoringEvents,
logInfraDrawerFilterCustomizedEvent,
} from 'constants/events';
import { InfraMonitoringEvents } from 'constants/events';
import Controls from 'container/Controls';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
@@ -109,13 +106,6 @@ function EntityTracesContent({
view: InfraMonitoringEvents.TracesView,
});
logInfraDrawerFilterCustomizedEvent(
category,
'traces',
newUserExpression || '',
'search',
);
refetch();
}
},

View File

@@ -2,7 +2,6 @@ import { TableColumnsType as ColumnsType } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import { Typography } from '@signozhq/ui/typography';
import HttpStatusBadge from 'components/HttpStatusBadge/HttpStatusBadge';
import { TextNoData } from '../../components/TextNoData';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import {
BlockLink,
@@ -136,11 +135,12 @@ export const getTraceListColumns = (
if (!isValidCode) {
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
{numericCode === 0 || !statusCode ? (
<TextNoData type="typography" className={styles.cellText} />
) : (
<Typography className={styles.cellText}>{statusCode}</Typography>
)}
<Typography
className={styles.cellText}
data-novalue={numericCode === 0 || !statusCode}
>
{numericCode === 0 || !statusCode ? '-' : statusCode}
</Typography>
</BlockLink>
);
}

View File

@@ -63,7 +63,6 @@ export function createPodMetricsTab<T>({
getEntityQueryPayload={getQueryPayload}
queryKey={queryKey}
category={category}
view={VIEW_TYPES.POD_METRICS}
/>
),
};

View File

@@ -1,103 +0,0 @@
.entityDetailDrawer {
border-color: var(--l2-border) !important;
background: var(--l2-background) !important;
box-shadow: -4px 10px 16px 2px var(--l1-shadow);
width: 70%;
// TODO(H4ad): Improve this in a component level
max-width: 70% !important;
overscroll-behavior: contain;
&:focus-visible,
&:focus {
outline: none;
}
[data-slot='drawer-header'] {
padding: var(--spacing-4) var(--spacing-8);
border-bottom: 1px solid var(--l1-border);
background: var(--l2-background);
}
[data-slot='drawer-description'] {
display: flex;
flex: 1;
flex-direction: column;
overflow-y: auto;
padding: var(--spacing-8);
margin: 0;
min-width: 900px;
}
}
.closeButton {
--button-padding: var(--spacing-2);
}
.title {
font-family: var(--periscope-font-family-mono);
--typography-margin: 0px var(--spacing-4) 0px 0px;
}
.entityDetailsEntity {
display: flex;
flex-direction: column;
}
.entityDetailsGrid {
display: flex;
flex-direction: column;
}
.labelsRow,
.valuesRow {
display: grid;
grid-template-columns: 1.5fr 1.5fr 1.5fr 1.5fr;
gap: 30px;
align-items: center;
}
.labelsRow {
margin-bottom: var(--spacing-4);
}
.entityDetailsMetadataLabel {
letter-spacing: 0.44px;
text-transform: uppercase;
}
.entityDetailsMetadataValue {
font-family: var(--periscope-font-family-mono);
}
.viewsTabsContainer {
margin-top: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
[data-slot='toggle-group-item'] {
width: 114px;
}
}
.viewsTabs {
color: var(--l2-foreground);
}
.viewTitle {
display: flex;
gap: var(--margin-2);
align-items: center;
justify-content: center;
}
.compassButton {
width: 30px;
height: 30px;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
box-shadow: 0 0 8px 0 var(--l1-shadow);
}

View File

@@ -0,0 +1,462 @@
// Entity Details
.entity-detail-drawer {
border-left: 1px solid var(--l1-border);
background: var(--l2-background);
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
.ant-drawer-header {
padding: 8px 16px;
border-bottom: none;
align-items: stretch;
border-bottom: 1px solid var(--l1-border);
background: var(--l2-background);
}
.ant-drawer-close {
margin-inline-end: 0px;
}
.ant-drawer-body {
display: flex;
flex-direction: column;
padding: 16px;
}
.title {
color: var(--l2-foreground);
font-family: 'Geist Mono';
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.radio-button {
display: flex;
align-items: center;
justify-content: center;
padding-top: var(--padding-1);
border: 1px solid var(--l1-border);
background: var(--l3-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
.entity-detail-drawer__entity {
.entity-details-grid {
.labels-row,
.values-row {
display: grid;
grid-template-columns: 1.5fr 1.5fr 1.5fr 1.5fr;
gap: 30px;
align-items: center;
}
.labels-row {
margin-bottom: 8px;
}
.entity-details-metadata-label {
color: var(--l2-foreground);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
}
.entity-details-metadata-value {
color: var(--l2-foreground);
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.status-tag {
margin: 0;
&.active {
color: var(--success-500);
background: var(--success-100);
border-color: var(--success-500);
}
&.inactive {
color: var(--error-500);
background: var(--error-100);
border-color: var(--error-500);
}
}
.progress-container {
width: 158px;
span {
font-weight: 600;
}
}
.ant-card {
&.ant-card-bordered {
border: 1px solid var(--l1-border) !important;
}
}
}
}
.tabs-and-search {
display: flex;
justify-content: space-between;
align-items: center;
margin: 16px 0;
.action-btn {
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
}
.views-tabs-container {
margin-top: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
.views-tabs {
color: var(--l2-foreground);
.view-title {
display: flex;
gap: var(--margin-2);
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
font-style: normal;
font-weight: var(--font-weight-normal);
}
> button {
border: 1px solid var(--l1-border);
width: 114px;
&::before {
background: var(--l1-border);
}
&[data-state='on'] {
background: var(--l1-border);
color: var(--l1-foreground);
border: 1px solid var(--l1-border);
&::before {
background: var(--l1-border);
}
}
}
}
.compass-button {
width: 30px;
height: 30px;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
.ant-drawer-close {
padding: 0px;
}
}
.entity-metric-traces {
margin-top: 1rem;
.entity-metric-traces-header {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
gap: 8px;
padding: 12px;
border-radius: 3px;
border: 1px solid var(--l1-border);
.filter-section {
flex: 1;
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--l1-border) !important;
background-color: var(--l3-background) !important;
input {
font-size: 12px;
}
[data-slot='badge'] .ant-typography {
font-size: 12px;
}
}
}
}
.entity-metric-traces-table {
.ant-table-content {
overflow: hidden !important;
}
.ant-table {
border-radius: 3px;
border: 1px solid var(--l1-border);
.ant-table-thead > tr > th {
padding: 12px;
font-weight: 500;
font-size: 12px;
line-height: 18px;
background: color-mix(in srgb, var(--bg-robin-200) 1%, transparent);
border-bottom: none;
color: var(--l2-foreground);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
text-transform: uppercase;
&::before {
background-color: transparent;
}
}
.ant-table-thead > tr > th:has(.entityname-column-header) {
background: var(--l2-background);
}
.ant-table-cell {
padding: 12px;
font-size: 13px;
line-height: 20px;
color: var(--l1-foreground);
background: color-mix(in srgb, var(--bg-robin-200) 1%, transparent);
}
.ant-table-cell:has(.entityname-column-value) {
background: var(--l2-background);
}
.entityname-column-value {
color: var(--l1-foreground);
font-family: 'Geist Mono';
font-style: normal;
font-weight: 600;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.status-cell {
.active-tag {
color: var(--bg-forest-500);
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
}
.progress-container {
}
.ant-table-tbody > tr:hover > td {
background: color-mix(in srgb, var(--l1-foreground) 4%, transparent);
}
.ant-table-cell:first-child {
text-align: justify;
}
.ant-table-cell:nth-child(2) {
padding-left: 16px;
padding-right: 16px;
}
.ant-table-cell:nth-child(n + 3) {
padding-right: 24px;
}
.column-header-right {
text-align: right;
}
.ant-table-tbody > tr > td {
border-bottom: none;
}
.ant-table-thead
> tr
> th:not(:last-child):not(.ant-table-selection-column):not(
.ant-table-row-expand-icon-cell
):not([colspan])::before {
background-color: transparent;
}
.ant-empty-normal {
visibility: hidden;
}
}
.ant-table-container::after {
content: none;
}
}
}
.entity-metrics-logs-container {
margin-top: 1rem;
.filter-section {
flex: 1;
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--l1-border) !important;
background-color: var(--l3-background) !important;
input {
font-size: 12px;
}
[data-slot='badge'] .ant-typography {
font-size: 12px;
}
}
}
.entity-metrics-logs-header {
display: flex;
justify-content: space-between;
gap: 8px;
padding: 12px;
border-radius: 3px;
border: 1px solid var(--l1-border);
}
.entity-metrics-logs {
margin-top: 1rem;
.virtuoso-list {
overflow-y: hidden !important;
&::-webkit-scrollbar {
width: 0.3rem;
height: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--l3-background);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--l3-background);
}
.ant-row {
width: fit-content;
}
}
.skeleton-container {
height: 100%;
padding: 16px;
}
}
}
.entity-metrics-logs-list-container {
flex: 1;
height: calc(100vh - 272px) !important;
display: flex;
height: 100%;
.raw-log-content {
width: 100%;
text-wrap: inherit;
word-wrap: break-word;
}
}
.entity-metrics-logs-list-card {
width: 100%;
margin-top: 12px;
.ant-card-body {
padding: 0;
height: 100%;
width: 100%;
}
}
.logs-loading-skeleton {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 0;
.ant-skeleton-input-sm {
height: 18px;
}
}
.no-logs-found {
height: 50vh;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 24px;
box-sizing: border-box;
.ant-typography {
display: flex;
align-items: center;
gap: 16px;
}
}
.entity-date-time-selector {
display: flex;
align-items: center;
gap: 8px;
}

View File

@@ -64,7 +64,7 @@
display: flex;
flex-direction: column;
align-items: stretch;
min-width: 800px;
min-width: 595px;
overflow-x: auto;
> * {

View File

@@ -1,11 +1,9 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import * as Sentry from '@sentry/react';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Button, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import {
QuickFilterChangeEventData,
QuickFilterCheckboxUseFieldApis,
QuickFiltersSource,
} from 'components/QuickFilters/types';
@@ -51,11 +49,7 @@ import {
} from './hooks';
import styles from './InfraMonitoringK8s.module.scss';
import {
logInfraFilterCustomizedEvent,
logInfraMonitoringListViewedEvent,
InfraMonitoringEvents,
} from 'constants/events';
import { InfraMonitoringEvents } from 'constants/events';
import logEvent from 'api/common/logEvent';
import { NANO_SECOND_MULTIPLIER, useGlobalTimeStore } from 'store/globalTime';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
@@ -126,28 +120,12 @@ export default function InfraMonitoringK8s(): JSX.Element {
category: selectedCategory,
view: InfraMonitoringEvents.QuickFiltersView,
});
// The category query param is a plain string; entities are validated by
// getEntityConfig before rendering
logInfraMonitoringListViewedEvent(selectedCategory as InfraMonitoringEntity);
}, [selectedCategory]);
const handleFilterVisibilityChange = useCallback((): void => {
setShowFilters((show) => !show);
}, []);
const handleQuickFilterChange = useCallback(
(data: QuickFilterChangeEventData): void => {
logInfraFilterCustomizedEvent(
selectedCategory as InfraMonitoringEntity,
'quick_filter',
data.expression,
data.filterItemKeys,
);
},
[selectedCategory],
);
const categories = useMemo(
() => [
{
@@ -240,15 +218,14 @@ export default function InfraMonitoringK8s(): JSX.Element {
<>
{!showFilters && (
<div className={styles.k8SOpenQuickFilters}>
<TooltipSimple title="Open Filters" arrow side="left">
<Button
variant="ghost"
size="icon"
color="secondary"
onClick={handleFilterVisibilityChange}
prefix={<Filter size={14} />}
/>
</TooltipSimple>
<Button
className="periscope-btn ghost"
type="text"
size="small"
onClick={handleFilterVisibilityChange}
>
<Filter size={14} />
</Button>
</div>
)}
</>
@@ -269,13 +246,13 @@ export default function InfraMonitoringK8s(): JSX.Element {
Viewing · Resource
</Typography.Text>
<div className={styles.sectionLine} />
<TooltipSimple title="Collapse Filters" arrow>
<Tooltip title="Collapse Filters">
<ArrowUpToLine
style={{ transform: 'rotate(270deg)' }}
onClick={handleFilterVisibilityChange}
size="md"
/>
</TooltipSimple>
</Tooltip>
</div>
<div className={styles.categoryCard}>
<div className={styles.categoryList}>
@@ -312,7 +289,6 @@ export default function InfraMonitoringK8s(): JSX.Element {
config={selectedCategoryConfig}
handleFilterVisibilityChange={handleFilterVisibilityChange}
useFieldApis={selectedCategoryUseFieldApis}
onQuickFilterChange={handleQuickFilterChange}
/>
)}
</div>

View File

@@ -9,9 +9,9 @@ import K8sGroupCell from '../Base/K8sGroupCell';
import { SelectedItemParams } from '../hooks';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
GroupedStatusCounts,
TextNoData,
ValidateColumnValueWrapper,
} from '../components';
import {
@@ -79,9 +79,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
enableMove: false,
pin: 'left',
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => (
<TanStackTable.Text>{value}</TanStackTable.Text>
),
cell: ({ value }): React.ReactNode => {
const jobName = value as string;
return <CellValueTooltip value={jobName} />;
},
},
{
id: 'namespaceName',
@@ -95,9 +96,10 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
width: { min: 160 },
enableSort: false,
enableResize: true,
cell: ({ value }): React.ReactNode => (
<TanStackTable.Text>{value}</TanStackTable.Text>
),
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return <CellValueTooltip value={namespaceName} />;
},
},
{
id: 'pod_counts_by_status',
@@ -114,7 +116,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
cell: ({ row, rowId }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TextNoData type="tanstack" />;
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts
@@ -127,7 +129,7 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
{
id: 'completion',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#completions">
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#completion">
Completions
</ColumnHeader>
),
@@ -159,7 +161,8 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
id: 'cpu_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-req-usage-">
CPU Request Usage (%)
CPU Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.jobCPURequest,
@@ -167,14 +170,13 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpuRequest = value as number;
return (
<ValidateColumnValueWrapper
value={cpuRequest}
entity={InfraMonitoringEntity.JOBS}
attribute="CPU Request"
rowId={rowId}
>
<EntityProgressBar value={cpuRequest} type="request" />
</ValidateColumnValueWrapper>
@@ -185,21 +187,21 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
id: 'cpu_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-limit-usage-">
CPU Limit Usage (%)
CPU Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.jobCPULimit,
width: { min: 200, default: 200 },
enableSort: true,
enableResize: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpuLimit = value as number;
return (
<ValidateColumnValueWrapper
value={cpuLimit}
entity={InfraMonitoringEntity.JOBS}
attribute="CPU Limit"
rowId={rowId}
>
<EntityProgressBar value={cpuLimit} type="limit" />
</ValidateColumnValueWrapper>
@@ -210,21 +212,21 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
id: 'cpu',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#cpu-usage-cores">
CPU Usage (cores)
CPU Usage
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.jobCPU,
width: { min: 160 },
enableSort: true,
enableResize: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.JOBS}
attribute="CPU metric"
rowId={rowId}
>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
@@ -235,7 +237,8 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
id: 'memory_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-req-usage-">
Memory Request Usage (%)
Memory Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.jobMemoryRequest,
@@ -243,14 +246,13 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memoryRequest = value as number;
return (
<ValidateColumnValueWrapper
value={memoryRequest}
entity={InfraMonitoringEntity.JOBS}
attribute="Memory Request"
rowId={rowId}
>
<EntityProgressBar value={memoryRequest} type="request" />
</ValidateColumnValueWrapper>
@@ -261,21 +263,21 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
id: 'memory_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-limit-usage-">
Memory Limit Usage (%)
Memory Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.jobMemoryLimit,
width: { min: 180 },
enableSort: true,
enableResize: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memoryLimit = value as number;
return (
<ValidateColumnValueWrapper
value={memoryLimit}
entity={InfraMonitoringEntity.JOBS}
attribute="Memory Limit"
rowId={rowId}
>
<EntityProgressBar value={memoryLimit} type="limit" />
</ValidateColumnValueWrapper>
@@ -286,21 +288,21 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/jobs#mem-usage-wss">
Memory Usage (WSS)
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.jobMemory,
width: { min: 160 },
enableSort: true,
enableResize: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memory = value as number;
return (
<ValidateColumnValueWrapper
value={memory}
entity={InfraMonitoringEntity.JOBS}
attribute="memory metric"
rowId={rowId}
>
<TanStackTable.Text>{formatBytes(memory)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
@@ -318,14 +320,13 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
width: { min: 100 },
enableSort: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const activePods = value as number;
return (
<ValidateColumnValueWrapper
value={activePods}
entity={InfraMonitoringEntity.JOBS}
attribute="active pod"
rowId={rowId}
>
<TanStackTable.Text>{activePods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
@@ -343,14 +344,13 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
width: { min: 100 },
enableSort: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const failedPods = value as number;
return (
<ValidateColumnValueWrapper
value={failedPods}
entity={InfraMonitoringEntity.JOBS}
attribute="failed pod"
rowId={rowId}
>
<TanStackTable.Text>{failedPods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
@@ -368,14 +368,13 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
width: { min: 120 },
enableSort: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const successfulPods = value as number;
return (
<ValidateColumnValueWrapper
value={successfulPods}
entity={InfraMonitoringEntity.JOBS}
attribute="successful pod"
rowId={rowId}
>
<TanStackTable.Text>{successfulPods}</TanStackTable.Text>
</ValidateColumnValueWrapper>
@@ -393,14 +392,13 @@ export const k8sJobsColumnsConfig: JobTableColumnConfig[] = [
width: { min: 160 },
enableSort: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const desiredSuccessfulPods = value as number;
return (
<ValidateColumnValueWrapper
value={desiredSuccessfulPods}
entity={InfraMonitoringEntity.JOBS}
attribute="desired successful pod"
rowId={rowId}
>
<TanStackTable.Text>{desiredSuccessfulPods}</TanStackTable.Text>
</ValidateColumnValueWrapper>

View File

@@ -8,8 +8,8 @@ import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import {
CellValueTooltip,
GroupedStatusCounts,
TextNoData,
ValidateColumnValueWrapper,
} from '../components';
import {
@@ -81,9 +81,10 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
enableMove: false,
pin: 'left',
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => (
<TanStackTable.Text>{value}</TanStackTable.Text>
),
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return <CellValueTooltip value={namespaceName} />;
},
},
{
id: 'clusterName',
@@ -116,7 +117,7 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
cell: ({ row, rowId }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TextNoData type="tanstack" />;
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts
@@ -136,14 +137,13 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
accessorFn: (row): number => row.namespaceCPU,
width: { min: 190 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
return (
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.NAMESPACES}
attribute="CPU metric"
rowId={rowId}
>
<TanStackTable.Text>{cpu.toFixed(2)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
@@ -160,14 +160,13 @@ export const k8sNamespacesColumnsConfig: NamespaceTableColumnConfig[] = [
accessorFn: (row): number => row.namespaceMemory,
width: { min: 220 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memory = Number(value);
return (
<ValidateColumnValueWrapper
value={memory}
entity={InfraMonitoringEntity.NAMESPACES}
attribute="memory metric"
rowId={rowId}
>
<TanStackTable.Text>{formatBytes(memory)}</TanStackTable.Text>
</ValidateColumnValueWrapper>

View File

@@ -10,8 +10,8 @@ import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import { INFRA_MONITORING_ATTR_KEYS } from '../constants';
import {
CellValueTooltip,
GroupedStatusCounts,
TextNoData,
ValidateColumnValueWrapper,
} from '../components';
import { InfraMonitoringEntity } from '../constants';
@@ -83,9 +83,10 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
enableMove: false,
pin: 'left',
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => (
<TanStackTable.Text>{value}</TanStackTable.Text>
),
cell: ({ value }): React.ReactNode => {
const nodeName = value as string;
return <CellValueTooltip value={nodeName} />;
},
},
{
id: 'condition',
@@ -101,11 +102,6 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
if (!groupMeta) {
const color =
NODE_CONDITION_COLORS[row.condition] || NODE_CONDITION_COLORS.no_data;
if (color === NODE_CONDITION_COLORS.no_data) {
return <TextNoData type="tanstack" />;
}
return (
<Badge color={color} variant="outline">
{NODE_CONDITION_LABEL_MAP[row.condition] || 'Unknown'}
@@ -146,7 +142,7 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
cell: ({ row, rowId }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TextNoData type="tanstack" />;
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts
@@ -176,17 +172,17 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
id: 'cpu',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#cpu-usage-cores">
CPU Usage (cores)
CPU Usage
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.nodeCPU,
width: { min: 160, default: 160 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={cpu}
entity={InfraMonitoringEntity.NODES}
attribute="CPU metric"
@@ -200,17 +196,17 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
id: 'cpu_allocatable',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#cpu-alloc-cores">
CPU Allocatable (cores)
CPU Allocatable
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.nodeCPUAllocatable,
width: { min: 160, default: 190 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpuAllocatable = Number(value);
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={cpuAllocatable}
entity={InfraMonitoringEntity.NODES}
attribute="CPU allocatable metric"
@@ -224,17 +220,17 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#memory-usage-wss">
Memory Usage (WSS)
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.nodeMemory,
width: { min: 220, default: 220 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memory = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={memory}
entity={InfraMonitoringEntity.NODES}
attribute="memory metric"
@@ -248,17 +244,17 @@ export const k8sNodesColumnsConfig: NodeTableColumnConfig[] = [
id: 'memory_allocatable',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/nodes#memory-allocatable">
Memory Allocatable
Memory
<br /> Allocatable
</ColumnHeader>
),
accessorFn: (row): number => row.nodeMemoryAllocatable,
width: { min: 200, default: 200 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memoryAllocatable = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={memoryAllocatable}
entity={InfraMonitoringEntity.NODES}
attribute="memory allocatable metric"

View File

@@ -67,7 +67,7 @@ export const podWidgetInfo = [
{
title: 'CPU Usage (cores)',
yAxisUnit: '',
docPath: '/infrastructure-monitoring/kubernetes/pods/#cpu-usage-cores',
docPath: '/infrastructure-monitoring/kubernetes/pods/#cpu-usage-cores-1',
},
{
title: 'CPU Request, Limit Utilization',

View File

@@ -1,5 +1,6 @@
import { Container } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import {
InframonitoringtypesPodRecordDTO,
InframonitoringtypesPodStatusDTO,
@@ -16,15 +17,16 @@ import {
POD_STATUS_COLORS,
} from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
GroupedStatusCounts,
TextNoData,
ValidateColumnValueWrapper,
} from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
} from '../constants';
import { Typography } from '@signozhq/ui/typography';
import { formatAge } from 'utils/timeUtils';
export function getK8sPodRowKey(pod: InframonitoringtypesPodRecordDTO): string {
@@ -84,9 +86,10 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
enableMove: false,
pin: 'left',
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => (
<TanStackTable.Text>{value}</TanStackTable.Text>
),
cell: ({ value }): React.ReactNode => {
const podName = value as string;
return <CellValueTooltip value={podName} />;
},
},
{
id: 'podStatus',
@@ -104,12 +107,11 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
return <></>;
}
if (row.podStatus === InframonitoringtypesPodStatusDTO.no_data) {
return <TextNoData type="tanstack" />;
}
const color = POD_STATUS_COLORS[row.podStatus] || POD_STATUS_COLORS.unknown;
const label = row.podStatus.charAt(0).toUpperCase() + row.podStatus.slice(1);
const label =
row.podStatus === InframonitoringtypesPodStatusDTO.no_data
? 'No Data'
: row.podStatus.charAt(0).toUpperCase() + row.podStatus.slice(1);
return (
<Badge color={color} variant="outline">
{label}
@@ -132,7 +134,7 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
cell: ({ row, rowId }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TextNoData type="tanstack" />;
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts
@@ -145,25 +147,23 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
{
id: 'podAge',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#pod-age">
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#age">
Age
</ColumnHeader>
),
accessorFn: (row): number => row.podAge,
width: { min: 100 },
enableSort: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const age = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={age}
entity={InfraMonitoringEntity.PODS}
attribute="age"
>
<TanStackTable.Text>{formatAge(age)}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
if (age === -1) {
return (
<TooltipSimple title="No data">
<Typography.Text>-</Typography.Text>
</TooltipSimple>
);
}
return <TanStackTable.Text>{formatAge(age)}</TanStackTable.Text>;
},
},
{
@@ -176,36 +176,34 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
accessorFn: (row): number => row.podRestarts,
width: { min: 140 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const restarts = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={restarts}
entity={InfraMonitoringEntity.PODS}
attribute="Restarts"
>
<TanStackTable.Text>{restarts}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
if (restarts === -1) {
return (
<TooltipSimple title="No data">
<Typography.Text>-</Typography.Text>
</TooltipSimple>
);
}
return <TanStackTable.Text>{restarts}</TanStackTable.Text>;
},
},
{
id: 'cpu_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#cpu-req-usage-">
CPU Request Usage (%)
CPU Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.podCPURequest,
width: { min: 210 },
enableSort: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpuRequest = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={cpuRequest}
entity={InfraMonitoringEntity.PODS}
attribute="CPU Request"
@@ -219,17 +217,17 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
id: 'cpu_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#cpu-limit-usage-">
CPU Limit Usage (%)
CPU Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.podCPULimit,
width: { min: 220 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpuLimit = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={cpuLimit}
entity={InfraMonitoringEntity.PODS}
attribute="CPU Limit"
@@ -243,17 +241,17 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
id: 'cpu',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#cpu-usage-cores">
CPU Usage (cores)
CPU Usage
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.podCPU,
width: { min: 160 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={cpu}
entity={InfraMonitoringEntity.PODS}
attribute="CPU metric"
@@ -267,18 +265,18 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
id: 'memory_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#mem-req-usage-">
Memory Request Usage (%)
Memory Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.podMemoryRequest,
width: { min: 210 },
enableSort: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memoryRequest = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={memoryRequest}
entity={InfraMonitoringEntity.PODS}
attribute="Memory Request"
@@ -292,17 +290,17 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
id: 'memory_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#mem-limit-usage-">
Memory Limit Usage (%)
Memory Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.podMemoryLimit,
width: { min: 220 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memoryLimit = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={memoryLimit}
entity={InfraMonitoringEntity.PODS}
attribute="Memory Limit"
@@ -316,17 +314,17 @@ export const k8sPodColumnsConfig: PodTableColumnConfig[] = [
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/pods#mem-usage-wss">
Memory Usage (WSS)
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.podMemory,
width: { min: 210, default: '100%' },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memory = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={memory}
entity={InfraMonitoringEntity.PODS}
attribute="memory metric"

View File

@@ -9,9 +9,9 @@ import K8sGroupCell from '../Base/K8sGroupCell';
import { SelectedItemParams } from '../hooks';
import { formatBytes, getPodStatusItems } from '../commonUtils';
import {
CellValueTooltip,
EntityProgressBar,
GroupedStatusCounts,
TextNoData,
ValidateColumnValueWrapper,
} from '../components';
import {
@@ -86,9 +86,10 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
enableMove: false,
pin: 'left',
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => (
<TanStackTable.Text>{value}</TanStackTable.Text>
),
cell: ({ value }): React.ReactNode => {
const statefulsetName = value as string;
return <CellValueTooltip value={statefulsetName} />;
},
},
{
id: 'namespaceName',
@@ -102,9 +103,10 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
width: { min: 180 },
enableSort: false,
enableResize: true,
cell: ({ value }): React.ReactNode => (
<TanStackTable.Text>{value}</TanStackTable.Text>
),
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return <CellValueTooltip value={namespaceName} />;
},
},
{
id: 'pod_counts_by_status',
@@ -123,7 +125,7 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
cell: ({ row, rowId }): React.ReactNode => {
const podCountsByStatus = row.podCountsByStatus;
if (!podCountsByStatus) {
return <TextNoData type="tanstack" />;
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
<GroupedStatusCounts
@@ -166,7 +168,8 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
id: 'cpu_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-req-usage-">
CPU Request Usage (%)
CPU Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.statefulSetCPURequest,
@@ -174,11 +177,10 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpuRequest = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={cpuRequest}
entity={InfraMonitoringEntity.STATEFULSETS}
attribute="CPU Request"
@@ -192,18 +194,18 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
id: 'cpu_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-limit-usage-">
CPU Limit Usage (%)
CPU Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.statefulSetCPULimit,
width: { min: 200, default: 200 },
enableSort: true,
enableResize: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpuLimit = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={cpuLimit}
entity={InfraMonitoringEntity.STATEFULSETS}
attribute="CPU Limit"
@@ -217,7 +219,8 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
id: 'cpu',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#cpu-usage-cores">
CPU Usage (cores)
CPU Usage
<br /> (cores)
</ColumnHeader>
),
accessorFn: (row): number => row.statefulSetCPU,
@@ -225,11 +228,10 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const cpu = Number(value);
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={cpu}
entity={InfraMonitoringEntity.STATEFULSETS}
attribute="CPU metric"
@@ -243,7 +245,8 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
id: 'memory_request',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-req-usage-">
Memory Request Usage (%)
Memory Request
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.statefulSetMemoryRequest,
@@ -251,11 +254,10 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memoryRequest = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={memoryRequest}
entity={InfraMonitoringEntity.STATEFULSETS}
attribute="Memory Request"
@@ -269,18 +271,18 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
id: 'memory_limit',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-limit-usage-">
Memory Limit Usage (%)
Memory Limit
<br /> Usage (%)
</ColumnHeader>
),
accessorFn: (row): number => row.statefulSetMemoryLimit,
width: { min: 180 },
enableSort: true,
enableResize: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memoryLimit = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={memoryLimit}
entity={InfraMonitoringEntity.STATEFULSETS}
attribute="Memory Limit"
@@ -294,7 +296,8 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
id: 'memory',
header: (): React.ReactNode => (
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/statefulsets#mem-usage-wss">
Memory Usage (WSS)
Memory Usage
<br /> (WSS)
</ColumnHeader>
),
accessorFn: (row): number => row.statefulSetMemory,
@@ -302,11 +305,10 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
enableSort: true,
enableResize: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const memory = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={memory}
entity={InfraMonitoringEntity.STATEFULSETS}
attribute="memory metric"
@@ -327,11 +329,10 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
width: { min: 120 },
enableSort: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const currentPods = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={currentPods}
entity={InfraMonitoringEntity.STATEFULSETS}
attribute="current pod"
@@ -352,11 +353,10 @@ export const k8sStatefulSetsColumnsConfig: TableColumnDef<InframonitoringtypesSt
width: { min: 120 },
enableSort: true,
defaultVisibility: false,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const desiredPods = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={desiredPods}
entity={InfraMonitoringEntity.STATEFULSETS}
attribute="desired pod"

View File

@@ -69,29 +69,27 @@ export const volumeWidgetInfo = [
{
title: 'Volume available',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-available-1',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-available',
},
{
title: 'Volume capacity',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-capacity-1',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-capacity',
},
{
title: 'Volume inodes used',
yAxisUnit: 'short',
docPath:
'/infrastructure-monitoring/kubernetes/volumes/#volume-inodes-used-1',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-inodes-used',
},
{
title: 'Volume inodes',
yAxisUnit: 'short',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-inodes-1',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-inodes',
},
{
title: 'Volume inodes free',
yAxisUnit: 'short',
docPath:
'/infrastructure-monitoring/kubernetes/volumes/#volume-inodes-free-1',
docPath: '/infrastructure-monitoring/kubernetes/volumes/#volume-inodes-free',
},
];

View File

@@ -6,7 +6,7 @@ import ColumnHeader from '../Base/ColumnHeader';
import EntityGroupHeader from '../Base/EntityGroupHeader';
import K8sGroupCell from '../Base/K8sGroupCell';
import { formatBytes } from '../commonUtils';
import { ValidateColumnValueWrapper } from '../components';
import { CellValueTooltip, ValidateColumnValueWrapper } from '../components';
import {
INFRA_MONITORING_ATTR_KEYS,
InfraMonitoringEntity,
@@ -79,9 +79,10 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
enableMove: false,
pin: 'left',
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => (
<TanStackTable.Text>{value}</TanStackTable.Text>
),
cell: ({ value }): React.ReactNode => {
const pvcName = value as string;
return <CellValueTooltip value={pvcName} />;
},
},
{
id: 'namespaceName',
@@ -94,9 +95,10 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
row.meta?.[INFRA_MONITORING_ATTR_KEYS.K8S_NAMESPACE_NAME] || '',
width: { min: 220 },
enableSort: false,
cell: ({ value }): React.ReactNode => (
<TanStackTable.Text>{value}</TanStackTable.Text>
),
cell: ({ value }): React.ReactNode => {
const namespaceName = value as string;
return <CellValueTooltip value={namespaceName} />;
},
},
{
id: 'capacity',
@@ -108,11 +110,10 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
accessorFn: (row): number => row.volumeCapacity,
width: { min: 140 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const capacity = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={capacity}
entity={InfraMonitoringEntity.VOLUMES}
attribute="capacity metric"
@@ -132,11 +133,10 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
accessorFn: (row): number => row.volumeUsage,
width: { min: 140 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const usage = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={usage}
entity={InfraMonitoringEntity.VOLUMES}
attribute="utilization metric"
@@ -156,11 +156,10 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
accessorFn: (row): number => row.volumeAvailable,
width: { min: 140 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const available = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={available}
entity={InfraMonitoringEntity.VOLUMES}
attribute="available metric"
@@ -180,11 +179,10 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
accessorFn: (row): number => row.volumeInodes,
width: { min: 140 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const inodes = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={inodes}
entity={InfraMonitoringEntity.VOLUMES}
attribute="inodes metric"
@@ -204,11 +202,10 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
accessorFn: (row): number => row.volumeInodesUsed,
width: { min: 160 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const inodesUsed = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={inodesUsed}
entity={InfraMonitoringEntity.VOLUMES}
attribute="inodes used metric"
@@ -228,11 +225,10 @@ export const k8sVolumesColumnsConfig: VolumeTableColumnConfig[] = [
accessorFn: (row): number => row.volumeInodesFree,
width: { min: 160 },
enableSort: true,
cell: ({ value, rowId }): React.ReactNode => {
cell: ({ value }): React.ReactNode => {
const inodesFree = value as number;
return (
<ValidateColumnValueWrapper
rowId={rowId}
value={inodesFree}
entity={InfraMonitoringEntity.VOLUMES}
attribute="inodes free metric"

View File

@@ -0,0 +1,58 @@
.tooltipContent {
display: flex;
flex-direction: column;
max-width: 280px;
}
.actions {
display: flex;
align-items: center;
gap: var(--spacing-1);
padding: var(--spacing-2);
justify-content: flex-end;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
}
.actionButton {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
padding: 0;
border: none;
border-radius: var(--radius-1);
background: transparent;
color: var(--l2-foreground);
cursor: pointer;
transition:
background 0.15s ease,
color 0.15s ease;
&:hover:not(:disabled) {
background: var(--l2-background-hover);
color: var(--l1-foreground);
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
.tooltipContentWrapper {
--tooltip-background: var(--l3-background);
--tooltip-border-radius: 4px;
--tooltip-border-width: 1px;
--tooltip-border-color: rgba(255, 255, 255, 0.14);
--tooltip-padding: 0px;
}
.divider {
--divider-color: rgba(255, 255, 255, 0.14);
}
.value {
width: fit-content;
}

View File

@@ -0,0 +1,99 @@
import { useCallback, type MouseEvent } from 'react';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { toast } from '@signozhq/ui/sonner';
import { Copy, Minus, Plus } from '@signozhq/icons';
import { useCopyToClipboard } from 'react-use';
import TanStackTable from 'components/TanStackTableView';
import { useInfraMonitoringCellActionsStore } from './useInfraMonitoringCellActionsStore';
import styles from './CellValueTooltip.module.scss';
import { Divider } from '@signozhq/ui/divider';
export interface CellValueTooltipProps {
value: string;
}
export function CellValueTooltip({
value,
}: CellValueTooltipProps): JSX.Element {
const [, copyToClipboard] = useCopyToClipboard();
const { lineClamp, increaseLineClamp, decreaseLineClamp } =
useInfraMonitoringCellActionsStore();
const handleCopy = useCallback(
(e: MouseEvent): void => {
e.stopPropagation();
copyToClipboard(value);
toast.success('Copied to clipboard');
},
[copyToClipboard, value],
);
const handleIncreaseLineClamp = useCallback(
(e: MouseEvent): void => {
e.stopPropagation();
increaseLineClamp();
},
[increaseLineClamp],
);
const handleDecreaseLineClamp = useCallback(
(e: MouseEvent): void => {
e.stopPropagation();
decreaseLineClamp();
},
[decreaseLineClamp],
);
const tooltipContent = (
<div className={styles.tooltipContent}>
<div className={styles.actions}>
<button
type="button"
className={styles.actionButton}
onClick={handleDecreaseLineClamp}
disabled={lineClamp <= 1}
data-testid="cell-value-decrease-line-height"
title="Decrease line height"
>
<Minus size={14} />
</button>
<button
type="button"
className={styles.actionButton}
onClick={handleIncreaseLineClamp}
disabled={lineClamp >= 10}
data-testid="cell-value-increase-line-height"
title="Increase line height"
>
<Plus size={14} />
</button>
<Divider type="vertical" className={styles.divider} />
<button
type="button"
className={styles.actionButton}
onClick={handleCopy}
data-testid="cell-value-copy"
title="Copy value"
>
<Copy size={14} />
</button>
</div>
</div>
);
return (
<TooltipSimple
title={tooltipContent}
arrow
tooltipContentProps={{
className: styles.tooltipContentWrapper,
}}
>
<TanStackTable.Text className={styles.value}>{value}</TanStackTable.Text>
</TooltipSimple>
);
}

View File

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

View File

@@ -0,0 +1,39 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export interface IInfraMonitoringCellActionsStore {
lineClamp: number;
increaseLineClamp: () => void;
decreaseLineClamp: () => void;
}
const MIN_LINE_CLAMP = 1;
const MAX_LINE_CLAMP = 10;
const DEFAULT_LINE_CLAMP = 2;
export const useInfraMonitoringCellActionsStore =
create<IInfraMonitoringCellActionsStore>()(
persist(
(set, get) => ({
lineClamp: DEFAULT_LINE_CLAMP,
increaseLineClamp: (): void => {
const current = get().lineClamp;
if (current < MAX_LINE_CLAMP) {
set({ lineClamp: current + 1 });
}
},
decreaseLineClamp: (): void => {
const current = get().lineClamp;
if (current > MIN_LINE_CLAMP) {
set({ lineClamp: current - 1 });
}
},
}),
{
name: '@signoz/infra-monitoring-cell-actions',
},
),
);
export const useInfraMonitoringLineClamp = (): number =>
useInfraMonitoringCellActionsStore((s) => s.lineClamp);

View File

@@ -1,9 +1,6 @@
// oxlint-disable jsx-a11y/click-events-have-key-events
import styles from './GroupedStatusCounts.module.scss';
import TanStackTable from 'components/TanStackTableView';
import { Typography } from '@signozhq/ui/typography';
import { TextNoData } from './TextNoData';
import { MouseEventHandler } from 'react';
export interface StatusBreakdownItem {
label: string;
@@ -24,25 +21,18 @@ interface GroupedStatusCountsProps {
}
function buildTooltipContent(item: StatusCountItem): React.ReactNode {
const onClickHandle: MouseEventHandler = (e) => {
e.preventDefault();
e.stopPropagation();
};
if (!item.breakdown || item.breakdown.length === 0) {
return (
<div onClick={onClickHandle}>
<Typography.Text>
{item.label}: {item.value}
</Typography.Text>
</div>
<Typography.Text>
{item.label}: {item.value}
</Typography.Text>
);
}
const nonZeroBreakdown = item.breakdown.filter((b) => b.value > 0);
if (nonZeroBreakdown.length === 0) {
return (
<div className={styles.tooltipContent} onClick={onClickHandle}>
<div className={styles.tooltipContent}>
<Typography.Text className={styles.tooltipHeader}>
{item.label}
</Typography.Text>
@@ -53,7 +43,7 @@ function buildTooltipContent(item: StatusCountItem): React.ReactNode {
}
return (
<div className={styles.tooltipContent} onClick={onClickHandle}>
<div className={styles.tooltipContent}>
<Typography.Text className={styles.tooltipHeader}>
{item.label}
</Typography.Text>
@@ -78,7 +68,7 @@ export function GroupedStatusCounts({
showZeroValues === false ? items.filter((item) => item.value > 0) : items;
if (visibleItems.length === 0) {
return <TextNoData type="tanstack" />;
return <TanStackTable.Text>-</TanStackTable.Text>;
}
return (
@@ -91,20 +81,12 @@ export function GroupedStatusCounts({
arrow
align="start"
>
{item.value ? (
<TanStackTable.Text
className={styles.item}
style={{ '--gsc-color': item.color } as React.CSSProperties}
>
{item.value}
</TanStackTable.Text>
) : (
<TextNoData
type="tanstack"
className={styles.item}
style={{ '--gsc-color': item.color } as React.CSSProperties}
/>
)}
<TanStackTable.Text
className={styles.item}
style={{ '--gsc-color': item.color } as React.CSSProperties}
>
{item.value || '-'}
</TanStackTable.Text>
</TanStackTable.HoverTooltip>
))}
</div>

View File

@@ -1,39 +0,0 @@
import { CSSProperties, forwardRef } from 'react';
import { Typography } from '@signozhq/ui/typography';
import TanStackTable from 'components/TanStackTableView';
export interface TextNoDataProps {
type: 'typography' | 'tanstack';
className?: string;
style?: CSSProperties;
}
export const TextNoData = forwardRef<HTMLSpanElement, TextNoDataProps>(
function TextNoData({ type, className, style, ...props }, ref) {
const combinedStyle = { opacity: 0.6, ...style };
if (type === 'tanstack') {
return (
<TanStackTable.Text
ref={ref}
className={className}
style={combinedStyle}
{...props}
>
-
</TanStackTable.Text>
);
}
return (
<Typography.Text
ref={ref}
className={className}
style={combinedStyle}
{...props}
>
-
</Typography.Text>
);
},
);

View File

@@ -1,33 +1,29 @@
import { Tooltip } from 'antd';
import TanStackTable from 'components/TanStackTableView';
import {
getInvalidValueTooltipText,
InfraMonitoringEntity,
} from '../constants';
import { TextNoData } from './TextNoData';
import TanStackTable from 'components/TanStackTableView';
export function ValidateColumnValueWrapper({
children,
value,
entity,
attribute,
rowId,
}: {
children: React.ReactNode;
value: number;
entity?: InfraMonitoringEntity;
attribute?: string;
rowId: string;
}): JSX.Element {
if (value === -1 || Number.isNaN(value)) {
let element = <TextNoData type="tanstack" />;
let element = <TanStackTable.Text>-</TanStackTable.Text>;
if (entity && attribute) {
element = (
<TanStackTable.HoverTooltip
rowId={rowId}
title={getInvalidValueTooltipText(entity, attribute)}
>
<Tooltip title={getInvalidValueTooltipText(entity, attribute)}>
{element}
</TanStackTable.HoverTooltip>
</Tooltip>
);
}

View File

@@ -5,4 +5,8 @@ export {
GroupedStatusCounts,
type StatusCountItem,
} from './GroupedStatusCounts';
export { TextNoData } from './TextNoData';
export {
CellValueTooltip,
useInfraMonitoringCellActionsStore,
useInfraMonitoringLineClamp,
} from './CellValueTooltip';

View File

@@ -55,7 +55,6 @@ import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useGetGlobalConfig } from 'api/generated/services/global';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useNotifications } from 'hooks/useNotifications';
import { serialize } from 'lib/compositeQuery/serializer';
import { cloneDeep, isNil, isUndefined } from 'lodash-es';
import {
ArrowUpRight,
@@ -80,7 +79,6 @@ import {
UpdateLimitProps,
} from 'types/api/ingestionKeys/limits/types';
import { PaginationProps } from 'types/api/ingestionKeys/types';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { MeterAggregateOperator } from 'types/common/queryBuilder';
import { USER_ROLES } from 'types/roles';
import { getDaysUntilExpiry } from 'utils/timeUtils';
@@ -900,6 +898,8 @@ function MultiIngestionSettings(): JSX.Element {
},
};
const stringifiedQuery = JSON.stringify(query);
const thresholds = cloneDeep(INITIAL_ALERT_THRESHOLD_STATE.thresholds);
thresholds[0].thresholdValue = thresholdValue;
thresholds[0].unit = thresholdUnit;
@@ -909,15 +909,20 @@ function MultiIngestionSettings(): JSX.Element {
? `[ingestion][${signal.signal}] ${keyName} has exceeded daily ingestion limit`
: `[ingestion][${signal.signal}] ${signal.signal} has exceeded daily ingestion limit`;
const params = serialize(query as Query);
params.set(QueryParams.thresholds, JSON.stringify(thresholds));
params.set(QueryParams.ruleName, ruleName);
params.set(QueryParams.yAxisUnit, yAxisUnit);
// Declare the metering prefill explicitly: "in total" + the meter window.
params.set(QueryParams.matchType, AlertThresholdMatchType.IN_TOTAL);
params.set(QueryParams.evaluationWindowPreset, EvaluationWindowPreset.METER);
const URL = `${ROUTES.ALERTS_NEW}?${
QueryParams.compositeQuery
}=${encodeURIComponent(stringifiedQuery)}&${
QueryParams.thresholds
}=${encodeURIComponent(JSON.stringify(thresholds))}&${
QueryParams.ruleName
}=${encodeURIComponent(ruleName)}&${
QueryParams.yAxisUnit
}=${encodeURIComponent(yAxisUnit)}&${QueryParams.matchType}=${
AlertThresholdMatchType.IN_TOTAL
}&${QueryParams.evaluationWindowPreset}=${EvaluationWindowPreset.METER}`;
history.push(`${ROUTES.ALERTS_NEW}?${params.toString()}`);
history.push(URL);
};
const columns: AntDTableProps<GatewaytypesIngestionKeyDTO>['columns'] = [

View File

@@ -1,6 +1,5 @@
import { GatewaytypesGettableIngestionKeysDTO } from 'api/generated/services/sigNoz.schemas';
import { QueryParams } from 'constants/query';
import { deserialize } from 'lib/compositeQuery/serializer';
import { rest, server } from 'mocks-server/server';
import {
fireEvent,
@@ -133,19 +132,17 @@ describe('MultiIngestionSettings Page', () => {
expect(thresholds[0].thresholdValue).toBe(1000);
expect(thresholds[0].unit).toBe('{count}');
const compositeQuery = deserialize(urlParams);
expect(compositeQuery).not.toBeNull();
expect(compositeQuery?.unit).toBe('{count}');
expect(compositeQuery?.builder.queryData).toBeDefined();
const compositeQuery = JSON.parse(
urlParams.get(QueryParams.compositeQuery) || '{}',
);
expect(compositeQuery.unit).toBe('{count}');
expect(compositeQuery.builder.queryData).toBeDefined();
const firstQueryData = compositeQuery?.builder.queryData[0];
expect(firstQueryData?.filter?.expression).toContain(
const firstQueryData = compositeQuery.builder.queryData[0];
expect(firstQueryData.filter.expression).toContain(
"signoz.workspace.key.id='k1'",
);
const firstAggregation = firstQueryData?.aggregations?.[0] as {
metricName: string;
};
expect(firstAggregation.metricName).toBe(
expect(firstQueryData.aggregations[0].metricName).toBe(
'signoz.meter.metric.datapoint.count',
);
@@ -216,18 +213,18 @@ describe('MultiIngestionSettings Page', () => {
expect(thresholds[0].thresholdValue).toBe(400);
expect(thresholds[0].unit).toBe('GiBy');
const compositeQuery = deserialize(urlParams);
expect(compositeQuery).not.toBeNull();
expect(compositeQuery?.unit).toBe('bytes');
const compositeQuery = JSON.parse(
urlParams.get(QueryParams.compositeQuery) || '{}',
);
expect(compositeQuery.unit).toBe('bytes');
const firstQueryData = compositeQuery?.builder.queryData[0];
expect(firstQueryData?.filter?.expression).toContain(
const firstQueryData = compositeQuery.builder.queryData[0];
expect(firstQueryData.filter.expression).toContain(
"signoz.workspace.key.id='k2'",
);
const firstAggregation = firstQueryData?.aggregations?.[0] as {
metricName: string;
};
expect(firstAggregation.metricName).toBe('signoz.meter.log.size');
expect(firstQueryData.aggregations[0].metricName).toBe(
'signoz.meter.log.size',
);
expect(urlParams.get(QueryParams.yAxisUnit)).toBe('bytes');
expect(urlParams.get(QueryParams.ruleName)).toContain('logs');

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