hyperdx/packages/app/tests/e2e/features/dashboard.spec.ts
Drew Davis 61db3e8b43
refactor: Create TileAlertEditor component (#2085)
## Summary

This PR extracts a TileAlertEditor component for future re-use in the Raw-SQL Alert UI. The UI has been updated to make the alert section collapsible and co-locate the "Remove Alert" button within the alert section. The collapsibility will be more important in the Raw SQL case, since the Raw SQL Editor is already pretty vertically tall.

### Screenshots or video

https://github.com/user-attachments/assets/4e595fc6-06f0-4ccd-ab1f-08dcb9895c89

### How to test locally or on Vercel

This must be tested locally, since alerts are not supported in local mode.

### References



- Linear Issue: Related to HDX-1605
- Related PRs:
2026-04-09 17:21:34 +00:00

972 lines
35 KiB
TypeScript

import { DisplayType } from '@hyperdx/common-utils/dist/types';
import { AlertsPage } from '../page-objects/AlertsPage';
import { DashboardPage } from '../page-objects/DashboardPage';
import { DashboardsListPage } from '../page-objects/DashboardsListPage';
import { getApiUrl, getSources } from '../utils/api-helpers';
import { expect, test } from '../utils/base-test';
import {
DEFAULT_LOGS_SOURCE_NAME,
DEFAULT_METRICS_SOURCE_NAME,
} from '../utils/constants';
test.describe('Dashboard', { tag: ['@dashboard'] }, () => {
let dashboardPage: DashboardPage;
let dashboardsListPage: DashboardsListPage;
test.beforeEach(async ({ page }) => {
dashboardPage = new DashboardPage(page);
dashboardsListPage = new DashboardsListPage(page);
await dashboardPage.goto();
});
test(
'should display the "temporary dashboard" banner until the dashboard is created',
{ tag: '@full-stack' },
async () => {
await test.step('Verify that banner is initially displayed', async () => {
await expect(dashboardPage.temporaryDashboardBanner).toBeVisible();
});
await test.step('Add a tile, verify that banner is still displayed', async () => {
await dashboardPage.addTileWithConfig('Test tile');
await expect(dashboardPage.temporaryDashboardBanner).toBeVisible();
});
await test.step('Create the dashboard, verify the banner is no longer displayed', async () => {
await dashboardPage.createNewDashboard();
await expect(dashboardPage.temporaryDashboardBanner).toBeHidden();
});
},
);
test('should persist dashboard across page reloads', {}, async () => {
const uniqueDashboardName = `Test Dashboard ${Date.now()}`;
await test.step('Create and name a new dashboard', async () => {
// Create dashboard using page object
await expect(dashboardPage.createButton).toBeVisible();
await dashboardPage.createNewDashboard();
// Edit dashboard name using page object method
await dashboardPage.editDashboardName(uniqueDashboardName);
});
await test.step('Add a tile to the dashboard', async () => {
// Open add tile modal
await expect(dashboardPage.addButton).toBeVisible();
await dashboardPage.addTile();
// Create chart using chart editor component
await expect(dashboardPage.chartEditor.nameInput).toBeVisible();
await dashboardPage.chartEditor.createBasicChart(
'Persistence Test Chart',
);
// Wait for tile to appear first (wrapper element)
const dashboardTiles = dashboardPage.getTiles();
await expect(dashboardTiles).toHaveCount(1, { timeout: 10000 });
// Then verify chart rendered inside (recharts can take time to initialize)
const chartContainers = dashboardPage.getChartContainers();
await expect(chartContainers).toHaveCount(1, { timeout: 10000 });
});
let dashboardUrl: string;
await test.step('Save dashboard URL', async () => {
dashboardUrl = dashboardPage.page.url();
console.log(`Dashboard URL: ${dashboardUrl}`);
});
await test.step('Navigate away from dashboard', async () => {
await dashboardPage.page.goto('/search');
await expect(dashboardPage.page).toHaveURL(/.*\/search/);
});
await test.step('Return to dashboard and verify persistence', async () => {
await dashboardPage.page.goto(dashboardUrl);
// Wait for dashboard to load by checking for tiles first
const dashboardTiles = dashboardPage.getTiles();
await expect(dashboardTiles).toHaveCount(1);
// Verify dashboard name persisted (displayed as h3 title)
const dashboardNameHeading =
dashboardPage.getDashboardHeading(uniqueDashboardName);
await expect(dashboardNameHeading).toBeVisible({ timeout: 5000 });
// Verify chart still shows
const chartContainers = dashboardPage.getChartContainers();
await expect(chartContainers.first()).toBeVisible();
});
await test.step('Verify dashboard appears in dashboards list', async () => {
await dashboardsListPage.goto();
// Look for our dashboard in the list
const dashboardLink = dashboardPage.page.locator(
`text="${uniqueDashboardName}"`,
);
await expect(dashboardLink).toBeVisible({ timeout: 10000 });
// Click on it and verify it loads
await dashboardPage.goToDashboardByName(uniqueDashboardName);
// Verify we're on the right dashboard
const dashboardTiles = dashboardPage.getTiles();
await expect(dashboardTiles).toHaveCount(1);
});
});
test('Comprehensive dashboard workflow - create, add tiles, configure, and test', async () => {
test.setTimeout(60000);
await test.step('Create new dashboard', async () => {
await expect(dashboardPage.createButton).toBeVisible();
await dashboardPage.createNewDashboard();
});
await test.step('Add first tile to dashboard', async () => {
await expect(dashboardPage.addButton).toBeVisible();
await dashboardPage.addTile();
// Create basic chart
await expect(dashboardPage.chartEditor.nameInput).toBeVisible();
await dashboardPage.chartEditor.createBasicChart('Test Chart');
// Verify tile was added (chart content depends on data availability)
const dashboardTiles = dashboardPage.getTiles();
await expect(dashboardTiles).toHaveCount(1, { timeout: 10000 });
});
await test.step('Add second tile with Demo Metrics', async () => {
await expect(dashboardPage.addButton).toBeVisible();
await dashboardPage.addTile();
// Select source and create chart with specific metric
await expect(dashboardPage.chartEditor.source).toBeVisible();
await dashboardPage.chartEditor.createChartWithMetric(
'K8s Pod CPU Chart',
DEFAULT_METRICS_SOURCE_NAME,
'k8s.pod.cpu.utilization',
'k8s.pod.cpu.utilization:::::::gauge',
);
});
await test.step('Verify dashboard tiles and interactions', async () => {
const dashboardTiles = dashboardPage.getTiles();
await expect(dashboardTiles).toHaveCount(2, { timeout: 10000 });
// Hover over first tile to reveal action buttons
await dashboardPage.hoverOverTile(0);
// Verify all action buttons are visible
const buttons: Array<'edit' | 'duplicate' | 'delete' | 'alerts'> = [
'edit',
'duplicate',
'delete',
'alerts',
];
for (const button of buttons) {
const buttonLocator = dashboardPage.getTileButton(button);
await expect(buttonLocator).toBeVisible();
}
});
await test.step('Test duplicate tile', async () => {
const dashboardTiles = dashboardPage.getTiles();
const tileCount = await dashboardTiles.count();
// Duplicate the first tile
await dashboardPage.duplicateTile(0);
// Verify tile count increased
const dashboardTilesNow = dashboardPage.getTiles();
await expect(dashboardTilesNow).toHaveCount(tileCount + 1);
});
await test.step('Update time range to Last 12 hours', async () => {
await expect(dashboardPage.timePicker.input).toBeVisible();
await dashboardPage.timePicker.selectRelativeTime('Last 12 hours');
});
await test.step('Test Live view functionality', async () => {
// Toggle live mode on
await dashboardPage.toggleLiveMode();
// Turn off live mode to prevent continuous updates
const liveButtonVisible = await dashboardPage.page
.locator('button:has-text("Live")')
.isVisible({ timeout: 1000 })
.catch(() => false);
if (liveButtonVisible) {
await dashboardPage.toggleLiveMode();
}
});
await test.step('Test global dashboard filters', async () => {
await expect(dashboardPage.filterInput).toBeVisible();
await dashboardPage.setGlobalFilter('ServiceName:accounting');
});
await test.step('Delete the tile and confirm deletion', async () => {
const dashboardTiles = dashboardPage.getTiles();
const tileCountBefore = await dashboardTiles.count();
// Delete first tile
await dashboardPage.deleteTile(0);
// Verify tile count decreased (use toHaveCount for auto-waiting)
await expect(dashboardTiles).toHaveCount(tileCountBefore - 1);
});
});
test(
'should update charts when granularity is changed',
{ tag: '@dashboard' },
async () => {
await test.step('Create dashboard with a time series chart', async () => {
await dashboardPage.createNewDashboard();
// Add a time series tile
await dashboardPage.addTile();
await dashboardPage.chartEditor.createBasicChart(
'Time Series Test Chart',
);
// Wait for chart to render
const chartContainers = dashboardPage.getChartContainers();
await expect(chartContainers).toHaveCount(1, { timeout: 10000 });
});
await test.step('Change granularity and verify UI updates', async () => {
// Find granularity dropdown (typically labeled "Granularity" or shows current value like "Auto")
const granularityDropdown = dashboardPage.granularityPicker;
await expect(granularityDropdown).toBeVisible();
// Get current value
const currentValue = await granularityDropdown.inputValue();
// Change to a different granularity (e.g., "1m")
await dashboardPage.changeGranularity('1 Minute Granularity');
// Verify the value changed
const newValue = granularityDropdown;
await expect(newValue).not.toHaveValue(currentValue);
// Verify chart is still visible (validates that the change worked)
const chartContainers = dashboardPage.getChartContainers();
await expect(chartContainers).toHaveCount(1, { timeout: 10000 });
});
},
);
test('should warn when closing tile editor with unsaved changes', async () => {
await dashboardPage.openNewTileEditor();
await dashboardPage.chartEditor.setChartName('My Unsaved Chart');
await dashboardPage.page.keyboard.press('Escape');
await expect(dashboardPage.unsavedChangesConfirmModal).toBeAttached({
timeout: 5000,
});
await dashboardPage.unsavedChangesConfirmCancelButton.click();
await expect(dashboardPage.unsavedChangesConfirmModal).toBeHidden();
await expect(dashboardPage.chartEditor.nameInput).toHaveValue(
'My Unsaved Chart',
);
await dashboardPage.page.keyboard.press('Escape');
await expect(dashboardPage.unsavedChangesConfirmModal).toBeAttached({
timeout: 5000,
});
await dashboardPage.unsavedChangesConfirmDiscardButton.click();
await expect(dashboardPage.chartEditor.nameInput).toBeHidden({
timeout: 5000,
});
});
test('should add and remove alert on Number type chart', async () => {
test.setTimeout(60000);
const ts = Date.now();
const tileName = `E2E Alert Number Chart ${ts}`;
const webhookUrl = `https://example.com/number-chart-${ts}`;
const alertsPage = new AlertsPage(dashboardPage.page);
await test.step('Create new dashboard', async () => {
await expect(dashboardPage.createButton).toBeVisible();
await dashboardPage.createNewDashboard();
});
await test.step('create a Number type chart with alert', async () => {
await expect(dashboardPage.addButton).toBeVisible();
await dashboardPage.addTile();
await expect(dashboardPage.chartEditor.source).toBeVisible();
await dashboardPage.chartEditor.waitForDataToLoad();
await dashboardPage.chartEditor.setChartType(DisplayType.Number);
await dashboardPage.chartEditor.selectSource(DEFAULT_METRICS_SOURCE_NAME);
await dashboardPage.chartEditor.selectMetric(
'k8s.pod.cpu.utilization',
'k8s.pod.cpu.utilization:::::::gauge',
);
await dashboardPage.chartEditor.setChartName(tileName);
await expect(dashboardPage.chartEditor.alertButton).toHaveText(
'Add Alert',
);
await dashboardPage.chartEditor.clickAddAlert();
await dashboardPage.chartEditor.addNewWebhookButton.click();
await dashboardPage.chartEditor.webhookAlertModal.addWebhook(
'Generic',
`Test Webhook ${ts}`,
webhookUrl,
);
await dashboardPage.saveTile();
});
await test.step('Verify dashboard tiles and interactions', async () => {
const dashboardTiles = dashboardPage.getTiles();
await expect(dashboardTiles).toHaveCount(1, { timeout: 10000 });
// Hover over first tile to reveal action buttons
await dashboardPage.hoverOverTile(0);
// Verify all action buttons are visible
const buttons: Array<'edit' | 'duplicate' | 'delete' | 'alerts'> = [
'edit',
'duplicate',
'delete',
'alerts',
];
for (const button of buttons) {
const buttonLocator = dashboardPage.getTileButton(button);
await expect(buttonLocator).toBeVisible();
}
});
let dashboardUrl: string;
await test.step('Save dashboard URL', async () => {
dashboardUrl = dashboardPage.page.url();
console.log(`Dashboard URL: ${dashboardUrl}`);
});
await test.step('Navigate to alerts page', async () => {
await alertsPage.goto();
});
await test.step('Verify alerts page loads with content', async () => {
await expect(alertsPage.pageContainer).toBeVisible();
await expect(
alertsPage.pageContainer
.getByRole('link')
.filter({ hasText: tileName }),
).toBeVisible({ timeout: 10000 });
});
await test.step('Navigate back to dashboard page', async () => {
await dashboardPage.page.goto(dashboardUrl);
});
await test.step('edit the tile to remove the alert', async () => {
// Hover over first tile to reveal edit button
await dashboardPage.editTile(0);
await dashboardPage.chartEditor.clickRemoveAlert();
await dashboardPage.saveTile();
});
await test.step('Navigate to alerts page', async () => {
await alertsPage.goto();
});
await test.step('Verify alerts page loads with no alerts', async () => {
await expect(alertsPage.pageContainer).toBeVisible();
await expect(
alertsPage.pageContainer
.getByRole('link')
.filter({ hasText: tileName }),
).toBeHidden({ timeout: 10000 });
});
});
test('should close tile editor without confirm when there are no unsaved changes', async () => {
await dashboardPage.openNewTileEditor();
await dashboardPage.page.keyboard.press('Escape');
await expect(dashboardPage.chartEditor.nameInput).toBeHidden({
timeout: 5000,
});
await expect(dashboardPage.unsavedChangesConfirmModal).toBeHidden();
});
test('should create and populate filters', {}, async () => {
test.setTimeout(30000);
await test.step('Create new dashboard', async () => {
await expect(dashboardPage.createButton).toBeVisible();
await dashboardPage.createNewDashboard();
});
await test.step('Create a table tile to filter', async () => {
await dashboardPage.addTile();
await dashboardPage.chartEditor.createTable({
chartName: 'Test Table',
sourceName: DEFAULT_LOGS_SOURCE_NAME,
groupBy: 'ServiceName',
});
const accountCell = dashboardPage.page.getByTitle('accounting', {
exact: true,
});
const adCell = dashboardPage.page.getByTitle('ad', { exact: true });
await expect(accountCell).toBeVisible();
await expect(adCell).toBeVisible();
});
await test.step('Add ServiceName filter to dashboard', async () => {
await dashboardPage.openEditFiltersModal();
await expect(dashboardPage.emptyFiltersList).toBeVisible();
await dashboardPage.addFilterToDashboard(
'Service',
DEFAULT_LOGS_SOURCE_NAME,
'ServiceName',
);
await expect(dashboardPage.getFilterItemByName('Service')).toBeVisible();
await dashboardPage.closeFiltersModal();
});
await test.step('Add MetricName filter to dashboard', async () => {
await dashboardPage.openEditFiltersModal();
await expect(dashboardPage.filtersList).toBeVisible();
await dashboardPage.addFilterToDashboard(
'Metric',
DEFAULT_METRICS_SOURCE_NAME,
'MetricName',
'gauge',
);
await expect(dashboardPage.getFilterItemByName('Metric')).toBeVisible();
await dashboardPage.closeFiltersModal();
});
await test.step('Verify tiles are filtered', async () => {
// Select 'accounting' in Service filter
await dashboardPage.clickFilterOption('Service', 'accounting');
const accountCell = dashboardPage.page.getByTitle('accounting', {
exact: true,
});
await expect(accountCell).toBeVisible();
// 'ad' ServiceName row should be filtered out
const adCell = dashboardPage.page.getByTitle('ad', { exact: true });
await expect(adCell).toHaveCount(0);
});
await test.step('Verify metric filter is populated', async () => {
await dashboardPage.clickFilterOption(
'Metric',
'container.cpu.utilization',
);
});
await test.step('Delete a filter and verify it is removed', async () => {
await dashboardPage.openEditFiltersModal();
await dashboardPage.deleteFilterFromDashboard('Metric');
// Service filter should still be visible
await expect(dashboardPage.getFilterItemByName('Service')).toBeVisible();
// Metric filter should be gone
await expect(dashboardPage.getFilterItemByName('Metric')).toHaveCount(0);
});
});
test('should save and restore query and filter values', {}, async () => {
const testQuery = 'SeverityText:error';
let dashboardUrl: string;
await test.step('Create dashboard with chart', async () => {
await dashboardPage.createNewDashboard();
// Add a tile so dashboard is saveable
await dashboardPage.addTile();
await dashboardPage.chartEditor.createBasicChart('Test Chart');
const chartContainers = dashboardPage.getChartContainers();
await expect(chartContainers).toHaveCount(1, { timeout: 10000 });
// Save dashboard URL for later
dashboardUrl = dashboardPage.page.url();
});
await test.step('Add ServiceName filter to dashboard', async () => {
await dashboardPage.openEditFiltersModal();
await expect(dashboardPage.emptyFiltersList).toBeVisible();
await dashboardPage.addFilterToDashboard(
'Service',
DEFAULT_LOGS_SOURCE_NAME,
'ServiceName',
);
await expect(dashboardPage.getFilterItemByName('Service')).toBeVisible();
await dashboardPage.closeFiltersModal();
});
await test.step('Select a filter value', async () => {
await dashboardPage.clickFilterOption('Service', 'accounting');
// Verify the filter is applied
const filterSelect = dashboardPage.getFilterSelectByName('Service');
await expect(
filterSelect.locator('..').getByText('accounting'),
).toBeVisible();
});
await test.step('Enter query in search bar', async () => {
const searchInput = dashboardPage.searchInput;
await expect(searchInput).toBeVisible();
await searchInput.fill(testQuery);
});
await test.step('Click save query button', async () => {
await dashboardPage.saveQueryAndFiltersAsDefault();
// Wait for success notification
const notification = dashboardPage.page.locator(
'text=/Filter query and dropdown values/i',
);
await expect(notification).toBeVisible({ timeout: 5000 });
});
await test.step('Navigate away from dashboard', async () => {
await dashboardPage.page.goto('/search');
await expect(dashboardPage.page).toHaveURL(/.*\/search/);
});
await test.step('Return to dashboard and verify query and filters are restored', async () => {
await dashboardPage.page.goto(dashboardUrl);
// Wait for dashboard controls to load
await expect(
dashboardPage.page.getByTestId('dashboard-page'),
).toBeVisible({ timeout: 10000 });
await expect(dashboardPage.searchInput).toBeVisible({ timeout: 10000 });
// Verify saved query is restored in search input
const searchInput = dashboardPage.searchInput;
await expect(searchInput).toHaveValue(testQuery);
});
await test.step('Clear URL params and verify query persists', async () => {
// Extract dashboard ID and navigate without query params
const dashboardId = dashboardUrl.split('/').pop()?.split('?')[0];
await dashboardPage.page.goto(`/dashboards/${dashboardId}`);
// Verify saved query still loads
const searchInput = dashboardPage.searchInput;
await expect(searchInput).toHaveValue(testQuery);
// Verify the saved filter value is populated
const filterSelect = dashboardPage.getFilterSelectByName('Service');
await expect(
filterSelect.locator('..').getByText('accounting'),
).toBeVisible();
});
});
test(
'should handle URL query params overriding saved query',
{},
async () => {
const savedQuery = 'level:error';
const urlQuery = 'status:active';
let dashboardId: string;
await test.step('Create dashboard and save query', async () => {
await dashboardPage.createNewDashboard();
// Add a tile
await dashboardPage.addTile();
await dashboardPage.chartEditor.createBasicChart('Test Chart');
// Enter and save query
const searchInput = dashboardPage.searchInput;
await searchInput.fill(savedQuery);
await dashboardPage.saveQueryAndFiltersAsDefault();
// Wait for save confirmation
await dashboardPage.page.waitForTimeout(1000);
// Extract dashboard ID
const url = dashboardPage.page.url();
dashboardId = url.split('/').pop()?.split('?')[0] || '';
});
await test.step('Navigate with URL query param', async () => {
// Navigate to dashboard with URL query param
await dashboardPage.page.goto(
`/dashboards/${dashboardId}?where=${encodeURIComponent(urlQuery)}`,
);
// Wait for dashboard controls to load
await expect(
dashboardPage.page.getByTestId('dashboard-page'),
).toBeVisible({ timeout: 10000 });
await expect(dashboardPage.searchInput).toBeVisible({ timeout: 10000 });
// Verify URL query takes precedence over saved query
const searchInput = dashboardPage.searchInput;
await expect(searchInput).toHaveValue(urlQuery);
await expect(searchInput).not.toHaveValue(savedQuery);
});
await test.step('Navigate without URL params to verify saved query', async () => {
await dashboardPage.page.goto(`/dashboards/${dashboardId}`);
// Verify saved query is restored when no URL params
const searchInput = dashboardPage.searchInput;
await expect(searchInput).toHaveValue(savedQuery);
});
},
);
test.describe('Raw SQL Dashboard Tiles', () => {
const LINE_SQL = `SELECT toStartOfInterval(TimestampTime, INTERVAL {intervalSeconds:Int64} SECOND) AS ts, count() AS count FROM default.e2e_otel_logs WHERE TimestampTime >= fromUnixTimestamp64Milli({startDateMilliseconds:Int64}) AND TimestampTime < fromUnixTimestamp64Milli({endDateMilliseconds:Int64}) GROUP BY ts ORDER BY ts ASC`;
const TABLE_SQL = `SELECT ServiceName, count() AS count FROM default.e2e_otel_logs WHERE TimestampTime >= fromUnixTimestamp64Milli({startDateMilliseconds:Int64}) AND TimestampTime <= fromUnixTimestamp64Milli({endDateMilliseconds:Int64}) GROUP BY ServiceName LIMIT 200`;
const NUMBER_SQL = `SELECT 1234 FROM default.e2e_otel_logs WHERE TimestampTime >= fromUnixTimestamp64Milli({startDateMilliseconds:Int64}) AND TimestampTime < fromUnixTimestamp64Milli({endDateMilliseconds:Int64})`;
const PIE_SQL = `SELECT ServiceName, count() FROM default.e2e_otel_logs WHERE TimestampTime >= fromUnixTimestamp64Milli({startDateMilliseconds:Int64}) AND TimestampTime < fromUnixTimestamp64Milli({endDateMilliseconds:Int64}) GROUP BY ServiceName`;
test.beforeEach(async () => {
await dashboardPage.createNewDashboard();
});
test('Line chart renders with Raw SQL query', async () => {
test.setTimeout(60000);
const ts = Date.now();
const chartName = `E2E Raw SQL Line ${ts}`;
await test.step('Open the tile editor', async () => {
await dashboardPage.addTile();
await expect(dashboardPage.chartEditor.nameInput).toBeVisible();
await dashboardPage.chartEditor.waitForDataToLoad();
});
await test.step('Configure Raw SQL Line chart', async () => {
await dashboardPage.chartEditor.setChartType(DisplayType.Line);
await dashboardPage.chartEditor.setChartName(chartName);
await dashboardPage.chartEditor.switchToSqlMode();
await dashboardPage.chartEditor.typeSqlQuery(LINE_SQL);
});
await test.step('Run query and save', async () => {
await dashboardPage.chartEditor.runQuery();
await dashboardPage.saveTile();
});
await test.step('Verify the line chart renders on the dashboard', async () => {
const tile = dashboardPage.getTiles().filter({ hasText: chartName });
await expect(
tile.locator('.recharts-responsive-container'),
).toBeVisible({ timeout: 15000 });
});
});
test('Table chart renders with Raw SQL query', async () => {
test.setTimeout(60000);
const ts = Date.now();
const chartName = `E2E Raw SQL Table ${ts}`;
await test.step('Open the tile editor', async () => {
await dashboardPage.addTile();
await expect(dashboardPage.chartEditor.nameInput).toBeVisible();
await dashboardPage.chartEditor.waitForDataToLoad();
});
await test.step('Configure Raw SQL Table chart', async () => {
await dashboardPage.chartEditor.setChartType(DisplayType.Table);
await dashboardPage.chartEditor.setChartName(chartName);
await dashboardPage.chartEditor.switchToSqlMode();
await dashboardPage.chartEditor.typeSqlQuery(TABLE_SQL);
});
await test.step('Run query and save', async () => {
await dashboardPage.chartEditor.runQuery(false);
await dashboardPage.saveTile();
});
await test.step('Verify the table chart renders on the dashboard', async () => {
const tile = dashboardPage.getTiles().filter({ hasText: chartName });
await expect(tile.locator('table')).toBeVisible({ timeout: 15000 });
});
});
test('Number chart renders with Raw SQL query', async () => {
test.setTimeout(60000);
const ts = Date.now();
const chartName = `E2E Raw SQL Number ${ts}`;
await test.step('Open the tile editor', async () => {
await dashboardPage.addTile();
await expect(dashboardPage.chartEditor.nameInput).toBeVisible();
await dashboardPage.chartEditor.waitForDataToLoad();
});
await test.step('Configure Raw SQL Number chart', async () => {
await dashboardPage.chartEditor.setChartType(DisplayType.Number);
await dashboardPage.chartEditor.setChartName(chartName);
await dashboardPage.chartEditor.switchToSqlMode();
await dashboardPage.chartEditor.typeSqlQuery(NUMBER_SQL);
});
await test.step('Run query and save', async () => {
await dashboardPage.chartEditor.runQuery(false);
await dashboardPage.saveTile();
});
await test.step('Verify the number chart renders on the dashboard', async () => {
const tile = dashboardPage.getTiles().filter({ hasText: chartName });
await expect(tile).toContainText('1234');
});
});
test('Pie chart renders with Raw SQL query', async () => {
test.setTimeout(60000);
const ts = Date.now();
const chartName = `E2E Raw SQL Pie ${ts}`;
await test.step('Open the tile editor', async () => {
await dashboardPage.addTile();
await expect(dashboardPage.chartEditor.nameInput).toBeVisible();
await dashboardPage.chartEditor.waitForDataToLoad();
});
await test.step('Configure Raw SQL Pie chart', async () => {
await dashboardPage.chartEditor.setChartType(DisplayType.Pie);
await dashboardPage.chartEditor.setChartName(chartName);
await dashboardPage.chartEditor.switchToSqlMode();
await dashboardPage.chartEditor.typeSqlQuery(PIE_SQL);
});
await test.step('Run query and save', async () => {
await dashboardPage.chartEditor.runQuery();
await dashboardPage.saveTile();
});
await test.step('Verify the pie chart renders on the dashboard', async () => {
const tile = dashboardPage.getTiles().filter({ hasText: chartName });
await expect(
tile.locator('.recharts-responsive-container'),
).toBeVisible({ timeout: 15000 });
});
});
});
test(
'should deselect and hide the Custom aggregation function when switching to a metric source',
{ tag: '@full-stack' },
async () => {
await test.step('Navigate to dashboard and open new tile editor', async () => {
await dashboardPage.openNewTileEditor();
});
await test.step('Select the "Custom" aggregation function', async () => {
await dashboardPage.chartEditor.selectAggFn('Custom');
const selectedAggFn =
await dashboardPage.chartEditor.getSelectedAggFn();
expect(selectedAggFn).toBe('Custom');
});
await test.step('Switch the source to a metric source', async () => {
await dashboardPage.chartEditor.selectSource(
DEFAULT_METRICS_SOURCE_NAME,
);
});
await test.step('Verify the aggregation function was automatically changed away from "Custom"', async () => {
const selectedAggFn =
await dashboardPage.chartEditor.getSelectedAggFn();
expect(selectedAggFn).toBe('Count of Events');
});
await test.step('Verify the "Custom" option is NOT available in the aggregation dropdown', async () => {
const isCustomAvailable =
await dashboardPage.chartEditor.isAggFnOptionAvailable('Custom');
expect(isCustomAvailable).toBe(false);
});
},
);
test('should show error message and allow editing when tile source is missing', async ({
page,
}) => {
const apiUrl = getApiUrl();
const DELETABLE_SOURCE_NAME = `E2E Deletable Source ${Date.now()}`;
// Get an existing log source to copy its connection
const logSources = await getSources(page, 'log');
const { connection, from } = logSources[0];
// Create a dedicated source for this test via the API
const createResponse = await page.request.post(`${apiUrl}/sources`, {
data: {
kind: 'log',
name: DELETABLE_SOURCE_NAME,
connection,
from,
timestampValueExpression: 'TimestampTime',
defaultTableSelectExpression:
'Timestamp, ServiceName, SeverityText, Body',
serviceNameExpression: 'ServiceName',
implicitColumnExpression: 'Body',
},
});
expect(createResponse.ok()).toBeTruthy();
const createdSource = await createResponse.json();
await test.step('Create dashboard with tile using the deletable source', async () => {
await dashboardPage.goto();
await dashboardPage.createNewDashboard();
await dashboardPage.addTile();
await dashboardPage.chartEditor.waitForDataToLoad();
await dashboardPage.chartEditor.setChartName('Missing Source Tile');
await dashboardPage.chartEditor.selectSource(DELETABLE_SOURCE_NAME);
await dashboardPage.chartEditor.runQuery();
await dashboardPage.saveTile();
await expect(dashboardPage.getTiles()).toHaveCount(1, {
timeout: 10000,
});
});
await test.step('Delete the source and reload the dashboard', async () => {
const dashboardUrl = page.url();
const deleteResponse = await page.request.delete(
`${apiUrl}/sources/${createdSource.id}`,
);
expect(deleteResponse.ok()).toBeTruthy();
await page.goto(dashboardUrl);
await expect(dashboardPage.getTiles()).toHaveCount(1, {
timeout: 10000,
});
});
await test.step('Verify tile shows error message for missing source', async () => {
const tile = dashboardPage.getTiles().first();
await expect(tile).toContainText(
'The data source for this tile no longer exists',
);
});
await test.step('Verify tile can be edited when source is missing', async () => {
await dashboardPage.hoverOverTile(0);
const editButton = dashboardPage.getTileButton('edit');
await expect(editButton).toBeVisible();
await editButton.click();
await expect(dashboardPage.chartEditor.nameInput).toBeVisible({
timeout: 5000,
});
});
});
test(
'should clear saved query when WHERE input is cleared and saved',
{},
async () => {
const testQuery = 'level:warn';
let dashboardUrl: string;
await test.step('Create dashboard with chart', async () => {
await dashboardPage.createNewDashboard();
// Add a tile so dashboard is saveable
await dashboardPage.addTile();
await dashboardPage.chartEditor.createBasicChart('Test Chart');
const chartContainers = dashboardPage.getChartContainers();
await expect(chartContainers).toHaveCount(1, { timeout: 10000 });
// Save dashboard URL for later
dashboardUrl = dashboardPage.page.url();
});
await test.step('Enter and save query', async () => {
const searchInput = dashboardPage.searchInput;
await expect(searchInput).toBeVisible();
await searchInput.fill(testQuery);
await dashboardPage.saveQueryAndFiltersAsDefault();
// Wait for success notification
const notification = dashboardPage.page.locator(
'text=/Filter query and dropdown values/i',
);
await expect(notification).toBeVisible({ timeout: 5000 });
});
await test.step('Navigate away and verify query persists', async () => {
await dashboardPage.page.goto('/search');
await dashboardPage.page.goto(dashboardUrl);
const searchInput = dashboardPage.searchInput;
await expect(searchInput).toHaveValue(testQuery);
});
await test.step('Clear the query and save', async () => {
const searchInput = dashboardPage.searchInput;
await searchInput.clear();
await dashboardPage.saveQueryAndFiltersAsDefault();
// Wait for success notification
const notification = dashboardPage.page.locator(
'text=/Filter query and dropdown values/i',
);
await expect(notification).toBeVisible({ timeout: 5000 });
});
await test.step('Navigate away and verify query is cleared', async () => {
await dashboardPage.page.goto('/search');
// Extract dashboard ID and navigate back
const dashboardId = dashboardUrl.split('/').pop()?.split('?')[0];
await dashboardPage.page.goto(`/dashboards/${dashboardId}`);
// Wait for dashboard to load
const chartContainers = dashboardPage.getChartContainers();
await expect(chartContainers).toHaveCount(1, { timeout: 10000 });
// Verify search input is empty (saved query was removed)
const searchInput = dashboardPage.searchInput;
await expect(searchInput).toHaveValue('');
});
},
);
});