mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-23 08:49:29 +00:00
Compare commits
3 Commits
main
...
fix/remove
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43509681fa | ||
|
|
ff5fcc0e98 | ||
|
|
122d88c4d2 |
@@ -1,22 +1,3 @@
|
|||||||
.chart-manager-series-label {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
max-width: 100%;
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
background-color: transparent;
|
|
||||||
color: inherit;
|
|
||||||
text-align: left;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-manager-container {
|
.chart-manager-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: calc(40% - 40px);
|
max-height: calc(40% - 40px);
|
||||||
|
|||||||
@@ -1,28 +1,24 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Button, Input } from 'antd';
|
import { Button, Input } from 'antd';
|
||||||
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
|
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
|
import { getGraphManagerTableColumns } from 'container/GridCardLayout/GridCard/FullView/TableRender/GraphManagerColumns';
|
||||||
|
import { ExtendedChartDataset } from 'container/GridCardLayout/GridCard/FullView/types';
|
||||||
|
import { getDefaultTableDataSet } from 'container/GridCardLayout/GridCard/FullView/utils';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||||
import { usePlotContext } from 'lib/uPlotV2/context/PlotContext';
|
import { usePlotContext } from 'lib/uPlotV2/context/PlotContext';
|
||||||
import useLegendsSync from 'lib/uPlotV2/hooks/useLegendsSync';
|
import useLegendsSync from 'lib/uPlotV2/hooks/useLegendsSync';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
|
||||||
import { getChartManagerColumns } from './getChartMangerColumns';
|
|
||||||
import { ExtendedChartDataset, getDefaultTableDataSet } from './utils';
|
|
||||||
|
|
||||||
import './ChartManager.styles.scss';
|
import './ChartManager.styles.scss';
|
||||||
|
|
||||||
interface ChartManagerProps {
|
interface ChartManagerProps {
|
||||||
config: UPlotConfigBuilder;
|
config: UPlotConfigBuilder;
|
||||||
alignedData: uPlot.AlignedData;
|
alignedData: uPlot.AlignedData;
|
||||||
yAxisUnit?: string;
|
yAxisUnit?: string;
|
||||||
decimalPrecision?: PrecisionOption;
|
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const X_AXIS_INDEX = 0;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ChartManager provides a tabular view to manage the visibility of
|
* ChartManager provides a tabular view to manage the visibility of
|
||||||
* individual series on a uPlot chart.
|
* individual series on a uPlot chart.
|
||||||
@@ -32,12 +28,16 @@ const X_AXIS_INDEX = 0;
|
|||||||
* - filter series by label
|
* - filter series by label
|
||||||
* - toggle individual series on/off
|
* - toggle individual series on/off
|
||||||
* - persist the visibility configuration to local storage.
|
* - persist the visibility configuration to local storage.
|
||||||
|
*
|
||||||
|
* @param config - `UPlotConfigBuilder` instance used to derive chart options.
|
||||||
|
* @param alignedData - uPlot aligned data used to build the initial table dataset.
|
||||||
|
* @param yAxisUnit - Optional unit label for Y-axis values shown in the table.
|
||||||
|
* @param onCancel - Optional callback invoked when the user cancels the dialog.
|
||||||
*/
|
*/
|
||||||
export default function ChartManager({
|
export default function ChartManager({
|
||||||
config,
|
config,
|
||||||
alignedData,
|
alignedData,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
decimalPrecision = PrecisionOptionsEnum.TWO,
|
|
||||||
onCancel,
|
onCancel,
|
||||||
}: ChartManagerProps): JSX.Element {
|
}: ChartManagerProps): JSX.Element {
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
@@ -53,13 +53,8 @@ export default function ChartManager({
|
|||||||
const { isDashboardLocked } = useDashboard();
|
const { isDashboardLocked } = useDashboard();
|
||||||
|
|
||||||
const [tableDataSet, setTableDataSet] = useState<ExtendedChartDataset[]>(() =>
|
const [tableDataSet, setTableDataSet] = useState<ExtendedChartDataset[]>(() =>
|
||||||
getDefaultTableDataSet(
|
getDefaultTableDataSet(config.getConfig() as uPlot.Options, alignedData),
|
||||||
config.getConfig() as uPlot.Options,
|
|
||||||
alignedData,
|
|
||||||
decimalPrecision,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
const [filterValue, setFilterValue] = useState('');
|
|
||||||
|
|
||||||
const graphVisibilityState = useMemo(
|
const graphVisibilityState = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -72,62 +67,46 @@ export default function ChartManager({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTableDataSet(
|
setTableDataSet(
|
||||||
getDefaultTableDataSet(
|
getDefaultTableDataSet(config.getConfig() as uPlot.Options, alignedData),
|
||||||
config.getConfig() as uPlot.Options,
|
|
||||||
alignedData,
|
|
||||||
decimalPrecision,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
setFilterValue('');
|
}, [alignedData, config]);
|
||||||
}, [alignedData, config, decimalPrecision]);
|
|
||||||
|
|
||||||
const handleFilterChange = useCallback(
|
const filterHandler = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLInputElement>): void => {
|
(event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
setFilterValue(e.target.value.toLowerCase());
|
const value = event.target.value.toString().toLowerCase();
|
||||||
|
const updatedDataSet = tableDataSet.map((item) => {
|
||||||
|
if (item.label?.toLocaleLowerCase().includes(value)) {
|
||||||
|
return { ...item, show: true };
|
||||||
|
}
|
||||||
|
return { ...item, show: false };
|
||||||
|
});
|
||||||
|
setTableDataSet(updatedDataSet);
|
||||||
},
|
},
|
||||||
[],
|
[tableDataSet],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleToggleSeriesOnOff = useCallback(
|
const dataSource = useMemo(
|
||||||
(index: number): void => {
|
() =>
|
||||||
onToggleSeriesOnOff(index);
|
tableDataSet.filter(
|
||||||
},
|
(item, index) => index !== 0 && item.show, // skipping the first item as it is the x-axis
|
||||||
[onToggleSeriesOnOff],
|
),
|
||||||
|
[tableDataSet],
|
||||||
);
|
);
|
||||||
|
|
||||||
const dataSource = useMemo(() => {
|
|
||||||
const filter = filterValue.trim();
|
|
||||||
return tableDataSet.filter((item, index) => {
|
|
||||||
if (index === X_AXIS_INDEX) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!filter) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return item.label?.toLowerCase().includes(filter) ?? false;
|
|
||||||
});
|
|
||||||
}, [tableDataSet, filterValue]);
|
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() =>
|
() =>
|
||||||
getChartManagerColumns({
|
getGraphManagerTableColumns({
|
||||||
tableDataSet,
|
tableDataSet,
|
||||||
|
checkBoxOnChangeHandler: (_e, index) => {
|
||||||
|
onToggleSeriesOnOff(index);
|
||||||
|
},
|
||||||
graphVisibilityState,
|
graphVisibilityState,
|
||||||
onToggleSeriesOnOff: handleToggleSeriesOnOff,
|
labelClickedHandler: onToggleSeriesVisibility,
|
||||||
onToggleSeriesVisibility,
|
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
isGraphDisabled: isDashboardLocked,
|
isGraphDisabled: isDashboardLocked,
|
||||||
decimalPrecision,
|
|
||||||
}),
|
}),
|
||||||
[
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
tableDataSet,
|
[tableDataSet, graphVisibilityState, yAxisUnit, isDashboardLocked],
|
||||||
graphVisibilityState,
|
|
||||||
handleToggleSeriesOnOff,
|
|
||||||
onToggleSeriesVisibility,
|
|
||||||
yAxisUnit,
|
|
||||||
isDashboardLocked,
|
|
||||||
decimalPrecision,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSave = useCallback((): void => {
|
const handleSave = useCallback((): void => {
|
||||||
@@ -135,18 +114,15 @@ export default function ChartManager({
|
|||||||
notifications.success({
|
notifications.success({
|
||||||
message: 'The updated graphs & legends are saved',
|
message: 'The updated graphs & legends are saved',
|
||||||
});
|
});
|
||||||
onCancel?.();
|
if (onCancel) {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
}, [syncSeriesVisibilityToLocalStorage, notifications, onCancel]);
|
}, [syncSeriesVisibilityToLocalStorage, notifications, onCancel]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chart-manager-container">
|
<div className="chart-manager-container">
|
||||||
<div className="chart-manager-header">
|
<div className="chart-manager-header">
|
||||||
<Input
|
<Input onChange={filterHandler} placeholder="Filter Series" />
|
||||||
placeholder="Filter Series"
|
|
||||||
value={filterValue}
|
|
||||||
onChange={handleFilterChange}
|
|
||||||
data-testid="filter-input"
|
|
||||||
/>
|
|
||||||
<div className="chart-manager-actions-container">
|
<div className="chart-manager-actions-container">
|
||||||
<Button type="default" onClick={onCancel}>
|
<Button type="default" onClick={onCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -160,10 +136,10 @@ export default function ChartManager({
|
|||||||
<ResizeTable
|
<ResizeTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
|
virtual
|
||||||
rowKey="index"
|
rowKey="index"
|
||||||
scroll={{ y: 200 }}
|
scroll={{ y: 200 }}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
virtual
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import { Tooltip } from 'antd';
|
|
||||||
|
|
||||||
import './ChartManager.styles.scss';
|
|
||||||
|
|
||||||
interface SeriesLabelProps {
|
|
||||||
label: string;
|
|
||||||
labelIndex: number;
|
|
||||||
onClick: (idx: number) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SeriesLabel({
|
|
||||||
label,
|
|
||||||
labelIndex,
|
|
||||||
onClick,
|
|
||||||
disabled,
|
|
||||||
}: SeriesLabelProps): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Tooltip placement="topLeft" title={label}>
|
|
||||||
<button
|
|
||||||
className="chart-manager-series-label"
|
|
||||||
disabled={disabled}
|
|
||||||
type="button"
|
|
||||||
data-testid={`series-label-button-${labelIndex}`}
|
|
||||||
onClick={(): void => onClick(labelIndex)}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
import userEvent from '@testing-library/user-event';
|
|
||||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
|
||||||
import { render, screen } from 'tests/test-utils';
|
|
||||||
|
|
||||||
import ChartManager from '../ChartManager';
|
|
||||||
|
|
||||||
const mockSyncSeriesVisibilityToLocalStorage = jest.fn();
|
|
||||||
const mockNotificationsSuccess = jest.fn();
|
|
||||||
|
|
||||||
jest.mock('lib/uPlotV2/context/PlotContext', () => ({
|
|
||||||
usePlotContext: (): {
|
|
||||||
onToggleSeriesOnOff: jest.Mock;
|
|
||||||
onToggleSeriesVisibility: jest.Mock;
|
|
||||||
syncSeriesVisibilityToLocalStorage: jest.Mock;
|
|
||||||
} => ({
|
|
||||||
onToggleSeriesOnOff: jest.fn(),
|
|
||||||
onToggleSeriesVisibility: jest.fn(),
|
|
||||||
syncSeriesVisibilityToLocalStorage: mockSyncSeriesVisibilityToLocalStorage,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('lib/uPlotV2/hooks/useLegendsSync', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: (): {
|
|
||||||
legendItemsMap: { [key: number]: { show: boolean; label: string } };
|
|
||||||
} => ({
|
|
||||||
legendItemsMap: {
|
|
||||||
0: { show: true, label: 'Time' },
|
|
||||||
1: { show: true, label: 'Series 1' },
|
|
||||||
2: { show: true, label: 'Series 2' },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('providers/Dashboard/Dashboard', () => ({
|
|
||||||
useDashboard: (): { isDashboardLocked: boolean } => ({
|
|
||||||
isDashboardLocked: false,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('hooks/useNotifications', () => ({
|
|
||||||
useNotifications: (): { notifications: { success: jest.Mock } } => ({
|
|
||||||
notifications: {
|
|
||||||
success: mockNotificationsSuccess,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('components/ResizeTable', () => {
|
|
||||||
const MockTable = ({
|
|
||||||
dataSource,
|
|
||||||
columns,
|
|
||||||
}: {
|
|
||||||
dataSource: { index: number; label?: string }[];
|
|
||||||
columns: { key: string; title: string }[];
|
|
||||||
}): JSX.Element => (
|
|
||||||
<div data-testid="resize-table">
|
|
||||||
{columns.map((col) => (
|
|
||||||
<span key={col.key}>{col.title}</span>
|
|
||||||
))}
|
|
||||||
{dataSource.map((row) => (
|
|
||||||
<div key={row.index} data-testid={`row-${row.index}`}>
|
|
||||||
{row.label}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return { ResizeTable: MockTable };
|
|
||||||
});
|
|
||||||
|
|
||||||
const createMockConfig = (): { getConfig: () => uPlot.Options } => ({
|
|
||||||
getConfig: (): uPlot.Options => ({
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
series: [
|
|
||||||
{ label: 'Time', value: 'time' },
|
|
||||||
{ label: 'Series 1', scale: 'y' },
|
|
||||||
{ label: 'Series 2', scale: 'y' },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const createAlignedData = (): uPlot.AlignedData => [
|
|
||||||
[1000, 2000, 3000],
|
|
||||||
[10, 20, 30],
|
|
||||||
[1, 2, 3],
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('ChartManager', () => {
|
|
||||||
const mockOnCancel = jest.fn();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders filter input and action buttons', () => {
|
|
||||||
render(
|
|
||||||
<ChartManager
|
|
||||||
config={createMockConfig() as any}
|
|
||||||
alignedData={createAlignedData()}
|
|
||||||
onCancel={mockOnCancel}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByPlaceholderText('Filter Series')).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole('button', { name: /Cancel/ })).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole('button', { name: /Save/ })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders ResizeTable with data', () => {
|
|
||||||
render(
|
|
||||||
<ChartManager
|
|
||||||
config={createMockConfig() as UPlotConfigBuilder}
|
|
||||||
alignedData={createAlignedData()}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByTestId('resize-table')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls onCancel when Cancel button is clicked', async () => {
|
|
||||||
render(
|
|
||||||
<ChartManager
|
|
||||||
config={createMockConfig() as UPlotConfigBuilder}
|
|
||||||
alignedData={createAlignedData()}
|
|
||||||
onCancel={mockOnCancel}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
await userEvent.click(screen.getByRole('button', { name: /Cancel/ }));
|
|
||||||
|
|
||||||
expect(mockOnCancel).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('filters table data when typing in filter input', async () => {
|
|
||||||
render(
|
|
||||||
<ChartManager
|
|
||||||
config={createMockConfig() as UPlotConfigBuilder}
|
|
||||||
alignedData={createAlignedData()}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Before filter: both Series 1 and Series 2 rows are visible
|
|
||||||
expect(screen.getByTestId('row-1')).toBeInTheDocument();
|
|
||||||
expect(screen.getByTestId('row-2')).toBeInTheDocument();
|
|
||||||
|
|
||||||
const filterInput = screen.getByTestId('filter-input');
|
|
||||||
await userEvent.type(filterInput, 'Series 1');
|
|
||||||
|
|
||||||
// After filter: only Series 1 row is visible, Series 2 row is filtered out
|
|
||||||
expect(screen.getByTestId('row-1')).toBeInTheDocument();
|
|
||||||
expect(screen.queryByTestId('row-2')).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls syncSeriesVisibilityToLocalStorage, notifications.success, and onCancel when Save is clicked', async () => {
|
|
||||||
render(
|
|
||||||
<ChartManager
|
|
||||||
config={createMockConfig() as UPlotConfigBuilder}
|
|
||||||
alignedData={createAlignedData()}
|
|
||||||
onCancel={mockOnCancel}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
await userEvent.click(screen.getByRole('button', { name: /Save/ }));
|
|
||||||
|
|
||||||
expect(mockSyncSeriesVisibilityToLocalStorage).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockNotificationsSuccess).toHaveBeenCalledWith({
|
|
||||||
message: 'The updated graphs & legends are saved',
|
|
||||||
});
|
|
||||||
expect(mockOnCancel).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import userEvent from '@testing-library/user-event';
|
|
||||||
import { render, screen } from 'tests/test-utils';
|
|
||||||
|
|
||||||
import { SeriesLabel } from '../SeriesLabel';
|
|
||||||
|
|
||||||
describe('SeriesLabel', () => {
|
|
||||||
it('renders the label text', () => {
|
|
||||||
render(
|
|
||||||
<SeriesLabel label="Test Series Label" labelIndex={1} onClick={jest.fn()} />,
|
|
||||||
);
|
|
||||||
expect(screen.getByTestId('series-label-button-1')).toHaveTextContent(
|
|
||||||
'Test Series Label',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls onClick with labelIndex when clicked', async () => {
|
|
||||||
const onClick = jest.fn();
|
|
||||||
render(<SeriesLabel label="Series A" labelIndex={2} onClick={onClick} />);
|
|
||||||
|
|
||||||
await userEvent.click(screen.getByTestId('series-label-button-2'));
|
|
||||||
|
|
||||||
expect(onClick).toHaveBeenCalledWith(2);
|
|
||||||
expect(onClick).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders disabled button when disabled prop is true', () => {
|
|
||||||
render(
|
|
||||||
<SeriesLabel label="Disabled" labelIndex={0} onClick={jest.fn()} disabled />,
|
|
||||||
);
|
|
||||||
const button = screen.getByTestId('series-label-button-0');
|
|
||||||
expect(button).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has chart-manager-series-label class', () => {
|
|
||||||
render(<SeriesLabel label="Label" labelIndex={0} onClick={jest.fn()} />);
|
|
||||||
const button = screen.getByTestId('series-label-button-0');
|
|
||||||
expect(button).toHaveClass('chart-manager-series-label');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
import { render } from '@testing-library/react';
|
|
||||||
import { Y_AXIS_UNIT_NAMES } from 'components/YAxisUnitSelector/constants';
|
|
||||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
|
||||||
|
|
||||||
import { getChartManagerColumns } from '../getChartMangerColumns';
|
|
||||||
import { ExtendedChartDataset } from '../utils';
|
|
||||||
|
|
||||||
const createMockDataset = (
|
|
||||||
index: number,
|
|
||||||
overrides: Partial<ExtendedChartDataset> = {},
|
|
||||||
): ExtendedChartDataset =>
|
|
||||||
({
|
|
||||||
index,
|
|
||||||
label: `Series ${index}`,
|
|
||||||
show: true,
|
|
||||||
sum: 100,
|
|
||||||
avg: 50,
|
|
||||||
min: 10,
|
|
||||||
max: 90,
|
|
||||||
stroke: '#ff0000',
|
|
||||||
...overrides,
|
|
||||||
} as ExtendedChartDataset);
|
|
||||||
|
|
||||||
describe('getChartManagerColumns', () => {
|
|
||||||
const tableDataSet: ExtendedChartDataset[] = [
|
|
||||||
createMockDataset(0, { label: 'Time' }),
|
|
||||||
createMockDataset(1),
|
|
||||||
createMockDataset(2),
|
|
||||||
];
|
|
||||||
const graphVisibilityState = [true, true, false];
|
|
||||||
const onToggleSeriesOnOff = jest.fn();
|
|
||||||
const onToggleSeriesVisibility = jest.fn();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns columns with expected structure', () => {
|
|
||||||
const columns = getChartManagerColumns({
|
|
||||||
tableDataSet,
|
|
||||||
graphVisibilityState,
|
|
||||||
onToggleSeriesOnOff,
|
|
||||||
onToggleSeriesVisibility,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(columns).toHaveLength(6);
|
|
||||||
expect(columns[0].key).toBe('index');
|
|
||||||
expect(columns[1].key).toBe('label');
|
|
||||||
expect(columns[2].key).toBe('avg');
|
|
||||||
expect(columns[3].key).toBe('sum');
|
|
||||||
expect(columns[4].key).toBe('max');
|
|
||||||
expect(columns[5].key).toBe('min');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('includes Label column with title', () => {
|
|
||||||
const columns = getChartManagerColumns({
|
|
||||||
tableDataSet,
|
|
||||||
graphVisibilityState,
|
|
||||||
onToggleSeriesOnOff,
|
|
||||||
onToggleSeriesVisibility,
|
|
||||||
});
|
|
||||||
|
|
||||||
const labelCol = columns.find((c) => c.key === 'label');
|
|
||||||
expect(labelCol!.title).toBe('Label');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('formats column titles with yAxisUnit', () => {
|
|
||||||
const columns = getChartManagerColumns({
|
|
||||||
tableDataSet,
|
|
||||||
graphVisibilityState,
|
|
||||||
onToggleSeriesOnOff,
|
|
||||||
onToggleSeriesVisibility,
|
|
||||||
yAxisUnit: 'ms',
|
|
||||||
});
|
|
||||||
|
|
||||||
const avgCol = columns.find((c) => c.key === 'avg');
|
|
||||||
expect(avgCol!.title).toBe(
|
|
||||||
`Avg (in ${Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.MILLISECONDS]})`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('numeric column render returns formatted string with yAxisUnit', () => {
|
|
||||||
const columns = getChartManagerColumns({
|
|
||||||
tableDataSet,
|
|
||||||
graphVisibilityState,
|
|
||||||
onToggleSeriesOnOff,
|
|
||||||
onToggleSeriesVisibility,
|
|
||||||
yAxisUnit: 'ms',
|
|
||||||
});
|
|
||||||
|
|
||||||
const avgCol = columns.find((c) => c.key === 'avg');
|
|
||||||
const renderFn = avgCol?.render as
|
|
||||||
| ((val: number, record: ExtendedChartDataset, index: number) => string)
|
|
||||||
| undefined;
|
|
||||||
expect(renderFn).toBeDefined();
|
|
||||||
const output = renderFn!(123.45, tableDataSet[1], 1);
|
|
||||||
expect(output).toBe('123.45 ms');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('numeric column render formats zero when value is undefined', () => {
|
|
||||||
const columns = getChartManagerColumns({
|
|
||||||
tableDataSet,
|
|
||||||
graphVisibilityState,
|
|
||||||
onToggleSeriesOnOff,
|
|
||||||
onToggleSeriesVisibility,
|
|
||||||
yAxisUnit: 'none',
|
|
||||||
});
|
|
||||||
|
|
||||||
const sumCol = columns.find((c) => c.key === 'sum');
|
|
||||||
const renderFn = sumCol?.render as
|
|
||||||
| ((
|
|
||||||
val: number | undefined,
|
|
||||||
record: ExtendedChartDataset,
|
|
||||||
index: number,
|
|
||||||
) => string)
|
|
||||||
| undefined;
|
|
||||||
const output = renderFn!(undefined, tableDataSet[1], 1);
|
|
||||||
expect(output).toBe('0');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('label column render displays label text and is clickable', () => {
|
|
||||||
const columns = getChartManagerColumns({
|
|
||||||
tableDataSet,
|
|
||||||
graphVisibilityState,
|
|
||||||
onToggleSeriesOnOff,
|
|
||||||
onToggleSeriesVisibility,
|
|
||||||
});
|
|
||||||
|
|
||||||
const labelCol = columns.find((c) => c.key === 'label');
|
|
||||||
const renderFn = labelCol!.render as
|
|
||||||
| ((
|
|
||||||
label: string,
|
|
||||||
record: ExtendedChartDataset,
|
|
||||||
index: number,
|
|
||||||
) => JSX.Element)
|
|
||||||
| undefined;
|
|
||||||
expect(renderFn).toBeDefined();
|
|
||||||
const renderResult = renderFn!('Series 1', tableDataSet[1], 1);
|
|
||||||
|
|
||||||
const { getByRole } = render(renderResult);
|
|
||||||
expect(getByRole('button', { name: 'Series 1' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('index column render renders checkbox with correct checked state', () => {
|
|
||||||
const columns = getChartManagerColumns({
|
|
||||||
tableDataSet,
|
|
||||||
graphVisibilityState,
|
|
||||||
onToggleSeriesOnOff,
|
|
||||||
onToggleSeriesVisibility,
|
|
||||||
});
|
|
||||||
|
|
||||||
const indexCol = columns.find((c) => c.key === 'index');
|
|
||||||
const renderFn = indexCol!.render as
|
|
||||||
| ((
|
|
||||||
_val: unknown,
|
|
||||||
record: ExtendedChartDataset,
|
|
||||||
index: number,
|
|
||||||
) => JSX.Element)
|
|
||||||
| undefined;
|
|
||||||
expect(renderFn).toBeDefined();
|
|
||||||
const { container } = render(renderFn!(null, tableDataSet[1], 1));
|
|
||||||
|
|
||||||
const checkbox = container.querySelector('input[type="checkbox"]');
|
|
||||||
expect(checkbox).toBeInTheDocument();
|
|
||||||
expect(checkbox).toBeChecked(); // graphVisibilityState[1] is true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import { PrecisionOptionsEnum } from 'components/Graph/types';
|
|
||||||
|
|
||||||
import {
|
|
||||||
formatTableValueWithUnit,
|
|
||||||
getDefaultTableDataSet,
|
|
||||||
getTableColumnTitle,
|
|
||||||
} from '../utils';
|
|
||||||
|
|
||||||
describe('ChartManager utils', () => {
|
|
||||||
describe('getDefaultTableDataSet', () => {
|
|
||||||
const createOptions = (seriesCount: number): uPlot.Options => ({
|
|
||||||
series: Array.from({ length: seriesCount }, (_, i) =>
|
|
||||||
i === 0
|
|
||||||
? { label: 'Time', value: 'time' }
|
|
||||||
: { label: `Series ${i}`, scale: 'y' },
|
|
||||||
),
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns one row per series with computed stats', () => {
|
|
||||||
const options = createOptions(3);
|
|
||||||
const data: uPlot.AlignedData = [
|
|
||||||
[1000, 2000, 3000],
|
|
||||||
[10, 20, 30],
|
|
||||||
[1, 2, 3],
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = getDefaultTableDataSet(options, data);
|
|
||||||
|
|
||||||
expect(result).toHaveLength(3);
|
|
||||||
expect(result[0]).toMatchObject({
|
|
||||||
index: 0,
|
|
||||||
label: 'Time',
|
|
||||||
show: true,
|
|
||||||
});
|
|
||||||
expect(result[1]).toMatchObject({
|
|
||||||
index: 1,
|
|
||||||
label: 'Series 1',
|
|
||||||
show: true,
|
|
||||||
sum: 60,
|
|
||||||
avg: 20,
|
|
||||||
max: 30,
|
|
||||||
min: 10,
|
|
||||||
});
|
|
||||||
expect(result[2]).toMatchObject({
|
|
||||||
index: 2,
|
|
||||||
label: 'Series 2',
|
|
||||||
show: true,
|
|
||||||
sum: 6,
|
|
||||||
avg: 2,
|
|
||||||
max: 3,
|
|
||||||
min: 1,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles empty data arrays', () => {
|
|
||||||
const options = createOptions(2);
|
|
||||||
const data: uPlot.AlignedData = [[], []];
|
|
||||||
|
|
||||||
const result = getDefaultTableDataSet(options, data);
|
|
||||||
|
|
||||||
expect(result[0]).toMatchObject({
|
|
||||||
sum: 0,
|
|
||||||
avg: 0,
|
|
||||||
max: 0,
|
|
||||||
min: 0,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('respects decimalPrecision parameter', () => {
|
|
||||||
const options = createOptions(2);
|
|
||||||
const data: uPlot.AlignedData = [[1000], [123.454]];
|
|
||||||
|
|
||||||
const resultTwo = getDefaultTableDataSet(
|
|
||||||
options,
|
|
||||||
data,
|
|
||||||
PrecisionOptionsEnum.TWO,
|
|
||||||
);
|
|
||||||
expect(resultTwo[1].avg).toBe(123.45);
|
|
||||||
|
|
||||||
const resultZero = getDefaultTableDataSet(
|
|
||||||
options,
|
|
||||||
data,
|
|
||||||
PrecisionOptionsEnum.ZERO,
|
|
||||||
);
|
|
||||||
expect(resultZero[1].avg).toBe(123);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('formatTableValueWithUnit', () => {
|
|
||||||
it('formats value with unit', () => {
|
|
||||||
const result = formatTableValueWithUnit(1234.56, 'ms');
|
|
||||||
expect(result).toBe('1.23 s');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('falls back to none format when yAxisUnit is undefined', () => {
|
|
||||||
const result = formatTableValueWithUnit(123.45);
|
|
||||||
expect(result).toBe('123.45');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getTableColumnTitle', () => {
|
|
||||||
it('returns title only when yAxisUnit is undefined', () => {
|
|
||||||
expect(getTableColumnTitle('Avg')).toBe('Avg');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns title with unit when yAxisUnit is provided', () => {
|
|
||||||
const result = getTableColumnTitle('Avg', 'ms');
|
|
||||||
expect(result).toBe('Avg (in Milliseconds (ms))');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import { ColumnType } from 'antd/es/table';
|
|
||||||
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
|
|
||||||
import CustomCheckBox from 'container/GridCardLayout/GridCard/FullView/TableRender/CustomCheckBox';
|
|
||||||
|
|
||||||
import { SeriesLabel } from './SeriesLabel';
|
|
||||||
import {
|
|
||||||
ExtendedChartDataset,
|
|
||||||
formatTableValueWithUnit,
|
|
||||||
getTableColumnTitle,
|
|
||||||
} from './utils';
|
|
||||||
|
|
||||||
export interface GetChartManagerColumnsParams {
|
|
||||||
tableDataSet: ExtendedChartDataset[];
|
|
||||||
graphVisibilityState: boolean[];
|
|
||||||
onToggleSeriesOnOff: (index: number) => void;
|
|
||||||
onToggleSeriesVisibility: (index: number) => void;
|
|
||||||
yAxisUnit?: string;
|
|
||||||
decimalPrecision?: PrecisionOption;
|
|
||||||
isGraphDisabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getChartManagerColumns({
|
|
||||||
tableDataSet,
|
|
||||||
graphVisibilityState,
|
|
||||||
onToggleSeriesOnOff,
|
|
||||||
onToggleSeriesVisibility,
|
|
||||||
yAxisUnit,
|
|
||||||
decimalPrecision = PrecisionOptionsEnum.TWO,
|
|
||||||
isGraphDisabled,
|
|
||||||
}: GetChartManagerColumnsParams): ColumnType<ExtendedChartDataset>[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
width: 50,
|
|
||||||
dataIndex: 'index',
|
|
||||||
key: 'index',
|
|
||||||
render: (_: unknown, record: ExtendedChartDataset): JSX.Element => (
|
|
||||||
<CustomCheckBox
|
|
||||||
data={tableDataSet}
|
|
||||||
graphVisibilityState={graphVisibilityState}
|
|
||||||
index={record.index}
|
|
||||||
disabled={isGraphDisabled}
|
|
||||||
checkBoxOnChangeHandler={(_e, idx): void => onToggleSeriesOnOff(idx)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Label',
|
|
||||||
width: 300,
|
|
||||||
dataIndex: 'label',
|
|
||||||
key: 'label',
|
|
||||||
render: (label: string, record: ExtendedChartDataset): JSX.Element => (
|
|
||||||
<SeriesLabel
|
|
||||||
label={label ?? ''}
|
|
||||||
labelIndex={record.index}
|
|
||||||
disabled={isGraphDisabled}
|
|
||||||
onClick={onToggleSeriesVisibility}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: getTableColumnTitle('Avg', yAxisUnit),
|
|
||||||
width: 90,
|
|
||||||
dataIndex: 'avg',
|
|
||||||
key: 'avg',
|
|
||||||
render: (val: number | undefined): string =>
|
|
||||||
formatTableValueWithUnit(val ?? 0, yAxisUnit, decimalPrecision),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: getTableColumnTitle('Sum', yAxisUnit),
|
|
||||||
width: 90,
|
|
||||||
dataIndex: 'sum',
|
|
||||||
key: 'sum',
|
|
||||||
render: (val: number | undefined): string =>
|
|
||||||
formatTableValueWithUnit(val ?? 0, yAxisUnit, decimalPrecision),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: getTableColumnTitle('Max', yAxisUnit),
|
|
||||||
width: 90,
|
|
||||||
dataIndex: 'max',
|
|
||||||
key: 'max',
|
|
||||||
render: (val: number | undefined): string =>
|
|
||||||
formatTableValueWithUnit(val ?? 0, yAxisUnit, decimalPrecision),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: getTableColumnTitle('Min', yAxisUnit),
|
|
||||||
width: 90,
|
|
||||||
dataIndex: 'min',
|
|
||||||
key: 'min',
|
|
||||||
render: (val: number | undefined): string =>
|
|
||||||
formatTableValueWithUnit(val ?? 0, yAxisUnit, decimalPrecision),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
|
|
||||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
|
||||||
import { Y_AXIS_UNIT_NAMES } from 'components/YAxisUnitSelector/constants';
|
|
||||||
import uPlot from 'uplot';
|
|
||||||
|
|
||||||
/** Extended series with computed stats for table display */
|
|
||||||
export type ExtendedChartDataset = uPlot.Series & {
|
|
||||||
show: boolean;
|
|
||||||
sum: number;
|
|
||||||
avg: number;
|
|
||||||
min: number;
|
|
||||||
max: number;
|
|
||||||
index: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
function roundToDecimalPrecision(
|
|
||||||
value: number,
|
|
||||||
decimalPrecision: PrecisionOption = PrecisionOptionsEnum.TWO,
|
|
||||||
): number {
|
|
||||||
if (
|
|
||||||
typeof value !== 'number' ||
|
|
||||||
Number.isNaN(value) ||
|
|
||||||
value === Infinity ||
|
|
||||||
value === -Infinity
|
|
||||||
) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (decimalPrecision === PrecisionOptionsEnum.FULL) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// regex to match the decimal precision for the given decimal precision
|
|
||||||
const regex = new RegExp(`^-?\\d*\\.?0*\\d{0,${decimalPrecision}}`);
|
|
||||||
const matched = value ? value.toFixed(decimalPrecision).match(regex) : null;
|
|
||||||
return matched ? parseFloat(matched[0]) : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Build table dataset from uPlot options and aligned data */
|
|
||||||
export function getDefaultTableDataSet(
|
|
||||||
options: uPlot.Options,
|
|
||||||
data: uPlot.AlignedData,
|
|
||||||
decimalPrecision: PrecisionOption = PrecisionOptionsEnum.TWO,
|
|
||||||
): ExtendedChartDataset[] {
|
|
||||||
return options.series.map(
|
|
||||||
(series: uPlot.Series, index: number): ExtendedChartDataset => {
|
|
||||||
const arr = (data[index] as number[]) ?? [];
|
|
||||||
const sum = arr.reduce((a, b) => a + b, 0) || 0;
|
|
||||||
const count = arr.length || 1;
|
|
||||||
|
|
||||||
const hasValues = arr.length > 0;
|
|
||||||
return {
|
|
||||||
...series,
|
|
||||||
index,
|
|
||||||
show: true,
|
|
||||||
sum: roundToDecimalPrecision(sum, decimalPrecision),
|
|
||||||
avg: roundToDecimalPrecision(sum / count, decimalPrecision),
|
|
||||||
max: hasValues
|
|
||||||
? roundToDecimalPrecision(Math.max(...arr), decimalPrecision)
|
|
||||||
: 0,
|
|
||||||
min: hasValues
|
|
||||||
? roundToDecimalPrecision(Math.min(...arr), decimalPrecision)
|
|
||||||
: 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Format numeric value for table display using yAxisUnit */
|
|
||||||
export function formatTableValueWithUnit(
|
|
||||||
value: number,
|
|
||||||
yAxisUnit?: string,
|
|
||||||
decimalPrecision: PrecisionOption = PrecisionOptionsEnum.TWO,
|
|
||||||
): string {
|
|
||||||
return `${getYAxisFormattedValue(
|
|
||||||
String(value),
|
|
||||||
yAxisUnit ?? 'none',
|
|
||||||
decimalPrecision,
|
|
||||||
)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Format column header with optional unit */
|
|
||||||
export function getTableColumnTitle(title: string, yAxisUnit?: string): string {
|
|
||||||
if (!yAxisUnit) {
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
const universalName =
|
|
||||||
Y_AXIS_UNIT_NAMES[yAxisUnit as keyof typeof Y_AXIS_UNIT_NAMES];
|
|
||||||
if (!universalName) {
|
|
||||||
return `${title} (in ${yAxisUnit})`;
|
|
||||||
}
|
|
||||||
return `${title} (in ${universalName})`;
|
|
||||||
}
|
|
||||||
@@ -96,7 +96,6 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
|
|||||||
config={config}
|
config={config}
|
||||||
alignedData={chartData}
|
alignedData={chartData}
|
||||||
yAxisUnit={widget.yAxisUnit}
|
yAxisUnit={widget.yAxisUnit}
|
||||||
decimalPrecision={widget.decimalPrecision}
|
|
||||||
onCancel={onToggleModelHandler}
|
onCancel={onToggleModelHandler}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -106,7 +105,6 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
|
|||||||
chartData,
|
chartData,
|
||||||
widget.yAxisUnit,
|
widget.yAxisUnit,
|
||||||
onToggleModelHandler,
|
onToggleModelHandler,
|
||||||
widget.decimalPrecision,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const onPlotDestroy = useCallback(() => {
|
const onPlotDestroy = useCallback(() => {
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
|||||||
config={config}
|
config={config}
|
||||||
alignedData={chartData}
|
alignedData={chartData}
|
||||||
yAxisUnit={widget.yAxisUnit}
|
yAxisUnit={widget.yAxisUnit}
|
||||||
decimalPrecision={widget.decimalPrecision}
|
|
||||||
onCancel={onToggleModelHandler}
|
onCancel={onToggleModelHandler}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -105,7 +104,6 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
|||||||
chartData,
|
chartData,
|
||||||
widget.yAxisUnit,
|
widget.yAxisUnit,
|
||||||
onToggleModelHandler,
|
onToggleModelHandler,
|
||||||
widget.decimalPrecision,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -178,9 +178,7 @@ export default function HostsListTable({
|
|||||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||||
}}
|
}}
|
||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
rowKey={(record): string =>
|
rowKey={(record): string => record.hostName}
|
||||||
(record as HostRowData & { key: string }).key ?? record.hostName
|
|
||||||
}
|
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
onRow={(record): { onClick: () => void; className: string } => ({
|
onRow={(record): { onClick: () => void; className: string } => ({
|
||||||
onClick: (): void => handleRowClick(record),
|
onClick: (): void => handleRowClick(record),
|
||||||
|
|||||||
@@ -126,8 +126,7 @@
|
|||||||
background: var(--bg-ink-500);
|
background: var(--bg-ink-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-table-cell:has(.hostname-column-value),
|
.ant-table-cell:has(.hostname-column-value) {
|
||||||
.ant-table-cell:has(.hostname-cell-missing) {
|
|
||||||
background: var(--bg-ink-400);
|
background: var(--bg-ink-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,23 +139,6 @@
|
|||||||
letter-spacing: -0.07px;
|
letter-spacing: -0.07px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hostname-cell-missing {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hostname-cell-placeholder {
|
|
||||||
color: var(--Vanilla-400, #c0c1c3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hostname-cell-warning-icon {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-left: 4px;
|
|
||||||
cursor: help;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-cell {
|
.status-cell {
|
||||||
.active-tag {
|
.active-tag {
|
||||||
color: var(--bg-forest-500);
|
color: var(--bg-forest-500);
|
||||||
@@ -375,8 +357,7 @@
|
|||||||
color: var(--bg-ink-500);
|
color: var(--bg-ink-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-table-cell:has(.hostname-column-value),
|
.ant-table-cell:has(.hostname-column-value) {
|
||||||
.ant-table-cell:has(.hostname-cell-missing) {
|
|
||||||
background: var(--bg-vanilla-100);
|
background: var(--bg-vanilla-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,10 +365,6 @@
|
|||||||
color: var(--bg-ink-300);
|
color: var(--bg-ink-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hostname-cell-placeholder {
|
|
||||||
color: var(--text-ink-300);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-table-tbody > tr:hover > td {
|
.ant-table-tbody > tr:hover > td {
|
||||||
background: rgba(0, 0, 0, 0.04);
|
background: rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,13 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import { HostData, TimeSeries } from 'api/infraMonitoring/getHostLists';
|
|
||||||
|
|
||||||
import {
|
import { formatDataForTable, GetHostsQuickFiltersConfig } from '../utils';
|
||||||
formatDataForTable,
|
|
||||||
GetHostsQuickFiltersConfig,
|
|
||||||
HostnameCell,
|
|
||||||
} from '../utils';
|
|
||||||
|
|
||||||
const PROGRESS_BAR_CLASS = '.progress-bar';
|
const PROGRESS_BAR_CLASS = '.progress-bar';
|
||||||
|
|
||||||
const emptyTimeSeries: TimeSeries = {
|
|
||||||
labels: {},
|
|
||||||
labelsArray: [],
|
|
||||||
values: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('InfraMonitoringHosts utils', () => {
|
describe('InfraMonitoringHosts utils', () => {
|
||||||
describe('formatDataForTable', () => {
|
describe('formatDataForTable', () => {
|
||||||
it('should format host data correctly', () => {
|
it('should format host data correctly', () => {
|
||||||
const mockData: HostData[] = [
|
const mockData = [
|
||||||
{
|
{
|
||||||
hostName: 'test-host',
|
hostName: 'test-host',
|
||||||
active: true,
|
active: true,
|
||||||
@@ -27,12 +16,8 @@ describe('InfraMonitoringHosts utils', () => {
|
|||||||
wait: 0.05,
|
wait: 0.05,
|
||||||
load15: 2.5,
|
load15: 2.5,
|
||||||
os: 'linux',
|
os: 'linux',
|
||||||
cpuTimeSeries: emptyTimeSeries,
|
|
||||||
memoryTimeSeries: emptyTimeSeries,
|
|
||||||
waitTimeSeries: emptyTimeSeries,
|
|
||||||
load15TimeSeries: emptyTimeSeries,
|
|
||||||
},
|
},
|
||||||
];
|
] as any;
|
||||||
|
|
||||||
const result = formatDataForTable(mockData);
|
const result = formatDataForTable(mockData);
|
||||||
|
|
||||||
@@ -61,7 +46,7 @@ describe('InfraMonitoringHosts utils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle inactive hosts', () => {
|
it('should handle inactive hosts', () => {
|
||||||
const mockData: HostData[] = [
|
const mockData = [
|
||||||
{
|
{
|
||||||
hostName: 'test-host',
|
hostName: 'test-host',
|
||||||
active: false,
|
active: false,
|
||||||
@@ -70,12 +55,12 @@ describe('InfraMonitoringHosts utils', () => {
|
|||||||
wait: 0.02,
|
wait: 0.02,
|
||||||
load15: 1.2,
|
load15: 1.2,
|
||||||
os: 'linux',
|
os: 'linux',
|
||||||
cpuTimeSeries: emptyTimeSeries,
|
cpuTimeSeries: [],
|
||||||
memoryTimeSeries: emptyTimeSeries,
|
memoryTimeSeries: [],
|
||||||
waitTimeSeries: emptyTimeSeries,
|
waitTimeSeries: [],
|
||||||
load15TimeSeries: emptyTimeSeries,
|
load15TimeSeries: [],
|
||||||
},
|
},
|
||||||
];
|
] as any;
|
||||||
|
|
||||||
const result = formatDataForTable(mockData);
|
const result = formatDataForTable(mockData);
|
||||||
|
|
||||||
@@ -83,65 +68,6 @@ describe('InfraMonitoringHosts utils', () => {
|
|||||||
expect(inactiveTag.container.textContent).toBe('INACTIVE');
|
expect(inactiveTag.container.textContent).toBe('INACTIVE');
|
||||||
expect(inactiveTag.container.querySelector('.inactive')).toBeTruthy();
|
expect(inactiveTag.container.querySelector('.inactive')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set hostName to empty string when host has no hostname', () => {
|
|
||||||
const mockData: HostData[] = [
|
|
||||||
{
|
|
||||||
hostName: '',
|
|
||||||
active: true,
|
|
||||||
cpu: 0.5,
|
|
||||||
memory: 0.4,
|
|
||||||
wait: 0.01,
|
|
||||||
load15: 1.0,
|
|
||||||
os: 'linux',
|
|
||||||
cpuTimeSeries: emptyTimeSeries,
|
|
||||||
memoryTimeSeries: emptyTimeSeries,
|
|
||||||
waitTimeSeries: emptyTimeSeries,
|
|
||||||
load15TimeSeries: emptyTimeSeries,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = formatDataForTable(mockData);
|
|
||||||
expect(result[0].hostName).toBe('');
|
|
||||||
expect(result[0].key).toBe('-0');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('HostnameCell', () => {
|
|
||||||
it('should render hostname when present (case A: no icon)', () => {
|
|
||||||
const { container } = render(<HostnameCell hostName="gke-prod-1" />);
|
|
||||||
expect(container.querySelector('.hostname-column-value')).toBeTruthy();
|
|
||||||
expect(container.textContent).toBe('gke-prod-1');
|
|
||||||
expect(container.querySelector('.hostname-cell-missing')).toBeFalsy();
|
|
||||||
expect(container.querySelector('.hostname-cell-warning-icon')).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render placeholder and icon when hostName is empty (case B)', () => {
|
|
||||||
const { container } = render(<HostnameCell hostName="" />);
|
|
||||||
expect(screen.getByText('-')).toBeTruthy();
|
|
||||||
expect(container.querySelector('.hostname-cell-missing')).toBeTruthy();
|
|
||||||
const iconWrapper = container.querySelector('.hostname-cell-warning-icon');
|
|
||||||
expect(iconWrapper).toBeTruthy();
|
|
||||||
expect(iconWrapper?.getAttribute('aria-label')).toBe(
|
|
||||||
'Missing host.name metadata',
|
|
||||||
);
|
|
||||||
expect(iconWrapper?.getAttribute('tabindex')).toBe('0');
|
|
||||||
// Tooltip with "Learn how to configure →" link is shown on hover/focus
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render placeholder and icon when hostName is whitespace only (case C)', () => {
|
|
||||||
const { container } = render(<HostnameCell hostName=" " />);
|
|
||||||
expect(screen.getByText('-')).toBeTruthy();
|
|
||||||
expect(container.querySelector('.hostname-cell-missing')).toBeTruthy();
|
|
||||||
expect(container.querySelector('.hostname-cell-warning-icon')).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render placeholder and icon when hostName is undefined (case D)', () => {
|
|
||||||
const { container } = render(<HostnameCell hostName={undefined} />);
|
|
||||||
expect(screen.getByText('-')).toBeTruthy();
|
|
||||||
expect(container.querySelector('.hostname-cell-missing')).toBeTruthy();
|
|
||||||
expect(container.querySelector('.hostname-cell-warning-icon')).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GetHostsQuickFiltersConfig', () => {
|
describe('GetHostsQuickFiltersConfig', () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Dispatch, SetStateAction } from 'react';
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Progress, TabsProps, Tag, Tooltip, Typography } from 'antd';
|
import { Progress, TabsProps, Tag, Tooltip } from 'antd';
|
||||||
import { ColumnType } from 'antd/es/table';
|
import { ColumnType } from 'antd/es/table';
|
||||||
import {
|
import {
|
||||||
HostData,
|
HostData,
|
||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
} from 'components/QuickFilters/types';
|
} from 'components/QuickFilters/types';
|
||||||
import TabLabel from 'components/TabLabel';
|
import TabLabel from 'components/TabLabel';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { TriangleAlert } from 'lucide-react';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
@@ -25,7 +24,6 @@ import HostsList from './HostsList';
|
|||||||
import './InfraMonitoring.styles.scss';
|
import './InfraMonitoring.styles.scss';
|
||||||
|
|
||||||
export interface HostRowData {
|
export interface HostRowData {
|
||||||
key?: string;
|
|
||||||
hostName: string;
|
hostName: string;
|
||||||
cpu: React.ReactNode;
|
cpu: React.ReactNode;
|
||||||
memory: React.ReactNode;
|
memory: React.ReactNode;
|
||||||
@@ -34,59 +32,6 @@ export interface HostRowData {
|
|||||||
active: React.ReactNode;
|
active: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HOSTNAME_DOCS_URL =
|
|
||||||
'https://signoz.io/docs/infrastructure-monitoring/hostmetrics/#host-name-is-blankempty';
|
|
||||||
|
|
||||||
export function HostnameCell({
|
|
||||||
hostName,
|
|
||||||
}: {
|
|
||||||
hostName?: string | null;
|
|
||||||
}): React.ReactElement {
|
|
||||||
const isEmpty = !hostName || !hostName.trim();
|
|
||||||
if (!isEmpty) {
|
|
||||||
return <div className="hostname-column-value">{hostName}</div>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="hostname-cell-missing">
|
|
||||||
<Typography.Text type="secondary" className="hostname-cell-placeholder">
|
|
||||||
-
|
|
||||||
</Typography.Text>
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
<div>
|
|
||||||
Missing host.name metadata.
|
|
||||||
<br />
|
|
||||||
<a
|
|
||||||
href={HOSTNAME_DOCS_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
onClick={(e): void => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
Learn how to configure →
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
trigger={['hover', 'focus']}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="hostname-cell-warning-icon"
|
|
||||||
tabIndex={0}
|
|
||||||
role="img"
|
|
||||||
aria-label="Missing host.name metadata"
|
|
||||||
onClick={(e): void => e.stopPropagation()}
|
|
||||||
onKeyDown={(e): void => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TriangleAlert size={14} color={Color.BG_CHERRY_500} />
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HostsListTableProps {
|
export interface HostsListTableProps {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isError: boolean;
|
isError: boolean;
|
||||||
@@ -130,8 +75,8 @@ export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
|
|||||||
dataIndex: 'hostName',
|
dataIndex: 'hostName',
|
||||||
key: 'hostName',
|
key: 'hostName',
|
||||||
width: 250,
|
width: 250,
|
||||||
render: (value: string | undefined): React.ReactNode => (
|
render: (value: string): React.ReactNode => (
|
||||||
<HostnameCell hostName={value ?? ''} />
|
<div className="hostname-column-value">{value}</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -103,9 +103,10 @@ function K8sClustersList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -105,9 +105,10 @@ function K8sDaemonSetsList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -106,9 +106,10 @@ function K8sDeploymentsList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -101,9 +101,10 @@ function K8sJobsList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
|
|||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import { safeParseJSON } from './commonUtils';
|
||||||
import { INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory } from './constants';
|
import { INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory } from './constants';
|
||||||
import K8sFiltersSidePanel from './K8sFiltersSidePanel/K8sFiltersSidePanel';
|
import K8sFiltersSidePanel from './K8sFiltersSidePanel/K8sFiltersSidePanel';
|
||||||
import { IEntityColumn } from './utils';
|
import { IEntityColumn } from './utils';
|
||||||
@@ -58,9 +59,10 @@ function K8sHeader({
|
|||||||
const urlFilters = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS);
|
const urlFilters = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS);
|
||||||
let { filters } = currentQuery.builder.queryData[0];
|
let { filters } = currentQuery.builder.queryData[0];
|
||||||
if (urlFilters) {
|
if (urlFilters) {
|
||||||
const decoded = decodeURIComponent(urlFilters);
|
const parsed = safeParseJSON<IBuilderQuery['filters']>(urlFilters);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
filters = parsed;
|
filters = parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...currentQuery,
|
...currentQuery,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -104,9 +104,10 @@ function K8sNamespacesList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -99,9 +99,10 @@ function K8sNodesList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -92,9 +92,10 @@ function K8sPodsList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -105,9 +105,10 @@ function K8sStatefulSetsList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
|||||||
|
|
||||||
import { FeatureKeys } from '../../../constants/features';
|
import { FeatureKeys } from '../../../constants/features';
|
||||||
import { useAppContext } from '../../../providers/App/App';
|
import { useAppContext } from '../../../providers/App/App';
|
||||||
import { getOrderByFromParams } from '../commonUtils';
|
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
|
||||||
import {
|
import {
|
||||||
GetK8sEntityToAggregateAttribute,
|
GetK8sEntityToAggregateAttribute,
|
||||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||||
@@ -105,9 +105,10 @@ function K8sVolumesList({
|
|||||||
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
|
||||||
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
const decoded = decodeURIComponent(groupBy);
|
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
|
||||||
const parsed = JSON.parse(decoded);
|
if (parsed) {
|
||||||
return parsed as IBuilderQuery['groupBy'];
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
/* eslint-disable prefer-destructuring */
|
/* eslint-disable prefer-destructuring */
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import * as Sentry from '@sentry/react';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Table, Tooltip, Typography } from 'antd';
|
import { Table, Tooltip, Typography } from 'antd';
|
||||||
import { Progress } from 'antd/lib';
|
import { Progress } from 'antd/lib';
|
||||||
@@ -260,6 +261,19 @@ export const filterDuplicateFilters = (
|
|||||||
return uniqueFilters;
|
return uniqueFilters;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const safeParseJSON = <T,>(value: string): T | null => {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(value) as T;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing JSON from URL parameter:', e);
|
||||||
|
// TODO: Should we capture this error in Sentry?
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const getOrderByFromParams = (
|
export const getOrderByFromParams = (
|
||||||
searchParams: URLSearchParams,
|
searchParams: URLSearchParams,
|
||||||
returnNullAsDefault = false,
|
returnNullAsDefault = false,
|
||||||
@@ -271,9 +285,12 @@ export const getOrderByFromParams = (
|
|||||||
INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
|
INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
|
||||||
);
|
);
|
||||||
if (orderByFromParams) {
|
if (orderByFromParams) {
|
||||||
const decoded = decodeURIComponent(orderByFromParams);
|
const parsed = safeParseJSON<{ columnName: string; order: 'asc' | 'desc' }>(
|
||||||
const parsed = JSON.parse(decoded);
|
orderByFromParams,
|
||||||
return parsed as { columnName: string; order: 'asc' | 'desc' };
|
);
|
||||||
|
if (parsed) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (returnNullAsDefault) {
|
if (returnNullAsDefault) {
|
||||||
return null;
|
return null;
|
||||||
@@ -287,13 +304,7 @@ export const getFiltersFromParams = (
|
|||||||
): IBuilderQuery['filters'] | null => {
|
): IBuilderQuery['filters'] | null => {
|
||||||
const filtersFromParams = searchParams.get(queryKey);
|
const filtersFromParams = searchParams.get(queryKey);
|
||||||
if (filtersFromParams) {
|
if (filtersFromParams) {
|
||||||
try {
|
return safeParseJSON<IBuilderQuery['filters']>(filtersFromParams);
|
||||||
const decoded = decodeURIComponent(filtersFromParams);
|
|
||||||
const parsed = JSON.parse(decoded);
|
|
||||||
return parsed as IBuilderQuery['filters'];
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user