mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
Co-authored-by: Shrinath-O2 <214651540+Shrinath-O2@users.noreply.github.com> Co-authored-by: ktx-akshay <akshay.patil@kiara.tech>
This commit is contained in:
co-authored by
Shrinath-O2
ktx-akshay
parent
18961f7c59
commit
20ddfa4e8f
@@ -99,7 +99,8 @@
|
||||
"dashboard-multi-sql.spec.js",
|
||||
"crossLinkMultiStream.spec.js",
|
||||
"dashboards-form-validation.spec.js",
|
||||
"dashboard-refresh-without-cache.spec.js"
|
||||
"dashboard-refresh-without-cache.spec.js",
|
||||
"dashboard-print-layout.spec.js"
|
||||
],
|
||||
"disabled": [
|
||||
{
|
||||
|
||||
@@ -377,6 +377,74 @@ class APICleanup {
|
||||
return { dashboardId, folderId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dashboard with `panelCount` stacked bar panels, each laid out at
|
||||
* full width (w:24, h:8) and offset vertically (y: 0, 9, 18, ...) so the grid
|
||||
* is tall enough to exceed one print page. Used by the dashboard print-layout
|
||||
* specs, which need a deterministic multi-panel grid geometry.
|
||||
* @param {string} title - Dashboard title
|
||||
* @param {number} [panelCount=4] - Number of stacked panels
|
||||
* @param {string} [streamName='e2e_automate'] - Logs stream each panel queries
|
||||
* @returns {Promise<{dashboardId: string, folderId: string}>}
|
||||
*/
|
||||
async createDashboardWithStackedPanels(title, panelCount = 4, streamName = 'e2e_automate') {
|
||||
const panels = [];
|
||||
for (let i = 0; i < panelCount; i++) {
|
||||
panels.push({
|
||||
id: `Panel_ID${Date.now()}_${i}`,
|
||||
type: 'bar',
|
||||
title: `${title} Panel ${i + 1}`,
|
||||
description: '',
|
||||
config: { show_legends: false, decimals: 2, drilldown: [] },
|
||||
queryType: 'sql',
|
||||
queries: [
|
||||
{
|
||||
query: `SELECT histogram(_timestamp) as "x_axis_1", count(_timestamp) as "y_axis_1" FROM "${streamName}" GROUP BY x_axis_1`,
|
||||
vrlFunctionQuery: '',
|
||||
customQuery: false,
|
||||
fields: {
|
||||
stream: streamName,
|
||||
stream_type: 'logs',
|
||||
x: [{ label: 'Timestamp', alias: 'x_axis_1', column: '_timestamp', color: null, aggregationFunction: 'histogram' }],
|
||||
y: [{ label: 'Count', alias: 'y_axis_1', column: '_timestamp', color: '#5960b2', aggregationFunction: 'count' }],
|
||||
z: [],
|
||||
breakdown: [],
|
||||
filter: { filterType: 'group', logicalOperator: 'AND', conditions: [] },
|
||||
},
|
||||
config: { promql_legend: '', layer_type: 'scatter', weight_fixed: 1, limit: 0, min: 0, max: 100 },
|
||||
},
|
||||
],
|
||||
layout: { x: 0, y: i * 9, w: 24, h: 8, i: i + 1 },
|
||||
});
|
||||
}
|
||||
const payload = {
|
||||
version: 5,
|
||||
title,
|
||||
description: '',
|
||||
role: '',
|
||||
owner: this.email,
|
||||
tabs: [{ tabId: 'default', name: 'Default', panels }],
|
||||
variables: {},
|
||||
};
|
||||
const response = await this._fetch(
|
||||
`${this.baseUrl}/api/${this.org}/dashboards?folder=${encodeURIComponent('default')}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': this.authHeader, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
throw new Error(`createDashboardWithStackedPanels: HTTP ${response.status} — ${body}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
const inner = result[`v${result.version}`] || result;
|
||||
const dashboardId = inner.dashboardId || inner.dashboard_id || result.dashboard_id || result.id;
|
||||
testLogger.info('Created dashboard with stacked panels', { dashboardId, panelCount, folderId: 'default' });
|
||||
return { dashboardId, folderId: 'default' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a single dashboard
|
||||
* @param {string} dashboardId - The dashboard ID
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
// Dashboard Print Layout page object — print mode entry/exit + layout state.
|
||||
const { expect } = require("@playwright/test");
|
||||
|
||||
export default class DashboardPrintPage {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
this.printBtn = page.locator('[data-test="dashboard-print-btn"]');
|
||||
this.printModeContainer = page.locator(".print-mode-container");
|
||||
this.backBtn = page.locator('[data-test="dashboard-back-btn"]');
|
||||
this.refreshBtn = page.locator('[data-test="dashboard-refresh-btn"]');
|
||||
this.gridStack = page.locator(".grid-stack");
|
||||
this.panelContainer = page.locator('[data-test="dashboard-panel-container"]');
|
||||
this.tablePanel = page.locator(
|
||||
'[data-test="dashboard-panel-container"][data-panel-type="table"]'
|
||||
);
|
||||
this.printPageStyle = page.locator("#o2-print-page");
|
||||
this.captureReadySpan = page.locator("#dashboardVariablesAndPanelsDataLoaded");
|
||||
this.emptyStateArt = page.locator('[data-test="empty-panel-art"]');
|
||||
this.emptyStateAddPanelBtn = page.locator(
|
||||
'[data-test="dashboard-if-no-panel-add-panel-btn"]'
|
||||
);
|
||||
}
|
||||
|
||||
async navigateToDashboardView(dashboardId, folderId, print = false) {
|
||||
let url = `${process.env.ZO_BASE_URL}/web/dashboards/view?org_identifier=${process.env.ORGNAME}&dashboard=${dashboardId}&folder=${folderId}`;
|
||||
if (print) url += "&print=true";
|
||||
await this.page.goto(url, { waitUntil: "domcontentloaded" });
|
||||
}
|
||||
|
||||
async clickPrintButton() {
|
||||
await this.printBtn.waitFor({ state: "visible", timeout: 30000 });
|
||||
await this.printBtn.click();
|
||||
}
|
||||
|
||||
// Wait for the first panel to mount (grid is laid out with a real width).
|
||||
async waitForPanelMounted(timeout = 30000) {
|
||||
await this.panelContainer.first().waitFor({ state: "visible", timeout });
|
||||
}
|
||||
|
||||
// Multi-panel print layout: injected @page style + non-empty grid height +
|
||||
// every panel force-mounted (no placeholders left).
|
||||
async waitForPrintLayout(timeout = 30000) {
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
this.page.evaluate(() => {
|
||||
const style = document.getElementById("o2-print-page");
|
||||
const grid = document.querySelector(".grid-stack");
|
||||
const placeholders = document.querySelectorAll(
|
||||
'[data-test^="dashboard-panel-placeholder-"]'
|
||||
).length;
|
||||
const height = grid ? (grid.style.height || "").trim() : "";
|
||||
return (
|
||||
!!style &&
|
||||
(style.textContent || "").includes("@page") &&
|
||||
height !== "" &&
|
||||
placeholders === 0
|
||||
);
|
||||
}),
|
||||
{ timeout, intervals: [500, 1000, 2000] }
|
||||
)
|
||||
.toBe(true);
|
||||
}
|
||||
|
||||
async enterPrintMode() {
|
||||
await this.clickPrintButton();
|
||||
await this.waitForPrintLayout();
|
||||
}
|
||||
|
||||
// Single-table print branch: no grid, one full-width table panel, print chrome on.
|
||||
async waitForSingleTablePrintMode(timeout = 30000) {
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
this.page.evaluate(() => {
|
||||
const gridCount = document.querySelectorAll(".grid-stack").length;
|
||||
const tableCount = document.querySelectorAll(
|
||||
'[data-test="dashboard-panel-container"][data-panel-type="table"]'
|
||||
).length;
|
||||
const containerCount = document.querySelectorAll(
|
||||
".print-mode-container"
|
||||
).length;
|
||||
return gridCount === 0 && tableCount === 1 && containerCount === 1;
|
||||
}),
|
||||
{ timeout, intervals: [500, 1000, 2000] }
|
||||
)
|
||||
.toBe(true);
|
||||
}
|
||||
|
||||
// Print layout cleared = px top overrides restored to calc(). GridStack
|
||||
// re-initializes on exit and re-sets a natural (non-empty) inline height, so
|
||||
// the print override is measured via panel tops, not an empty grid height.
|
||||
async waitForPrintModeCleared(timeout = 30000) {
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
this.page.evaluate(() => {
|
||||
const style = document.getElementById("o2-print-page");
|
||||
const containerCount = document.querySelectorAll(
|
||||
".print-mode-container"
|
||||
).length;
|
||||
const overrideCount = Array.from(
|
||||
document.querySelectorAll(".grid-stack-item")
|
||||
).filter((el) => /px$/.test((el.style.top || "").trim())).length;
|
||||
return !style && containerCount === 0 && overrideCount === 0;
|
||||
}),
|
||||
{ timeout, intervals: [500, 1000, 2000] }
|
||||
)
|
||||
.toBe(true);
|
||||
}
|
||||
|
||||
async exitPrintMode() {
|
||||
await this.clickPrintButton();
|
||||
await this.waitForPrintModeCleared();
|
||||
}
|
||||
|
||||
// The capture-readiness span is `display:none`, so it is asserted as attached.
|
||||
async waitForCaptureReady(timeout = 60000) {
|
||||
await expect(this.captureReadySpan).toBeAttached({ timeout });
|
||||
}
|
||||
|
||||
async getGridStackInlineHeight() {
|
||||
return this.page.evaluate(() => {
|
||||
const grid = document.querySelector(".grid-stack");
|
||||
return grid ? (grid.style.height || "").trim() : "";
|
||||
});
|
||||
}
|
||||
|
||||
// preparePrintLayout overrides every panel top with a plain `px` value; before
|
||||
// print mode GridStack positions items with a calc() formula, so px count > 0
|
||||
// proves the reflow ran.
|
||||
async getPanelTopOverrideCount() {
|
||||
return this.page.evaluate(() => {
|
||||
return Array.from(document.querySelectorAll(".grid-stack-item")).filter((el) =>
|
||||
/px$/.test((el.style.top || "").trim())
|
||||
).length;
|
||||
});
|
||||
}
|
||||
|
||||
// ----- assertions -----
|
||||
|
||||
async expectPrintChromeApplied() {
|
||||
await expect(this.printModeContainer).toHaveCount(1);
|
||||
await expect(this.backBtn).toBeHidden();
|
||||
await expect(this.refreshBtn).toBeHidden();
|
||||
}
|
||||
|
||||
async expectPrintChromeCleared() {
|
||||
await expect(this.printModeContainer).toHaveCount(0);
|
||||
await expect(this.backBtn).toBeVisible();
|
||||
await expect(this.refreshBtn).toBeVisible();
|
||||
}
|
||||
|
||||
async expectPrintModeContainerVisible() {
|
||||
await expect(this.printModeContainer).toBeVisible();
|
||||
}
|
||||
|
||||
async expectPrintPageStyleInjected() {
|
||||
await expect(this.printPageStyle).toHaveCount(1);
|
||||
const text = await this.printPageStyle.textContent();
|
||||
expect(text).toContain("@page");
|
||||
}
|
||||
|
||||
async expectPrintPageStyleRemoved() {
|
||||
await expect(this.printPageStyle).toHaveCount(0);
|
||||
}
|
||||
|
||||
async expectGridStackAbsent() {
|
||||
await expect(this.gridStack).toHaveCount(0);
|
||||
}
|
||||
|
||||
async expectSingleTableFullWidth() {
|
||||
await expect(this.tablePanel).toHaveCount(1);
|
||||
await expect(this.tablePanel).toBeVisible();
|
||||
}
|
||||
|
||||
async expectEmptyStateRendered() {
|
||||
await expect(this.emptyStateArt).toBeVisible();
|
||||
}
|
||||
|
||||
async expectEmptyStateAddPanelBtnHidden() {
|
||||
await expect(this.emptyStateAddPanelBtn).toHaveCount(0);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import DashboardLegendsCopy from "./dashboardPages/dashboard-legends-copy";
|
||||
import DashboardFilter from "./dashboardPages/dashboard-filter";
|
||||
import DashboardImport from "./dashboardPages/dashboard-import.js";
|
||||
import DashboardShareExportPage from "./dashboardPages/dashboard-share-export";
|
||||
import DashboardPrintPage from "./dashboardPages/dashboard-print";
|
||||
import DashboardTimeRefresh from "./dashboardPages/dashboard-refresh";
|
||||
import DateTimeHelper from "./dashboardPages/dashboard-time";
|
||||
import DashboardPanelTime from "./dashboardPages/dashboard-panel-time";
|
||||
@@ -156,6 +157,7 @@ class PageManager {
|
||||
this.dashboardFilter = new DashboardFilter(page);
|
||||
this.dashboardImport = new DashboardImport(page);
|
||||
this.dashboardShareExport = new DashboardShareExportPage(page);
|
||||
this.dashboardPrint = new DashboardPrintPage(page);
|
||||
this.dashboardTimeRefresh = new DashboardTimeRefresh(page);
|
||||
this.dateTimeHelper = new DateTimeHelper(page);
|
||||
this.dashboardPanelTime = new DashboardPanelTime(page);
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import * as crypto from "crypto";
|
||||
import { test, expect, navigateToBase } from "../utils/enhanced-baseFixtures.js";
|
||||
import testLogger from "../utils/test-logger.js";
|
||||
import PageManager from "../../pages/page-manager.js";
|
||||
import { ingestion } from "./utils/dashIngestion.js";
|
||||
import { setupTestDashboard, deleteDashboard } from "./utils/dashCreation.js";
|
||||
import {
|
||||
generateDashboardName,
|
||||
setupTablePanel,
|
||||
} from "./utils/configPanelHelpers.js";
|
||||
|
||||
test.describe("Dashboard Print Layout testcases", () => {
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
let pm;
|
||||
let apiDashboard = null;
|
||||
let uiDashboardName = null;
|
||||
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
testLogger.testStart(testInfo.title, testInfo.file);
|
||||
apiDashboard = null;
|
||||
uiDashboardName = null;
|
||||
await navigateToBase(page);
|
||||
pm = new PageManager(page);
|
||||
await ingestion(page);
|
||||
testLogger.info("Test setup completed");
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
if (apiDashboard) {
|
||||
await pm.apiCleanup
|
||||
.deleteDashboard(apiDashboard.dashboardId, apiDashboard.folderId)
|
||||
.catch(() => {});
|
||||
apiDashboard = null;
|
||||
}
|
||||
if (uiDashboardName) {
|
||||
try {
|
||||
await page.goto(
|
||||
`${process.env["ZO_BASE_URL"]}/web/dashboards?org_identifier=${process.env["ORGNAME"]}`,
|
||||
{ waitUntil: "domcontentloaded" }
|
||||
);
|
||||
await deleteDashboard(page, uiDashboardName);
|
||||
} catch (e) {
|
||||
testLogger.warn("UI dashboard cleanup failed (non-fatal)", { error: e.message });
|
||||
}
|
||||
uiDashboardName = null;
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
"should enter print mode via the toolbar button and render the print layout",
|
||||
{ tag: ["@dashboard-print-layout", "@all", "@P0"] },
|
||||
async ({ page }) => {
|
||||
const title = `PrintLayout_${Date.now()}_${crypto.randomBytes(3).toString("hex")}`;
|
||||
const { dashboardId, folderId } = await pm.apiCleanup.createDashboardWithStackedPanels(title, 4);
|
||||
apiDashboard = { dashboardId, folderId };
|
||||
|
||||
await pm.dashboardPrint.navigateToDashboardView(dashboardId, folderId);
|
||||
await pm.dashboardPrint.waitForPanelMounted();
|
||||
|
||||
await pm.dashboardPrint.enterPrintMode();
|
||||
|
||||
await expect(page).toHaveURL(/print=true/);
|
||||
await pm.dashboardPrint.expectPrintChromeApplied();
|
||||
await pm.dashboardPrint.expectPrintPageStyleInjected();
|
||||
|
||||
const gridHeight = await pm.dashboardPrint.getGridStackInlineHeight();
|
||||
expect(gridHeight).not.toBe("");
|
||||
const overrideCount = await pm.dashboardPrint.getPanelTopOverrideCount();
|
||||
expect(overrideCount).toBeGreaterThan(0);
|
||||
|
||||
testLogger.info("Print layout rendered", { gridHeight, overrideCount });
|
||||
}
|
||||
);
|
||||
|
||||
test(
|
||||
"should exit print mode and clear the print layout",
|
||||
{ tag: ["@dashboard-print-layout", "@all", "@P0"] },
|
||||
async ({ page }) => {
|
||||
const title = `PrintLayoutExit_${Date.now()}_${crypto.randomBytes(3).toString("hex")}`;
|
||||
const { dashboardId, folderId } = await pm.apiCleanup.createDashboardWithStackedPanels(title, 4);
|
||||
apiDashboard = { dashboardId, folderId };
|
||||
|
||||
await pm.dashboardPrint.navigateToDashboardView(dashboardId, folderId);
|
||||
await pm.dashboardPrint.waitForPanelMounted();
|
||||
await pm.dashboardPrint.enterPrintMode();
|
||||
|
||||
await pm.dashboardPrint.exitPrintMode();
|
||||
|
||||
await expect(page).not.toHaveURL(/print=true/);
|
||||
await pm.dashboardPrint.expectPrintPageStyleRemoved();
|
||||
await pm.dashboardPrint.expectPrintChromeCleared();
|
||||
|
||||
const overrideCount = await pm.dashboardPrint.getPanelTopOverrideCount();
|
||||
expect(overrideCount).toBe(0);
|
||||
|
||||
testLogger.info("Print layout cleared on exit");
|
||||
}
|
||||
);
|
||||
|
||||
test(
|
||||
"should enter print mode via the ?print=true URL and persist across reload",
|
||||
{ tag: ["@dashboard-print-layout", "@all", "@P1"] },
|
||||
async ({ page }) => {
|
||||
const title = `PrintLayoutUrl_${Date.now()}_${crypto.randomBytes(3).toString("hex")}`;
|
||||
const { dashboardId, folderId } = await pm.apiCleanup.createDashboardWithStackedPanels(title, 4);
|
||||
apiDashboard = { dashboardId, folderId };
|
||||
|
||||
await pm.dashboardPrint.navigateToDashboardView(dashboardId, folderId, true);
|
||||
await pm.dashboardPrint.waitForPrintLayout();
|
||||
await pm.dashboardPrint.expectPrintChromeApplied();
|
||||
await pm.dashboardPrint.expectPrintPageStyleInjected();
|
||||
|
||||
await page.reload();
|
||||
await pm.dashboardPrint.waitForPrintLayout();
|
||||
await pm.dashboardPrint.expectPrintChromeApplied();
|
||||
|
||||
testLogger.info("Print mode persisted across reload");
|
||||
}
|
||||
);
|
||||
|
||||
test(
|
||||
"should render a single-table-panel dashboard full-width in print mode",
|
||||
{ tag: ["@dashboard-print-layout", "@all", "@P1"] },
|
||||
async ({ page }) => {
|
||||
const dashboardName = generateDashboardName();
|
||||
uiDashboardName = dashboardName;
|
||||
|
||||
await setupTablePanel(page, pm, dashboardName);
|
||||
await pm.dashboardPanelActions.savePanel();
|
||||
|
||||
await pm.dashboardPrint.clickPrintButton();
|
||||
await pm.dashboardPrint.waitForSingleTablePrintMode();
|
||||
|
||||
await pm.dashboardPrint.expectGridStackAbsent();
|
||||
await pm.dashboardPrint.expectSingleTableFullWidth();
|
||||
await pm.dashboardPrint.expectPrintModeContainerVisible();
|
||||
|
||||
testLogger.info("Single-table full-width print branch verified");
|
||||
}
|
||||
);
|
||||
|
||||
test(
|
||||
"should show the print-capture readiness flag once panels finish loading",
|
||||
{ tag: ["@dashboard-print-layout", "@all", "@P1"] },
|
||||
async ({ page }) => {
|
||||
const title = `PrintLayoutReady_${Date.now()}_${crypto.randomBytes(3).toString("hex")}`;
|
||||
const { dashboardId, folderId } = await pm.apiCleanup.createDashboardWithStackedPanels(title, 4);
|
||||
apiDashboard = { dashboardId, folderId };
|
||||
|
||||
await pm.dashboardPrint.navigateToDashboardView(dashboardId, folderId);
|
||||
await pm.dashboardPrint.waitForPanelMounted();
|
||||
|
||||
await pm.dashboardPrint.enterPrintMode();
|
||||
await pm.dashboardPrint.waitForCaptureReady();
|
||||
|
||||
testLogger.info("Print-capture readiness flag appeared");
|
||||
}
|
||||
);
|
||||
|
||||
test(
|
||||
"should inject no print page style for an empty dashboard in print mode",
|
||||
{ tag: ["@dashboard-print-layout", "@all", "@P2"] },
|
||||
async ({ page }) => {
|
||||
const dashboardName = generateDashboardName();
|
||||
uiDashboardName = dashboardName;
|
||||
|
||||
await setupTestDashboard(page, pm, dashboardName);
|
||||
await pm.dashboardPrint.clickPrintButton();
|
||||
await pm.dashboardPrint.expectPrintModeContainerVisible();
|
||||
|
||||
await pm.dashboardPrint.expectPrintPageStyleRemoved();
|
||||
await pm.dashboardPrint.expectGridStackAbsent();
|
||||
await pm.dashboardPrint.expectEmptyStateRendered();
|
||||
await pm.dashboardPrint.expectEmptyStateAddPanelBtnHidden();
|
||||
|
||||
testLogger.info("Empty dashboard in print mode verified");
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -4,6 +4,7 @@
|
||||
* and (where indicated) opens the config sidebar, ready for assertions.
|
||||
*/
|
||||
|
||||
import * as crypto from "crypto";
|
||||
import { setupTestDashboard, deleteDashboard } from "./dashCreation.js";
|
||||
import { ingestionForMaps } from "./dashIngestion.js";
|
||||
import testLogger from '../../utils/test-logger.js';
|
||||
@@ -81,7 +82,7 @@ export async function discardAndCleanupTestDashboard(page, dashboardName) {
|
||||
}
|
||||
|
||||
export const generateDashboardName = () =>
|
||||
"Dashboard_" + Math.random().toString(36).slice(2, 11) + "_" + Date.now();
|
||||
"Dashboard_" + crypto.randomBytes(6).toString("hex") + "_" + Date.now();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Base panel builder — all other helpers delegate here
|
||||
|
||||
Reference in New Issue
Block a user