Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/kubernetes-tables-refresh-pulse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@hyperdx/app': patch
---

fix: keep Kubernetes dashboard tables on screen while the page refreshes

On a refresh, the Pods, Nodes and Namespaces tables dropped their rows and
showed a loading skeleton until the new range loaded. They now keep the
current rows on screen and pulse while the refetch runs, like the charts
beside them.
71 changes: 59 additions & 12 deletions packages/app/src/KubernetesDashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Head from 'next/head';
import Link from 'next/link';
import cx from 'classnames';
import sub from 'date-fns/sub';
import { isEqual, omit } from 'lodash';
import { useQueryState } from 'nuqs';
import { useForm, useWatch } from 'react-hook-form';
import { convertDateRangeToGranularityString } from '@hyperdx/common-utils/dist/core/utils';
Expand Down Expand Up @@ -113,6 +114,22 @@ const Th = React.memo<{

const TABLE_FETCH_LIMIT = 10000;

const withoutDateRange = (part: unknown) =>
typeof part === 'object' && part !== null ? omit(part, ['dateRange']) : part;

// Whether the previous query ran this config over another time range. Rows
// from another source or filter would be read with the wrong resource
// attributes, so only a time-range change keeps them on screen.
function isSameQueryIgnoringDateRange(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — isSameQueryIgnoringDateRange repeats differsOnlyInDateRange in useOffsetPaginatedQuery, which breaks the REQUIRED DRY rule

packages/app/src/hooks/useOffsetPaginatedQuery.tsx:66 (differsOnlyInDateRange) already runs the same check: isEqual(omit(a, ['dateRange']), omit(b, ['dateRange'])). It also compares the other key parts (prefix, timeout), which the new copy skips. The new copy only asks whether some key element equals the config, so a change in enableQueryChunking, enableParallelQueries, minGranularitySeconds or queryKeyPrefix still keeps the stale rows. Move the check into useQueriedChartConfig as a keepPreviousData option, as useOffsetPaginatedQuery does for DBTableChart. The hook knows its own key layout, so each part can be compared by position instead of scanned with .some, and the three tables pass keepPreviousData: true instead of repeating the same placeholderData closure.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving this into useQueriedChartConfig as a keepPreviousData option would change a shared hook this PR doesn't touch, so per AGENTS.md I've kept it local; on these tables the other key parts (chunking, parallel, granularity) never change without the config changing too, and I'm happy to do the hook option as a follow-up.

prevKey: readonly unknown[] | undefined,
config: object,
) {
const target = withoutDateRange(config);
return (
prevKey?.some(part => isEqual(withoutDateRange(part), target)) ?? false
);
}

type InfraPodsStatusTableColumn =
| 'restarts'
| 'uptime'
Expand Down Expand Up @@ -175,7 +192,7 @@ export const InfraPodsStatusTable = ({
});

const groupBy = ['k8s.pod.name', 'k8s.namespace.name', 'k8s.node.name'];
const { data, isError, isLoading } = useQueriedChartConfig({
const queryConfig = {
...convertV1ChartConfigToV2(
{
series: [
Expand Down Expand Up @@ -244,7 +261,16 @@ export const InfraPodsStatusTable = ({
},
),
limit: { limit: TABLE_FETCH_LIMIT, offset: 0 },
});
};
const { data, isError, isLoading, isPlaceholderData } = useQueriedChartConfig(
queryConfig,
{
placeholderData: (prev, prevQuery) =>
isSameQueryIgnoringDateRange(prevQuery?.queryKey, queryConfig)
? prev
: undefined,
},
);

const resourceAttr = metricSource.resourceAttributesExpression;

Expand Down Expand Up @@ -376,7 +402,7 @@ export const InfraPodsStatusTable = ({
/>
</Group>
</Card.Section>
{isAtFetchLimit && !isLoading && !isError && podsList.length > 0 && (
{isAtFetchLimit && !isError && podsList.length > 0 && (
<Card.Section px="md" py="xs">
<Alert variant="light" color="blue">
Showing first {TABLE_FETCH_LIMIT.toLocaleString()} pods. Use the
Expand All @@ -387,12 +413,13 @@ export const InfraPodsStatusTable = ({
<Card.Section>
<div
ref={tableContainerRef}
className={isPlaceholderData ? 'effect-pulse' : undefined}
style={{
height: '300px',
overflow: 'auto',
}}
>
{isLoading ? (
{isLoading && !data ? (
<TableLoading />
) : isError ? (
<div className="p-4 text-center text-muted fs-8">
Expand Down Expand Up @@ -539,7 +566,7 @@ export const InfraPodsStatusTable = ({
);
};

const NodesTable = ({
export const NodesTable = ({
metricSource,
where,
dateRange,
Expand All @@ -550,7 +577,7 @@ const NodesTable = ({
}) => {
const groupBy = ['k8s.node.name'];

const { data, isError, isLoading } = useQueriedChartConfig({
const queryConfig = {
...convertV1ChartConfigToV2(
{
series: [
Expand Down Expand Up @@ -595,7 +622,16 @@ const NodesTable = ({
},
),
limit: { limit: TABLE_FETCH_LIMIT, offset: 0 },
});
};
const { data, isError, isLoading, isPlaceholderData } = useQueriedChartConfig(
queryConfig,
{
placeholderData: (prev, prevQuery) =>
isSameQueryIgnoringDateRange(prevQuery?.queryKey, queryConfig)
? prev
: undefined,
},
);

const getLink = React.useCallback((nodeName: string) => {
const searchParams = new URLSearchParams(window.location.search);
Expand Down Expand Up @@ -637,12 +673,13 @@ const NodesTable = ({
<Card.Section>
<div
ref={nodesContainerRef}
className={isPlaceholderData ? 'effect-pulse' : undefined}
style={{
height: '300px',
overflow: 'auto',
}}
>
{isLoading ? (
{isLoading && !data ? (
<TableLoading />
) : isError ? (
<div className="p-4 text-center text-muted fs-8">
Expand Down Expand Up @@ -742,7 +779,7 @@ const NodesTable = ({
);
};

const NamespacesTable = ({
export const NamespacesTable = ({
dateRange,
metricSource,
where,
Expand All @@ -753,7 +790,7 @@ const NamespacesTable = ({
}) => {
const groupBy = ['k8s.namespace.name'];

const { data, isError, isLoading } = useQueriedChartConfig({
const queryConfig = {
...convertV1ChartConfigToV2(
{
series: [
Expand Down Expand Up @@ -795,7 +832,16 @@ const NamespacesTable = ({
},
),
limit: { limit: TABLE_FETCH_LIMIT, offset: 0 },
});
};
const { data, isError, isLoading, isPlaceholderData } = useQueriedChartConfig(
queryConfig,
{
placeholderData: (prev, prevQuery) =>
isSameQueryIgnoringDateRange(prevQuery?.queryKey, queryConfig)
? prev
: undefined,
},
);

const resourceAttr = metricSource.resourceAttributesExpression;

Expand Down Expand Up @@ -836,12 +882,13 @@ const NamespacesTable = ({
<Card.Section>
<div
ref={namespacesContainerRef}
className={isPlaceholderData ? 'effect-pulse' : undefined}
style={{
height: '300px',
overflow: 'auto',
}}
>
{isLoading ? (
{isLoading && !data ? (
<TableLoading />
) : isError ? (
<div className="p-4 text-center text-muted fs-8">
Expand Down
Loading
Loading