mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
fix: sort on the client side in KubernetedDashboardPage (#1350)
Also fixes hardcoded ResourceAttributes Fixes: HDX-2790, HDX-2792
This commit is contained in:
parent
211460273d
commit
3b2a8633dd
3 changed files with 184 additions and 35 deletions
5
.changeset/silver-parents-develop.md
Normal file
5
.changeset/silver-parents-develop.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
fix: sort on the client side in KubernetedDashboardPage
|
||||
|
|
@ -167,9 +167,6 @@ export const InfraPodsStatusTable = ({
|
|||
aggFn: 'last_value',
|
||||
where,
|
||||
groupBy,
|
||||
...(sortState.column === 'phase' && {
|
||||
sortOrder: sortState.order,
|
||||
}),
|
||||
},
|
||||
{
|
||||
table: 'metrics',
|
||||
|
|
@ -178,9 +175,6 @@ export const InfraPodsStatusTable = ({
|
|||
aggFn: 'last_value',
|
||||
where,
|
||||
groupBy,
|
||||
...(sortState.column === 'restarts' && {
|
||||
sortOrder: sortState.order,
|
||||
}),
|
||||
},
|
||||
{
|
||||
table: 'metrics',
|
||||
|
|
@ -189,9 +183,6 @@ export const InfraPodsStatusTable = ({
|
|||
aggFn: undefined,
|
||||
where,
|
||||
groupBy,
|
||||
...(sortState.column === 'uptime' && {
|
||||
sortOrder: sortState.order,
|
||||
}),
|
||||
},
|
||||
{
|
||||
table: 'metrics',
|
||||
|
|
@ -208,9 +199,6 @@ export const InfraPodsStatusTable = ({
|
|||
aggFn: 'avg',
|
||||
where,
|
||||
groupBy,
|
||||
...(sortState.column === 'cpuLimit' && {
|
||||
sortOrder: sortState.order,
|
||||
}),
|
||||
},
|
||||
{
|
||||
table: 'metrics',
|
||||
|
|
@ -227,9 +215,6 @@ export const InfraPodsStatusTable = ({
|
|||
aggFn: 'avg',
|
||||
where,
|
||||
groupBy,
|
||||
...(sortState.column === 'memLimit' && {
|
||||
sortOrder: sortState.order,
|
||||
}),
|
||||
},
|
||||
],
|
||||
dateRange,
|
||||
|
|
@ -242,13 +227,15 @@ export const InfraPodsStatusTable = ({
|
|||
limit: { limit: TABLE_FETCH_LIMIT, offset: 0 },
|
||||
});
|
||||
|
||||
const resourceAttr = metricSource.resourceAttributesExpression;
|
||||
|
||||
// TODO: Use useTable
|
||||
const podsList = React.useMemo(() => {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Filter first to reduce the number of objects we create
|
||||
// Filter by phase
|
||||
const phaseFilteredData = data.data.filter((row: any) => {
|
||||
if (phaseFilter === 'all') {
|
||||
return true;
|
||||
|
|
@ -258,21 +245,53 @@ export const InfraPodsStatusTable = ({
|
|||
);
|
||||
});
|
||||
|
||||
// Transform only the filtered data
|
||||
return phaseFilteredData.map((row: any, index: number) => ({
|
||||
id: `pod-${index}`, // Use index-based ID instead of random makeId()
|
||||
name: row["arrayElement(ResourceAttributes, 'k8s.pod.name')"],
|
||||
namespace: row["arrayElement(ResourceAttributes, 'k8s.namespace.name')"],
|
||||
node: row["arrayElement(ResourceAttributes, 'k8s.node.name')"],
|
||||
restarts: row['last_value(k8s.container.restarts)'],
|
||||
uptime: row['undefined(k8s.pod.uptime)'],
|
||||
cpuAvg: row['avg(k8s.pod.cpu.utilization)'],
|
||||
cpuLimitUtilization: row['avg(k8s.pod.cpu_limit_utilization)'],
|
||||
memAvg: row['avg(k8s.pod.memory.usage)'],
|
||||
memLimitUtilization: row['avg(k8s.pod.memory_limit_utilization)'],
|
||||
phase: row['last_value(k8s.pod.phase)'],
|
||||
}));
|
||||
}, [data, phaseFilter]);
|
||||
// Transform the filtered data
|
||||
const transformedData = phaseFilteredData.map(
|
||||
(row: any, index: number) => ({
|
||||
id: `pod-${index}`, // Use index-based ID instead of random makeId()
|
||||
name: row[`arrayElement(${resourceAttr}, 'k8s.pod.name')`],
|
||||
namespace: row[`arrayElement(${resourceAttr}, 'k8s.namespace.name')`],
|
||||
node: row[`arrayElement(${resourceAttr}, 'k8s.node.name')`],
|
||||
restarts: row['last_value(k8s.container.restarts)'],
|
||||
uptime: row['undefined(k8s.pod.uptime)'],
|
||||
cpuAvg: row['avg(k8s.pod.cpu.utilization)'],
|
||||
cpuLimitUtilization: row['avg(k8s.pod.cpu_limit_utilization)'],
|
||||
memAvg: row['avg(k8s.pod.memory.usage)'],
|
||||
memLimitUtilization: row['avg(k8s.pod.memory_limit_utilization)'],
|
||||
phase: row['last_value(k8s.pod.phase)'],
|
||||
}),
|
||||
);
|
||||
|
||||
// Sort the data client-side
|
||||
return transformedData.sort((a, b) => {
|
||||
const getValue = (pod: (typeof transformedData)[0]) => {
|
||||
switch (sortState.column) {
|
||||
case 'phase':
|
||||
return pod.phase;
|
||||
case 'restarts':
|
||||
return pod.restarts;
|
||||
case 'uptime':
|
||||
return pod.uptime;
|
||||
case 'cpuLimit':
|
||||
return pod.cpuLimitUtilization;
|
||||
case 'memLimit':
|
||||
return pod.memLimitUtilization;
|
||||
}
|
||||
};
|
||||
|
||||
const aValue = getValue(a);
|
||||
const bValue = getValue(b);
|
||||
|
||||
// Handle null/undefined - push to end
|
||||
if (aValue == null && bValue == null) return 0;
|
||||
if (aValue == null) return 1;
|
||||
if (bValue == null) return -1;
|
||||
|
||||
// Compare and apply sort order
|
||||
const comparison = aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
|
||||
return sortState.order === 'asc' ? comparison : -comparison;
|
||||
});
|
||||
}, [data, phaseFilter, sortState, resourceAttr]);
|
||||
|
||||
// Check if we're hitting the fetch limit (indicating there might be more data)
|
||||
const isAtFetchLimit = data?.data && data.data.length >= TABLE_FETCH_LIMIT;
|
||||
|
|
@ -561,6 +580,8 @@ const NodesTable = ({
|
|||
return window.location.pathname + '?' + searchParams.toString();
|
||||
}, []);
|
||||
|
||||
const resourceAttr = metricSource.resourceAttributesExpression;
|
||||
|
||||
const nodesList = React.useMemo(() => {
|
||||
if (!data) {
|
||||
return [];
|
||||
|
|
@ -568,14 +589,14 @@ const NodesTable = ({
|
|||
|
||||
return data.data.map((row: any) => {
|
||||
return {
|
||||
name: row["arrayElement(ResourceAttributes, 'k8s.node.name')"],
|
||||
name: row[`arrayElement(${resourceAttr}, 'k8s.node.name')`],
|
||||
cpuAvg: row['avg(k8s.node.cpu.utilization)'],
|
||||
memAvg: row['avg(k8s.node.memory.usage)'],
|
||||
ready: row['avg(k8s.node.condition_ready)'],
|
||||
uptime: row['undefined(k8s.node.uptime)'],
|
||||
};
|
||||
});
|
||||
}, [data]);
|
||||
}, [data, resourceAttr]);
|
||||
|
||||
const {
|
||||
containerRef: nodesContainerRef,
|
||||
|
|
@ -753,6 +774,8 @@ const NamespacesTable = ({
|
|||
limit: { limit: TABLE_FETCH_LIMIT, offset: 0 },
|
||||
});
|
||||
|
||||
const resourceAttr = metricSource.resourceAttributesExpression;
|
||||
|
||||
const namespacesList = React.useMemo(() => {
|
||||
if (!data) {
|
||||
return [];
|
||||
|
|
@ -760,13 +783,13 @@ const NamespacesTable = ({
|
|||
|
||||
return data.data.map((row: any) => {
|
||||
return {
|
||||
name: row["arrayElement(ResourceAttributes, 'k8s.namespace.name')"],
|
||||
name: row[`arrayElement(${resourceAttr}, 'k8s.namespace.name')`],
|
||||
cpuAvg: row['sum(k8s.pod.cpu.utilization)'],
|
||||
memAvg: row['sum(k8s.pod.memory.usage)'],
|
||||
phase: row['last_value(k8s.namespace.phase)'],
|
||||
};
|
||||
});
|
||||
}, [data]);
|
||||
}, [data, resourceAttr]);
|
||||
|
||||
const {
|
||||
containerRef: namespacesContainerRef,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from '../utils/base-test';
|
||||
|
||||
test.describe('Kubernetes Dashboard', { tag: ['@kubernetes'] }, () => {
|
||||
|
|
@ -154,6 +156,10 @@ test.describe('Kubernetes Dashboard', { tag: ['@kubernetes'] }, () => {
|
|||
}) => {
|
||||
// Verify initial state is "Running"
|
||||
const podsTable = page.getByTestId('k8s-pods-table');
|
||||
|
||||
// Wait for table to load
|
||||
await expect(podsTable.locator('tbody tr').first()).toBeVisible();
|
||||
|
||||
const runningTab = podsTable.getByRole('radio', { name: 'Running' });
|
||||
await expect(runningTab).toBeChecked();
|
||||
|
||||
|
|
@ -168,4 +174,119 @@ test.describe('Kubernetes Dashboard', { tag: ['@kubernetes'] }, () => {
|
|||
const allTab = podsTable.getByRole('radio', { name: 'All' });
|
||||
await expect(allTab).toBeChecked();
|
||||
});
|
||||
|
||||
test.describe('Pods Table Sorting', () => {
|
||||
const SORT_ICON_SELECTOR = 'i.bi-caret-down-fill, i.bi-caret-up-fill';
|
||||
|
||||
async function waitForTableLoad(page: Page): Promise<Locator> {
|
||||
const podsTable = page.getByTestId('k8s-pods-table');
|
||||
await expect(podsTable.locator('tbody tr').first()).toBeVisible();
|
||||
return podsTable;
|
||||
}
|
||||
|
||||
function getColumnHeader(podsTable: Locator, columnName: string): Locator {
|
||||
return podsTable.locator('thead th').filter({ hasText: columnName });
|
||||
}
|
||||
|
||||
function getSortIcon(header: Locator): Locator {
|
||||
return header.locator(SORT_ICON_SELECTOR);
|
||||
}
|
||||
|
||||
test('should sort by restarts column', async ({ page }) => {
|
||||
const podsTable = await waitForTableLoad(page);
|
||||
const restartsHeader = getColumnHeader(podsTable, 'Restarts');
|
||||
|
||||
await expect(restartsHeader.locator('i.bi-caret-down-fill')).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
const firstRestartsBefore = await podsTable
|
||||
.locator('tbody tr')
|
||||
.first()
|
||||
.locator('td')
|
||||
.last()
|
||||
.textContent();
|
||||
|
||||
await restartsHeader.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(restartsHeader.locator('i.bi-caret-up-fill')).toBeVisible();
|
||||
|
||||
const firstRestartsAfter = await podsTable
|
||||
.locator('tbody tr')
|
||||
.first()
|
||||
.locator('td')
|
||||
.last()
|
||||
.textContent();
|
||||
|
||||
expect(firstRestartsBefore).not.toBe(firstRestartsAfter);
|
||||
});
|
||||
|
||||
test('should sort by status column', async ({ page }) => {
|
||||
const podsTable = await waitForTableLoad(page);
|
||||
const statusHeader = getColumnHeader(podsTable, 'Status');
|
||||
const sortIcon = getSortIcon(statusHeader);
|
||||
|
||||
await expect(sortIcon).toHaveCount(0);
|
||||
|
||||
await statusHeader.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(sortIcon).toBeVisible();
|
||||
});
|
||||
|
||||
test('should sort by CPU/Limit column', async ({ page }) => {
|
||||
const podsTable = await waitForTableLoad(page);
|
||||
const cpuLimitHeader = getColumnHeader(podsTable, 'CPU/Limit');
|
||||
const sortIcon = getSortIcon(cpuLimitHeader);
|
||||
|
||||
await cpuLimitHeader.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(sortIcon).toBeVisible();
|
||||
|
||||
await cpuLimitHeader.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(sortIcon).toBeVisible();
|
||||
});
|
||||
|
||||
test('should sort by Memory/Limit column', async ({ page }) => {
|
||||
const podsTable = await waitForTableLoad(page);
|
||||
const memLimitHeader = getColumnHeader(podsTable, 'Mem/Limit');
|
||||
|
||||
await memLimitHeader.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(getSortIcon(memLimitHeader)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should sort by Age column', async ({ page }) => {
|
||||
const podsTable = await waitForTableLoad(page);
|
||||
const ageHeader = getColumnHeader(podsTable, 'Age');
|
||||
|
||||
await ageHeader.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(getSortIcon(ageHeader)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should maintain sort when switching phase filters', async ({
|
||||
page,
|
||||
}) => {
|
||||
const podsTable = await waitForTableLoad(page);
|
||||
const ageHeader = getColumnHeader(podsTable, 'Age');
|
||||
const sortIcon = getSortIcon(ageHeader);
|
||||
|
||||
await ageHeader.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(sortIcon).toBeVisible();
|
||||
|
||||
await podsTable.getByText('All', { exact: true }).click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(sortIcon).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue