mirror of
https://github.com/grafana/grafana.git
synced 2026-02-18 18:20:52 -05:00
parent
7dd80e973f
commit
299d550441
7 changed files with 733 additions and 4 deletions
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
|
|
@ -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
|
||||
|
|
|
|||
206
apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-logstable/logs-table.v42.json
vendored
Normal file
206
apps/dashboard/pkg/migration/testdata/dev-dashboards-output/panel-logstable/logs-table.v42.json
vendored
Normal file
File diff suppressed because one or more lines are too long
212
devenv/dev-dashboards/panel-logstable/logs-table.json
Normal file
212
devenv/dev-dashboards/panel-logstable/logs-table.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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'),
|
||||
|
|
|
|||
301
e2e-playwright/panels-suite/logstable.spec.ts
Normal file
301
e2e-playwright/panels-suite/logstable.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 />;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue