mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 16:10:43 +00:00
Design at: https://github.com/openobserve/designs/blob/main/dashboards/panel-level-time-range/design.md ### **PR Type** Enhancement, Documentation ___ ### **Description** - Add defaults for panel-level time settings in front-end - Extend backend PanelConfig with time range fields - Provide a comprehensive implementation guide document ___ <details> <summary><h3> File Walkthrough</h3></summary> <table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table> <tr> <td> <details> <summary><strong>useDashboardPanel.ts</strong><dd><code>Add default panel time settings fields</code> </dd></summary> <hr> web/src/composables/useDashboardPanel.ts <ul><li>Added <code>allow_panel_time</code> default (false)<br> <li> Introduced <code>panel_time_mode</code> default ("individual")<br> <li> Initialized <code>panel_time_range</code> to null</ul> </details> </td> <td><a href="https://github.com/openobserve/openobserve/pull/10288/files#diff-fbba1700750053b9eaf38ddd30dab84531bc44131c795ebb713811e4930cd7cb">+3/-0</a> </td> </tr> <tr> <td> <details> <summary><strong>mod.rs</strong><dd><code>Extend PanelConfig with time fields</code> </dd></summary> <hr> src/config/src/meta/dashboards/v8/mod.rs <ul><li>Added <code>allow_panel_time: Option<bool></code> to PanelConfig<br> <li> Added <code>panel_time_mode: Option<String></code> field<br> <li> Added <code>panel_time_range: Option<DateTimeOptions></code> field</ul> </details> </td> <td><a href="https://github.com/openobserve/openobserve/pull/10288/files#diff-cbf3fe971525ba58892400391dfd7dac7e43d6049d6b4f5a9840ad104ebc5e08">+12/-0</a> </td> </tr> </table></td></tr><tr><td><strong>Documentation</strong></td><td><table> <tr> <td> <details> <summary><strong>PANEL_TIME_RANGE_IMPLEMENTATION.md</strong><dd><code>Add panel time range implementation guide</code> </dd></summary> <hr> web/src/components/dashboards/PANEL_TIME_RANGE_IMPLEMENTATION.md <ul><li>Added detailed implementation guide (v3.0)<br> <li> Outlined requirements, UI, URL sync, testing<br> <li> Documented data flow, edge cases, and timeline</ul> </details> </td> <td><a href="https://github.com/openobserve/openobserve/pull/10288/files#diff-39b68cc51ce3089b41061b75e18c45448659cab57702dd39b01a9aad63598345">+2718/-0</a></td> </tr> </table></td></tr></tr></tbody></table> </details> ___ --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: ktx-akshay <akshay.patil@kiara.tech>
257 lines
8.1 KiB
JavaScript
257 lines
8.1 KiB
JavaScript
/**
|
|
* Panel Time Testing Helper Utilities
|
|
* Provides common methods for panel time API monitoring, assertions, and state validation
|
|
*/
|
|
|
|
import testLogger from './test-logger.js';
|
|
|
|
/**
|
|
* Monitor panel data API calls (query endpoint)
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {Object} options - Configuration options
|
|
* @param {number} options.expectedCount - Expected number of API calls (default: 1)
|
|
* @param {number} options.timeout - Timeout in milliseconds (default: 15000)
|
|
* @param {string} options.panelId - Optional panel ID to filter specific panel calls
|
|
* @returns {Promise<Object>} - {success, actualCount, calls, timedOut}
|
|
*/
|
|
export async function monitorPanelDataAPICalls(page, options = {}) {
|
|
const { expectedCount = 1, timeout = 15000, panelId = null } = options;
|
|
const startTime = Date.now();
|
|
const apiCalls = [];
|
|
|
|
return new Promise((resolve) => {
|
|
const responseHandler = async (response) => {
|
|
const url = response.url();
|
|
|
|
// Match query endpoints that panels use to fetch data
|
|
if (url.includes('/query') || url.includes('/_search')) {
|
|
try {
|
|
const status = response.status();
|
|
const callInfo = {
|
|
url,
|
|
status,
|
|
timestamp: Date.now(),
|
|
duration: Date.now() - startTime,
|
|
panelId: panelId || 'unknown',
|
|
};
|
|
|
|
apiCalls.push(callInfo);
|
|
|
|
testLogger.info(`Panel data API call ${apiCalls.length}/${expectedCount}: ${status}`);
|
|
|
|
if (apiCalls.length >= expectedCount) {
|
|
page.off('response', responseHandler);
|
|
resolve({
|
|
success: true,
|
|
actualCount: apiCalls.length,
|
|
calls: apiCalls,
|
|
timedOut: false,
|
|
totalDuration: Date.now() - startTime
|
|
});
|
|
}
|
|
} catch (error) {
|
|
testLogger.debug(`Error reading response: ${error.message}`);
|
|
}
|
|
}
|
|
};
|
|
|
|
page.on('response', responseHandler);
|
|
|
|
setTimeout(() => {
|
|
page.off('response', responseHandler);
|
|
testLogger.warn(`Panel data API monitoring timed out: ${apiCalls.length}/${expectedCount} calls completed`);
|
|
resolve({
|
|
success: apiCalls.length >= expectedCount,
|
|
actualCount: apiCalls.length,
|
|
calls: apiCalls,
|
|
timedOut: true,
|
|
totalDuration: timeout
|
|
});
|
|
}, timeout);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Assert that API calls were triggered successfully
|
|
* @param {Object} result - Result from monitorPanelDataAPICalls
|
|
* @param {Object} expectations - Expected values
|
|
*/
|
|
export function assertPanelDataAPITriggered(result, expectations = {}) {
|
|
const {
|
|
success = true,
|
|
minCalls = 1,
|
|
maxDuration = 10000
|
|
} = expectations;
|
|
|
|
if (success) {
|
|
if (!result.success) {
|
|
throw new Error(`Expected API calls to succeed, but got ${result.actualCount} calls`);
|
|
}
|
|
if (result.actualCount < minCalls) {
|
|
throw new Error(`Expected at least ${minCalls} API calls, got ${result.actualCount}`);
|
|
}
|
|
}
|
|
|
|
if (maxDuration && result.totalDuration > maxDuration) {
|
|
testLogger.warn(`API calls took ${result.totalDuration}ms, expected max ${maxDuration}ms`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wait for URL to contain specific panel time parameter
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {string} panelId - Panel ID
|
|
* @param {string} expectedValue - Expected time value (e.g., "1h", "7d")
|
|
* @param {number} timeout - Timeout in milliseconds
|
|
*/
|
|
export async function waitForPanelTimeInURL(page, panelId, expectedValue, timeout = 5000) {
|
|
const startTime = Date.now();
|
|
const expectedParam = `pt-period.${panelId}=${expectedValue}`;
|
|
|
|
while (Date.now() - startTime < timeout) {
|
|
const url = page.url();
|
|
if (url.includes(expectedParam)) {
|
|
return true;
|
|
}
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
}
|
|
|
|
throw new Error(`URL did not contain ${expectedParam} within ${timeout}ms. Current URL: ${page.url()}`);
|
|
}
|
|
|
|
/**
|
|
* Wait for panel time parameter to be removed from URL
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {string} panelId - Panel ID
|
|
* @param {number} timeout - Timeout in milliseconds
|
|
*/
|
|
export async function waitForPanelTimeRemovedFromURL(page, panelId, timeout = 5000) {
|
|
const startTime = Date.now();
|
|
const paramPeriod = `pt-period.${panelId}`;
|
|
const paramFrom = `pt-from.${panelId}`;
|
|
|
|
while (Date.now() - startTime < timeout) {
|
|
const url = page.url();
|
|
if (!url.includes(paramPeriod) && !url.includes(paramFrom)) {
|
|
return true;
|
|
}
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
}
|
|
|
|
throw new Error(`URL still contains panel time params after ${timeout}ms. Current URL: ${page.url()}`);
|
|
}
|
|
|
|
/**
|
|
* Get panel time value from URL
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {string} panelId - Panel ID
|
|
* @returns {Object|null} - {type: 'relative'|'absolute', value: string, from: number, to: number}
|
|
*/
|
|
export function getPanelTimeFromURL(page, panelId) {
|
|
const url = new URL(page.url());
|
|
const relativeParam = url.searchParams.get(`pt-period.${panelId}`);
|
|
const fromParam = url.searchParams.get(`pt-from.${panelId}`);
|
|
const toParam = url.searchParams.get(`pt-to.${panelId}`);
|
|
|
|
if (relativeParam) {
|
|
return {
|
|
type: 'relative',
|
|
value: relativeParam
|
|
};
|
|
}
|
|
|
|
if (fromParam && toParam) {
|
|
return {
|
|
type: 'absolute',
|
|
from: parseInt(fromParam),
|
|
to: parseInt(toParam)
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Verify that panel data has NOT refreshed (no API calls made)
|
|
* This is used to verify that data doesn't refresh before Apply is clicked
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {number} waitTime - Time to wait in milliseconds (default: 2000)
|
|
* @returns {Promise<boolean>} - true if no API calls detected
|
|
*/
|
|
export async function verifyNoDataRefresh(page, waitTime = 2000) {
|
|
let apiCallDetected = false;
|
|
|
|
const responseHandler = async (response) => {
|
|
const url = response.url();
|
|
if (url.includes('/query') || url.includes('/_search')) {
|
|
apiCallDetected = true;
|
|
}
|
|
};
|
|
|
|
page.on('response', responseHandler);
|
|
|
|
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
|
|
page.off('response', responseHandler);
|
|
|
|
if (apiCallDetected) {
|
|
throw new Error('Unexpected API call detected - panel should not refresh yet');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Wait for panel to start loading (loading indicator appears)
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {string} panelId - Panel ID
|
|
* @param {number} timeout - Timeout in milliseconds
|
|
*/
|
|
export async function waitForPanelLoadStart(page, panelId, timeout = 5000) {
|
|
const loadingSelector = `[data-test="panel-${panelId}-loading"]`;
|
|
try {
|
|
await page.waitForSelector(loadingSelector, { state: "visible", timeout });
|
|
return true;
|
|
} catch (error) {
|
|
testLogger.debug(`Panel ${panelId} loading indicator did not appear within ${timeout}ms`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse time range display text to standard format
|
|
* @param {string} displayText - Display text from time picker (e.g., "Last 1 hour", "Last 7 days")
|
|
* @returns {string} - Standard format (e.g., "1h", "7d")
|
|
*/
|
|
export function parseTimeDisplayText(displayText) {
|
|
const text = displayText.toLowerCase().trim();
|
|
|
|
const patterns = [
|
|
{ regex: /(\d+)\s*minute?s?/, unit: 'm' },
|
|
{ regex: /(\d+)\s*hour?s?/, unit: 'h' },
|
|
{ regex: /(\d+)\s*day?s?/, unit: 'd' },
|
|
{ regex: /(\d+)\s*week?s?/, unit: 'w' },
|
|
{ regex: /(\d+)\s*month?s?/, unit: 'M' },
|
|
];
|
|
|
|
for (const pattern of patterns) {
|
|
const match = text.match(pattern.regex);
|
|
if (match) {
|
|
return `${match[1]}${pattern.unit}`;
|
|
}
|
|
}
|
|
|
|
return text;
|
|
}
|
|
|
|
/**
|
|
* Wait for network idle with custom timeout
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {number} timeout - Timeout in milliseconds
|
|
*/
|
|
export async function waitForNetworkIdle(page, timeout = 3000) {
|
|
await page.waitForLoadState('networkidle', { timeout }).catch(() => {
|
|
testLogger.debug('Network did not become idle within timeout');
|
|
});
|
|
}
|