Logs Table: E2E (#117715)

* test(e2e): logs table panel e2e init
This commit is contained in:
Galen Kistler 2026-02-12 13:40:01 -06:00 committed by GitHub
parent 7dd80e973f
commit 299d550441
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 733 additions and 4 deletions

3
.github/CODEOWNERS vendored
View file

@ -87,6 +87,7 @@
/apps/quotas/ @grafana/grafana-search-and-storage
/apps/live/ @grafana/grafana-app-platform-squad
/apps/dashboard/ @grafana/grafana-app-platform-squad @grafana/dashboards-squad
/apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-logstable/logs-table.v42.json @grafana/observability-logs
/apps/folder/ @grafana/grafana-app-platform-squad
/apps/playlist/ @grafana/grafana-app-platform-squad
/apps/plugins/ @grafana/plugins-platform-backend
@ -231,6 +232,7 @@
/devenv/datasources.yaml @grafana/grafana-backend-group
/devenv/datasources_docker.yaml @grafana/grafana-backend-group
/devenv/dev-dashboards-without-uid/ @grafana/dashboards-squad
/devenv/dev-dashboards/panel-logstable/logs-table.json @grafana/observability-logs
/devenv/scopes/ @grafana/grafana-operator-experience-squad
/devenv/dev-dashboards/annotations @grafana/dataviz-squad
@ -487,6 +489,7 @@ i18next.config.ts @grafana/grafana-frontend-platform
/e2e-playwright/panels-suite/panelEdit_base.spec.ts @grafana/dashboards-squad
/e2e-playwright/panels-suite/panelEdit_queries.spec.ts @grafana/dashboards-squad
/e2e-playwright/panels-suite/panelEdit_transforms.spec.ts @grafana/datapro
/e2e-playwright/panels-suite/logstable.spec.ts @grafana/observability-logs
/e2e-playwright/plugin-e2e/ @grafana/oss-big-tent @grafana/partner-datasources
/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend
/e2e-playwright/smoke-tests-suite/ @grafana/grafana-frontend-platform

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -69,6 +69,7 @@
"live-flakey-refresh": (import '../dev-dashboards/live/live-flakey-refresh.json'),
"live-publish": (import '../dev-dashboards/live/live-publish.json'),
"live-streams": (import '../dev-dashboards/live/live-streams.json'),
"logs-table": (import '../dev-dashboards/panel-logstable/logs-table.json'),
"loki_fakedata": (import '../dev-dashboards/datasource-loki/loki_fakedata.json'),
"loki_query_splitting": (import '../dev-dashboards/datasource-loki/loki_query_splitting.json'),
"migrations": (import '../dev-dashboards/migrations/migrations.json'),

View file

@ -0,0 +1,301 @@
import { test, expect } from '@grafana/plugin-e2e';
const DASHBOARD_UID = 'adhjhtt';
test.use({ viewport: { width: 2000, height: 1080 } });
test.describe('Panels test: LogsTable', { tag: ['@panels', '@logstable'] }, () => {
test.describe('Defaults', () => {
test('Should render logs table panel', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '2' }),
});
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Default Logs Table Panel'))
).toBeVisible();
// View log line button should be defined by default
await expect(page.getByLabel('View log line').first(), 'View log line button defined by default').toBeVisible();
// timestamp and log body headers should be visible
await expect(
page.getByRole('columnheader', { name: 'timestamp' }),
'table timestamp column is visible'
).toBeVisible();
await expect(page.getByRole('columnheader', { name: 'body' }), 'Table "body" column is visible').toBeVisible();
await expect(
page.getByRole('columnheader', { name: 'bytes' }),
'Table "timestamp" column is not present'
).toHaveCount(0);
// timestamp and body columns are selected
await expect(
page.getByRole('checkbox', { name: 'timestamp' }),
'Field selector "timestamp" field is checked'
).toBeChecked();
await expect(
page.getByRole('checkbox', { name: 'body' }),
'Field selector "body" field is checked'
).toBeChecked();
await expect(
page.getByRole('checkbox', { name: 'bytes' }),
'Field selector "bytes" field is visible'
).toBeVisible();
// bytes field is not selected
await expect(
page.getByRole('checkbox', { name: 'bytes' }),
'Field selector "bytes" field is not checked'
).not.toBeChecked({ timeout: 400 });
// Select bytes field
await page.getByText('bytes', { exact: true }).click();
await expect(
page.getByRole('checkbox', { name: 'bytes' }),
'Field selector "bytes" field is checked'
).toBeChecked();
await expect(page.getByRole('columnheader', { name: 'bytes' }), 'Table "bytes" column is visible').toBeVisible();
// Reset
await page.getByRole('button', { name: 'Reset' }).click();
await expect(
page.getByRole('columnheader', { name: 'bytes' }),
'Table "bytes" column is no longer visible after reset'
).toHaveCount(0);
await expect(
page.getByRole('checkbox', { name: 'bytes' }),
'Field selector "bytes" field is not checked after reset'
).not.toBeChecked({ timeout: 400 });
// Search input is visible
await expect(
page.getByRole('textbox', { name: 'Search fields by name' }),
'Field selector search input is visible'
).toBeVisible();
await page.getByRole('textbox', { name: 'Search fields by name' }).fill('btyes'); // Fuzzy search matches "bytes" against "btyes" (Levenshtein distance < 2)
await expect(
page.getByRole('checkbox', { name: 'bytes' }),
'Field selector "bytes" field is not checked'
).not.toBeChecked({ timeout: 400 });
await expect(
page.getByRole('columnheader', { name: 'bytes' }),
'Table "bytes" field is not in the table'
).toHaveCount(0);
await page.getByText('bytes', { exact: true }).click();
await expect(
page.getByRole('checkbox', { name: 'bytes' }),
'Field selector "bytes" field is checked'
).toBeChecked();
await expect(
page.getByRole('columnheader', { name: 'bytes' }),
'Table "bytes" field is in the table'
).toBeVisible();
// Clear search input
await page.getByRole('button', { name: 'Clear' }).click();
// Reset
await page.getByRole('button', { name: 'Reset' }).click();
await expect(
page.getByRole('columnheader', { name: 'bytes' }),
'Table "bytes" field is not in the table'
).toHaveCount(0);
await expect(
page.getByRole('checkbox', { name: 'bytes' }),
'Field selector "bytes" field is checked'
).not.toBeChecked({ timeout: 400 });
// Selected fields collapse
await expect(page.getByText('Selected fields'), 'Field selector title is visible').toBeVisible();
await page.getByRole('button', { name: 'Collapse sidebar' }).click();
await expect(page.getByText('Selected fields'), 'Field selector title is no longer visible').not.toBeVisible();
});
test('Show inspect button', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '2' }),
});
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Default Logs Table Panel'))
).toBeVisible();
await expect(
page.getByRole('columnheader', { name: 'timestamp' }),
'table timestamp column is visible'
).toBeVisible();
await page.getByRole('columnheader', { name: 'timestamp' }).click();
// Hide body
// @todo locator.click does not work to deselect these draggable checkboxes, just causes infinite click loop without updating state, when clicking on the label or the checkbox. Is there a less hacky way to deselect?
await expect
.poll(() => page.getByRole('checkbox', { name: 'body' }).isChecked(), 'Field selector body field is checked')
.toEqual(true);
page.getByLabel(/body/).first().dispatchEvent('click');
await expect
.poll(
() => page.getByRole('checkbox', { name: 'body' }).isChecked(),
'Field selector body field is no longer checked'
)
.toEqual(false);
// Click inspect on 9th row
await page.getByRole('gridcell').getByLabel('View log line').nth(9).click();
// Assert drawer header is visible
await expect(
page.getByRole('heading', { name: 'Inspect value' }),
'Inspect drawer title is visible'
).toBeVisible();
// Assert the inspect drawer shows the correct log line body
await expect(
page.getByLabel('Drawer title Inspect value').locator('.view-lines'),
'Drawer contains correct log line'
).toContainText(
`level=info ts=2026-02-06T18:42:42.083508023Z caller=flush.go:253 msg="completing block" userid=29 blockID=73zco`
);
});
});
test.describe('Options', () => {
test('Inspect button', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '2' }),
});
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Default Logs Table Panel'))
).toBeVisible();
const optionWrapper = page.getByLabel('Logs Table Show inspect button field property editor');
const option = optionWrapper.getByLabel(/Show inspect button/);
const inspectLogLineButton = page.getByLabel('View log line');
await expect(option, 'Inspect button panel option is in the document').toHaveCount(1);
await expect(option, 'Inspect button panel option is initially checked').toBeChecked();
await expect(inspectLogLineButton.nth(0), 'Inspect button is visible in the logs table viz').toBeVisible();
await optionWrapper.click();
await expect(option, 'Inspect button panel option is no longer checked').not.toBeChecked({ timeout: 400 });
await expect(inspectLogLineButton, 'Inspect button is no longer in the logs table viz').toHaveCount(0);
});
test('Copy log line button', async ({ page, gotoDashboardPage, selectors, context }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '2' }),
});
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Default Logs Table Panel'))
).toBeVisible();
const optionWrapper = page.getByLabel('Logs Table Show copy log link button field property editor');
const option = optionWrapper.getByLabel(/Show copy log link button/);
const copyLogLineButton = page.getByLabel('Copy link to log line');
await expect(option, 'Show log line panel option is in the document').toHaveCount(1);
await expect(option, 'Show log line panel option is not checked').not.toBeChecked({ timeout: 400 });
await expect(copyLogLineButton, 'Show log line button is not in the table viz').toHaveCount(0);
await optionWrapper.click();
await expect(option, 'Show log line panel option is now checked').toBeChecked();
await expect(copyLogLineButton.nth(0), 'Show log line button is visible in the table viz').toBeVisible();
await copyLogLineButton.nth(9).click();
// Copy to clipboard doesn't work in CI since it is run in an unsecure context that isn't on localhost, hardcoding the panel state for now instead of removing the test.
await page.goto(
'/d/adhjhtt/logstable-kitchen-sink?orgId=1&panelState=%7B"logs":%7B"id":"1770403366020954082_3665f3ae"%7D%7D&editPanel=2'
);
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Default Logs Table Panel'))
).toBeVisible();
const selectedRow = page.locator('[role="row"][aria-selected="true"]');
await expect(selectedRow, 'Row is selected').toBeVisible();
});
test('Show controls', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '2' }),
});
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Default Logs Table Panel'))
).toBeVisible();
const optionWrapper = page.getByLabel('Logs Table Show controls field property editor');
const optionLabel = optionWrapper.getByText(/Show controls/);
const option = optionWrapper.getByLabel(/Show controls/);
const controlsExpandButtonDefault = page.getByLabel('Collapse', { exact: true });
const controlsExpandButtonChecked = page.getByLabel('Expand', { exact: true });
const controlsSortByButtonNewest = page.getByLabel('Sorted by newest logs first - Click to show oldest first', {
exact: true,
});
const controlsSortByButtonOldest = page.getByLabel('Sorted by oldest logs first - Click to show newest first', {
exact: true,
});
// Assert default option state
await expect(option, 'Show controls panel option is in the document').toHaveCount(1);
await expect(option, 'Show controls panel option is not checked by default').not.toBeChecked({ timeout: 400 });
await expect(controlsExpandButtonDefault, 'Logs control collapse button is not in the controls').toHaveCount(0);
await expect(controlsSortByButtonNewest, 'Logs controls sort order button is not in controls').toHaveCount(0);
// Toggle option on
await optionLabel.click();
await expect(option, 'Show controls panel option is now checked').toBeChecked();
await expect(controlsExpandButtonDefault, 'Logs control collapse button is now in the controls').toHaveCount(1);
await expect(controlsSortByButtonNewest, 'Logs control sort order button is now in the controls').toHaveCount(1);
// Sort by should update state (but won't change logs in test data source)
await controlsSortByButtonNewest.click();
await expect(controlsSortByButtonOldest, 'Logs control sort order button state is toggled').toHaveCount(1);
// Collapse expanded options sidebar
await controlsExpandButtonDefault.click();
await expect(controlsExpandButtonChecked, 'Logs control state is now collapsed').toHaveCount(1);
});
});
test.describe('No data', () => {
test('Invalid logs frame', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '3' }),
});
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Not logs panel'))
).toBeVisible();
await expect(
page.getByTestId(selectors.components.Panels.Panel.PanelDataErrorMessage),
'Panel error message is visible'
).toBeVisible();
await expect(
page.getByTestId(selectors.components.Panels.Panel.PanelDataErrorMessage),
'Panel error message is "missing a string field"'
).toContainText('Data is missing a string field');
});
test('Empty logs frame', async ({ page, gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '4' }),
});
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('No data panel'))
).toBeVisible();
await expect(
page.getByTestId(selectors.components.Panels.Panel.PanelDataErrorMessage),
'Panel error message is visible'
).toBeVisible();
await expect(
page.getByTestId(selectors.components.Panels.Panel.PanelDataErrorMessage),
'Panel error message is "no data"'
).toContainText('No data');
});
});
});

View file

@ -27,6 +27,9 @@
"e2e:enterprise:dev": "./e2e/start-and-run-suite enterprise dev",
"e2e:enterprise:debug": "./e2e/start-and-run-suite enterprise debug",
"e2e:playwright": "NODE_OPTIONS='-C @grafana-app/source' yarn playwright test --grep-invert @cloud-plugins",
"e2e:pw": "NODE_OPTIONS='-C @grafana-app/source' yarn playwright test",
"e2e:playwright:debug": "yarn e2e:pw --debug -x --trace on",
"e2e:playwright:10x": "yarn e2e:pw --repeat-each=10",
"e2e:playwright:cloud-plugins": "NODE_OPTIONS='-C @grafana-app/source' yarn playwright test --grep @cloud-plugins",
"e2e:playwright:storybook": "NODE_OPTIONS='-C @grafana-app/source' yarn playwright test -c playwright.storybook.config.ts",
"e2e:acceptance": "NODE_OPTIONS='-C @grafana-app/source' yarn playwright test --grep @acceptance",

View file

@ -145,11 +145,14 @@ export const LogsTable = ({
return data;
}, [organizedFrame, data, frameIndex]);
const noSeries = data.series.length === 0;
const noValues = data.series[frameIndex]?.fields?.[0]?.values?.length === 0;
// Logs frame be null for non logs frames
const noLogsFrame = !logsFrame;
// Show no data state if query returns nothing
if (
(data.series.length === 0 || data.series[frameIndex].fields[0].values.length === 0) &&
data.state === LoadingState.Done
) {
if ((noSeries || noValues || noLogsFrame) && data.state === LoadingState.Done) {
return <PanelDataErrorView fieldConfig={fieldConfig} panelId={id} data={data} needsStringField />;
}