From 564d479aae46b50267c148ad343407ee1142637c Mon Sep 17 00:00:00 2001 From: Ashwin Bhatkal Date: Tue, 21 Jul 2026 17:14:30 +0530 Subject: [PATCH 1/4] test(e2e): fix teardown crashes (network endpoint race + eager keeper lookup) (#12204) * test(e2e): retry network teardown to survive async endpoint detach `--teardown` fails on CI with `docker.errors.APIError: 403 ... error while removing network ... has active endpoints`: containers are torn down before the network, but Docker detaches endpoints asynchronously, so the network can still report active endpoints for a moment. Retry the removal, force-disconnecting any stragglers each round. * test(e2e): resolve keeper coordinator lazily so --teardown doesn't crash `create_clickhouse` / `create_clickhouse_cluster` computed `next(iter(keeper.container_configs.values()))` eagerly. Under `--teardown` the keeper fixture is empty, so it raised StopIteration during teardown setup, before any finalizer ran. Move the lookup inside create(). --- tests/fixtures/clickhouse.py | 7 ++++--- tests/fixtures/network.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/tests/fixtures/clickhouse.py b/tests/fixtures/clickhouse.py index ba334ae6286..172a32e53ff 100644 --- a/tests/fixtures/clickhouse.py +++ b/tests/fixtures/clickhouse.py @@ -203,9 +203,9 @@ def create_clickhouse( # pylint: disable=too-many-arguments,too-many-positional pytestconfig: pytest.Config, cache_key: str = "clickhouse", ) -> types.TestContainerClickhouse: - coordinator = next(iter(keeper.container_configs.values())) - def create() -> types.TestContainerClickhouse: + # Lazy: the keeper fixture is empty under --teardown (never created). + coordinator = next(iter(keeper.container_configs.values())) clickhouse_version = request.config.getoption("--clickhouse-version") container = ClickHouseContainer( @@ -381,9 +381,10 @@ def create_clickhouse_cluster( # pylint: disable=too-many-arguments,too-many-po migration goes through. Per-node containers are exposed via `nodes` so tests can assert shard-local state. """ - coordinator = next(iter(keeper.container_configs.values())) def create() -> types.TestContainerClickhouse: + # Lazy: the keeper fixture is empty under --teardown (never created). + coordinator = next(iter(keeper.container_configs.values())) clickhouse_version = request.config.getoption("--clickhouse-version") # Unique aliases per creation: docker allows duplicate network aliases diff --git a/tests/fixtures/network.py b/tests/fixtures/network.py index ddcdb6442ec..6d7f08f241a 100644 --- a/tests/fixtures/network.py +++ b/tests/fixtures/network.py @@ -1,3 +1,5 @@ +import time + import docker import docker.errors import pytest @@ -23,12 +25,37 @@ def create() -> types.Network: def delete(nw: types.Network): client = docker.from_env() try: - client.networks.get(network_id=nw.id).remove() + network = client.networks.get(network_id=nw.id) except docker.errors.NotFound: logger.info( "Skipping removal of Network, Network(%s) not found. Maybe it was manually removed?", {"name": nw.name, "id": nw.id}, ) + return + + # Docker detaches endpoints asynchronously, so the network can briefly + # report "has active endpoints" after its containers are gone. Retry, + # force-disconnecting any stragglers. + last_err: docker.errors.APIError | None = None + for _ in range(10): + try: + network.remove() + return + except docker.errors.NotFound: + return + except docker.errors.APIError as err: + if "has active endpoints" not in str(err): + raise + last_err = err + network.reload() + for container_id in network.attrs.get("Containers") or {}: + try: + network.disconnect(container_id, force=True) + except docker.errors.APIError: + pass + time.sleep(1) + + raise last_err def restore(existing: dict) -> types.Network: client = docker.from_env() From 6d0f618a9e7c7c3a4b16024cdc20d4196a546dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vinicius=20Louren=C3=A7o?= <12551007+H4ad@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:58:26 -0300 Subject: [PATCH 2/4] feat(infra-monitoring): persist custom time on drawer across categories (#12038) * feat(globalTime): add method to reset time of child to parent * refactor(infra-monitoring): extract content from K8sBaseDetails * feat(entity-details): add support to persist custom time across categories of k8s * test(entity-details): update existing tests * test(entity-details): add regression test for time conversion * fix(k8s-list): ensure default value for list is 30m * fix(entity-counts-section): address todo around inherit selected time * fix(pr): missing fix for tab validation, lost of moving/merge --- frontend/src/__tests__/fields_api.util.ts | 20 + .../Base/K8sBaseDetails.tsx | 587 ++---------------- .../Base/K8sBaseDetailsContent.tsx | 401 ++++++++++++ .../InfraMonitoringK8sV2/Base/K8sHeader.tsx | 1 + .../EntityCountsSection.tsx | 39 +- .../InfraMonitoringK8sV2/Base/types.ts | 111 ++++ .../EntityDateTimeSelector.tsx | 78 +++ .../useEntityDetailsTime.ts | 95 +++ .../EntityEvents/EntityEvents.tsx | 51 +- .../__tests__/EntityEvents.test.tsx | 35 +- .../__tests__/EntityEvents.timeRange.test.tsx | 81 +++ .../EntityLogs/EntityLogs.tsx | 40 +- .../EntityLogs/__tests__/EntityLogs.test.tsx | 35 +- .../__tests__/EntityLogs.timeRange.test.tsx | 93 +++ .../EntityMetrics/EntityMetrics.tsx | 48 +- .../__tests__/EntityMetrics.test.tsx | 42 +- .../EntityMetrics.timeRange.test.tsx | 119 ++++ .../EntityTraces/EntityTraces.tsx | 56 +- .../__tests__/EntityTraces.default.test.tsx | 8 +- .../__tests__/EntityTraces.timeRange.test.tsx | 81 +++ .../EntityTraces/__tests__/testUtils.tsx | 61 +- .../createPodMetricsTab.tsx | 30 +- .../entityDetails.styles.scss | 6 + .../InfraMonitoringK8sV2/constants.ts | 3 + .../store/globalTime/GlobalTimeContext.tsx | 1 + .../__tests__/GlobalTimeContext.test.tsx | 288 ++++++--- .../src/store/globalTime/globalTimeStore.ts | 39 +- frontend/src/store/globalTime/index.ts | 44 +- frontend/src/store/globalTime/types.ts | 24 + frontend/src/store/globalTime/useUrlSync.ts | 11 + 30 files changed, 1646 insertions(+), 882 deletions(-) create mode 100644 frontend/src/__tests__/fields_api.util.ts create mode 100644 frontend/src/container/InfraMonitoringK8sV2/Base/K8sBaseDetailsContent.tsx create mode 100644 frontend/src/container/InfraMonitoringK8sV2/EntityDetailsUtils/EntityDateTimeSelector/EntityDateTimeSelector.tsx create mode 100644 frontend/src/container/InfraMonitoringK8sV2/EntityDetailsUtils/EntityDateTimeSelector/useEntityDetailsTime.ts create mode 100644 frontend/src/container/InfraMonitoringK8sV2/EntityDetailsUtils/EntityEvents/__tests__/EntityEvents.timeRange.test.tsx create mode 100644 frontend/src/container/InfraMonitoringK8sV2/EntityDetailsUtils/EntityLogs/__tests__/EntityLogs.timeRange.test.tsx create mode 100644 frontend/src/container/InfraMonitoringK8sV2/EntityDetailsUtils/EntityMetrics/__tests__/EntityMetrics.timeRange.test.tsx create mode 100644 frontend/src/container/InfraMonitoringK8sV2/EntityDetailsUtils/EntityTraces/__tests__/EntityTraces.timeRange.test.tsx diff --git a/frontend/src/__tests__/fields_api.util.ts b/frontend/src/__tests__/fields_api.util.ts new file mode 100644 index 00000000000..85149405992 --- /dev/null +++ b/frontend/src/__tests__/fields_api.util.ts @@ -0,0 +1,20 @@ +import { ENVIRONMENT } from 'constants/env'; +import { server } from 'mocks-server/server'; +import { rest } from 'msw'; + +export function mockFieldsAPIsWithEmptyResponse(): void { + server.use( + rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/keys`, (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ status: 'success', data: { keys: {}, complete: true } }), + ), + ), + rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/values`, (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ status: 'success', data: { values: {}, complete: true } }), + ), + ), + ); +} diff --git a/frontend/src/container/InfraMonitoringK8sV2/Base/K8sBaseDetails.tsx b/frontend/src/container/InfraMonitoringK8sV2/Base/K8sBaseDetails.tsx index 4d3ce224328..13db22f17b2 100644 --- a/frontend/src/container/InfraMonitoringK8sV2/Base/K8sBaseDetails.tsx +++ b/frontend/src/container/InfraMonitoringK8sV2/Base/K8sBaseDetails.tsx @@ -1,148 +1,39 @@ -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { useQuery } from 'react-query'; import { Color, Spacing } from '@signozhq/design-tokens'; -import { Button, Drawer, Tooltip } from 'antd'; -import { ToggleGroupSimple } from '@signozhq/ui/toggle-group'; +import { X } from '@signozhq/icons'; import { Divider } from '@signozhq/ui/divider'; 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 { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils'; import { InfraMonitoringEvents } from 'constants/events'; -import { QueryParams } from 'constants/query'; -import { - initialQueryBuilderFormValuesMap, - initialQueryState, -} from 'constants/queryBuilder'; -import ROUTES from 'constants/routes'; -import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants'; -import { - CustomTimeType, - Time, -} from 'container/TopNav/DateTimeSelectionV2/types'; import { useIsDarkMode } from 'hooks/useDarkMode'; -import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; -import GetMinMax from 'lib/getMinMax'; -import { - BarChart, - ChevronsLeftRight, - Compass, - DraftingCompass, - ScrollText, - X, -} from '@signozhq/icons'; -import { isCustomTimeRange, useGlobalTimeStore } from 'store/globalTime'; -import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils'; import { - LogsAggregatorOperator, - TracesAggregatorOperator, -} from 'types/common/queryBuilder'; -import { openInNewTab } from 'utils/navigation'; + GlobalTimeProvider, + NANO_SECOND_MULTIPLIER, + useGlobalTimeStore, +} from 'store/globalTime'; -import { InfraMonitoringEntity, VIEW_TYPES } from '../constants'; -import EntityEvents from '../EntityDetailsUtils/EntityEvents'; -import EntityLogs from '../EntityDetailsUtils/EntityLogs'; -import { K8S_ENTITY_LOGS_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityLogs/hooks'; -import EntityMetrics from '../EntityDetailsUtils/EntityMetrics'; -import EntityTraces from '../EntityDetailsUtils/EntityTraces'; -import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityTraces/hooks'; -import { - SelectedItemParams, - useInfraMonitoringEventsFilters, - useInfraMonitoringLogFilters, - useInfraMonitoringSelectedItemParams, - useInfraMonitoringTracesFilters, - useInfraMonitoringView, -} from '../hooks'; +import { INFRA_MONITORING_K8S_PARAMS_KEYS } from '../constants'; +import { useInfraMonitoringSelectedItemParams } from '../hooks'; import LoadingContainer from '../LoadingContainer'; -import '../EntityDetailsUtils/entityDetails.styles.scss'; -import { parseAsString, useQueryState } from 'nuqs'; -import { - EntityCountConfig, - EntityCountsSection, -} from './components/EntityCountsSection/EntityCountsSection'; - -const TimeRangeOffset = 1000000000; - -export interface K8sDetailsMetadataConfig { - label: string; - getValue: (entity: T) => string | number; - render?: (value: string | number, entity: T) => React.ReactNode; -} - -export type K8sDetailsCountConfig = EntityCountConfig; +import K8sBaseDetailsContent from './K8sBaseDetailsContent'; +import { K8sBaseDetailsProps } from './types'; -export interface K8sDetailsFilters { - filter: { expression: string }; - start: number; - end: number; -} - -export interface CustomTabRenderProps { - entity: T; - timeRange: { startTime: number; endTime: number }; - selectedInterval: Time; - handleTimeChange: ( - interval: Time | CustomTimeType, - dateTimeRange?: [number, number], - ) => void; -} - -export interface CustomTab { - key: string; - label: string; - icon: React.ReactNode; - render: (props: CustomTabRenderProps) => React.ReactNode; -} +import '../EntityDetailsUtils/entityDetails.styles.scss'; -export interface K8sBaseDetailsProps { - category: InfraMonitoringEntity; - eventCategory: string; - // Data fetching configuration - getSelectedItemExpression: (params: SelectedItemParams) => string; - fetchEntityData: ( - filters: K8sDetailsFilters, - signal?: AbortSignal, - ) => Promise<{ data: T | null; error?: APIError | null }>; - // Entity configuration - getEntityName: (entity: T) => string; - getInitialLogTracesExpression: (entity: T) => string; - getInitialEventsExpression: (entity: T) => string; - metadataConfig: K8sDetailsMetadataConfig[]; - countsConfig?: K8sDetailsCountConfig[]; - getCountsFilterExpression?: (entity: T) => string; - entityWidgetInfo: { - title: string; - yAxisUnit: string; - docPath?: string; - }[]; - getEntityQueryPayload: ( - entity: T, - start: number, - end: number, - dotMetricsEnabled: boolean, - ) => GetQueryResultsProps[]; - queryKeyPrefix: string; - /** When true, only metrics are shown and the Metrics/Logs/Traces/Events tab bar is hidden. */ - hideDetailViewTabs?: boolean; - tabsConfig?: { - showMetrics?: boolean; - showLogs?: boolean; - showTraces?: boolean; - showEvents?: boolean; - }; - customTabs?: Array>; -} +export type { + CustomTab, + CustomTabRenderProps, + K8sBaseDetailsProps, + K8sDetailsCountConfig, + K8sDetailsFilters, + K8sDetailsMetadataConfig, +} from './types'; -// eslint-disable-next-line sonarjs/cognitive-complexity export default function K8sBaseDetails({ category, eventCategory, @@ -163,7 +54,6 @@ export default function K8sBaseDetails({ }: K8sBaseDetailsProps): JSX.Element { const selectedTime = useGlobalTimeStore((s) => s.selectedTime); const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime); - const lastComputedMinMax = useGlobalTimeStore((s) => s.lastComputedMinMax); const getAutoRefreshQueryKey = useGlobalTimeStore( (s) => s.getAutoRefreshQueryKey, ); @@ -237,86 +127,9 @@ export default function K8sBaseDetails({ const entityName = entity ? getEntityName(entity) : ''; - // Content state (previously in K8sBaseDetailsContent) - const tabVisibility = useMemo( - () => ({ - showMetrics: true, - showLogs: true, - showTraces: true, - showEvents: true, - ...tabsConfig, - }), - [tabsConfig], - ); - - const { startMs, endMs } = useMemo( - () => ({ - startMs: Math.floor(lastComputedMinMax.minTime / NANO_SECOND_MULTIPLIER), - endMs: Math.floor(lastComputedMinMax.maxTime / NANO_SECOND_MULTIPLIER), - }), - [lastComputedMinMax], - ); - - const [modalTimeRange, setModalTimeRange] = useState(() => ({ - startTime: startMs, - endTime: endMs, - })); - - // TODO(h4ad): Remove this and use context/zustand - const lastSelectedInterval = useRef