mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 00:08:55 +00:00
272 lines
11 KiB
JavaScript
272 lines
11 KiB
JavaScript
// Dashboard Multi-SQL Query Page Object
|
|
// Encapsulates selectors and actions for multi-query tab management
|
|
|
|
const testLogger = require("../../playwright-tests/utils/test-logger.js");
|
|
|
|
export default class DashboardMultiSQL {
|
|
constructor(page) {
|
|
this.page = page;
|
|
|
|
// Query tab selectors
|
|
this.addQueryBtn = page.locator('[data-test="dashboard-panel-query-tab-add"]');
|
|
this.warningBanner = page.locator(".dashboard-multi-query-warning");
|
|
this.trellisDropdown = page.locator('[data-test="dashboard-trellis-chart"]');
|
|
this.queryInspectorBtn = page.locator('[data-test="dashboard-panel-data-view-query-inspector-btn"]');
|
|
// QueryInspector is an <ODialog data-test="query-inspector">; ODialog
|
|
// forwards its data-test to the dialog panel and exposes the close
|
|
// button as data-test="o-dialog-close-btn" (see ODialog.vue:397).
|
|
this.queryInspectorClose = page.locator(
|
|
'[data-test="query-inspector"] [data-test="o-dialog-close-btn"]',
|
|
);
|
|
this.customQueryBtn = page.locator('[data-test="dashboard-custom-query-type"]');
|
|
this.queryEditor = page.locator('[data-test="dashboard-panel-query-editor"]');
|
|
this.discardBtn = page.locator('[data-test="dashboard-panel-discard"]');
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Dynamic locators (index-based)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Locator for the query tab at given index */
|
|
queryTab(index) {
|
|
return this.page.locator(`[data-test="dashboard-panel-query-tab-${index}"]`);
|
|
}
|
|
|
|
/**
|
|
* Locator for the query tab name wrapper div at given index.
|
|
* Uses the wrapper div with data-test attribute that holds the dblclick handler.
|
|
*/
|
|
queryTabLabel(index) {
|
|
return this.page.locator(`[data-test="dashboard-panel-query-tab-name-${index}"]`);
|
|
}
|
|
|
|
/** Locator for the rename input at given index.
|
|
* The OInput wrapper carries the `...-input-${index}` data-test; the actual
|
|
* <input> element is the inner `...-field` node (OInput convention), which is
|
|
* what fill()/press() must target. */
|
|
queryTabNameInput(index) {
|
|
return this.page.locator(`[data-test="dashboard-panel-query-tab-name-input-${index}-field"]`);
|
|
}
|
|
|
|
/** Locator for the remove (close) icon at given index */
|
|
queryTabRemove(index) {
|
|
return this.page.locator(`[data-test="dashboard-panel-query-tab-remove-${index}"]`);
|
|
}
|
|
|
|
/** Locator for the visibility (eye) icon at given index */
|
|
queryTabVisibility(index) {
|
|
return this.page.locator(`[data-test="dashboard-panel-query-tab-visibility-${index}"]`);
|
|
}
|
|
|
|
/** Locator for the config panel query tab at given index */
|
|
configQueryTab(index) {
|
|
return this.page.locator(`[data-test="dashboard-config-query-tab-${index}"]`);
|
|
}
|
|
|
|
/** Locator for the config legend (per-query label) input at given index.
|
|
* The OInput wrapper carries the `dashboard-config-legend-${index}` data-test;
|
|
* the editable <input> is the inner `...-field` node (OInput convention). */
|
|
configLegend(index) {
|
|
return this.page.locator(`[data-test="dashboard-config-legend-${index}-field"]`);
|
|
}
|
|
|
|
/**
|
|
* Locator for the trellis-chart OSelect when it is disabled.
|
|
* OSelect renders an inner trigger button that carries the same
|
|
* `data-test` as the wrapper, plus the standard `disabled` attribute
|
|
* when the prop is set. The `.q-field--disabled` class no longer exists
|
|
* after the v1 UX revamp.
|
|
*/
|
|
trellisDisabled() {
|
|
return this.page.locator(
|
|
'[data-test="dashboard-trellis-chart"][disabled], [data-test="dashboard-trellis-chart"] [disabled]',
|
|
);
|
|
}
|
|
|
|
/** Locator for the query name label shown inside the query inspector at given index */
|
|
queryInspectorQueryName(index) {
|
|
return this.page.locator(`[data-test="query-inspector-query-name-${index}"]`);
|
|
}
|
|
|
|
/**
|
|
* Locator for the X-axis alias inconsistency warning icon.
|
|
* Rendered by PanelErrorButtons.vue inside the panel editor header (reactive,
|
|
* no Apply needed — updates as fields are added/removed).
|
|
*/
|
|
get xAliasInconsistencyWarning() {
|
|
return this.page.locator('[data-test="panel-x-alias-inconsistency-warning"]');
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Actions
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Click the "+" button to add a new query tab and wait for it to appear.
|
|
* @param {number} expectedTabIndex - Index of the new tab (1 for Q2, 2 for Q3)
|
|
*/
|
|
async addQueryTab(expectedTabIndex = 1) {
|
|
await this.addQueryBtn.waitFor({ state: "visible", timeout: 15000 });
|
|
await this.addQueryBtn.click();
|
|
await this.queryTab(expectedTabIndex).waitFor({ state: "visible", timeout: 15000 });
|
|
// The newly-added tab becomes the active tab synchronously (currentQueryIndex
|
|
// is set to the new index). Wait for that deterministic state instead of a
|
|
// fixed sleep, so the sidebar reflects the new query before it is configured.
|
|
await this.waitForActiveQueryTab(expectedTabIndex);
|
|
testLogger.debug("Added query tab", { expectedTabIndex });
|
|
}
|
|
|
|
/**
|
|
* Wait until the query tab at the given index is the ACTIVE tab.
|
|
* Reka UI's TabsTrigger marks the active tab with data-state="active"
|
|
* (and aria-selected="true") on the same button that carries the data-test.
|
|
* This is the deterministic replacement for fixed "settle" sleeps.
|
|
* @param {number} index
|
|
* @param {number} timeout
|
|
*/
|
|
async waitForActiveQueryTab(index, timeout = 15000) {
|
|
await this.page
|
|
.locator(`[data-test="dashboard-panel-query-tab-${index}"][data-state="active"]`)
|
|
.waitFor({ state: "visible", timeout });
|
|
}
|
|
|
|
/**
|
|
* Switch to the query tab at given index.
|
|
* @param {number} index
|
|
*/
|
|
async switchToQueryTab(index) {
|
|
await this.queryTab(index).click();
|
|
// Wait for the clicked tab to actually become active (its fields/editor are
|
|
// rendered for that query) instead of a fixed sleep.
|
|
await this.waitForActiveQueryTab(index, 10000);
|
|
}
|
|
|
|
/**
|
|
* Remove the query tab at given index.
|
|
* @param {number} index
|
|
*/
|
|
async removeQueryTab(index) {
|
|
await this.queryTabRemove(index).click();
|
|
}
|
|
|
|
/**
|
|
* Toggle the visibility (eye icon) of the query tab at given index.
|
|
* @param {number} index
|
|
*/
|
|
async toggleQueryVisibility(index) {
|
|
await this.queryTabVisibility(index).click();
|
|
}
|
|
|
|
/**
|
|
* Type a custom SQL query into the Monaco editor for the active query tab.
|
|
* The tab must already be in custom-query mode.
|
|
* @param {string} sql
|
|
*/
|
|
async enterCustomSQL(sql) {
|
|
await this.queryEditor.waitFor({ state: "visible", timeout: 10000 });
|
|
const monacoCode = this.queryEditor.getByRole("code");
|
|
await monacoCode.click({ clickCount: 3 });
|
|
await this.page.keyboard.press("Control+a");
|
|
await this.page.keyboard.press("Backspace");
|
|
await this.page.keyboard.type(sql, { delay: 20 });
|
|
await this.page.keyboard.press("Escape");
|
|
// Wait deterministically for Monaco to reflect the typed SQL (debounce flush)
|
|
// instead of a fixed sleep — mirrors the no-timeout VRL editor pattern.
|
|
const snippet = sql.trim().substring(0, 12);
|
|
if (snippet) {
|
|
await this.page.waitForFunction(
|
|
({ sel, text }) => {
|
|
const el = document.querySelector(sel);
|
|
return !!el && (el.textContent || "").includes(text);
|
|
},
|
|
{ sel: '[data-test="dashboard-panel-query-editor"]', text: snippet },
|
|
{ timeout: 10000 },
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply the current panel config then save.
|
|
* Switches to Q1 tab first so validation uses Q1's valid fields.
|
|
* Handles the "unsaved changes" dialog automatically.
|
|
* @param {object} pm - PageManager instance
|
|
*/
|
|
async applyAndSave(pm) {
|
|
const dialogHandler = (dialog) => dialog.accept();
|
|
this.page.on("dialog", dialogHandler);
|
|
try {
|
|
// Switch to first query tab so validation uses Q1's valid fields
|
|
const firstTab = this.queryTab(0);
|
|
if (await firstTab.isVisible().catch(() => false)) {
|
|
await firstTab.click();
|
|
await this.waitForActiveQueryTab(0, 10000).catch(() => {});
|
|
}
|
|
await pm.dashboardPanelActions.applyDashboardBtn();
|
|
// Wait for the apply/query cycle to finish (apply button re-enabled)
|
|
// instead of a fixed sleep before saving.
|
|
await pm.dashboardPanelActions.waitForChartToRender().catch(() => {});
|
|
await pm.dashboardPanelActions.savePanel();
|
|
// Wait for navigation away from add_panel page after save
|
|
await this.page.waitForURL((url) => !url.pathname.includes("add_panel"), {
|
|
timeout: 30000,
|
|
}).catch(() => {
|
|
testLogger.warn("applyAndSave: did not navigate away from add_panel within timeout");
|
|
});
|
|
} finally {
|
|
this.page.off("dialog", dialogHandler);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Discard the current panel and return to the dashboard view page.
|
|
* Use this for teardown when the panel intentionally contains an
|
|
* incomplete/invalid query (which cannot pass save validation) or when the
|
|
* test does not need to persist the panel. Discarding an edited panel fires a
|
|
* native window.confirm — Playwright auto-dismisses dialogs by default, which
|
|
* would cancel navigation and leave the page on add_panel — so accept it.
|
|
*/
|
|
async discardPanel() {
|
|
const dialogHandler = (dialog) => dialog.accept();
|
|
this.page.on("dialog", dialogHandler);
|
|
try {
|
|
await this.discardBtn.waitFor({ state: "visible", timeout: 15000 });
|
|
await this.discardBtn.click();
|
|
// Discard navigates to the dashboard view page (/dashboards/view).
|
|
await this.page.waitForURL((url) => !url.pathname.includes("add_panel"), {
|
|
timeout: 30000,
|
|
});
|
|
} finally {
|
|
this.page.off("dialog", dialogHandler);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open the query inspector and return the page content for assertions.
|
|
* Closes the inspector when done.
|
|
* @returns {Promise<string>} The page content HTML
|
|
*/
|
|
async openQueryInspector() {
|
|
await this.queryInspectorBtn.waitFor({ state: "visible", timeout: 15000 });
|
|
await this.queryInspectorBtn.click();
|
|
// Gate on the inspector CONTENT (first query name), NOT the ODialog wrapper:
|
|
// the wrapper's data-test is a teleported/zero-size anchor that never reports
|
|
// "visible", so waiting on it times out. The rendered query name is reliable.
|
|
await this.queryInspectorQueryName(0).waitFor({
|
|
state: "visible",
|
|
timeout: 15000,
|
|
});
|
|
return await this.page.content();
|
|
}
|
|
|
|
/**
|
|
* Close the query inspector.
|
|
*/
|
|
async closeQueryInspector() {
|
|
if (await this.queryInspectorClose.isVisible()) {
|
|
await this.queryInspectorClose.click();
|
|
} else {
|
|
await this.page.keyboard.press("Escape");
|
|
}
|
|
}
|
|
}
|