mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-31 18:20:36 +01:00
Compare commits
11 Commits
fix/rule-c
...
feat/alert
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c95bf63da | ||
|
|
2f257fa04f | ||
|
|
c08eb2f70a | ||
|
|
a185a504f8 | ||
|
|
7e62d1edf7 | ||
|
|
221063c91d | ||
|
|
99f1c8333e | ||
|
|
dd502444d0 | ||
|
|
f5e1c25b23 | ||
|
|
eea7b3eba5 | ||
|
|
cbbd96ab50 |
@@ -38,7 +38,8 @@ COPY ./pkg/ ./pkg/
|
||||
COPY ./templates /root/templates
|
||||
|
||||
COPY Makefile Makefile
|
||||
RUN TARGET_DIR=/root ARCHS=${TARGETARCH} ZEUS_URL=${ZEUSURL} LICENSE_URL=${ZEUSURL}/api/v1 make go-build-enterprise-race
|
||||
|
||||
RUN TARGET_DIR=/root ARCHS=${TARGETARCH} ZEUS_URL=${ZEUSURL} LICENSE_URL=${ZEUSURL}/api/v1 make go-build-enterprise
|
||||
RUN mv /root/linux-${TARGETARCH}/signoz /root/signoz
|
||||
|
||||
COPY --from=build /opt/build ./web/
|
||||
|
||||
@@ -7430,8 +7430,6 @@ components:
|
||||
- below
|
||||
- equal
|
||||
- not_equal
|
||||
- above_or_equal
|
||||
- below_or_equal
|
||||
- outside_bounds
|
||||
type: string
|
||||
RuletypesCumulativeSchedule:
|
||||
|
||||
@@ -227,11 +227,14 @@ cd tests/e2e
|
||||
# Single feature dir
|
||||
npx playwright test tests/alerts/ --project=chromium
|
||||
|
||||
# Single sub-area
|
||||
npx playwright test tests/alerts/history/ --project=chromium
|
||||
|
||||
# Single file
|
||||
npx playwright test tests/alerts/alerts.spec.ts --project=chromium
|
||||
npx playwright test tests/alerts/page.spec.ts --project=chromium
|
||||
|
||||
# Single test by title grep
|
||||
npx playwright test --project=chromium -g "TC-01"
|
||||
npx playwright test --project=chromium -g "AL-01"
|
||||
```
|
||||
|
||||
### Iterative modes
|
||||
@@ -265,7 +268,14 @@ yarn test:staging
|
||||
| `SIGNOZ_E2E_PASSWORD` | Admin password. Bootstrap writes the integration-test default. |
|
||||
| `SIGNOZ_E2E_SEEDER_URL` | Seeder HTTP base URL — hit by specs that need per-test telemetry. |
|
||||
|
||||
Loading order in `playwright.config.ts`: `.env` first (user-provided, staging), then `.env.local` with `override: true` (bootstrap-generated, local mode). Anything already set in `process.env` at yarn-test time wins because dotenv doesn't touch vars that are already present.
|
||||
Precedence in `playwright.config.ts`, lowest to highest: `.env` (user-provided, staging) → `.env.local` (bootstrap-generated, local mode) → whatever is already in `process.env`. The config parses both files itself and only fills in keys the environment does not already define, so exporting a variable always wins:
|
||||
|
||||
```bash
|
||||
# runs against a locally served frontend, not whatever .env.local points at
|
||||
SIGNOZ_E2E_BASE_URL=http://127.0.0.1:3301 pnpm test tests/alerts
|
||||
```
|
||||
|
||||
This is deliberately not `dotenv.config({ override: true })`. That flag makes the *file* beat `process.env`, which silently discarded exported values — including the `SIGNOZ_E2E_BASE_URL` in `pnpm test:staging`, whenever a `.env.local` happened to exist.
|
||||
|
||||
### Playwright options
|
||||
|
||||
|
||||
@@ -8479,8 +8479,6 @@ export enum RuletypesCompareOperatorDTO {
|
||||
below = 'below',
|
||||
equal = 'equal',
|
||||
not_equal = 'not_equal',
|
||||
above_or_equal = 'above_or_equal',
|
||||
below_or_equal = 'below_or_equal',
|
||||
outside_bounds = 'outside_bounds',
|
||||
}
|
||||
export interface RuletypesBasicRuleThresholdDTO {
|
||||
|
||||
@@ -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'>;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')];
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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 & {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -29,6 +29,7 @@ function PopoverContent({
|
||||
<Link
|
||||
to={`${ROUTES.LOGS_EXPLORER}?${relatedLogsLink}`}
|
||||
className="contributor-row-popover-buttons__button"
|
||||
data-testid="alert-popover-view-logs"
|
||||
>
|
||||
<div className="icon">
|
||||
<LogsIcon />
|
||||
@@ -40,6 +41,7 @@ function PopoverContent({
|
||||
<Link
|
||||
to={`${ROUTES.TRACES_EXPLORER}?${relatedTracesLink}`}
|
||||
className="contributor-row-popover-buttons__button"
|
||||
data-testid="alert-popover-view-traces"
|
||||
>
|
||||
<div className="icon">
|
||||
<DraftingCompass
|
||||
|
||||
@@ -26,7 +26,10 @@ function ChangePercentage({
|
||||
}: ChangePercentageProps): JSX.Element {
|
||||
if (direction > 0) {
|
||||
return (
|
||||
<div className="change-percentage change-percentage--success">
|
||||
<div
|
||||
className="change-percentage change-percentage--success"
|
||||
data-testid="stats-card-change"
|
||||
>
|
||||
<div className="change-percentage__icon">
|
||||
<ArrowDownLeft size={14} color={Color.BG_FOREST_500} />
|
||||
</div>
|
||||
@@ -38,7 +41,10 @@ function ChangePercentage({
|
||||
}
|
||||
if (direction < 0) {
|
||||
return (
|
||||
<div className="change-percentage change-percentage--error">
|
||||
<div
|
||||
className="change-percentage change-percentage--error"
|
||||
data-testid="stats-card-change"
|
||||
>
|
||||
<div className="change-percentage__icon">
|
||||
<ArrowUpRight size={14} color={Color.BG_CHERRY_500} />
|
||||
</div>
|
||||
@@ -50,7 +56,10 @@ function ChangePercentage({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="change-percentage change-percentage--no-previous-data">
|
||||
<div
|
||||
className="change-percentage change-percentage--no-previous-data"
|
||||
data-testid="stats-card-change"
|
||||
>
|
||||
<div className="change-percentage__label">no previous data</div>
|
||||
</div>
|
||||
);
|
||||
@@ -103,7 +112,12 @@ function StatsCard({
|
||||
const formattedEndTimeForTooltip = convertTimestampToLocaleDateString(endTime);
|
||||
|
||||
return (
|
||||
<div className={`stats-card ${isEmpty ? 'stats-card--empty' : ''}`}>
|
||||
<div
|
||||
className={`stats-card ${isEmpty ? 'stats-card--empty' : ''}`}
|
||||
data-testid="stats-card"
|
||||
data-stats-title={title}
|
||||
data-empty={isEmpty ? 'true' : 'false'}
|
||||
>
|
||||
<div className="stats-card__title-wrapper">
|
||||
<div className="title">{title}</div>
|
||||
<div className="duration-indicator">
|
||||
@@ -123,7 +137,7 @@ function StatsCard({
|
||||
</div>
|
||||
|
||||
<div className="stats-card__stats">
|
||||
<div className="count-label">
|
||||
<div className="count-label" data-testid="stats-card-value">
|
||||
{isEmpty ? emptyMessage : displayValue || totalCurrentCount}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -81,7 +81,11 @@ function StatsGraph({ timeSeries, changeDirection }: Props): JSX.Element {
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
|
||||
<div
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
ref={graphRef}
|
||||
data-testid="stats-card-sparkline"
|
||||
>
|
||||
<Uplot data={[xData, yData]} options={options} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -48,11 +48,16 @@ function TopContributorsCard({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="top-contributors-card">
|
||||
<div className="top-contributors-card" data-testid="top-contributors-card">
|
||||
<div className="top-contributors-card__header">
|
||||
<div className="title">top contributors</div>
|
||||
{topContributorsData.length > 3 && (
|
||||
<Button type="text" className="view-all" onClick={toggleViewAllDrawer}>
|
||||
<Button
|
||||
type="text"
|
||||
className="view-all"
|
||||
onClick={toggleViewAllDrawer}
|
||||
data-testid="top-contributors-view-all"
|
||||
>
|
||||
<div className="label">View all</div>
|
||||
<div className="icon">
|
||||
<ArrowRight
|
||||
|
||||
@@ -68,7 +68,10 @@ function TopContributorsRows({
|
||||
relatedTracesLink={record.relatedTracesLink}
|
||||
relatedLogsLink={record.relatedLogsLink}
|
||||
>
|
||||
<div className="total-contribution">
|
||||
<div
|
||||
className="total-contribution"
|
||||
data-testid="top-contributors-row-count"
|
||||
>
|
||||
{count}/{totalCurrentTriggers}
|
||||
</div>
|
||||
</ConditionalAlertPopover>
|
||||
@@ -78,7 +81,10 @@ function TopContributorsRows({
|
||||
|
||||
const handleRowClick = (
|
||||
record: AlertRuleTopContributors,
|
||||
): HTMLAttributes<AlertRuleTimelineTableResponse> => ({
|
||||
): HTMLAttributes<AlertRuleTimelineTableResponse> & {
|
||||
'data-testid': string;
|
||||
} => ({
|
||||
'data-testid': 'top-contributors-row',
|
||||
onClick: (): void => {
|
||||
logEvent('Alert history: Top contributors row: Clicked', {
|
||||
labels: record.labels,
|
||||
|
||||
@@ -31,7 +31,10 @@ function ViewAllDrawer({
|
||||
}}
|
||||
title="Viewing All Contributors"
|
||||
>
|
||||
<div className="top-contributors-card--view-all">
|
||||
<div
|
||||
className="top-contributors-card--view-all"
|
||||
data-testid="top-contributors-drawer"
|
||||
>
|
||||
<div className="top-contributors-card__content">
|
||||
<TopContributorsRows
|
||||
topContributors={topContributorsData}
|
||||
|
||||
@@ -32,8 +32,8 @@ function GraphWrapper({
|
||||
}, [data?.data]);
|
||||
|
||||
return (
|
||||
<div className="timeline-graph">
|
||||
<div className="timeline-graph__title">
|
||||
<div className="timeline-graph" data-testid="timeline-graph">
|
||||
<div className="timeline-graph__title" data-testid="timeline-graph-title">
|
||||
{totalCurrentTriggers} triggers in {relativeTime}
|
||||
</div>
|
||||
<div className="timeline-graph__chart">
|
||||
|
||||
@@ -118,7 +118,10 @@ function TimelineTableContent(): JSX.Element {
|
||||
|
||||
const handleRowClick = (
|
||||
record: AlertRuleTimelineTableResponse,
|
||||
): HTMLAttributes<AlertRuleTimelineTableResponse> => ({
|
||||
): HTMLAttributes<AlertRuleTimelineTableResponse> & {
|
||||
'data-testid': string;
|
||||
} => ({
|
||||
'data-testid': 'timeline-row',
|
||||
onClick: (): void => {
|
||||
void logEvent('Alert history: Timeline table row: Clicked', {
|
||||
ruleId: record.ruleID,
|
||||
@@ -128,12 +131,15 @@ function TimelineTableContent(): JSX.Element {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="timeline-table">
|
||||
<div className="timeline-table" data-testid="timeline-table">
|
||||
{/* If we don't wait to have the keys, the QuerySearch will not render them at first usage */}
|
||||
{!isLoadingKeys && hardcodedAttributeKeys ? (
|
||||
<div className="timeline-table__filter">
|
||||
<div className="timeline-table__filter-row">
|
||||
<div className="timeline-table__filter-search">
|
||||
<div
|
||||
className="timeline-table__filter-search"
|
||||
data-testid="timeline-filter-search"
|
||||
>
|
||||
<QuerySearch
|
||||
onChange={querySearchOnChange}
|
||||
queryData={queryData}
|
||||
@@ -155,6 +161,7 @@ function TimelineTableContent(): JSX.Element {
|
||||
<Skeleton.Input
|
||||
className="timeline-table__filter--loading-skeleton"
|
||||
active
|
||||
data-testid="timeline-filter-skeleton"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -172,14 +179,17 @@ function TimelineTableContent(): JSX.Element {
|
||||
locale={{
|
||||
emptyText:
|
||||
isError && apiError ? (
|
||||
<div className="timeline-table__error">
|
||||
<div className="timeline-table__error" data-testid="timeline-error">
|
||||
<ErrorContent error={apiError} />
|
||||
</div>
|
||||
) : undefined,
|
||||
}}
|
||||
footer={(): JSX.Element => (
|
||||
<div className="timeline-table__pagination">
|
||||
<div className="timeline-table__pagination-info">
|
||||
<div
|
||||
className="timeline-table__pagination-info"
|
||||
data-testid="timeline-footer-range"
|
||||
>
|
||||
{paginationConfig.showTotal?.(totalItems, [
|
||||
totalItems === 0
|
||||
? 0
|
||||
|
||||
@@ -21,7 +21,7 @@ export const timelineTableColumns = ({
|
||||
sorter: true,
|
||||
width: 140,
|
||||
render: (value): JSX.Element => (
|
||||
<div className="alert-rule-state">
|
||||
<div className="alert-rule-state" data-testid="timeline-row-state">
|
||||
<AlertState state={value} showLabel />
|
||||
</div>
|
||||
),
|
||||
@@ -30,7 +30,7 @@ export const timelineTableColumns = ({
|
||||
title: 'LABELS',
|
||||
dataIndex: 'labels',
|
||||
render: (labels): JSX.Element => (
|
||||
<div className="alert-rule-labels">
|
||||
<div className="alert-rule-labels" data-testid="timeline-row-labels">
|
||||
<AlertLabels labels={labels} />
|
||||
</div>
|
||||
),
|
||||
@@ -40,7 +40,10 @@ export const timelineTableColumns = ({
|
||||
dataIndex: 'unixMilli',
|
||||
width: 200,
|
||||
render: (value): JSX.Element => (
|
||||
<div className="alert-rule__created-at">
|
||||
<div
|
||||
className="alert-rule__created-at"
|
||||
data-testid="timeline-row-created-at"
|
||||
>
|
||||
{formatTimezoneAdjustedTimestamp(value, DATE_TIME_FORMATS.DASH_DATETIME)}
|
||||
</div>
|
||||
),
|
||||
@@ -53,7 +56,7 @@ export const timelineTableColumns = ({
|
||||
if (!record.relatedTracesLink && !record.relatedLogsLink) {
|
||||
return (
|
||||
<Tooltip title="No links available for this item">
|
||||
<Button type="text" ghost disabled>
|
||||
<Button type="text" ghost disabled data-testid="timeline-row-actions">
|
||||
<Ellipsis className="dropdown-icon" size="md" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
@@ -65,7 +68,7 @@ export const timelineTableColumns = ({
|
||||
relatedTracesLink={record.relatedTracesLink ?? ''}
|
||||
relatedLogsLink={record.relatedLogsLink ?? ''}
|
||||
>
|
||||
<Button type="text" ghost>
|
||||
<Button type="text" ghost data-testid="timeline-row-actions">
|
||||
<Ellipsis className="dropdown-icon" size="md" />
|
||||
</Button>
|
||||
</ConditionalAlertPopover>
|
||||
|
||||
@@ -23,6 +23,7 @@ function TimelineTabs(): JSX.Element {
|
||||
{
|
||||
value: TimelineTab.OVERALL_STATUS,
|
||||
label: 'Overall Status',
|
||||
testId: 'timeline-tab-overall-status',
|
||||
},
|
||||
{
|
||||
value: TimelineTab.TOP_5_CONTRIBUTORS,
|
||||
@@ -33,6 +34,7 @@ function TimelineTabs(): JSX.Element {
|
||||
</div>
|
||||
),
|
||||
disabled: true,
|
||||
testId: 'timeline-tab-top-contributors',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -57,14 +59,17 @@ function TimelineFilters(): JSX.Element {
|
||||
{
|
||||
value: TimelineFilter.ALL,
|
||||
label: 'All',
|
||||
testId: 'timeline-filter-all',
|
||||
},
|
||||
{
|
||||
value: TimelineFilter.FIRED,
|
||||
label: 'Fired',
|
||||
testId: 'timeline-filter-fired',
|
||||
},
|
||||
{
|
||||
value: TimelineFilter.RESOLVED,
|
||||
label: 'Resolved',
|
||||
testId: 'timeline-filter-resolved',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -66,10 +66,6 @@ function ThresholdItem({
|
||||
return '=';
|
||||
case AlertThresholdOperator.IS_NOT_EQUAL_TO:
|
||||
return '!=';
|
||||
case AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO:
|
||||
return '>=';
|
||||
case AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO:
|
||||
return '<=';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -83,10 +83,6 @@ const getOperatorWord = (op: AlertThresholdOperator): string => {
|
||||
return 'equal';
|
||||
case AlertThresholdOperator.IS_NOT_EQUAL_TO:
|
||||
return 'not equal';
|
||||
case AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO:
|
||||
return 'equal or exceed';
|
||||
case AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO:
|
||||
return 'equal or fall below';
|
||||
default:
|
||||
return 'exceed';
|
||||
}
|
||||
@@ -102,10 +98,6 @@ const getThresholdValue = (op: AlertThresholdOperator): number => {
|
||||
return 100;
|
||||
case AlertThresholdOperator.IS_NOT_EQUAL_TO:
|
||||
return 0;
|
||||
case AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO:
|
||||
return 80;
|
||||
case AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO:
|
||||
return 50;
|
||||
default:
|
||||
return 80;
|
||||
}
|
||||
@@ -124,8 +116,6 @@ const getDataPoints = (
|
||||
[AlertThresholdOperator.IS_EQUAL_TO]: [95, 100, 105, 90, 100],
|
||||
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [5, 0, 10, 15, 0],
|
||||
[AlertThresholdOperator.IS_ABOVE]: [75, 85, 90, 78, 95],
|
||||
[AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO]: [75, 80, 90, 78, 95],
|
||||
[AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO]: [60, 50, 40, 55, 35],
|
||||
[AlertThresholdOperator.ABOVE_BELOW]: [75, 85, 90, 78, 95],
|
||||
},
|
||||
[AlertThresholdMatchType.ALL_THE_TIME]: {
|
||||
@@ -133,8 +123,6 @@ const getDataPoints = (
|
||||
[AlertThresholdOperator.IS_EQUAL_TO]: [100, 100, 100, 100, 100],
|
||||
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [5, 10, 15, 8, 12],
|
||||
[AlertThresholdOperator.IS_ABOVE]: [85, 87, 90, 88, 95],
|
||||
[AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO]: [80, 87, 90, 88, 95],
|
||||
[AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO]: [50, 40, 35, 42, 38],
|
||||
[AlertThresholdOperator.ABOVE_BELOW]: [85, 87, 90, 88, 95],
|
||||
},
|
||||
[AlertThresholdMatchType.ON_AVERAGE]: {
|
||||
@@ -142,8 +130,6 @@ const getDataPoints = (
|
||||
[AlertThresholdOperator.IS_EQUAL_TO]: [95, 105, 100, 95, 105],
|
||||
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [5, 10, 15, 8, 12],
|
||||
[AlertThresholdOperator.IS_ABOVE]: [75, 85, 90, 78, 95],
|
||||
[AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO]: [70, 85, 90, 75, 80],
|
||||
[AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO]: [60, 40, 55, 45, 50],
|
||||
[AlertThresholdOperator.ABOVE_BELOW]: [75, 85, 90, 78, 95],
|
||||
},
|
||||
[AlertThresholdMatchType.IN_TOTAL]: {
|
||||
@@ -151,8 +137,6 @@ const getDataPoints = (
|
||||
[AlertThresholdOperator.IS_EQUAL_TO]: [20, 20, 20, 20, 20],
|
||||
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [10, 15, 25, 5, 30],
|
||||
[AlertThresholdOperator.IS_ABOVE]: [10, 15, 25, 5, 30],
|
||||
[AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO]: [10, 15, 25, 5, 25],
|
||||
[AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO]: [8, 5, 10, 12, 15],
|
||||
[AlertThresholdOperator.ABOVE_BELOW]: [10, 15, 25, 5, 30],
|
||||
},
|
||||
[AlertThresholdMatchType.LAST]: {
|
||||
@@ -160,8 +144,6 @@ const getDataPoints = (
|
||||
[AlertThresholdOperator.IS_EQUAL_TO]: [75, 85, 90, 78, 100],
|
||||
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [75, 85, 90, 78, 25],
|
||||
[AlertThresholdOperator.IS_ABOVE]: [75, 85, 90, 78, 95],
|
||||
[AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO]: [75, 85, 90, 78, 80],
|
||||
[AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO]: [75, 85, 90, 78, 50],
|
||||
[AlertThresholdOperator.ABOVE_BELOW]: [75, 85, 90, 78, 95],
|
||||
},
|
||||
};
|
||||
@@ -175,8 +157,6 @@ const getTooltipOperatorSymbol = (op: AlertThresholdOperator): string => {
|
||||
[AlertThresholdOperator.IS_BELOW]: '<',
|
||||
[AlertThresholdOperator.IS_EQUAL_TO]: '=',
|
||||
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: '!=',
|
||||
[AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO]: '>=',
|
||||
[AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO]: '<=',
|
||||
[AlertThresholdOperator.ABOVE_BELOW]: '>',
|
||||
};
|
||||
return symbolMap[op] || '>';
|
||||
@@ -272,10 +252,6 @@ export const getMatchTypeTooltip = (
|
||||
return p === thresholdValue;
|
||||
case AlertThresholdOperator.IS_NOT_EQUAL_TO:
|
||||
return p !== thresholdValue;
|
||||
case AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO:
|
||||
return p >= thresholdValue;
|
||||
case AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO:
|
||||
return p <= thresholdValue;
|
||||
default:
|
||||
return p > thresholdValue;
|
||||
}
|
||||
@@ -318,8 +294,7 @@ export const getMatchTypeTooltip = (
|
||||
matchType={matchType}
|
||||
>
|
||||
Alert triggers (all points {operatorWord} {thresholdValue})<br />
|
||||
If any point didn't {operatorWord} {thresholdValue}, no alert would
|
||||
fire
|
||||
If any point was {thresholdValue}, no alert would fire
|
||||
</TooltipExample>
|
||||
<TooltipLink />
|
||||
</TooltipContent>
|
||||
|
||||
@@ -532,7 +532,7 @@ describe('Footer utils', () => {
|
||||
['symbol', '>', 'at_least_once'],
|
||||
['literal', 'above', 'at_least_once'],
|
||||
['short', 'eq', 'avg'],
|
||||
['inclusive', 'above_or_equal', 'at_least_once'],
|
||||
['UI-unexposed', 'above_or_equal', 'at_least_once'],
|
||||
])(
|
||||
'round-trips %s op/matchType unchanged through the submit payload (%s / %s)',
|
||||
(_desc, op, matchType) => {
|
||||
|
||||
@@ -332,20 +332,25 @@ describe('CreateAlertV2 utils', () => {
|
||||
['not_equal', AlertThresholdOperator.IS_NOT_EQUAL_TO],
|
||||
['not_eq', AlertThresholdOperator.IS_NOT_EQUAL_TO],
|
||||
['!=', AlertThresholdOperator.IS_NOT_EQUAL_TO],
|
||||
['5', AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO],
|
||||
['above_or_equal', AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO],
|
||||
['above_or_eq', AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO],
|
||||
['>=', AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO],
|
||||
['6', AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO],
|
||||
['below_or_equal', AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO],
|
||||
['below_or_eq', AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO],
|
||||
['<=', AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO],
|
||||
['7', AlertThresholdOperator.ABOVE_BELOW],
|
||||
['outside_bounds', AlertThresholdOperator.ABOVE_BELOW],
|
||||
])('maps backend alias %s to canonical enum', (alias, expected) => {
|
||||
expect(normalizeOperator(alias)).toBe(expected);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['5', 'above_or_equal'],
|
||||
['above_or_equal', 'above_or_equal'],
|
||||
['above_or_eq', 'above_or_equal'],
|
||||
['>=', 'above_or_equal'],
|
||||
['6', 'below_or_equal'],
|
||||
['below_or_equal', 'below_or_equal'],
|
||||
['below_or_eq', 'below_or_equal'],
|
||||
['<=', 'below_or_equal'],
|
||||
])('returns undefined for UI-unexposed alias %s (%s family)', (alias) => {
|
||||
expect(normalizeOperator(alias)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for unknown values', () => {
|
||||
expect(normalizeOperator('gibberish')).toBeUndefined();
|
||||
expect(normalizeOperator(undefined)).toBeUndefined();
|
||||
@@ -408,8 +413,8 @@ describe('CreateAlertV2 utils', () => {
|
||||
['symbol', '>', 'at_least_once'],
|
||||
['short form', 'eq', 'avg'],
|
||||
['mixed numeric and literal', '7', 'last'],
|
||||
['inclusive literal operator', 'above_or_equal', 'at_least_once'],
|
||||
['inclusive numeric operator', '5', 'at_least_once'],
|
||||
['UI-unexposed operator', 'above_or_equal', 'at_least_once'],
|
||||
['UI-unexposed numeric operator', '5', 'at_least_once'],
|
||||
])('preserves %s op/matchType verbatim (%s / %s)', (_desc, op, matchType) => {
|
||||
const state = getThresholdStateFromAlertDef(buildDef(op, matchType));
|
||||
expect(state.operator).toBe(op);
|
||||
|
||||
@@ -2,8 +2,9 @@ import { AlertThresholdMatchType, AlertThresholdOperator } from './types';
|
||||
|
||||
// Mirrors the backend's CompareOperator.Normalize() in
|
||||
// pkg/types/ruletypes/compare.go. Maps any accepted alias to the enum value
|
||||
// the dropdown understands. Returns undefined for unknown values so callers
|
||||
// can keep the raw value on screen instead of silently rewriting it.
|
||||
// the dropdown understands. Returns undefined for aliases the UI does not
|
||||
// expose (e.g. above_or_equal, below_or_equal) so callers can keep the raw
|
||||
// value on screen instead of silently rewriting it.
|
||||
export function normalizeOperator(
|
||||
raw: string | undefined,
|
||||
): AlertThresholdOperator | undefined {
|
||||
@@ -26,16 +27,6 @@ export function normalizeOperator(
|
||||
case 'not_eq':
|
||||
case '!=':
|
||||
return AlertThresholdOperator.IS_NOT_EQUAL_TO;
|
||||
case '5':
|
||||
case 'above_or_equal':
|
||||
case 'above_or_eq':
|
||||
case '>=':
|
||||
return AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO;
|
||||
case '6':
|
||||
case 'below_or_equal':
|
||||
case 'below_or_eq':
|
||||
case '<=':
|
||||
return AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO;
|
||||
case '7':
|
||||
case 'outside_bounds':
|
||||
return AlertThresholdOperator.ABOVE_BELOW;
|
||||
|
||||
@@ -125,14 +125,6 @@ export const THRESHOLD_OPERATOR_OPTIONS = [
|
||||
{ value: AlertThresholdOperator.IS_BELOW, label: 'BELOW' },
|
||||
{ value: AlertThresholdOperator.IS_EQUAL_TO, label: 'EQUAL TO' },
|
||||
{ value: AlertThresholdOperator.IS_NOT_EQUAL_TO, label: 'NOT EQUAL TO' },
|
||||
{
|
||||
value: AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO,
|
||||
label: 'ABOVE OR EQUAL TO',
|
||||
},
|
||||
{
|
||||
value: AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO,
|
||||
label: 'BELOW OR EQUAL TO',
|
||||
},
|
||||
];
|
||||
|
||||
export const ANOMALY_THRESHOLD_OPERATOR_OPTIONS = [
|
||||
|
||||
@@ -99,8 +99,6 @@ export enum AlertThresholdOperator {
|
||||
IS_BELOW = 'below',
|
||||
IS_EQUAL_TO = 'equal',
|
||||
IS_NOT_EQUAL_TO = 'not_equal',
|
||||
IS_ABOVE_OR_EQUAL_TO = 'above_or_equal',
|
||||
IS_BELOW_OR_EQUAL_TO = 'below_or_equal',
|
||||
ABOVE_BELOW = 'outside_bounds',
|
||||
}
|
||||
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { sortByMeanDesc } from '../sortByMeanDesc';
|
||||
|
||||
interface Item {
|
||||
name: string;
|
||||
values: (number | null)[];
|
||||
}
|
||||
|
||||
const item = (name: string, values: (number | null)[]): Item => ({
|
||||
name,
|
||||
values,
|
||||
});
|
||||
|
||||
const sorted = (items: Item[]): string[] =>
|
||||
sortByMeanDesc(items, {
|
||||
getValues: (entry) => entry.values,
|
||||
getKey: (entry) => entry.name,
|
||||
}).map((entry) => entry.name);
|
||||
|
||||
describe('sortByMeanDesc', () => {
|
||||
it('orders items by descending mean', () => {
|
||||
expect(
|
||||
sorted([item('a', [1, 1]), item('b', [10, 20]), item('c', [5, 5])]),
|
||||
).toStrictEqual(['b', 'c', 'a']);
|
||||
});
|
||||
|
||||
it('sinks items with no finite values to the bottom', () => {
|
||||
expect(
|
||||
sorted([item('a', []), item('b', [NaN, Infinity]), item('c', [1])]),
|
||||
).toStrictEqual(['c', 'a', 'b']);
|
||||
});
|
||||
|
||||
it('ignores non-finite and null values when averaging', () => {
|
||||
// Mean over the finite values only is 10, so NaN must not poison it to last place.
|
||||
expect(
|
||||
sorted([item('a', [2, 2]), item('b', [10, NaN]), item('c', [4, null])]),
|
||||
).toStrictEqual(['b', 'c', 'a']);
|
||||
});
|
||||
|
||||
it('produces the same order whichever order the items arrive in', () => {
|
||||
const a = item('a', [5]);
|
||||
const b = item('b', [5]);
|
||||
const c = item('c', [9]);
|
||||
|
||||
expect(sorted([a, b, c])).toStrictEqual(sorted([c, b, a]));
|
||||
expect(sorted([b, a, c])).toStrictEqual(['c', 'a', 'b']);
|
||||
});
|
||||
|
||||
it('tiebreaks equal means on the key', () => {
|
||||
expect(sorted([item('b', [1]), item('a', [1])])).toStrictEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('tiebreaks on the key for items that have no finite values either', () => {
|
||||
expect(sorted([item('b', []), item('a', [NaN])])).toStrictEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('does not mutate the input array', () => {
|
||||
const items = [item('a', [1]), item('b', [9])];
|
||||
|
||||
expect(sorted(items)).toStrictEqual(['b', 'a']);
|
||||
expect(items.map((entry) => entry.name)).toStrictEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('returns an empty array for no items', () => {
|
||||
expect(sorted([])).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
type MeanInput = number | null | undefined;
|
||||
|
||||
function finiteMean(values: Iterable<MeanInput>): number | null {
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
for (const value of values) {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
sum += value;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
return count > 0 ? sum / count : null;
|
||||
}
|
||||
|
||||
export interface SortByMeanDescOptions<T> {
|
||||
/** Numeric values of an item; non-finite entries are ignored. */
|
||||
getValues: (item: T) => Iterable<MeanInput>;
|
||||
/** Stable identity of an item, used to break ties on equal means. */
|
||||
getKey: (item: T) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Orders series by descending mean, on a copy (V1 parity: `utils/getSortedSeriesData`). The wire
|
||||
* order can't stand in for it: the backend returns series in Go map-iteration order.
|
||||
*
|
||||
* Call this before building the uPlot config and aligned data — those two and click attribution
|
||||
* all index the list positionally, and uPlot draws a stacked bar's segments in series-index order.
|
||||
*/
|
||||
export function sortByMeanDesc<T>(
|
||||
items: T[],
|
||||
{ getValues, getKey }: SortByMeanDescOptions<T>,
|
||||
): T[] {
|
||||
return items
|
||||
.map((item) => ({ item, mean: finiteMean(getValues(item)) }))
|
||||
.sort((a, b) => {
|
||||
if (a.mean !== null && b.mean !== null && a.mean !== b.mean) {
|
||||
return b.mean - a.mean;
|
||||
}
|
||||
if ((a.mean === null) !== (b.mean === null)) {
|
||||
return a.mean === null ? 1 : -1;
|
||||
}
|
||||
return getKey(a.item).localeCompare(getKey(b.item));
|
||||
})
|
||||
.map(({ item }) => item);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
min-width: 800px;
|
||||
min-width: 595px;
|
||||
|
||||
> * {
|
||||
min-width: 0;
|
||||
|
||||
@@ -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'],
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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,14 +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 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';
|
||||
@@ -142,7 +134,7 @@ function EntityLogsContent({
|
||||
if (validation.isValid) {
|
||||
querySearchOnRun(newUserExpression);
|
||||
|
||||
void logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category,
|
||||
@@ -170,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(),
|
||||
[QueryParams.compositeQuery]: JSON.stringify({
|
||||
...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 (
|
||||
@@ -334,7 +287,6 @@ function EntityLogsContent({
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
onScrollToLog={handleScrollToLog}
|
||||
handleOpenInExplorer={(e): void => handleOpenInExplorer(e, activeLog)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
text-wrap: auto;
|
||||
}
|
||||
|
||||
.infoIcon {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Fragment, useEffect, useMemo, useRef } from 'react';
|
||||
import { Fragment, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
BarChart,
|
||||
ChevronsLeftRight,
|
||||
@@ -6,17 +6,12 @@ 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,
|
||||
@@ -47,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>({
|
||||
@@ -115,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();
|
||||
@@ -152,7 +133,6 @@ export default function K8sBaseDetailsContent<T>({
|
||||
category: eventCategory,
|
||||
view: value,
|
||||
});
|
||||
logInfraDrawerTabViewedEvent(category, value, false);
|
||||
};
|
||||
|
||||
const handleExplorePagesRedirect = (): void => {
|
||||
@@ -174,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,
|
||||
@@ -239,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>
|
||||
);
|
||||
})}
|
||||
@@ -288,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={[
|
||||
@@ -307,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>
|
||||
@@ -320,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>
|
||||
@@ -333,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>
|
||||
@@ -346,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>
|
||||
@@ -357,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>
|
||||
@@ -367,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>
|
||||
)}
|
||||
@@ -403,7 +358,6 @@ export default function K8sBaseDetailsContent<T>({
|
||||
getEntityQueryPayload={getEntityQueryPayload}
|
||||
category={category}
|
||||
queryKey={`${queryKeyPrefix}Metrics`}
|
||||
view={VIEW_TYPES.METRICS}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.LOGS && (
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -21,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,
|
||||
@@ -31,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';
|
||||
@@ -65,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,
|
||||
@@ -81,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();
|
||||
@@ -94,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, '_')}`,
|
||||
@@ -116,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],
|
||||
@@ -195,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 => {
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,19 +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 { 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 {
|
||||
@@ -21,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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,7 @@
|
||||
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';
|
||||
@@ -15,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> {
|
||||
@@ -30,8 +27,6 @@ interface EntityCountsSectionProps<T> {
|
||||
selectedItem: string;
|
||||
filterExpression: string;
|
||||
closeDrawer: () => void;
|
||||
entityType: InfraMonitoringEntity;
|
||||
activeTab: string;
|
||||
}
|
||||
|
||||
export function EntityCountsSection<T>({
|
||||
@@ -40,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];
|
||||
|
||||
@@ -135,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"
|
||||
@@ -163,7 +135,7 @@ export function EntityCountsSection<T>({
|
||||
className={styles.navigateButton}
|
||||
prefix={<Compass size={14} />}
|
||||
/>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -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();
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
|
||||
.listContainer {
|
||||
flex: 1;
|
||||
height: calc(100vh - 330px) !important;
|
||||
height: calc(100vh - 312px) !important;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
||||
|
||||
@@ -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,14 +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 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;
|
||||
@@ -68,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();
|
||||
@@ -99,10 +85,8 @@ function EntityLogsContent({
|
||||
: partExpression;
|
||||
|
||||
querySearchOnRun(newUser);
|
||||
|
||||
logInfraDrawerFilterCustomizedEvent(category, 'logs', newUser, 'logs');
|
||||
},
|
||||
[userExpression, querySearchOnRun, handleCloseLogDetail, category],
|
||||
[userExpression, querySearchOnRun, handleCloseLogDetail],
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -143,13 +127,6 @@ function EntityLogsContent({
|
||||
view: InfraMonitoringEvents.LogsView,
|
||||
});
|
||||
|
||||
logInfraDrawerFilterCustomizedEvent(
|
||||
category,
|
||||
'logs',
|
||||
newUserExpression || '',
|
||||
'search',
|
||||
);
|
||||
|
||||
refetch();
|
||||
}
|
||||
},
|
||||
@@ -171,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(),
|
||||
[QueryParams.compositeQuery]: JSON.stringify({
|
||||
...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 (
|
||||
@@ -318,7 +257,6 @@ function EntityLogsContent({
|
||||
{renderContent}
|
||||
</div>
|
||||
)}
|
||||
<div ref={logDetailContainerRef} data-log-detail-ignore="true" />
|
||||
{selectedTab && activeLog && (
|
||||
<LogDetail
|
||||
log={activeLog}
|
||||
@@ -329,10 +267,6 @@ function EntityLogsContent({
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
onScrollToLog={handleScrollToLog}
|
||||
handleOpenInExplorer={(e) => handleOpenInExplorer(e, activeLog)}
|
||||
getContainer={(): HTMLElement =>
|
||||
logDetailContainerRef.current || document.body
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,6 @@ export function createPodMetricsTab<T>({
|
||||
getEntityQueryPayload={getQueryPayload}
|
||||
queryKey={queryKey}
|
||||
category={category}
|
||||
view={VIEW_TYPES.POD_METRICS}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -64,7 +64,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
min-width: 800px;
|
||||
min-width: 595px;
|
||||
overflow-x: auto;
|
||||
|
||||
> * {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export {
|
||||
CellValueTooltip,
|
||||
type CellValueTooltipProps,
|
||||
} from './CellValueTooltip';
|
||||
export {
|
||||
useInfraMonitoringCellActionsStore,
|
||||
useInfraMonitoringLineClamp,
|
||||
} from './useInfraMonitoringCellActionsStore';
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user