Skip to content
Merged
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
6 changes: 6 additions & 0 deletions timeseriestable/cue.mod/module.cue
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ language: {
source: {
kind: "git"
}
deps: {
"github.com/perses/shared/cue@v0": {
v: "v0.53.0-rc.2"
default: true
}
}
9 changes: 8 additions & 1 deletion timeseriestable/schemas/time-series-table.cue
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,12 @@

package model

import (
"github.com/perses/shared/cue/common"
)

kind: "TimeSeriesTable"
spec: close({})
spec: close({
selection?: common.#selection
actions?: common.#actions
})
2 changes: 2 additions & 0 deletions timeseriestable/src/TimeSeriesTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import { PanelPlugin } from '@perses-dev/plugin-system';
import { TimeSeriesTablePanel, TimeSeriesTableProps } from './TimeSeriesTablePanel';
import { TimeSeriesTableItemSelectionActionsEditor } from './components';
import { TimeSeriesTableOptions } from './model';

/**
Expand All @@ -24,6 +25,7 @@ export const TimeSeriesTable: PanelPlugin<TimeSeriesTableOptions, TimeSeriesTabl
queryOptions: {
mode: 'instant',
},
panelOptionsEditorComponents: [{ label: 'Item Actions', content: TimeSeriesTableItemSelectionActionsEditor }],
createInitialOptions: () => {
return {};
},
Expand Down
179 changes: 143 additions & 36 deletions timeseriestable/src/TimeSeriesTablePanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,67 +11,174 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { ChartsProvider, SnackbarProvider, testChartsTheme } from '@perses-dev/components';
import {
ChartsProvider,
ItemActionsProvider,
SelectionProvider,
SnackbarProvider,
testChartsTheme,
} from '@perses-dev/components';
import { TimeSeriesData } from '@perses-dev/core';
import { VariableProvider } from '@perses-dev/dashboards';
import { TimeRangeProviderBasic, WebhookAction } from '@perses-dev/plugin-system';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TimeSeriesTableOptions } from './model';
import {
MOCK_TIME_SERIES_DATA_MULTIVALUE,
MOCK_TIME_SERIES_DATA_SINGLEVALUE,
MOCK_TIME_SERIES_QUERY_DEFINITION,
} from './test/mock-query-results';
import { TimeSeriesTablePanel, TimeSeriesTableProps } from './TimeSeriesTablePanel';

const TEST_TIME_SERIES_TABLE_PROPS: Omit<TimeSeriesTableProps, 'queryResults'> = {
contentDimensions: {
width: 500,
height: 500,
},
const TEST_PROPS: Omit<TimeSeriesTableProps, 'queryResults'> = {
contentDimensions: { width: 500, height: 500 },
spec: {},
};

const TEST_WEBHOOK_ACTION: WebhookAction = {
type: 'webhook',
name: 'Test Action',
url: 'https://example.com/action',
method: 'POST',
contentType: 'json',
batchMode: 'individual',
enabled: true,
};

const EXPECTED_SERIES_TEXT =
'{device="/dev/vda1", env="demo", fstype="ext4", instance="demo.do.prometheus.io:9100", job="node", mountpoint="/"}';

describe('TimeSeriesTablePanel', () => {
// Helper to render the panel with some context set
const renderPanel = (data: TimeSeriesData): void => {
const renderPanel = (data: TimeSeriesData, options?: TimeSeriesTableOptions) => {
render(
<SnackbarProvider>
<ChartsProvider chartsTheme={testChartsTheme}>
<TimeSeriesTablePanel
{...TEST_TIME_SERIES_TABLE_PROPS}
queryResults={[{ definition: MOCK_TIME_SERIES_QUERY_DEFINITION, data }]}
/>
</ChartsProvider>
</SnackbarProvider>
<QueryClientProvider client={new QueryClient()}>
<SnackbarProvider>
<TimeRangeProviderBasic initialTimeRange={{ pastDuration: '1m' }}>
<VariableProvider>
<SelectionProvider>
<ItemActionsProvider>
<ChartsProvider chartsTheme={testChartsTheme}>
<TimeSeriesTablePanel
{...TEST_PROPS}
spec={options ?? {}}
queryResults={[{ definition: MOCK_TIME_SERIES_QUERY_DEFINITION, data }]}
/>
</ChartsProvider>
</ItemActionsProvider>
</SelectionProvider>
</VariableProvider>
</TimeRangeProviderBasic>
</SnackbarProvider>
</QueryClientProvider>
);
};

const getCheckboxes = () => screen.findAllByRole('checkbox');

it('should render multi values with timestamps', async () => {
renderPanel(MOCK_TIME_SERIES_DATA_MULTIVALUE);

expect(
screen.getAllByText(
(_, element) =>
element?.textContent ===
'{device="/dev/vda1", env="demo", fstype="ext4", instance="demo.do.prometheus.io:9100", job="node", mountpoint="/"}'
).length
).toBeGreaterThan(0);

expect(await screen.findAllByRole('cell')).toHaveLength(4); // 2 lines with 2 column
expect(await screen.findAllByText('@1666479357903')).toHaveLength(2); // first timestamp appear once per line
expect(await screen.findAllByText('@1666479382282')).toHaveLength(2); // second timestamp appear once per line
expect(screen.getAllByText((_, el) => el?.textContent === EXPECTED_SERIES_TEXT).length).toBeGreaterThan(0);
expect(await screen.findAllByRole('cell')).toHaveLength(4);
expect(await screen.findAllByText('@1666479357903')).toHaveLength(2);
expect(await screen.findAllByText('@1666479382282')).toHaveLength(2);
});

it('should render single value without timestamp', async () => {
renderPanel(MOCK_TIME_SERIES_DATA_SINGLEVALUE);

expect(
screen.getAllByText(
(_, element) =>
element?.textContent ===
'{device="/dev/vda1", env="demo", fstype="ext4", instance="demo.do.prometheus.io:9100", job="node", mountpoint="/"}'
).length
).toBeGreaterThan(0);
expect(screen.getAllByText((_, el) => el?.textContent === EXPECTED_SERIES_TEXT).length).toBeGreaterThan(0);
expect(await screen.findAllByRole('cell')).toHaveLength(4);
expect(screen.queryByText('@')).toBeNull();
});

describe('Selection', () => {
it('should not render checkboxes when disabled', () => {
renderPanel(MOCK_TIME_SERIES_DATA_SINGLEVALUE, { selection: { enabled: false } });
expect(screen.queryAllByRole('checkbox')).toHaveLength(0);
});

it('should render checkboxes when enabled', async () => {
renderPanel(MOCK_TIME_SERIES_DATA_SINGLEVALUE, { selection: { enabled: true } });
expect(await getCheckboxes()).toHaveLength(3); // 1 header + 2 rows
});

it('should render table header with column labels', async () => {
renderPanel(MOCK_TIME_SERIES_DATA_SINGLEVALUE, { selection: { enabled: true } });

expect(await screen.findByRole('columnheader', { name: 'Series' })).toBeInTheDocument();
expect(await screen.findByRole('columnheader', { name: 'Value' })).toBeInTheDocument();
});

it('should toggle row selection when clicking checkbox', async () => {
renderPanel(MOCK_TIME_SERIES_DATA_SINGLEVALUE, { selection: { enabled: true } });

const [, rowCheckbox] = await getCheckboxes();
expect(rowCheckbox).not.toBeChecked();

userEvent.click(rowCheckbox!);
expect(rowCheckbox).toBeChecked();

userEvent.click(rowCheckbox!);
expect(rowCheckbox).not.toBeChecked();
});

it('should select all rows when clicking header checkbox', async () => {
renderPanel(MOCK_TIME_SERIES_DATA_SINGLEVALUE, { selection: { enabled: true } });

const [selectAll, row1, row2] = await getCheckboxes();
expect(selectAll).not.toBeChecked();

userEvent.click(selectAll!);
expect(selectAll).toBeChecked();
expect(row1).toBeChecked();
expect(row2).toBeChecked();
});

it('should deselect all rows when clicking header checkbox while all selected', async () => {
renderPanel(MOCK_TIME_SERIES_DATA_SINGLEVALUE, { selection: { enabled: true } });

const [selectAll] = await getCheckboxes();
userEvent.click(selectAll!);
expect(selectAll).toBeChecked();

userEvent.click(selectAll!);
expect(selectAll).not.toBeChecked();

const [, row1, row2] = await getCheckboxes();
expect(row1).not.toBeChecked();
expect(row2).not.toBeChecked();
});
});

describe('Item Actions', () => {
const actionsConfig = (name = 'Test Action'): TimeSeriesTableOptions => ({
selection: { enabled: true },
actions: {
enabled: true,
displayWithItem: true,
actionsList: [{ ...TEST_WEBHOOK_ACTION, name }],
},
});

it('should not render actions column when disabled', () => {
renderPanel(MOCK_TIME_SERIES_DATA_SINGLEVALUE, {
selection: { enabled: true },
actions: { enabled: false },
});
expect(screen.queryByRole('columnheader', { name: /Actions/i })).not.toBeInTheDocument();
});

it('should render actions column when enabled with displayWithItem', async () => {
renderPanel(MOCK_TIME_SERIES_DATA_SINGLEVALUE, actionsConfig());
expect(await screen.findByRole('columnheader', { name: /Actions/i })).toBeInTheDocument();
});

expect(await screen.findAllByRole('cell')).toHaveLength(4); // 2 lines with 2 column
expect(screen.queryByText('@')).toBeNull(); // No @ as no timestamp
it('should render action buttons for each row', async () => {
renderPanel(MOCK_TIME_SERIES_DATA_SINGLEVALUE, actionsConfig('My Test Action'));
expect(await screen.findAllByRole('button', { name: /My Test Action/i })).toHaveLength(2);
});
});
});
4 changes: 2 additions & 2 deletions timeseriestable/src/TimeSeriesTablePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { DataTable } from './components';
export type TimeSeriesTableProps = PanelProps<TimeSeriesTableOptions, TimeSeriesData>;

export function TimeSeriesTablePanel(props: TimeSeriesTableProps): ReactElement {
const { contentDimensions, queryResults } = props;
const { contentDimensions, queryResults, spec } = props;
const chartsTheme = useChartsTheme();
const contentPadding = chartsTheme.container.padding.default;

Expand All @@ -32,7 +32,7 @@ export function TimeSeriesTablePanel(props: TimeSeriesTableProps): ReactElement
style={{ height: contentDimensions?.height ?? 0 }}
sx={{ padding: `${contentPadding}px`, overflowY: 'scroll' }}
>
<DataTable queryResults={queryResults} />
<DataTable queryResults={queryResults} spec={spec} />
</Box>
);
}
Loading