Files
openobserve/tests/ui-testing/pages/dashboardPages/dashboard-panel-edit.js
T

363 lines
13 KiB
JavaScript

//dashboard panel edit page
// Methods: Duplicate panel, Edit panel, Delete panel, Download json, Download csv, Move to another tab, Fullscreen panel, Refresh panel, Edit layout, Go to logs, Create alert from panel
import { expect } from '@playwright/test';
export default class DashboardPanel {
constructor(page) {
this.page = page;
this.editArrow = page.locator(
'[data-test="dashboard-edit-panel-area-test-dropdown"]'
);
this.duplicate = page.locator('[data-test="dashboard-duplicate-panel"]');
this.dashboardsMenuItem = page.locator(
'[data-test="menu-link-/dashboards-item"]'
);
this.applyBtn = page.locator('[data-test="dashboard-apply"]');
this.namepanel = page.locator('[data-test="dashboard-panel-name"]');
this.saveBtn = page.locator('[data-test="dashboard-panel-save"]');
this.edit = page.locator('[data-test="dashboard-edit-panel"]');
this.delete = page.locator('[data-test="dashboard-delete-panel"]');
this.downloadJson = page.locator(
'[data-test="dashboard-panel-download-as-json-btn"]'
);
this.downloadCsv = page.locator(
'[data-test="dashboard-panel-download-as-csv-btn"]'
);
this.moveTab = page.locator(
'[data-test="dashboard-move-to-another-panel"]'
);
this.deleteConfirmBtn = page.locator(
'[data-test="confirm-dialog"] [data-test="o-dialog-primary-btn"]'
);
this.fullscreen = page.locator(
'[data-test="dashboard-panel-fullscreen-btn"]'
);
this.refreshBtn = page.locator(
'[data-test="dashboard-panel-refresh-panel-btn"]'
);
this.editLayout = page.locator('[data-test="dashboard-edit-layout"]');
this.layoutEditBtn = page.locator('[data-test="dashboard-edit-btn"]');
this.layoutEditSaveBtn = page.locator('[data-test="dashboard-edit-save-btn"]');
this.panelHeight = page.locator(
'[data-test="panel-layout-settings-height-input-field"]'
);
this.saveLayout = page.locator(
'[data-test="panel-layout-settings-drawer"] [data-test="o-dialog-primary-btn"]'
);
this.cancelLayout = page.locator(
'[data-test="panel-layout-settings-cancel"]'
);
this.goToLogs = page.locator('[data-test="dashboard-move-to-logs-module"]');
this.queryInspector = page.locator(
'[data-test="dashboard-query-inspector-panel"]'
);
// In-editor "data view" query inspector (opened from the panel data view,
// distinct from the panel-dropdown Inspector above).
this.dataViewQueryInspectorBtn = page.locator(
'[data-test="dashboard-panel-data-view-query-inspector-btn"]'
);
this.inspectorQueryEditor = page.locator('.inspector-query-editor');
this.queryInspectorCloseBtn = page.locator(
'[data-test="query-inspector-dialog"] [data-test="o-dialog-close-btn"]'
);
// VERIFIED: Create Alert from Panel Menu (PanelContainer.vue:289)
this.createAlertFromPanel = page.locator('[data-test="dashboard-create-alert-from-panel"]');
// VERIFIED: Alert Context Menu selectors (AlertContextMenu.vue)
this.alertContextMenu = page.locator('[data-test="alert-context-menu"]');
this.alertContextMenuAbove = page.locator('[data-test="alert-context-menu-above"]');
this.alertContextMenuBelow = page.locator('[data-test="alert-context-menu-below"]');
// VERIFIED: Chart renderer (ChartRenderer.vue:19)
this.chartRendererCanvas = page.locator('[data-test="chart-renderer"]');
// VERIFIED: no-data empty-state overlay (PanelSchemaRenderer.vue) — same
// locator dashboard-panel-actions.js exposes as noDataElement/getNoDataLocator().
this.noDataElement = page.locator('[data-test="no-data"]');
}
// Click the in-editor "data view" query inspector button
async clickDataViewQueryInspector() {
await this.dataViewQueryInspectorBtn.click();
}
// Wait for the query inspector to be open AND its query rows rendered. The
// dialog becomes visible before the rows are populated, so waiting on the
// dialog alone lets an assertion run against an empty body.
async waitForQueryInspector(index = 0) {
await this.page
.locator('[data-test="query-inspector-dialog"]')
.first()
.waitFor({ state: "visible", timeout: 15000 });
await this.getExecutedQuery(index).waitFor({
state: "visible",
timeout: 15000,
});
}
// Query inspector editor content locator (chain .filter()/.last() as needed)
getInspectorQueryEditor() {
return this.inspectorQueryEditor;
}
// Executed-query row in the query inspector (by index)
getExecutedQuery(index = 0) {
return this.page.locator(`[data-test="query-inspector-executed-query-${index}"]`);
}
// Original-query row in the query inspector (by index)
getOriginalQuery(index = 0) {
return this.page.locator(`[data-test="query-inspector-original-query-${index}"]`);
}
// Duplicate panel
async duplicatePanel(panelName) {
await this.page
.locator(`[data-test="dashboard-edit-panel-${panelName}-dropdown"]`)
.click();
await this.duplicate.waitFor({ state: "visible" });
await this.duplicate.click();
}
//Edit panel
async editPanel(panelName) {
await this.page
.locator(`[data-test="dashboard-edit-panel-${panelName}-dropdown"]`)
.click();
await this.edit.waitFor({ state: "visible" });
await this.edit.click();
}
//Delete panel
async deletePanel(panelName) {
await this.page
.locator(`[data-test="dashboard-edit-panel-${panelName}-dropdown"]`)
.click();
await this.delete.waitFor({ state: "visible" });
await this.delete.click();
await this.deleteConfirmBtn.waitFor({ state: "visible" });
await this.deleteConfirmBtn.click();
}
//Download json
async downloadJsonPanel(panelName) {
await this.page
.locator(`[data-test="dashboard-edit-panel-${panelName}-dropdown"]`)
.click();
await this.downloadJson.waitFor({ state: "visible" });
await this.downloadJson.click();
}
//Download csv
async downloadCsvPanel(panelName) {
await this.page
.locator(`[data-test="dashboard-edit-panel-${panelName}-dropdown"]`)
.click();
await this.downloadCsv.waitFor({ state: "visible" });
await this.downloadCsv.click();
}
//move to another tab
async movePanelToAnotherTab() {
await this.editArrow.click();
await this.moveTab.waitFor({ state: "visible" });
await this.moveTab.click();
}
//fullscreen panel
async fullscreenPanel() {
await this.page.locator('[data-test="dashboard-panel-container"]').hover();
await this.fullscreen.waitFor({ state: "visible" });
await this.fullscreen.click();
await this.page.waitForSelector(
'[data-test="dashboard-viewpanel-close-btn"]',
{ state: "visible" }
);
await this.page
.locator('[data-test="dashboard-viewpanel-close-btn"]')
.click();
}
//refresh panel
async refreshPanel(panelName) {
await this.page
.locator(`[data-test="dashboard-edit-panel-${panelName}-dropdown"]`)
.click();
await this.refreshBtn.waitFor({ state: "visible" });
await this.refreshBtn.click();
}
//edit layout
async editLayoutPanel(panelName, height) {
await this.layoutEditBtn.waitFor({ state: "visible" });
await this.layoutEditBtn.click();
const dropdownBtn = this.page.locator(`[data-test="dashboard-edit-panel-${panelName}-dropdown"]`);
await dropdownBtn.waitFor({ state: "visible" });
await dropdownBtn.click();
await this.editLayout.waitFor({ state: "visible" });
await this.editLayout.click();
await this.panelHeight.waitFor({ state: "visible" });
await this.panelHeight.click();
await this.panelHeight.fill(height);
await this.saveLayout.click();
await expect(this.layoutEditSaveBtn).toBeEnabled();
await this.layoutEditSaveBtn.click();
await this.layoutEditSaveBtn.waitFor({ state: "hidden" });
}
//Edit Panel: Go to logs
async goToLogsPanel(panelName) {
await this.page
.locator(`[data-test="dashboard-edit-panel-${panelName}-dropdown"]`)
.click();
await this.goToLogs.waitFor({ state: "visible" });
await this.goToLogs.click();
}
// Create Alert from panel dropdown menu
async createAlertFromPanelMenu(panelName) {
await this.page
.locator(`[data-test="dashboard-edit-panel-${panelName}-dropdown"]`)
.click();
await this.createAlertFromPanel.waitFor({ state: "visible" });
await this.createAlertFromPanel.click();
}
// Right-click on chart center to open alert context menu
// Retries because ECharts needs time after data load to register contextmenu handlers
async rightClickChartForAlert(maxRetries = 3) {
const { chartCanvas, position } = await this.getChartRightClickTarget();
for (let attempt = 0; attempt < maxRetries; attempt++) {
await chartCanvas.click({ button: "right", position });
const menuVisible = await this.alertContextMenu
.waitFor({ state: "visible", timeout: 3000 })
.then(() => true)
.catch(() => false);
if (menuVisible) return;
}
// Final attempt without catching - let it throw if still not visible
await chartCanvas.click({ button: "right", position });
}
// Single right-click with no menu retry, for series types that must not open the menu.
async rightClickChart() {
const { chartCanvas, position } = await this.getChartRightClickTarget();
await chartCanvas.click({ button: "right", position });
}
async getChartRightClickTarget() {
const chartCanvas = this.chartRendererCanvas.first();
await chartCanvas.waitFor({ state: "visible", timeout: 15000 });
// After savePanel() a same-sized no-data overlay intercepts clicks until the re-query renders.
await this.noDataElement
.first()
.waitFor({ state: "hidden", timeout: 20000 })
.catch(() => {});
const box = await chartCanvas.boundingBox();
const position = box
? { x: Math.floor(box.width / 2), y: Math.floor(box.height / 2) }
: { x: 200, y: 100 };
return { chartCanvas, position };
}
// Verify alert context menu is visible
async expectAlertContextMenuVisible() {
await expect(this.alertContextMenu).toBeVisible({ timeout: 10000 });
}
// Verify alert context menu is hidden
async expectAlertContextMenuHidden() {
await expect(this.alertContextMenu).not.toBeVisible({ timeout: 5000 });
}
// Select "above threshold" from alert context menu
// dispatchEvent is used because AlertContextMenu uses <teleport to="body"> which
// places the menu under overlays that intercept pointer events from click({ force: true }).
// dispatchEvent('click') dispatches directly on the element through Playwright's locator path.
async selectAlertAboveThreshold() {
await this.alertContextMenuAbove.waitFor({ state: "visible" });
await this.alertContextMenuAbove.dispatchEvent("click");
}
// Select "below threshold" from alert context menu
// dispatchEvent is used because AlertContextMenu uses <teleport to="body"> which
// places the menu under overlays that intercept pointer events from click({ force: true }).
// dispatchEvent('click') dispatches directly on the element through Playwright's locator path.
async selectAlertBelowThreshold() {
await this.alertContextMenuBelow.waitFor({ state: "visible" });
await this.alertContextMenuBelow.dispatchEvent("click");
}
//open Query inspector
async openQueryInspector(panelName) {
const dropdownBtn = this.page.locator(`[data-test="dashboard-edit-panel-${panelName}-dropdown"]`);
// Wait for dropdown button to be visible with retry
await dropdownBtn.waitFor({ state: "visible", timeout: 15000 });
await dropdownBtn.click();
// Wait for query inspector option with retry
try {
await this.queryInspector.waitFor({ state: "visible", timeout: 10000 });
} catch (e) {
// Menu may not have opened, retry click — click outside to dismiss any open dropdown
await this.page.locator('body').click({ position: { x: 10, y: 10 } }).catch(() => {});
await this.queryInspector.waitFor({ state: "hidden", timeout: 5000 }).catch(() => {});
await dropdownBtn.click();
await this.queryInspector.waitFor({ state: "visible", timeout: 10000 });
}
await this.queryInspector.click();
}
// Return the panel-name element locator (edit panel header).
getPanelNameLocator() {
return this.namepanel;
}
// Return the dashboard panel container locator (hover target for panel menu).
getPanelContainer() {
return this.page.locator('[data-test="dashboard-panel-container"]');
}
// Create Alert menu item (dropdown item) for disabled/state assertions.
getCreateAlertFromPanelItem() {
return this.createAlertFromPanel;
}
// The disabled-reason description rendered under the item (ODropdownItem).
getCreateAlertFromPanelReason() {
return this.page.locator(
'[data-test="dashboard-create-alert-from-panel-reason"]'
);
}
// Alert context menu options (shown on chart right-click).
getAlertContextMenuAbove() {
return this.alertContextMenuAbove;
}
getAlertContextMenuBelow() {
return this.alertContextMenuBelow;
}
// Click on an empty page corner to dismiss the alert context menu.
async clickOutsideContextMenu() {
await this.page.locator("body").click({ position: { x: 10, y: 10 } });
}
// Return the inspector query-editor entry that contains the given SQL text.
getInspectorQueryByText(sql) {
return this.inspectorQueryEditor.filter({ hasText: sql }).first();
}
// Close the query inspector dialog.
async closeQueryInspector() {
await this.queryInspectorCloseBtn.click();
}
}