Compare commits

..

7 Commits

Author SHA1 Message Date
grandwizard28
98745423d2 fix(alerts): support above_or_equal and below_or_equal operators in CreateAlertV2
Adds the two inclusive operators to the v2 alert form: selectable in the
threshold operator dropdown, normalized from all backend aliases
(5/6, above_or_eq/below_or_eq, >=/<=), rendered with their symbols in
threshold rows and match-type tooltips, and prefilled losslessly from
dashboard panel thresholds instead of collapsing onto the strict
variants. The v1 form is left untouched.
2026-07-31 20:24:38 +05:30
grandwizard28
57e2d25a6c fix(ruletypes): expose above_or_equal and below_or_equal in CompareOperator enum
The operators are accepted by Validate(), normalized, evaluated and
returned by the rules API, but were commented out of Enum(), so the
generated OpenAPI spec (and clients generated from it, e.g.
terraform-provider-signoz) rejected rules the server itself creates.
2026-07-31 20:03:45 +05:30
Vinicius Lourenço
30dcb944eb refactor(infrastructure-monitoring-v2): ui/ux improvements after bug hunting (#12245)
* fix(tanstack-table): reset prev column id ref when changing storageKey

Since all tables we have some far unmounts enterely
we didn't notice this bug until I refactor
the infrastructure monitoring to be shared
and dynamic via config

Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=090fcc6bcd198251bf3c883f842d8798&p=3a5fcc6bcd19800e92a3dd70bb3a08fd&pm=s

* refactor(infrastructure-monitoring): remove cellvalue tooltip component

Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a5fcc6bcd19803581aff083c0a65717&pm=s

* feat(k8s-base-details): add copy link

Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a5fcc6bcd19802aa8e4cc69b88d1fce&pm=s

* refactor(hosts): move disk usage closer to cpu/memory usage

Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a5fcc6bcd1980b292facdfb92b95fa0&pm=s

* refactor(k8s-base-details): change wording to go to explorer, and move explorer to right

Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a5fcc6bcd1980f19e61fedee47a5d5b&pm=s

* fix(k8s-base): use text-wrap instead of forcing line break

To be able to collapse line to one line on bigger screens

Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a5fcc6bcd19808a8a30ed9ddbfa029a&pm=s

* feat(tanstack-table): add support to reset scroll

Without this prop, infra monitoring
will keep the same scroll on horizontal
when changing category

* fix(infrastructure-monitoring): reset horizontal scroll on change category

To simulate, just move horizontal scroll
to right and then change category that
can have horizontal scroll as well

* refactor(k8s-list): reorganize filters

Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a5fcc6bcd19809a9a7fca0138558551&pm=s

* refactor(infra-monitoring): reorganize table options

Add support to specify font size, max lines per row, and the columns

Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a5fcc6bcd1980cfadcdeb2f590c3dc3&pm=s

* fix(infra-monitoring): add delay to show hover & do not close on click to copy

Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a5fcc6bcd19801c8ae8c6283357021b&pm=s

* refactor(infra-monitoring): handle no data in a consistent way

With opacity: 0.6 as recommended by design team

* refactor(infra-monitoring): move details drawer to signozhq/ui

Few problems with integration but I'm planning
to fix it later on our component lib

Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a5fcc6bcd1980babfc2f29686e86d91&pm=s

* fix(infra-monitoring-details-logs): disable group by/replace filter actions

This is so deep in the prop that is better to
 disable this than trying to support part
 of this (replace filter)

Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a6fcc6bcd1980d59ba1dd7d1658e9b3&pm=s

* fix(infra-monitoring-details-logs): explorer action inherit filters

On logs, if you add custom logs and then click in a
specific log to see the details, and you click in the
go to explorer, it will inherit filters you had added
earlier

Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a6fcc6bcd1980a68bedfd8bbc17e137&pm=s

* fix(infra-monitoring-logs): scroll not working & search not allowing type

This issue is caused because radix lock the scroll
for any component not under the drawer.
This also fixed the issue with search, not sure why
this was related

Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a6fcc6bcd1980648061f29a990712dd&pm=s

Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a6fcc6bcd19805d9404d3d0f78f2c75&pm=s

* fix(infra-monitoring-logs): issue with double scroll

Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a6fcc6bcd198099b764f8e03bc20ac1&pm=s

* refactor(infra-monitoring): remove more usages of antd components

* refactor(infra-monitoring): use toggle to hide columns

This is a similar pattern we have on dashboards

Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a6fcc6bcd198000b535ca511e4e8c97&pm=s

* fix(infra-monitoring): do not refetch when click in the row

To prevent doing two requests when data in memory
is already enough

* fix(infra-monitoring): docs path

Ensure all the docs path goes to correct column/chart docs

Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a6fcc6bcd19809581f3d96dd6ecdeab&pm=s

Ref: https://app.notion.com/p/signoz/3a5fcc6bcd19805f8929e549c646985d?v=65cfcc6bcd1982a6bee688d8fd55420c&p=3a6fcc6bcd19809d9c2bf2ec41a4c92b&pm=s

* chore(volumes): fix docs path for charts

* feat(infra-monitoring-v2-events): add event for page visited

* feat(infra-monitoring-v2-events): add event for filter customized

* feat(infra-monitoring-v2-events): add event for time range customized

* feat(infra-monitoring-v2-events): add event for column customized

* feat(infra-monitoring-v2-events): add event for column sorted

* feat(infra-monitoring-v2-events): add event for drawer time range customized

* feat(infra-monitoring-v2-events): add event for group by change

* feat(infra-monitoring-v2-events): add event for filter customized

* feat(infra-monitoring-v2-events): add event for tab viewed

* feat(infra-monitoring-v2-events): add event for explorer visited

* fix(infrastructure-monitoring-v2): migrate tooltip to be from tanstack

* fix(k8s-expanded-row): add missing pre-cache when row is expanded

* fix(infra-monitoring-v2): copy ID via react-use after hook removal

frontend/src/hooks/useCopyToClipboard.ts was deleted on main (#12333) in
favour of react-use's execCommand-based copy, leaving this import
dangling and breaking tsc and the frontend build.

---------

Co-authored-by: Ashwin Bhatkal <ashwin96@gmail.com>
2026-07-31 10:40:41 +00:00
Nikhil Mantri
e9726776ab feat(alert-channel-integrations): google chat backend (#12314)
* chore: added googlechat notify function

* chore: added content extraction and formatting for google chat content

* chore: added tests

* chore: adding processing logic for makrdown to google chat format

* chore: added utf 8 sanitization and capping the limit

* chore: comment correction

* chore: added one more test

* chore: error on empty title body with non-retryable error and give 429 retryable error due to limit

* chore: cardsv2 scaffolding

* chore: truncation logic optimized for both body and title

* fix(googlechat): guard nil HTTPConfig in New with a clean error

* feat(googlechat): render grouped alerts as per-alert card sections

Render each alert in its own card section with its own View Related
Logs/Traces buttons, and move Open in SigNoz to a shared footer button
(it's per-rule). Matches the slack/msteams per-alert layout; the default
template still produces one combined section.

Cap sections at 30 with a "+N more alerts" note: Google Chat allows 100
widgets per card and silently drops the section that crosses that count
and every section after it, so capping keeps the footer and trailing
alerts from vanishing. Cap is applied before the size guard so the trim
loop only touches rendered bodies.

Generalize the 32 KB size guard to trim the longest body, then the title.

* docs(googlechat): clarify content.bodies default-vs-custom alignment

* test(googlechat): cover default-body grouped alerts (single combined section)

* chore: modified comment

* fix(googlechat): keep Open-in-SigNoz button when no body renders

The footer button was sourced from the capped alert set, which is empty
when every body renders empty (cleared Description, or a custom body
template that expands to nothing). Source it from the original first
alert instead, since the rule-source link is always available.

* refactor(googlechat): fold types.go into googlechat.go

Other notifier packages (slack, msteamsv2, pagerduty, etc.) keep types
and logic in a single file. Match that convention instead of a separate
types.go.

* refactor(googlechat): validate webhook URL in UnmarshalYAML

Move the webhook-URL validation into GoogleChatReceiverConfig.UnmarshalYAML
so the config self-validates on unmarshal, matching the upstream receiver
convention. Drop the separate validateGoogleChatConfig pass in receiver.go;
validation now runs via the defaultedNotifierConfig yaml round-trip.

* chore(googlechat): note size-guard button-URL edge as TODO

The serialized-size loop trims bodies then title but not button URLs, so a
card with many long-URL buttons could still overflow. Not reachable in the
default path (few short buttons); add a TODO to drop trailing sections to
fit once per-alert templating makes it reachable.

* refactor(googlechat): inline webhook URL validation in UnmarshalYAML

Fold the validation checks directly into UnmarshalYAML instead of a
separate helper, and drive the test through NewReceiver.

* test(googlechat): use assert for value checks, require for guards

Split testify usage per convention: require for error/nil checks and
length guards before indexing, assert for the independent value
assertions so one failure doesn't mask the rest.
2026-07-31 10:04:44 +00:00
Naman Verma
a238ed367d fix: choose first panel during migration (#12353)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix: choose first panel during migration

* test: add unit test
2026-07-31 08:59:13 +00:00
Abhi kumar
9197cb8b3b fix(dashboard-v2): open the View modal and editor on the panel's own query (#12345)
* fix(dashboard-v2): seed the query builder before the View modal mounts

The query builder context is global and outlives the View modal, so it still
holds the previously-viewed panel's query when the modal for the next panel
mounts. The modal only re-seeded it in a mount effect, and the builder's fields
seed themselves on mount — PromQL inputs are uncontrolled and QueryAddOns picks
its visible rows once — so they kept rendering the previous panel's query until
the modal was closed and reopened.

Seed the context in the open handlers instead, before the modal renders.
resetQuery (not initQueryBuilderData): the latter sets a staged query, and
swapping one staged id for another makes useSyncTimeOnStagedQueryChange
re-anchor global time, refetching every panel on the grid. Clearing the staged
query also stops the previous panel's query being committed into the new
panel's draft, and keeps the provider's URL hydration running for drilldowns.

* fix: fixed bar panel ordering issue

* test(dashboard-v2): drop the userEvent delay in the View modal suites

`userEvent.setup()` defaults to `delay: 0`, which awaits a macrotask between
every sub-event of a click. Each await flushes React, so every one re-renders
the real query builder these two suites mount — enough to push both add-on
tests past jest's 5s default timeout on CI.

`delay: null` dispatches the sequence synchronously: the add-on tests drop from
3172ms/1171ms to ~495ms/262ms and PromQL from 815ms to ~455ms, with the full
tree still rendered and every assertion unchanged. Verified to still fail
against the unseeded builder, so the regressions stay pinned.

* fix(dashboard-v2): carry the opened panel's query in compositeQuery

The query builder context is mounted above the router, so opening the panel
editor or the View modal leaves the previously-opened panel's query in it.
Fields that read the query once on mount then keep showing that panel: PromQL
inputs are uncontrolled and their rows are keyed by query name, and QueryAddOns
picks its visible rows once.

Both surfaces now state the panel's query in the URL as compositeQuery, so they
derive from the URL and survive a refresh or a shared link (V1 does the same in
WidgetHeader.onEditHandler). The URL alone isn't enough — the provider applies
it in an effect, a tick after those fields have mounted — so the same query goes
into the context before navigating. resetQuery, not initQueryBuilderData:
swapping one staged id for another makes useSyncTimeOnStagedQueryChange
re-anchor global time and refetch the grid.

* fix(dashboard-v2): order time series by mean, sharing the bar sort

Review feedback: the sort shouldn't be a bar-panel local. The mean/tiebreak
rules move to the visualization layer as a generic `sortByMeanDesc`, with a
`PanelSeries` adapter in `Panels/utils` that the bar and time-series renderers
both call, so any other bar consumer can adopt it in one line.

Time series flipped for the same reason bar did — the backend returns a query's
series in Go map-iteration order — so it now sorts too: draw order, legend order
and the View modal's legend table are deterministic across refreshes. Colours
are unaffected, since `generateColor` hashes the label rather than the position.

The order still has to be settled before the uPlot config and aligned data are
built: those two and drilldown click attribution index the flattened list
positionally, and uPlot draws a stacked bar's segments in series-index order, so
sorting inside `BarChart` paints the accumulated total over every segment.

---------

Co-authored-by: Ashwin Bhatkal <ashwin96@gmail.com>
2026-07-31 06:20:40 +00:00
dependabot[bot]
a0a17a99a6 chore(deps): bump google.golang.org/grpc in /scripts/promqltestcorpus (#12321)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.79.3 to 1.82.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.79.3...v1.82.1)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.82.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-31 06:10:21 +00:00
154 changed files with 5193 additions and 4299 deletions

View File

@@ -39,8 +39,6 @@ jobs:
matrix:
suite:
- alerts
- alertmanager
- alertmanagerrotation
- basepath
- callbackauthn
- cloudintegrations

View File

@@ -7430,6 +7430,8 @@ components:
- below
- equal
- not_equal
- above_or_equal
- below_or_equal
- outside_bounds
type: string
RuletypesCumulativeSchedule:

View File

@@ -8479,6 +8479,8 @@ 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 {

View File

@@ -6,6 +6,7 @@ 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;
@@ -16,6 +17,8 @@ export type LogDetailProps = {
logs?: ILog[];
onNavigateLog?: (log: ILog) => void;
onScrollToLog?: (logId: string) => void;
handleOpenInExplorer?: MouseEventHandler;
getContainer?: DrawerProps['getContainer'];
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
Pick<DrawerProps, 'onClose'>;

View File

@@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useCopyToClipboard, useLocation } from 'react-use';
import { useCopyToClipboard } from 'react-use';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button } from '@signozhq/ui/button';
import { Drawer, Tooltip } from 'antd';
@@ -14,9 +13,6 @@ 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';
@@ -31,8 +27,6 @@ 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,
@@ -50,12 +44,9 @@ 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';
@@ -75,6 +66,8 @@ function LogDetailInner({
logs,
onNavigateLog,
onScrollToLog,
handleOpenInExplorer,
getContainer,
}: LogDetailInnerProps): JSX.Element {
const initialContextQuery = useInitialQuery(log);
const [contextQuery, setContextQuery] = useState<Query | undefined>(
@@ -91,7 +84,7 @@ function LogDetailInner({
const [filters, setFilters] = useState<TagFilter | null>(null);
const [isEdit, setIsEdit] = useState<boolean>(false);
const { stagedQuery, updateAllQueriesOperators } = useQueryBuilder();
const { stagedQuery } = useQueryBuilder();
// Handle clicks outside to close drawer, except on explicitly ignored regions
useEffect(() => {
@@ -186,11 +179,6 @@ function LogDetailInner({
});
const isDarkMode = useIsDarkMode();
const location = useLocation();
const { safeNavigate } = useSafeNavigate();
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { notifications } = useNotifications();
@@ -246,25 +234,6 @@ 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
@@ -333,13 +302,6 @@ 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 =
@@ -374,6 +336,7 @@ 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">
@@ -411,7 +374,7 @@ function LogDetailInner({
/>
</Tooltip>
</div>
{showOpenInExplorerBtn && (
{handleOpenInExplorer && (
<div>
<Button
variant="outlined"

View File

@@ -5,6 +5,7 @@ import { Skeleton } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import {
IQuickFiltersConfig,
QuickFilterChangeEventData,
QuickFiltersSource,
} from 'components/QuickFilters/types';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
@@ -28,11 +29,12 @@ 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 } = props;
const { source, filter, onFilterChange, onQuickFilterChange } = props;
const [searchText, setSearchText] = useState<string>('');
const activeQueryIndex = useActiveQueryIndex(source);
@@ -61,6 +63,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
attributeValues,
activeQueryIndex,
onFilterChange,
onQuickFilterChange,
});
const setSearchTextDebounced = useDebouncedFn((...args) => {

View File

@@ -1,5 +1,6 @@
import {
IQuickFiltersConfig,
QuickFilterChangeEventData,
QuickFiltersSource,
} from 'components/QuickFilters/types';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@@ -19,6 +20,7 @@ interface UseCheckboxFilterActionsProps {
attributeValues: string[];
activeQueryIndex: number;
onFilterChange?: ((query: Query) => void) | null;
onQuickFilterChange?: (data: QuickFilterChangeEventData) => void;
}
interface UseCheckboxFilterActionsReturn {
@@ -42,6 +44,7 @@ function useCheckboxFilterActions({
attributeValues,
activeQueryIndex,
onFilterChange,
onQuickFilterChange,
}: UseCheckboxFilterActionsProps): UseCheckboxFilterActionsReturn {
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
@@ -60,20 +63,34 @@ function useCheckboxFilterActions({
previousState?: CheckedState,
sectionType?: SectionType,
): void => {
dispatch(
applyCheckboxToggle({
currentQuery,
activeQueryIndex,
filter,
source,
attributeValues,
value,
checked,
isOnlyOrAllClicked,
previousState,
sectionType,
}),
);
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,
});
}
};
const onClear = (): void => {

View File

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

View File

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

View File

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

View File

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

View File

@@ -51,6 +51,7 @@ 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';
@@ -110,6 +111,7 @@ function TanStackTableInner<TData, TItemKey = string>(
suffixPaginationContent,
enableAlternatingRowColors,
disableVirtualScroll,
resetScrollKey,
}: TanStackTableProps<TData, TItemKey>,
forwardedRef: React.ForwardedRef<TanStackTableHandle>,
): JSX.Element {
@@ -324,6 +326,8 @@ function TanStackTableInner<TData, TItemKey = string>(
});
}, [flatIndexForActiveRow]);
useResetScroll(virtuosoRef, resetScrollKey);
const { sensors, columnIds, handleDragEnd } = useColumnDnd({
columns: effectiveColumns,
onColumnOrderChange: handleColumnOrderChange,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -66,6 +66,10 @@ 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 '';
}

View File

@@ -83,6 +83,10 @@ 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';
}
@@ -98,6 +102,10 @@ 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;
}
@@ -116,6 +124,8 @@ 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]: {
@@ -123,6 +133,8 @@ 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]: {
@@ -130,6 +142,8 @@ 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]: {
@@ -137,6 +151,8 @@ 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]: {
@@ -144,6 +160,8 @@ 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],
},
};
@@ -157,6 +175,8 @@ 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] || '>';
@@ -252,6 +272,10 @@ 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;
}
@@ -294,7 +318,8 @@ export const getMatchTypeTooltip = (
matchType={matchType}
>
Alert triggers (all points {operatorWord} {thresholdValue})<br />
If any point was {thresholdValue}, no alert would fire
If any point didn&apos;t {operatorWord} {thresholdValue}, no alert would
fire
</TooltipExample>
<TooltipLink />
</TooltipContent>

View File

@@ -532,7 +532,7 @@ describe('Footer utils', () => {
['symbol', '>', 'at_least_once'],
['literal', 'above', 'at_least_once'],
['short', 'eq', 'avg'],
['UI-unexposed', 'above_or_equal', 'at_least_once'],
['inclusive', 'above_or_equal', 'at_least_once'],
])(
'round-trips %s op/matchType unchanged through the submit payload (%s / %s)',
(_desc, op, matchType) => {

View File

@@ -332,25 +332,20 @@ 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();
@@ -413,8 +408,8 @@ describe('CreateAlertV2 utils', () => {
['symbol', '>', 'at_least_once'],
['short form', 'eq', 'avg'],
['mixed numeric and literal', '7', 'last'],
['UI-unexposed operator', 'above_or_equal', 'at_least_once'],
['UI-unexposed numeric operator', '5', 'at_least_once'],
['inclusive literal operator', 'above_or_equal', 'at_least_once'],
['inclusive 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);

View File

@@ -2,9 +2,8 @@ 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 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.
// the dropdown understands. Returns undefined for unknown values so callers
// can keep the raw value on screen instead of silently rewriting it.
export function normalizeOperator(
raw: string | undefined,
): AlertThresholdOperator | undefined {
@@ -27,6 +26,16 @@ 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;

View File

@@ -125,6 +125,14 @@ 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 = [

View File

@@ -99,6 +99,8 @@ 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',
}

View File

@@ -0,0 +1,66 @@
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([]);
});
});

View File

@@ -0,0 +1,45 @@
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);
}

View File

@@ -13,8 +13,15 @@ import {
import { AxiosError } from 'axios';
import APIError from 'types/api/error';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { InfraMonitoringEvents } from 'constants/events';
import {
QuickFilterChangeEventData,
QuickFiltersSource,
} from 'components/QuickFilters/types';
import {
InfraMonitoringEvents,
logInfraFilterCustomizedEvent,
logInfraMonitoringListViewedEvent,
} from 'constants/events';
import { initialQueriesMap } from 'constants/queryBuilder';
import K8sBaseDetails, {
K8sDetailsFilters,
@@ -186,6 +193,19 @@ 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
@@ -199,6 +219,10 @@ function Hosts(): JSX.Element {
</div>
) : undefined;
useEffect(() => {
logInfraMonitoringListViewedEvent(InfraMonitoringEntity.HOSTS);
}, []);
return (
<>
<div className={styles.infraMonitoringContainer}>
@@ -227,6 +251,7 @@ function Hosts(): JSX.Element {
startUnixMilli,
endUnixMilli,
}}
onQuickFilterChange={handleQuickFilterChange}
/>
</>
</OverlayScrollbar>

View File

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

View File

@@ -1,9 +1,12 @@
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';
@@ -19,11 +22,21 @@ const statusOptions: Array<{
function StatusFilter(): JSX.Element {
const [statusFilter, setStatusFilter] = useInfraMonitoringStatusFilter();
const [, setCurrentPage] = useInfraMonitoringPageListing();
const { currentQuery } = useQueryBuilder();
const handleChange = (value: string): void => {
if (value !== undefined) {
void setStatusFilter(value === 'all' ? '' : (value as StatusFilterValue));
void setCurrentPage(1);
const expression =
currentQuery.builder.queryData[0]?.filter?.expression || '';
logInfraFilterCustomizedEvent(
InfraMonitoringEntity.HOSTS,
'host_status_toggle',
expression,
value === 'all' ? [] : ['host_status'],
);
}
};

View File

@@ -2,7 +2,6 @@ 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,
@@ -10,6 +9,7 @@ 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>
) : (
<Typography.Text>-</Typography.Text>
<TextNoData type="typography" />
),
},
{

View File

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

View File

@@ -7,7 +7,7 @@ import {
IQuickFiltersConfig,
} from 'components/QuickFilters/types';
import { TriangleAlert } from '@signozhq/icons';
import { CellValueTooltip } from 'container/InfraMonitoringK8sV2/components';
import TanStackTable from 'components/TanStackTableView';
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 <CellValueTooltip value={hostName} />;
return <TanStackTable.Text>{hostName}</TanStackTable.Text>;
}
return (
<>

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useRef } from 'react';
import React, { useCallback, useMemo, useRef } from 'react';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { Card } from 'antd';
import logEvent from 'api/common/logEvent';
@@ -21,6 +21,9 @@ 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';
@@ -33,9 +36,14 @@ 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';
@@ -134,7 +142,7 @@ function EntityLogsContent({
if (validation.isValid) {
querySearchOnRun(newUserExpression);
logEvent(InfraMonitoringEvents.FilterApplied, {
void logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category,
@@ -162,6 +170,45 @@ 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 (
@@ -287,6 +334,7 @@ function EntityLogsContent({
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onScrollToLog={handleScrollToLog}
handleOpenInExplorer={(e): void => handleOpenInExplorer(e, activeLog)}
/>
)}
</div>

View File

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

View File

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

View File

@@ -1,15 +1,17 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import { Color, Spacing } from '@signozhq/design-tokens';
import { X } from '@signozhq/icons';
import { useCopyToClipboard } from 'react-use';
import { Copy, 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,
@@ -22,8 +24,10 @@ import LoadingContainer from '../LoadingContainer';
import K8sBaseDetailsContent from './K8sBaseDetailsContent';
import { K8sBaseDetailsProps } from './types';
import { useDrawerLifecycleStore } from './useDrawerLifecycleStore';
import '../EntityDetailsUtils/entityDetails.styles.scss';
import styles from '../EntityDetailsUtils/entityDetails.module.scss';
import { INFRA_MONITORING_DETAILS_CACHE_TIME } from 'constants/queryCacheTime';
export type {
CustomTab,
@@ -34,6 +38,23 @@ 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,
@@ -58,8 +79,6 @@ export default function K8sBaseDetails<T>({
(s) => s.getAutoRefreshQueryKey,
);
const isDarkMode = useIsDarkMode();
const [selectedItemParams, setSelectedItemParams] =
useInfraMonitoringSelectedItemParams();
const selectedItem = selectedItemParams.selectedItem;
@@ -101,6 +120,8 @@ 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,
});
@@ -121,10 +142,39 @@ 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(() => {
@@ -137,31 +187,49 @@ 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 (
<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}
<DrawerWrapper
open={!!selectedItem}
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 }} />}
onOpenChange={handleOpenChange}
direction="right"
title={drawerTitle}
showCloseButton={false}
drawerContentProps={DRAWER_MOTION_PROPS}
className={styles.entityDetailDrawer}
>
{(isEntityLoading || !selectedItem) && <LoadingContainer />}
@@ -210,6 +278,6 @@ export default function K8sBaseDetails<T>({
/>
</GlobalTimeProvider>
)}
</Drawer>
</DrawerWrapper>
);
}

View File

@@ -1,4 +1,4 @@
import { Fragment, useEffect, useMemo } from 'react';
import { Fragment, useEffect, useMemo, useRef } from 'react';
import {
BarChart,
ChevronsLeftRight,
@@ -6,12 +6,17 @@ 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 } from 'constants/events';
import {
InfraMonitoringEvents,
logInfraDrawerTabViewedEvent,
logInfraExplorerNavigatedEvent,
} from 'constants/events';
import { QueryParams } from 'constants/query';
import {
initialQueryBuilderFormValuesMap,
@@ -42,6 +47,9 @@ 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>({
@@ -107,6 +115,17 @@ 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();
@@ -133,6 +152,7 @@ export default function K8sBaseDetailsContent<T>({
category: eventCategory,
view: value,
});
logInfraDrawerTabViewedEvent(category, value, false);
};
const handleExplorePagesRedirect = (): void => {
@@ -154,6 +174,16 @@ 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,
@@ -209,34 +239,39 @@ export default function K8sBaseDetailsContent<T>({
return (
<>
<div className="entity-detail-drawer__entity">
<div className="entity-details-grid">
<div className="labels-row">
<div className={styles.entityDetailsEntity}>
<div className={styles.entityDetailsGrid}>
<div className={styles.labelsRow}>
{metadataConfig.map((config) => (
<Typography.Text
key={config.label}
color="muted"
className="entity-details-metadata-label"
size="small"
weight="medium"
className={styles.entityDetailsMetadataLabel}
>
{config.label}
</Typography.Text>
))}
</div>
<div className="values-row">
<div className={styles.valuesRow}>
{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}
className="entity-details-metadata-value"
size="small"
weight="medium"
className={styles.entityDetailsMetadataValue}
>
{config.render ? (
config.render(value, entity)
) : (
<Tooltip title={displayValue}>{displayValue}</Tooltip>
)}
{displayValue}
</Typography.Text>
);
})}
@@ -253,15 +288,17 @@ export default function K8sBaseDetailsContent<T>({
selectedItem={selectedItem}
filterExpression={getCountsFilterExpression(entity)}
closeDrawer={handleClose}
entityType={category}
activeTab={selectedView}
/>
)}
</div>
{!hideDetailViewTabs && (
<div className="views-tabs-container">
<div className={styles.viewsTabsContainer}>
<ToggleGroupSimple
type="single"
className="views-tabs"
className={styles.viewsTabs}
onChange={handleTabChange}
value={selectedView}
items={[
@@ -270,7 +307,7 @@ export default function K8sBaseDetailsContent<T>({
{
value: VIEW_TYPES.METRICS,
label: (
<div className="view-title">
<div className={styles.viewTitle}>
<BarChart size={14} />
Metrics
</div>
@@ -283,7 +320,7 @@ export default function K8sBaseDetailsContent<T>({
{
value: VIEW_TYPES.LOGS,
label: (
<div className="view-title">
<div className={styles.viewTitle}>
<ScrollText size={14} />
Logs
</div>
@@ -296,7 +333,7 @@ export default function K8sBaseDetailsContent<T>({
{
value: VIEW_TYPES.TRACES,
label: (
<div className="view-title">
<div className={styles.viewTitle}>
<DraftingCompass size={14} />
Traces
</div>
@@ -309,7 +346,7 @@ export default function K8sBaseDetailsContent<T>({
{
value: VIEW_TYPES.EVENTS,
label: (
<div className="view-title">
<div className={styles.viewTitle}>
<ChevronsLeftRight size={14} />
Events
</div>
@@ -320,7 +357,7 @@ export default function K8sBaseDetailsContent<T>({
...(customTabs?.map((tab) => ({
value: tab.key,
label: (
<div className="view-title">
<div className={styles.viewTitle}>
{tab.icon}
{tab.label}
</div>
@@ -330,22 +367,30 @@ export default function K8sBaseDetailsContent<T>({
/>
{selectedView === VIEW_TYPES.LOGS && (
<Tooltip title="Go to Logs Explorer" placement="left">
<TooltipSimple title="Go to Logs Explorer" side="left" arrow>
<Button
icon={<Compass size={18} />}
className="compass-button"
variant="ghost"
size="icon"
color="secondary"
className={styles.compassButton}
onClick={handleExplorePagesRedirect}
/>
</Tooltip>
>
<Compass size={18} />
</Button>
</TooltipSimple>
)}
{selectedView === VIEW_TYPES.TRACES && (
<Tooltip title="Go to Traces Explorer" placement="left">
<TooltipSimple title="Go to Traces Explorer" side="left" arrow>
<Button
icon={<Compass size={18} />}
className="compass-button"
variant="ghost"
size="icon"
color="secondary"
className={styles.compassButton}
onClick={handleExplorePagesRedirect}
/>
</Tooltip>
>
<Compass size={18} />
</Button>
</TooltipSimple>
)}
</div>
)}
@@ -358,6 +403,7 @@ export default function K8sBaseDetailsContent<T>({
getEntityQueryPayload={getEntityQueryPayload}
category={category}
queryKey={`${queryKeyPrefix}Metrics`}
view={VIEW_TYPES.METRICS}
/>
)}
{effectiveView === VIEW_TYPES.LOGS && (

View File

@@ -1,14 +1,19 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } 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 } from 'constants/events';
import {
InfraMonitoringEvents,
logInfraColumnSortedEvent,
logInfraTimeRangeCustomizedEvent,
} from 'constants/events';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGlobalTimeStore } from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
@@ -27,11 +32,16 @@ import {
useInfraMonitoringSelectedItemParams,
useInfraMonitoringStatusFilter,
} from '../hooks';
import { useInfraMonitoringLineClamp } from '../components';
import {
useInfraMonitoringFontSize,
useInfraMonitoringLineClamp,
} from './useInfraMonitoringTablePreferencesStore';
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';
@@ -104,6 +114,7 @@ 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();
@@ -113,6 +124,7 @@ export function K8sBaseList<
const columnStorageKey = `k8s-${entity}-columns`;
const hiddenColumnIds = useHiddenColumnIds(columnStorageKey);
const [isOptionsDrawerOpen, setIsOptionsDrawerOpen] = useState(false);
const { containerRef, calculatedPageSize } = useCalculatedPageSize({
rowHeight: 42,
@@ -217,6 +229,14 @@ 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();
@@ -235,6 +255,14 @@ 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) {
@@ -346,6 +374,7 @@ export function K8sBaseList<
extraQueryKeyParts={extraQueryKeyParts}
getRowKey={getRowKey}
getItemKey={getItemKey}
detailsQueryKeyPrefix={detailsQueryKeyPrefix}
/>
),
[
@@ -355,6 +384,7 @@ export function K8sBaseList<
getItemKey,
expandedRowColumns,
extraQueryKeyParts,
detailsQueryKeyPrefix,
],
);
@@ -363,6 +393,15 @@ 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?.({
@@ -392,17 +431,21 @@ 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'}
@@ -423,6 +466,7 @@ export function K8sBaseList<
getGroupKey={getGroupKeyFn}
onRowClick={handleRowClick}
onRowClickNewTab={handleRowClickNewTab}
onSort={handleSort}
renderExpandedRow={isGroupedByAttribute ? renderExpandedRow : undefined}
getRowCanExpand={isGroupedByAttribute ? getRowCanExpand : undefined}
className={cx(styles.k8SListTable)}
@@ -440,11 +484,21 @@ export function K8sBaseList<
onLimitChange: setLimit,
}}
plainTextCellLineClamp={lineClamp}
cellTypographySize={fontSize}
prefixPaginationContent={paginationWarningContent}
paginationClassname={styles.paginationContainer}
resetScrollKey={entity}
/>
)}
</div>
<K8sOptionsSidePanel
open={isOptionsDrawerOpen}
columns={tableColumns}
storageKey={columnStorageKey}
entity={entity}
onClose={handleCloseOptionsDrawer}
/>
</>
);
}

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import { useQuery, useQueryClient } from 'react-query';
import { useLocation } from 'react-router-dom';
import { Querybuildertypesv5QueryWarnDataDTO } from 'api/generated/services/sigNoz.schemas';
import { Button } from '@signozhq/ui/button';
@@ -21,6 +21,7 @@ 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,
@@ -30,6 +31,8 @@ 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';
@@ -62,9 +65,14 @@ 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 = string>({
export function K8sExpandedRow<
T,
TItemKey extends string | SelectedItemParams = string,
>({
rowKey,
groupMeta,
entity,
@@ -73,7 +81,9 @@ export function K8sExpandedRow<T, TItemKey = string>({
extraQueryKeyParts = [],
getRowKey,
getItemKey,
detailsQueryKeyPrefix,
}: K8sExpandedRowProps<T, TItemKey>): JSX.Element {
const fontSize = useInfraMonitoringFontSize();
const [, setGroupBy] = useInfraMonitoringGroupBy();
const [, setCurrentPage] = useInfraMonitoringPageListing();
const { currentQuery } = useQueryBuilder();
@@ -84,6 +94,7 @@ export function K8sExpandedRow<T, TItemKey = string>({
const { safeNavigate } = useSafeNavigate();
const urlQuery = useUrlQuery();
const location = useLocation();
const queryClient = useQueryClient();
const orderByParamKey = useMemo(
() => `orderBy_${rowKey.replace(/[^a-zA-Z0-9]/g, '_')}`,
@@ -105,6 +116,13 @@ export function K8sExpandedRow<T, TItemKey = string>({
const storageKey = `k8s-${entity}-columns-expanded`;
useLogEventForColumnCustomized({
entity,
source: 'expanded',
storageKey,
columns: tableColumns,
});
const expressionForRecord = useMemo(
() => buildExpressionFromGroupMeta(parentExpression || '', groupMeta),
[parentExpression, groupMeta],
@@ -177,18 +195,45 @@ export function K8sExpandedRow<T, TItemKey = string>({
const expandedData = data?.data ?? [];
const handleRowClick = useCallback(
(_row: T, itemKey: TItemKey): void => {
if (typeof itemKey === 'object' && itemKey !== null) {
setSelectedItemParams(itemKey as unknown as SelectedItemParams);
} else {
setSelectedItemParams({
selectedItem: itemKey as string,
clusterName: null,
namespaceName: null,
});
(row: T, itemKey: 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');
}
},
[setSelectedItemParams],
[entity],
);
const handleViewAllClick = (): void => {
@@ -257,6 +302,7 @@ export function K8sExpandedRow<T, TItemKey = string>({
getRowKey={getRowKey}
getItemKey={getItemKey}
onRowClick={handleRowClick}
onSort={handleSort}
enableQueryParams={{
orderBy: orderByParamKey,
}}
@@ -264,7 +310,7 @@ export function K8sExpandedRow<T, TItemKey = string>({
className: styles.expandedTable,
}}
disableVirtualScroll
cellTypographySize="medium"
cellTypographySize={fontSize}
/>
</TanStackTableStateProvider>
{!isLoading && expandedData.length > 0 && (

View File

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

View File

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

View File

@@ -5,25 +5,10 @@
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;
@@ -34,27 +19,6 @@
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;
}
@@ -64,17 +28,12 @@
}
.k8SDateTimeSelection {
flex: 0 0 auto;
margin-left: 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;
}
@@ -95,42 +54,3 @@
}
}
}
.groupByLabel {
min-width: max-content;
font-size: var(--periscope-font-size-base, 13px);
font-weight: var(--periscope-font-weight-regular, 400);
line-height: 18px;
letter-spacing: -0.07px;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--l2-border);
border-right: none;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
display: flex;
height: 32px;
padding: var(--spacing-3) var(--spacing-3) var(--spacing-3) var(--spacing-4);
justify-content: center;
align-items: center;
gap: var(--spacing-2);
}
.groupBySelect {
width: 100%;
:global(.ant-select-selector) {
border-left: none;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
}
.k8SListControlsRight {
min-width: 240px;
display: flex;
align-items: center;
gap: var(--spacing-2);
}

View File

@@ -1,29 +1,19 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import React, { useCallback, useMemo, useRef } 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 { useGetFieldsKeys } from 'api/generated/services/fields';
import {
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 {
InfraMonitoringEvents,
logInfraFilterCustomizedEvent,
} 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,
useGlobalTimeStore,
} from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
import { useGlobalTimeQueryInvalidate } from 'store/globalTime';
import { DataSource } from 'types/common/queryBuilder';
import {
@@ -31,37 +21,25 @@ import {
InfraMonitoringEntity,
METRIC_NAMESPACE_BY_ENTITY,
} from '../constants';
import {
useInfraMonitoringGroupBy,
useInfraMonitoringPageListing,
} from '../hooks';
import K8sFiltersSidePanel from './K8sFiltersSidePanel';
import { useInfraMonitoringPageListing } from '../hooks';
import styles from './K8sHeader.module.scss';
import { TooltipSimple } from '@signozhq/ui/tooltip';
interface K8sHeaderProps<TData> {
interface K8sHeaderProps {
controlListPrefix?: React.ReactNode;
leftFilters?: React.ReactNode;
entity: InfraMonitoringEntity;
showAutoRefresh: boolean;
columns: TableColumnDef<TData>[];
columnStorageKey: string;
isFetching?: boolean;
cancelQuery: () => void;
}
function K8sHeader<TData>({
function K8sHeader({
controlListPrefix,
leftFilters,
entity,
showAutoRefresh,
columns,
columnStorageKey,
isFetching = false,
cancelQuery,
}: K8sHeaderProps<TData>): JSX.Element {
const [isFiltersSidePanelOpen, setIsFiltersSidePanelOpen] = useState(false);
}: K8sHeaderProps): JSX.Element {
// 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);
@@ -121,6 +99,8 @@ function K8sHeader<TData>({
page: InfraMonitoringEvents.ListPage,
category: entity,
});
logInfraFilterCustomizedEvent(entity, 'search', finalExpression);
}
},
[
@@ -145,101 +125,11 @@ function K8sHeader<TData>({
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}
@@ -255,20 +145,6 @@ function K8sHeader<TData>({
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}>
@@ -283,13 +159,6 @@ function K8sHeader<TData>({
metricNamespace={METRIC_NAMESPACE_BY_ENTITY[entity]}
/>
</div>
<K8sFiltersSidePanel
open={isFiltersSidePanelOpen}
columns={columns}
storageKey={columnStorageKey}
onClose={onClickOutside}
/>
</div>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,9 @@
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';
@@ -13,6 +15,7 @@ import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
InfraMonitoringEntity,
} from '../../../constants';
import { getDrawerDurationMs } from '../../useDrawerLifecycleStore';
import styles from './EntityCountsSection.module.scss';
export interface EntityCountConfig<T> {
@@ -27,6 +30,8 @@ interface EntityCountsSectionProps<T> {
selectedItem: string;
filterExpression: string;
closeDrawer: () => void;
entityType: InfraMonitoringEntity;
activeTab: string;
}
export function EntityCountsSection<T>({
@@ -35,7 +40,21 @@ 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];
@@ -116,17 +135,26 @@ export function EntityCountsSection<T>({
>
{config.label}
</Typography.Text>
<Typography.Text className={styles.countValue} size="xl" weight="semibold">
{config.getValue(entity) || '-'}
</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} />
)}
<Link
to={buildNavigationUrl(config.targetCategory)}
onClick={closeDrawer}
onClick={(): void => handleCardNavigate(config.label)}
data-testid={`navigate-${config.label.toLowerCase().replace(/\s+/g, '-')}`}
>
<Tooltip
<TooltipSimple
title={`View ${config.label.toLowerCase()} of '${selectedItem}'`}
placement="top"
side="top"
arrow
>
<Button
size="icon"
@@ -135,7 +163,7 @@ export function EntityCountsSection<T>({
className={styles.navigateButton}
prefix={<Compass size={14} />}
/>
</Tooltip>
</TooltipSimple>
</Link>
</div>
))}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useRef } from 'react';
import React, { useCallback, useMemo, useRef } from 'react';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { Card } from 'antd';
import logEvent from 'api/common/logEvent';
@@ -20,7 +20,10 @@ import {
combineInitialAndUserExpression,
getUserExpressionFromCombined,
} from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
import { InfraMonitoringEvents } from 'constants/events';
import {
InfraMonitoringEvents,
logInfraDrawerFilterCustomizedEvent,
} from 'constants/events';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { FontSize } from 'container/OptionsMenu/types';
@@ -42,6 +45,14 @@ 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;
@@ -57,6 +68,9 @@ 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();
@@ -85,8 +99,10 @@ function EntityLogsContent({
: partExpression;
querySearchOnRun(newUser);
logInfraDrawerFilterCustomizedEvent(category, 'logs', newUser, 'logs');
},
[userExpression, querySearchOnRun, handleCloseLogDetail],
[userExpression, querySearchOnRun, handleCloseLogDetail, category],
);
const {
@@ -127,6 +143,13 @@ function EntityLogsContent({
view: InfraMonitoringEvents.LogsView,
});
logInfraDrawerFilterCustomizedEvent(
category,
'logs',
newUserExpression || '',
'search',
);
refetch();
}
},
@@ -148,6 +171,44 @@ 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 (
@@ -257,6 +318,7 @@ function EntityLogsContent({
{renderContent}
</div>
)}
<div ref={logDetailContainerRef} data-log-detail-ignore="true" />
{selectedTab && activeLog && (
<LogDetail
log={activeLog}
@@ -267,6 +329,10 @@ function EntityLogsContent({
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onScrollToLog={handleScrollToLog}
handleOpenInExplorer={(e) => handleOpenInExplorer(e, activeLog)}
getContainer={(): HTMLElement =>
logDetailContainerRef.current || document.body
}
/>
)}
</div>

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
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';
@@ -300,14 +301,16 @@ const renderEntityMetrics = (overrides = {}): any => {
return render(
<MemoryRouter>
<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}
/>
</TooltipProvider>
</MemoryRouter>,
);
};

View File

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

View File

@@ -2,6 +2,7 @@ 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,
@@ -135,12 +136,11 @@ export const getTraceListColumns = (
if (!isValidCode) {
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography
className={styles.cellText}
data-novalue={numericCode === 0 || !statusCode}
>
{numericCode === 0 || !statusCode ? '-' : statusCode}
</Typography>
{numericCode === 0 || !statusCode ? (
<TextNoData type="typography" className={styles.cellText} />
) : (
<Typography className={styles.cellText}>{statusCode}</Typography>
)}
</BlockLink>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,9 @@
// 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;
@@ -21,18 +24,25 @@ interface GroupedStatusCountsProps {
}
function buildTooltipContent(item: StatusCountItem): React.ReactNode {
const onClickHandle: MouseEventHandler = (e) => {
e.preventDefault();
e.stopPropagation();
};
if (!item.breakdown || item.breakdown.length === 0) {
return (
<Typography.Text>
{item.label}: {item.value}
</Typography.Text>
<div onClick={onClickHandle}>
<Typography.Text>
{item.label}: {item.value}
</Typography.Text>
</div>
);
}
const nonZeroBreakdown = item.breakdown.filter((b) => b.value > 0);
if (nonZeroBreakdown.length === 0) {
return (
<div className={styles.tooltipContent}>
<div className={styles.tooltipContent} onClick={onClickHandle}>
<Typography.Text className={styles.tooltipHeader}>
{item.label}
</Typography.Text>
@@ -43,7 +53,7 @@ function buildTooltipContent(item: StatusCountItem): React.ReactNode {
}
return (
<div className={styles.tooltipContent}>
<div className={styles.tooltipContent} onClick={onClickHandle}>
<Typography.Text className={styles.tooltipHeader}>
{item.label}
</Typography.Text>
@@ -68,7 +78,7 @@ export function GroupedStatusCounts({
showZeroValues === false ? items.filter((item) => item.value > 0) : items;
if (visibleItems.length === 0) {
return <TanStackTable.Text>-</TanStackTable.Text>;
return <TextNoData type="tanstack" />;
}
return (
@@ -81,12 +91,20 @@ export function GroupedStatusCounts({
arrow
align="start"
>
<TanStackTable.Text
className={styles.item}
style={{ '--gsc-color': item.color } as React.CSSProperties}
>
{item.value || '-'}
</TanStackTable.Text>
{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.HoverTooltip>
))}
</div>

View File

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

View File

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

View File

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

View File

@@ -1816,6 +1816,155 @@ export const getHostQueryPayload = (
start,
end,
},
{
selectedTime: 'GLOBAL_TIME',
graphType: PANEL_TYPES.TIME_SERIES,
query: {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'system_filesystem_usage--float64--Gauge--true',
key: fsUsageKey,
type: 'Gauge',
},
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
disabled: true,
expression: 'A',
filters: {
items: [
{
id: 'fs_f1',
key: {
dataType: DataTypes.String,
id: 'host_name--string--tag--false',
key: hostNameKey,
type: 'tag',
},
op: '=',
value: hostName,
},
{
id: 'fs_f2',
key: {
dataType: DataTypes.String,
id: 'state--string--tag--false',
key: 'state',
type: 'tag',
},
op: '=',
value: 'used',
},
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'mountpoint--string--tag--false',
key: 'mountpoint',
type: 'tag',
},
],
having: [
{
columnName: `SUM(${fsUsageKey})`,
op: '>',
value: 0,
},
],
legend: '{{mountpoint}}',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: ReduceOperators.AVG,
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
},
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'system_filesystem_usage--float64--Gauge--true',
key: fsUsageKey,
type: 'Gauge',
},
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
disabled: true,
expression: 'B',
filters: {
items: [
{
id: 'fs_f3',
key: {
dataType: DataTypes.String,
id: 'host_name--string--tag--false',
key: hostNameKey,
type: 'tag',
},
op: '=',
value: hostName,
},
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'mountpoint--string--tag--false',
key: 'mountpoint',
type: 'tag',
},
],
having: [
{
columnName: `SUM(${fsUsageKey})`,
op: '>',
value: 0,
},
],
legend: '{{mountpoint}}',
limit: null,
orderBy: [],
queryName: 'B',
reduceTo: ReduceOperators.AVG,
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
},
],
queryFormulas: [
{
disabled: false,
expression: 'A/B',
legend: '{{mountpoint}}',
queryName: 'F1',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
promql: [{ disabled: false, legend: '', name: 'A', query: '' }],
queryType: EQueryType.QUERY_BUILDER,
},
variables: {},
formatForWeb: false,
start,
end,
},
{
selectedTime: 'GLOBAL_TIME',
graphType: PANEL_TYPES.TIME_SERIES,
@@ -2660,155 +2809,6 @@ export const getHostQueryPayload = (
start,
end,
},
{
selectedTime: 'GLOBAL_TIME',
graphType: PANEL_TYPES.TIME_SERIES,
query: {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'system_filesystem_usage--float64--Gauge--true',
key: fsUsageKey,
type: 'Gauge',
},
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
disabled: true,
expression: 'A',
filters: {
items: [
{
id: 'fs_f1',
key: {
dataType: DataTypes.String,
id: 'host_name--string--tag--false',
key: hostNameKey,
type: 'tag',
},
op: '=',
value: hostName,
},
{
id: 'fs_f2',
key: {
dataType: DataTypes.String,
id: 'state--string--tag--false',
key: 'state',
type: 'tag',
},
op: '=',
value: 'used',
},
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'mountpoint--string--tag--false',
key: 'mountpoint',
type: 'tag',
},
],
having: [
{
columnName: `SUM(${fsUsageKey})`,
op: '>',
value: 0,
},
],
legend: '{{mountpoint}}',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: ReduceOperators.AVG,
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
},
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'system_filesystem_usage--float64--Gauge--true',
key: fsUsageKey,
type: 'Gauge',
},
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
disabled: true,
expression: 'B',
filters: {
items: [
{
id: 'fs_f3',
key: {
dataType: DataTypes.String,
id: 'host_name--string--tag--false',
key: hostNameKey,
type: 'tag',
},
op: '=',
value: hostName,
},
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: DataTypes.String,
id: 'mountpoint--string--tag--false',
key: 'mountpoint',
type: 'tag',
},
],
having: [
{
columnName: `SUM(${fsUsageKey})`,
op: '>',
value: 0,
},
],
legend: '{{mountpoint}}',
limit: null,
orderBy: [],
queryName: 'B',
reduceTo: ReduceOperators.AVG,
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'avg',
},
],
queryFormulas: [
{
disabled: false,
expression: 'A/B',
legend: '{{mountpoint}}',
queryName: 'F1',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
promql: [{ disabled: false, legend: '', name: 'A', query: '' }],
queryType: EQueryType.QUERY_BUILDER,
},
variables: {},
formatForWeb: false,
start,
end,
},
];
};
@@ -2875,12 +2875,18 @@ export const hostWidgetInfo = [
{
title: 'CPU Usage',
yAxisUnit: 'percentunit',
docPath: '/infrastructure-monitoring/host-monitoring/#cpu-usage',
docPath: '/infrastructure-monitoring/host-monitoring/#cpu-usage-1',
},
{
title: 'Memory Usage',
yAxisUnit: 'bytes',
docPath: '/infrastructure-monitoring/host-monitoring/#memory-usage',
docPath: '/infrastructure-monitoring/host-monitoring/#memory-usage-1',
},
{
title: 'Disk Usage (%) by mountpoint',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/host-monitoring/#disk-usage--by-mountpoint',
},
{
title: 'System Load Average',
@@ -2934,10 +2940,4 @@ export const hostWidgetInfo = [
docPath:
'/infrastructure-monitoring/host-monitoring/#system-disk-operation-times',
},
{
title: 'Disk Usage (%) by mountpoint',
yAxisUnit: 'percentunit',
docPath:
'/infrastructure-monitoring/host-monitoring/#disk-usage--by-mountpoint',
},
];

View File

@@ -144,9 +144,13 @@ export default function TableViewActions(
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
const { dataType, logType: fieldType } = getFieldAttributes(record.field);
// there is no option for where clause in old logs explorer and live logs page
// there is no option for where clause in old logs explorer and live logs page or infra monitoring
const isOldLogsExplorerOrLiveLogsPage = useMemo(
() => pathname === ROUTES.OLD_LOGS_EXPLORER || pathname === ROUTES.LIVE_LOGS,
() =>
pathname === ROUTES.OLD_LOGS_EXPLORER ||
pathname === ROUTES.LIVE_LOGS ||
pathname === ROUTES.INFRASTRUCTURE_MONITORING_HOSTS ||
pathname === ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES,
[pathname],
);

View File

@@ -0,0 +1,30 @@
import { getBaseUrl } from 'utils/basePath';
export type NavigationReferrer =
| 'deep_link'
| 'nav_sidebar'
| 'external_link'
| 'reload'
| 'back_forward';
export function getNavigationReferrer(): NavigationReferrer {
// Can be empty in some browsers and in jsdom
const navEntry = performance.getEntriesByType('navigation')[0] as
| PerformanceNavigationTiming
| undefined;
if (navEntry?.type === 'reload') {
return 'reload';
}
if (navEntry?.type === 'back_forward') {
return 'back_forward';
}
if (!document.referrer) {
return 'deep_link';
}
if (document.referrer.includes(getBaseUrl())) {
return 'nav_sidebar';
}
return 'external_link';
}

View File

@@ -0,0 +1,164 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useState } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { Provider as ReduxProvider } from 'react-redux';
import { MemoryRouter, Route, useHistory, useParams } from 'react-router-dom';
import { CompatRouter } from 'react-router-dom-v5-compat';
import { TooltipProvider } from '@signozhq/ui/tooltip';
import {
type DashboardtypesPanelDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import configureStore from 'redux-mock-store';
import appStore from 'store';
import { useOpenPanelEditor } from '../../hooks/useOpenPanelEditor';
import { usePanelEditorQuerySync } from '../hooks/usePanelEditorQuerySync';
import PanelEditorQueryBuilder from '../PanelEditorQueryBuilder/PanelEditorQueryBuilder';
// jest.config maps the real hook to a no-op mock; this suite needs real navigation.
jest.mock('hooks/useSafeNavigate', () => {
const { useHistory: useRouterHistory } =
jest.requireActual('react-router-dom');
return {
useSafeNavigate: (): unknown => {
const history = useRouterHistory();
return {
safeNavigate: (to: string): void => history.push(to),
};
},
};
});
jest.mock('../../store/useDashboardStore', () => ({
useDashboardStore: (selector: (s: unknown) => unknown): unknown =>
selector({ dashboardId: 'dash-1' }),
}));
function makePromPanel(name: string, promql: string): DashboardtypesPanelDTO {
return {
kind: 'Panel',
spec: {
display: { name },
plugin: { kind: 'signoz/TimeSeriesPanel', spec: {} },
queries: [
{
kind: 'time_series',
spec: {
plugin: {
kind: 'signoz/PromQLQuery',
spec: { name: 'A', query: promql, legend: '', disabled: false },
},
},
},
],
},
} as unknown as DashboardtypesPanelDTO;
}
const PANELS: Record<string, DashboardtypesPanelDTO> = {
A: makePromPanel('Panel A', 'up{job="alpha"}'),
B: makePromPanel('Panel B', 'up{job="bravo"}'),
};
const noop = (): void => {};
/** Stands in for the editor route: the same draft + builder sync `PanelEditorContainer` runs. */
function EditorRoute(): JSX.Element {
const { panelId } = useParams<{ panelId: string }>();
const [panel] = useState(PANELS[panelId]);
usePanelEditorQuerySync({
draft: panel,
panelType: PANEL_TYPES.TIME_SERIES,
setSpec: noop,
refetch: noop,
signal: TelemetrytypesSignalDTO.metrics,
savedQueries: panel.spec.queries,
});
return (
<PanelEditorQueryBuilder
panelKind="signoz/TimeSeriesPanel"
signal={TelemetrytypesSignalDTO.metrics}
isLoadingQueries={false}
onStageRunQuery={noop}
onCancelQuery={noop}
/>
);
}
function Harness(): JSX.Element {
const openPanelEditor = useOpenPanelEditor();
const history = useHistory();
return (
<>
<button
type="button"
data-testid="edit-a"
onClick={(): void => openPanelEditor('A', { panel: PANELS.A })}
>
edit A
</button>
<button
type="button"
data-testid="edit-b"
onClick={(): void => openPanelEditor('B', { panel: PANELS.B })}
>
edit B
</button>
<button
type="button"
data-testid="back"
onClick={(): void => history.push('/dashboard/dash-1')}
>
back
</button>
<Route
path="/dashboard/:dashboardId/panel/:panelId"
component={EditorRoute}
/>
</>
);
}
const renderHarness = (): void => {
render(
<MemoryRouter initialEntries={['/dashboard/dash-1']}>
<CompatRouter>
<QueryClientProvider client={new QueryClient()}>
<ReduxProvider store={configureStore([])(appStore.getState())}>
<TooltipProvider>
<QueryBuilderProvider>
<Harness />
</QueryBuilderProvider>
</TooltipProvider>
</ReduxProvider>
</QueryClientProvider>
</CompatRouter>
</MemoryRouter>,
);
};
describe('Panel editor route, PromQL panels', () => {
it('opens on the edited panel query, not the previously edited one', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderHarness();
await user.click(screen.getByTestId('edit-a'));
expect(screen.getByTestId('promql-query-input')).toHaveValue(
'up{job="alpha"}',
);
await user.click(screen.getByTestId('back'));
await user.click(screen.getByTestId('edit-b'));
expect(screen.getByTestId('promql-query-input')).toHaveValue(
'up{job="bravo"}',
);
});
});

View File

@@ -27,6 +27,7 @@ import { stepClickTimeRange } from '../../utils/drilldown/chartClickTimeRange';
import { enrichChartClick } from '../../utils/drilldown/enrichChartClick';
import { getBuilderQueries } from '../../utils/getBuilderQueries';
import { getPanelTimeRange } from '../../utils/getPanelTimeRange';
import { sortSeriesByMeanDesc } from '../../utils/sortSeriesByMean';
import { buildBarChartConfig } from './utils/buildConfig';
import { ChartClickData } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
@@ -70,7 +71,12 @@ function BarPanelRenderer({
const flatSeries = useMemo(
() =>
flattenTimeSeries(getTimeSeriesResults(data.response), data.legendMap ?? {}),
sortSeriesByMeanDesc(
flattenTimeSeries(
getTimeSeriesResults(data.response),
data.legendMap ?? {},
),
),
[data.response, data.legendMap],
);

View File

@@ -27,6 +27,7 @@ import { stepClickTimeRange } from '../../utils/drilldown/chartClickTimeRange';
import { enrichChartClick } from '../../utils/drilldown/enrichChartClick';
import { getBuilderQueries } from '../../utils/getBuilderQueries';
import { getPanelTimeRange } from '../../utils/getPanelTimeRange';
import { sortSeriesByMeanDesc } from '../../utils/sortSeriesByMean';
import { buildTimeSeriesConfig } from './utils/buildConfig';
import { ChartClickData } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
@@ -71,7 +72,12 @@ function TimeSeriesPanelRenderer({
const flatSeries = useMemo(
() =>
flattenTimeSeries(getTimeSeriesResults(data.response), data.legendMap ?? {}),
sortSeriesByMeanDesc(
flattenTimeSeries(
getTimeSeriesResults(data.response),
data.legendMap ?? {},
),
),
[data.response, data.legendMap],
);

View File

@@ -0,0 +1,95 @@
import type { PanelSeries } from 'pages/DashboardPageV2/DashboardContainer/queryV5/types';
import { sortSeriesByMeanDesc } from '../sortSeriesByMean';
function makeSeries(
queryName: string,
values: number[],
overrides: Partial<PanelSeries> = {},
): PanelSeries {
return {
queryName,
legend: '',
labels: {},
kind: 'series',
values: values.map((value, index) => ({
timestamp: (index + 1) * 1_000,
value,
})),
aggregation: { index: 0, alias: '' },
...overrides,
};
}
const names = (series: PanelSeries[]): string[] =>
series.map((item) => item.queryName);
describe('sortSeriesByMeanDesc', () => {
it('orders series by descending mean', () => {
const sorted = sortSeriesByMeanDesc([
makeSeries('A', [1, 1]),
makeSeries('B', [10, 20]),
makeSeries('C', [5, 5]),
]);
expect(names(sorted)).toStrictEqual(['B', 'C', 'A']);
});
it('sinks series with no finite points to the bottom', () => {
const sorted = sortSeriesByMeanDesc([
makeSeries('A', []),
makeSeries('B', [NaN, Infinity]),
makeSeries('C', [1]),
]);
expect(names(sorted)).toStrictEqual(['C', 'A', 'B']);
});
it('ignores non-finite points when averaging', () => {
const sorted = sortSeriesByMeanDesc([
makeSeries('A', [2, 2]),
// Mean over finite points only is 10, so NaN must not poison it to last place.
makeSeries('B', [10, NaN]),
]);
expect(names(sorted)).toStrictEqual(['B', 'A']);
});
it('produces the same order regardless of input order', () => {
const a = makeSeries('A', [5]);
const b = makeSeries('B', [5]);
const c = makeSeries('C', [9]);
expect(names(sortSeriesByMeanDesc([a, b, c]))).toStrictEqual(
names(sortSeriesByMeanDesc([c, b, a])),
);
expect(names(sortSeriesByMeanDesc([b, a, c]))).toStrictEqual(['C', 'A', 'B']);
});
it('tiebreaks equal means on labels when the query name matches', () => {
const forward = sortSeriesByMeanDesc([
makeSeries('A', [1], { labels: { host: 'b' } }),
makeSeries('A', [1], { labels: { host: 'a' } }),
]);
const reversed = sortSeriesByMeanDesc([
makeSeries('A', [1], { labels: { host: 'a' } }),
makeSeries('A', [1], { labels: { host: 'b' } }),
]);
expect(forward.map((item) => item.labels.host)).toStrictEqual(['a', 'b']);
expect(reversed.map((item) => item.labels.host)).toStrictEqual(['a', 'b']);
});
it('does not mutate the input array', () => {
const input = [makeSeries('A', [1]), makeSeries('B', [9])];
const sorted = sortSeriesByMeanDesc(input);
expect(names(input)).toStrictEqual(['A', 'B']);
expect(names(sorted)).toStrictEqual(['B', 'A']);
});
it('returns an empty array for no series', () => {
expect(sortSeriesByMeanDesc([])).toStrictEqual([]);
});
});

View File

@@ -0,0 +1,23 @@
import type { DashboardtypesPanelDTO } from 'api/generated/services/sigNoz.schemas';
import { initialQueriesMap } from 'constants/queryBuilder';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
import { getPanelDefinition } from '../registry';
import { PANEL_KIND_TO_PANEL_TYPE } from '../types/panelKind';
import { fromPerses } from '../../queryV5/persesQueryAdapters';
/**
* The panel's saved query as a builder `Query` — what the editor route and the View
* modal put in `compositeQuery` when they open. Matches the seed
* `usePanelEditorQuerySync` computes from the panel.
*/
export function getPanelBuilderQuery(panel: DashboardtypesPanelDTO): Query {
const kind = panel.spec.plugin.kind;
const [defaultSignal] = getPanelDefinition(kind).supportedSignals;
// A query-less panel seeds from the kind's first supported signal — `fromPerses`'s
// metrics default isn't authorable in every kind (e.g. List).
if (panel.spec.queries.length === 0 && defaultSignal) {
return initialQueriesMap[defaultSignal];
}
return fromPerses(panel.spec.queries, PANEL_KIND_TO_PANEL_TYPE[kind]);
}

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