fix: logs timechart required-fields error and URL refresh failure (#13244)
## Summary Fixes #12897 — two defects on the Logs page **Timechart** tab: 1. The error **"Please select required fields to render the chart"** appeared even though the chart should render. 2. **Refreshing the URL** broke the tab entirely: a *"Select \* query is not supported for visualization"* toast plus a permanent **No Data** state. ## Root cause ### 1. False "required fields" error (regression) `convertPanelData()` throws this error when a chart-type panel has empty `x`/`y` fields. The check was introduced in #10305, then **commented out in #11297 specifically because it broke the logs visualize toggle** (the NOTE explaining that was left in the code), and then **re-enabled in #11586**, which reintroduced the bug. The Timechart drives a **custom-query** panel whose axes are populated *asynchronously* from the result schema (`resetFields()` → `result_schema` network call → repopulate). Empty `x`/`y` is therefore a legitimate transient state for this panel — any render that landed inside that window (or after an extraction early-exit) hit the throw and pushed the error into the errors panel. ### 2. URL refresh broken Two ordering problems on page load: - `handleBeforeMount()` restores `logsVisualizeToggle` from the URL **before** the stream selection is restored, so the visualize toggle watcher fired with an empty stream and built literally `select * from "undefined"` → rejected with the SELECT \* toast (confirmed by instrumenting the running app). - Nothing ever re-ran the visualization after URL restoration completed — `loadVisualizeData()` only loads stream fields — so the tab stayed on **No Data** forever. ## The fix | Change | File | What it does | |---|---|---| | Scope the empty-fields guard to builder-mode panels (`!query.customQuery`) | `web/src/utils/dashboard/convertPanelData.ts` | Builder panels keep the protection (the panel editor's own validation also still blocks empty applies); custom-query panels (logs Timechart, alerts preview) are no longer blocked by their transient empty-fields state | | Bail out of the visualize toggle watcher when no stream is selected yet | `web/src/plugins/logs/Index.vue` | Stops the premature page-load run from building `select * from "undefined"` and toasting | | Run the visualization after URL restoration completes | `web/src/plugins/logs/Index.vue` (`setupLogsTab`) | Mirrors the manual-toggle setup via a shared `prepareVisualizeMode()` (quick-mode auto-enable, layout, `customQuery`, VRL copy), restores the saved chart type/config from `visualization_data` (`restoreVisualizationFromUrlOnLoad()`), then triggers the run. Scoped to the visualize toggle only — Search/build/patterns tabs untouched | | Broaden the stream-field readiness check | `web/src/utils/logs/visualizeStreamFields.ts` (new) | Extracted predicate also reloads fields when quick mode is on but `interestingFieldList` hasn't loaded yet — the state that produced SELECT \* mid-restore | ## Verification - **Unit tests (written failing-first against the unfixed code):** guard skips custom-query panels / still throws for builder panels / PromQL exempt; readiness-predicate cases. 75/75 pass across the two touched suites. - **End-to-end (local backend + dev UI + Playwright):** reproduced both symptoms on the unfixed build, then verified on this branch: run → render; chart-type switch (h-bar) → URL refresh → **chart type + config + query restored and chart rendered, zero errors**; VRL-function variant renders; dashboards builder empty-fields apply is still blocked by its validation; build-tab refresh behavior unchanged. - `eslint` clean on all touched files (the one `no-fallthrough` error in `convertPanelData.ts` pre-exists on `main`, untouched line); `vue-tsc` clean. ## Before / After Both recordings follow the identical flow: select stream → `match_all('error')` → open Timechart → switch chart type to horizontal bar → **refresh the URL**. ### Before (`main`) — refresh shows the SELECT \* toast and permanent No Data  📹 Full recording: [before-refresh-bug.webm](https://raw.githubusercontent.com/DHRUV6029/openobserve/pr-12897-assets/before-refresh-bug.webm) ### After (this branch) — refresh restores chart type, query, and renders  📹 Full recording: [after-fix-full-flow.webm](https://raw.githubusercontent.com/DHRUV6029/openobserve/pr-12897-assets/after-fix-full-flow.webm) <details> <summary>Screenshots — final states and pre-refresh states on both builds</summary> Final state after refresh (unfixed build):  Final state after refresh (this branch):  Timechart renders before refresh (unfixed build):  H-bar renders before refresh (unfixed build):  Timechart on this branch:  H-bar on this branch:  </details>
This commit is contained in:
parent
4772df1723
commit
e85baca92d
|
|
@ -376,6 +376,7 @@ import SearchBar from "@/plugins/logs/SearchBar.vue";
|
|||
import { type ActivationState, PageType } from "@/ts/interfaces/logs.ts";
|
||||
import { isWebSocketEnabled, isStreamingEnabled } from "@/utils/zincutils";
|
||||
import { allSelectionFieldsHaveAlias } from "@/utils/query/visualizationUtils";
|
||||
import { shouldReloadStreamFieldsForVisualize } from "@/utils/logs/visualizeStreamFields";
|
||||
import useAiChat from "@/composables/useAiChat";
|
||||
import { logsUtils } from "@/composables/useLogs/logsUtils";
|
||||
import { searchState } from "@/composables/useLogs/searchState";
|
||||
|
|
@ -958,7 +959,14 @@ export default defineComponent({
|
|||
// Main method for handling before mount logic
|
||||
async function handleBeforeMount() {
|
||||
if (Object.hasOwn(router.currentRoute.value?.query, "logs_visualize_toggle")) {
|
||||
searchObj.meta.logsVisualizeToggle = router.currentRoute.value.query.logs_visualize_toggle;
|
||||
const urlToggle = router.currentRoute.value.query.logs_visualize_toggle;
|
||||
// Restoring directly onto the Timechart tab: setupLogsTab() will run the
|
||||
// visualization once fields are ready, so tell the toggle watcher to skip
|
||||
// the page-load fire it is about to receive from the assignment below.
|
||||
if (urlToggle === "visualize") {
|
||||
isInitialVisualizeRestore.value = true;
|
||||
}
|
||||
searchObj.meta.logsVisualizeToggle = urlToggle;
|
||||
}
|
||||
|
||||
// Always setup logs tab on mount
|
||||
|
|
@ -1063,8 +1071,27 @@ export default defineComponent({
|
|||
await loadPatternsData();
|
||||
await extractPatternsForCurrentQuery();
|
||||
} else {
|
||||
loadVisualizeData();
|
||||
await loadVisualizeData();
|
||||
searchObj.loading = false;
|
||||
// The visualize toggle watcher bails out during page load because it
|
||||
// fires before URL restoration completes. Now that the
|
||||
// stream and its fields are restored, mirror the watcher's setup,
|
||||
// restore the saved chart type/config from the URL, and run the
|
||||
// visualization. Scoped to the visualize tab — the build tab loads
|
||||
// through BuildQueryPage and must not auto-run here.
|
||||
if (
|
||||
searchObj.meta.logsVisualizeToggle === "visualize" &&
|
||||
searchObj.data.stream.selectedStream?.length
|
||||
) {
|
||||
prepareVisualizeMode();
|
||||
// Suppress the chart-type watcher while restoring (it would
|
||||
// trigger a duplicate updateVisualization for the type change).
|
||||
isRestoringFromUrl.value = true;
|
||||
restoreVisualizationFromUrlOnLoad();
|
||||
await nextTick();
|
||||
isRestoringFromUrl.value = false;
|
||||
handleVisualizeTab();
|
||||
}
|
||||
}
|
||||
|
||||
store.dispatch("logs/setIsInitialized", true);
|
||||
|
|
@ -1727,6 +1754,115 @@ export default defineComponent({
|
|||
// Used to restore chart type from URL only on first toggle (for shared links)
|
||||
const isFirstBuildToggle = ref(true);
|
||||
|
||||
// On page load with the Timechart tab in the URL, handleBeforeMount() sets
|
||||
// the visualize toggle, which fires the toggle watcher before setupLogsTab()
|
||||
// has restored the stream and extracted fields. That early fire would build
|
||||
// a stale `select *` and show a spurious error. setupLogsTab() owns the
|
||||
// page-load restoration (it calls handleVisualizeTab() once fields are
|
||||
// ready), so the watcher skips its work exactly once on that initial fire.
|
||||
const isInitialVisualizeRestore = ref(false);
|
||||
|
||||
// Chart types the logs Timechart supports restoring from a shared URL
|
||||
const validLogsChartTypes = ["area", "bar", "h-bar", "line", "scatter", "table"];
|
||||
|
||||
// Shared setup for entering visualize (Timechart) mode. Used by the
|
||||
// logsVisualizeToggle watcher (manual toggle) and by setupLogsTab on
|
||||
// page load, so both entry paths behave identically.
|
||||
function prepareVisualizeMode() {
|
||||
// Enable quick mode automatically when switching to visualization if:
|
||||
// 1. SQL mode is disabled OR
|
||||
// 2. Query is "SELECT * FROM some_stream" (simple select all query)
|
||||
// 3. Default quick mode config is true
|
||||
const shouldEnableQuickMode =
|
||||
!searchObj.meta.sqlMode || isSimpleSelectAllQuery(searchObj.data.query);
|
||||
|
||||
const isQuickModeDisabled = !searchObj.meta.quickMode;
|
||||
const isQuickModeConfigEnabled = store.state.zoConfig.quick_mode_enabled === true;
|
||||
|
||||
if (shouldEnableQuickMode && isQuickModeDisabled && isQuickModeConfigEnabled) {
|
||||
searchObj.meta.quickMode = true;
|
||||
handleQuickModeChange();
|
||||
}
|
||||
|
||||
// close field list and splitter
|
||||
dashboardPanelData.layout.splitter = 0;
|
||||
dashboardPanelData.layout.showFieldList = false;
|
||||
|
||||
dashboardPanelData.data.queries[dashboardPanelData.layout.currentQueryIndex].customQuery =
|
||||
true;
|
||||
|
||||
// Copy VRL function query if present
|
||||
if (searchObj.data.tempFunctionContent && searchObj.data.transformType === "function") {
|
||||
dashboardPanelData.data.queries[
|
||||
dashboardPanelData.layout.currentQueryIndex
|
||||
].vrlFunctionQuery = searchObj.data.tempFunctionContent;
|
||||
} else {
|
||||
dashboardPanelData.data.queries[
|
||||
dashboardPanelData.layout.currentQueryIndex
|
||||
].vrlFunctionQuery = "";
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the chart type and panel config saved in the URL
|
||||
// (visualization_data) when the page loads directly on the Timechart tab.
|
||||
// The visualize toggle watcher normally does this, but on page load it
|
||||
// bails out before restoring because it fires ahead of URL/stream
|
||||
// restoration.
|
||||
function restoreVisualizationFromUrlOnLoad() {
|
||||
const visualizationDataParam = router.currentRoute.value.query.visualization_data;
|
||||
if (!visualizationDataParam || typeof visualizationDataParam !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
let restoredData = null;
|
||||
try {
|
||||
restoredData = decodeVisualizationConfig(visualizationDataParam);
|
||||
} catch (error) {
|
||||
console.warn("Failed to restore visualization config from URL:", error);
|
||||
return;
|
||||
}
|
||||
if (!restoredData || typeof restoredData !== "object") return;
|
||||
|
||||
if (
|
||||
isFirstVisualizationToggle.value &&
|
||||
restoredData.type &&
|
||||
typeof restoredData.type === "string" &&
|
||||
validLogsChartTypes.includes(restoredData.type)
|
||||
) {
|
||||
dashboardPanelData.data.type = restoredData.type;
|
||||
}
|
||||
|
||||
if (restoredData.config && typeof restoredData.config === "object") {
|
||||
dashboardPanelData.data.config = {
|
||||
...dashboardPanelData.data.config,
|
||||
connect_nulls: true,
|
||||
...restoredData.config,
|
||||
};
|
||||
}
|
||||
|
||||
// The URL restore counts as the first-toggle restoration.
|
||||
isFirstVisualizationToggle.value = false;
|
||||
}
|
||||
|
||||
// The effective SQL that visualization runs for the current logs query.
|
||||
// In SQL mode this is the raw user query; otherwise buildSearch() resolves
|
||||
// the field list (quick mode fields, or `*` when quick mode is off).
|
||||
const getEffectiveVisualizeQuery = (): string => {
|
||||
if (searchObj.meta.sqlMode) {
|
||||
return searchObj.data.query ?? "";
|
||||
}
|
||||
return buildSearch()?.query?.sql ?? "";
|
||||
};
|
||||
|
||||
// Table charts render the raw query columns, so a bare `SELECT *` is not a
|
||||
// meaningful table visualization. Histogram-based charts (line/bar/area/
|
||||
// scatter) ignore the SELECT columns and render it as a histogram, so this
|
||||
// only blocks the table chart. Quick mode yields `SELECT <fields>` (not
|
||||
// select-all), so tables render normally there.
|
||||
const isSelectStarForTable = (): boolean =>
|
||||
store.state.zoConfig.quick_mode_enabled === true &&
|
||||
isSimpleSelectAllQuery(getEffectiveVisualizeQuery());
|
||||
|
||||
watch(
|
||||
() => [searchObj?.meta?.logsVisualizeToggle],
|
||||
async () => {
|
||||
|
|
@ -1748,40 +1884,24 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
if (searchObj.meta.logsVisualizeToggle == "visualize") {
|
||||
// Enable quick mode automatically when switching to visualization if:
|
||||
// 1. SQL mode is disabled OR
|
||||
// 2. Query is "SELECT * FROM some_stream" (simple select all query)
|
||||
// 3. Default quick mode config is true
|
||||
const shouldEnableQuickMode =
|
||||
!searchObj.meta.sqlMode || isSimpleSelectAllQuery(searchObj.data.query);
|
||||
|
||||
const isQuickModeDisabled = !searchObj.meta.quickMode;
|
||||
const isQuickModeConfigEnabled = store.state.zoConfig.quick_mode_enabled === true;
|
||||
|
||||
if (shouldEnableQuickMode && isQuickModeDisabled && isQuickModeConfigEnabled) {
|
||||
searchObj.meta.quickMode = true;
|
||||
handleQuickModeChange();
|
||||
// Skip the initial page-load fire (see isInitialVisualizeRestore).
|
||||
// setupLogsTab() restores the stream, extracts fields, and then runs
|
||||
// the visualization via handleVisualizeTab(). Running here too would
|
||||
// race that flow with stale/empty fields and build a spurious
|
||||
// `select *` (which shows the "not supported" error). Genuine user
|
||||
// toggles after mount have the flag unset and fall through normally.
|
||||
if (isInitialVisualizeRestore.value) {
|
||||
isInitialVisualizeRestore.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// close field list and splitter
|
||||
dashboardPanelData.layout.splitter = 0;
|
||||
dashboardPanelData.layout.showFieldList = false;
|
||||
|
||||
dashboardPanelData.data.queries[
|
||||
dashboardPanelData.layout.currentQueryIndex
|
||||
].customQuery = true;
|
||||
|
||||
// Copy VRL function query if present
|
||||
if (searchObj.data.tempFunctionContent && searchObj.data.transformType === "function") {
|
||||
dashboardPanelData.data.queries[
|
||||
dashboardPanelData.layout.currentQueryIndex
|
||||
].vrlFunctionQuery = searchObj.data.tempFunctionContent;
|
||||
} else {
|
||||
dashboardPanelData.data.queries[
|
||||
dashboardPanelData.layout.currentQueryIndex
|
||||
].vrlFunctionQuery = "";
|
||||
// Defensive: no stream selected yet — nothing to visualize.
|
||||
if (!searchObj.data.stream.selectedStream?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
prepareVisualizeMode();
|
||||
|
||||
// Store current config and chart type to preserve them during rebuild
|
||||
const queryParams = router.currentRoute.value.query;
|
||||
let preservedConfig = null;
|
||||
|
|
@ -1816,7 +1936,6 @@ export default defineComponent({
|
|||
restoredData.type &&
|
||||
typeof restoredData.type === "string"
|
||||
) {
|
||||
const validLogsChartTypes = ["area", "bar", "h-bar", "line", "scatter", "table"];
|
||||
if (validLogsChartTypes.includes(restoredData.type)) {
|
||||
// Valid chart type found - set it and disable auto-selection
|
||||
dashboardPanelData.data.type = restoredData.type;
|
||||
|
|
@ -1835,8 +1954,12 @@ export default defineComponent({
|
|||
// finished populating interestingFieldList yet. Without fields,
|
||||
// buildSearch() produces SELECT * which is invalid for visualization.
|
||||
if (
|
||||
searchObj.data.stream.selectedStream?.length > 0 &&
|
||||
searchObj.data.stream.selectedStreamFields?.length === 0
|
||||
shouldReloadStreamFieldsForVisualize({
|
||||
selectedStream: searchObj.data.stream.selectedStream,
|
||||
selectedStreamFields: searchObj.data.stream.selectedStreamFields,
|
||||
interestingFieldList: searchObj.data.stream.interestingFieldList,
|
||||
quickMode: searchObj.meta.quickMode,
|
||||
})
|
||||
) {
|
||||
await getStreamList();
|
||||
await extractFields();
|
||||
|
|
@ -1848,14 +1971,12 @@ export default defineComponent({
|
|||
const queryBuild = buildSearch();
|
||||
logsPageQuery = queryBuild?.query?.sql ?? "";
|
||||
|
||||
// Check if query is SELECT * which is not supported for visualization
|
||||
if (
|
||||
store.state.zoConfig.quick_mode_enabled === true &&
|
||||
isSimpleSelectAllQuery(logsPageQuery)
|
||||
) {
|
||||
showErrorNotification(t("logs.index.selectStarNotSupportedForVisualization"));
|
||||
return;
|
||||
}
|
||||
// NOTE: `SELECT *` is intentionally allowed for histogram-based charts
|
||||
// (line/bar/area/scatter). They render histogram(_timestamp), count(*),
|
||||
// which ignores the query's SELECT columns, so `SELECT *` (produced when
|
||||
// quick mode is off or in SQL mode) is a valid input. The table chart is
|
||||
// the exception — it renders the raw query columns — and is guarded below
|
||||
// once the chart type is finalized.
|
||||
|
||||
// Use conditional auto-selection based on first toggle and URL chart type
|
||||
isRestoringFromUrl.value = true;
|
||||
|
|
@ -1895,6 +2016,13 @@ export default defineComponent({
|
|||
shouldUseHistogramQuery.value = false;
|
||||
}
|
||||
|
||||
// On entry/reload, if the finalized chart type is a table with a
|
||||
// bare `SELECT *`, surface the error (the table renders raw columns).
|
||||
if (dashboardPanelData.data.type === "table" && isSelectStarForTable()) {
|
||||
showErrorNotification(t("logs.index.selectStarNotSupportedForVisualization"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Only reuse cached search results if the current query matches
|
||||
// the query that produced those results. When the user modifies
|
||||
// the query in Build/Patterns mode and switches to Visualize,
|
||||
|
|
@ -2123,12 +2251,25 @@ export default defineComponent({
|
|||
|
||||
watch(
|
||||
() => dashboardPanelData.data.type,
|
||||
async () => {
|
||||
async (newType, oldType) => {
|
||||
// Skip processing if we're currently restoring from URL
|
||||
if (isRestoringFromUrl.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A table chart renders the raw query columns, so a bare `SELECT *`
|
||||
// (quick mode off / SQL mode) is not a meaningful table visualization.
|
||||
// Histogram-based charts ignore the SELECT columns, so this only blocks
|
||||
// the table chart. Surface the error and revert to the previous chart
|
||||
// type so the raw-`SELECT *` data is never shown.
|
||||
if (newType === "table" && isSelectStarForTable()) {
|
||||
showErrorNotification(t("logs.index.selectStarNotSupportedForVisualization"));
|
||||
if (oldType && oldType !== "table") {
|
||||
dashboardPanelData.data.type = oldType;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const currentQuery =
|
||||
dashboardPanelData.data.queries[dashboardPanelData.layout.currentQueryIndex].query;
|
||||
|
||||
|
|
@ -2304,24 +2445,24 @@ export default defineComponent({
|
|||
// finished populating interestingFieldList yet. Without fields,
|
||||
// buildSearch() produces SELECT * which is invalid for visualization.
|
||||
if (
|
||||
searchObj.data.stream.selectedStream?.length > 0 &&
|
||||
searchObj.data.stream.selectedStreamFields?.length === 0
|
||||
shouldReloadStreamFieldsForVisualize({
|
||||
selectedStream: searchObj.data.stream.selectedStream,
|
||||
selectedStreamFields: searchObj.data.stream.selectedStreamFields,
|
||||
interestingFieldList: searchObj.data.stream.interestingFieldList,
|
||||
quickMode: searchObj.meta.quickMode,
|
||||
})
|
||||
) {
|
||||
await getStreamList();
|
||||
await extractFields();
|
||||
}
|
||||
|
||||
let logsPageQuery = "";
|
||||
|
||||
// Build the query regardless of sqlMode
|
||||
const queryBuild = buildSearch();
|
||||
logsPageQuery = queryBuild?.query?.sql ?? "";
|
||||
|
||||
// Check if query is SELECT * which is not supported for visualization
|
||||
if (
|
||||
store.state.zoConfig.quick_mode_enabled === true &&
|
||||
isSimpleSelectAllQuery(logsPageQuery)
|
||||
) {
|
||||
// Build the query for its side effect (prunes interestingFieldList to
|
||||
// fields present in the stream). Histogram-based charts ignore the
|
||||
// SELECT columns (updateVisualization builds their histogram query),
|
||||
// so `SELECT *` is fine for them. The table chart renders the raw query
|
||||
// columns, so a bare `SELECT *` there is not a meaningful visualization.
|
||||
buildSearch();
|
||||
if (dashboardPanelData.data.type === "table" && isSelectStarForTable()) {
|
||||
showErrorNotification(t("logs.index.selectStarNotSupportedForVisualization"));
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -544,6 +544,86 @@ describe("convertPanelData", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("Required fields validation", () => {
|
||||
const emptyFields = { x: [], y: [], breakdown: [] };
|
||||
|
||||
it("should not throw for custom-query panels with empty x/y fields (logs Timechart)", async () => {
|
||||
// The logs Timechart drives a customQuery panel whose x/y fields are
|
||||
// populated asynchronously — they are legitimately empty during and
|
||||
// after extraction early-exits. Rendering must not be blocked.
|
||||
const panelSchema = {
|
||||
type: "line",
|
||||
queryType: "sql",
|
||||
queries: [
|
||||
{
|
||||
query:
|
||||
"SELECT histogram(_timestamp) AS zo_sql_key, count(*) AS zo_sql_num FROM test GROUP BY zo_sql_key",
|
||||
customQuery: true,
|
||||
fields: emptyFields,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = await convertPanelData(
|
||||
panelSchema,
|
||||
mockData,
|
||||
mockStore,
|
||||
mockChartPanelRef,
|
||||
mockHoveredSeriesState,
|
||||
mockResultMetaData,
|
||||
mockMetadata,
|
||||
mockChartPanelStyle,
|
||||
mockAnnotations,
|
||||
);
|
||||
|
||||
expect(result.chartType).toBe("line");
|
||||
});
|
||||
|
||||
it("should throw for builder-mode panels with empty x/y fields", async () => {
|
||||
const panelSchema = {
|
||||
type: "line",
|
||||
queryType: "sql",
|
||||
queries: [{ query: "", customQuery: false, fields: emptyFields }],
|
||||
};
|
||||
|
||||
await expect(
|
||||
convertPanelData(
|
||||
panelSchema,
|
||||
mockData,
|
||||
mockStore,
|
||||
mockChartPanelRef,
|
||||
mockHoveredSeriesState,
|
||||
mockResultMetaData,
|
||||
mockMetadata,
|
||||
mockChartPanelStyle,
|
||||
mockAnnotations,
|
||||
),
|
||||
).rejects.toThrow("Please select required fields to render the chart");
|
||||
});
|
||||
|
||||
it("should not throw for promql panels with empty x/y fields", async () => {
|
||||
const panelSchema = {
|
||||
type: "line",
|
||||
queryType: "promql",
|
||||
queries: [{ query: "up", fields: emptyFields }],
|
||||
};
|
||||
|
||||
const result = await convertPanelData(
|
||||
panelSchema,
|
||||
mockData,
|
||||
mockStore,
|
||||
mockChartPanelRef,
|
||||
mockHoveredSeriesState,
|
||||
mockResultMetaData,
|
||||
mockMetadata,
|
||||
mockChartPanelStyle,
|
||||
mockAnnotations,
|
||||
);
|
||||
|
||||
expect(result.chartType).toBe("line");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("should handle SQL conversion errors gracefully", async () => {
|
||||
const panelSchema = {
|
||||
|
|
|
|||
|
|
@ -56,13 +56,15 @@ export const convertPanelData = async (
|
|||
case "scatter":
|
||||
case "metric":
|
||||
case "gauge": {
|
||||
// NOTE: on logs to visualize toggle, it shows below error because breakdown field is not required for all the charts
|
||||
// Skip conversion if no fields are selected in builder mode
|
||||
// (prevents echarts errors like "axis.getAxesOnZeroOf is not a function")
|
||||
// PromQL queries don't use builder fields, so skip this check for them
|
||||
// PromQL queries don't use builder fields, so skip this check for them.
|
||||
// Custom-query panels (e.g. logs Timechart) derive axes from the
|
||||
// SQL result asynchronously, so empty x/y is a valid transient state there.
|
||||
const query = panelSchema?.queries?.[0];
|
||||
if (
|
||||
panelSchema?.queryType !== "promql" &&
|
||||
!query?.customQuery &&
|
||||
!query?.fields?.x?.length &&
|
||||
!query?.fields?.y?.length
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright 2026 OpenObserve Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { shouldReloadStreamFieldsForVisualize } from "./visualizeStreamFields";
|
||||
|
||||
describe("shouldReloadStreamFieldsForVisualize", () => {
|
||||
it("returns true when stream fields are not loaded yet", () => {
|
||||
expect(
|
||||
shouldReloadStreamFieldsForVisualize({
|
||||
selectedStream: ["default"],
|
||||
selectedStreamFields: [],
|
||||
interestingFieldList: [],
|
||||
quickMode: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true in quick mode when interesting fields are not loaded (refresh race)", () => {
|
||||
// On page reload the visualize toggle can run after selectedStreamFields
|
||||
// are restored but before interestingFieldList is populated. In quick
|
||||
// mode buildSearch() would then produce SELECT * and visualization
|
||||
// rejects it — so fields must be (re)extracted first.
|
||||
expect(
|
||||
shouldReloadStreamFieldsForVisualize({
|
||||
selectedStream: ["default"],
|
||||
selectedStreamFields: [{ name: "_timestamp" }],
|
||||
interestingFieldList: [],
|
||||
quickMode: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when stream and interesting fields are loaded", () => {
|
||||
expect(
|
||||
shouldReloadStreamFieldsForVisualize({
|
||||
selectedStream: ["default"],
|
||||
selectedStreamFields: [{ name: "_timestamp" }],
|
||||
interestingFieldList: ["_timestamp"],
|
||||
quickMode: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when no stream is selected", () => {
|
||||
expect(
|
||||
shouldReloadStreamFieldsForVisualize({
|
||||
selectedStream: [],
|
||||
selectedStreamFields: [],
|
||||
interestingFieldList: [],
|
||||
quickMode: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false outside quick mode when stream fields are loaded", () => {
|
||||
expect(
|
||||
shouldReloadStreamFieldsForVisualize({
|
||||
selectedStream: ["default"],
|
||||
selectedStreamFields: [{ name: "_timestamp" }],
|
||||
interestingFieldList: [],
|
||||
quickMode: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2026 OpenObserve Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Decides whether stream fields must be (re)extracted before building the
|
||||
* query for the logs visualization (Timechart) view.
|
||||
*
|
||||
* On page reload the visualize toggle can run before async field loading has
|
||||
* finished. Building the query too early produces SELECT * — which is
|
||||
* rejected for visualization — and leaves the panel without axis fields.
|
||||
* Two readiness signals matter:
|
||||
* - selectedStreamFields: schema fields for the selected stream(s)
|
||||
* - interestingFieldList: fields used by buildSearch() in quick mode
|
||||
*/
|
||||
export const shouldReloadStreamFieldsForVisualize = (state: {
|
||||
selectedStream: unknown[];
|
||||
selectedStreamFields: unknown[];
|
||||
interestingFieldList: string[];
|
||||
quickMode: boolean;
|
||||
}): boolean => {
|
||||
if (!state.selectedStream?.length) return false;
|
||||
|
||||
if (!state.selectedStreamFields?.length) return true;
|
||||
|
||||
return state.quickMode && !state.interestingFieldList?.length;
|
||||
};
|
||||
Loading…
Reference in New Issue