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

![before animation: refresh → error toast → No
Data](https://raw.githubusercontent.com/DHRUV6029/openobserve/pr-12897-assets/before-refresh-bug.gif)

📹 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

![after animation: refresh → chart type and query restored → chart
renders](https://raw.githubusercontent.com/DHRUV6029/openobserve/pr-12897-assets/after-fix.gif)

📹 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):

![before: refresh breaks with SELECT * toast and No
Data](https://raw.githubusercontent.com/DHRUV6029/openobserve/pr-12897-assets/03-before-BUG-refresh-toast-and-no-data.png)

Final state after refresh (this branch):

![after: refresh restores h-bar chart with no
errors](https://raw.githubusercontent.com/DHRUV6029/openobserve/pr-12897-assets/06-after-FIXED-refresh-restores-chart.png)

Timechart renders before refresh (unfixed build):

![before: timechart
renders](https://raw.githubusercontent.com/DHRUV6029/openobserve/pr-12897-assets/01-before-timechart-renders.png)

H-bar renders before refresh (unfixed build):

![before: h-bar
renders](https://raw.githubusercontent.com/DHRUV6029/openobserve/pr-12897-assets/02-before-hbar-renders.png)

Timechart on this branch:

![after: timechart
renders](https://raw.githubusercontent.com/DHRUV6029/openobserve/pr-12897-assets/04-after-timechart-renders.png)

H-bar on this branch:

![after: h-bar
renders](https://raw.githubusercontent.com/DHRUV6029/openobserve/pr-12897-assets/05-after-hbar-renders.png)

</details>
This commit is contained in:
Dhruv Patel 2026-07-31 00:45:44 -07:00 committed by GitHub
parent 4772df1723
commit e85baca92d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 398 additions and 59 deletions

View File

@ -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;
}

View File

@ -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 = {

View File

@ -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
) {

View File

@ -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);
});
});

View File

@ -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;
};