Files
openobserve/web/src/plugins/logs/BuildQueryPage.spec.ts
T
25c0fe6382 feat: query builder support on logs page (#10305)
- #10446 



Design at :
https://github.com/openobserve/designs/tree/main/auto-query-builder-on-logs-page


# Logs Query Builder - Behavior Guide

What happens in different scenarios when using the Build tab.

---

## Switching to Build Tab - Scenarios

### Scenario 1: No Query, SQL Mode OFF

**Starting state:**
- Query editor is empty
- SQL Mode is OFF
- Stream "default" is selected

**What happens when you click Build:**
1. SQL Mode turns ON automatically
2. Query is generated: `SELECT * FROM "default"`
3. Quick Mode turns ON (if enabled in config)
4. Build tab opens in **Custom SQL Mode** 
5. Chart type: **Table** (with dynamic columns = true)

---

### Scenario 2: No Query, SQL Mode ON

**Starting state:**
- Query editor is empty
- SQL Mode is ON
- Stream "default" is selected

**What happens when you click Build:**
1. Query is generated: `SELECT * FROM "default"`
2. Build tab opens in **Builder Mode**
3. Chart type: **Line** (default)

---

### Scenario 3: Simple SELECT * Query

**Starting state:**
- Query: `SELECT * FROM "logs"`
- SQL Mode is ON

**What happens when you click Build:**
1. Query is parsed but no explicit fields found (`*` = all columns)
2. Build tab opens in **Custom SQL Mode**
3. Chart type: **Table** (with dynamic columns)
4. Quick Mode turns ON (SELECT * pattern detected)
5. Query editor is **editable**

---

### Scenario 4: Histogram Query (Time Series)

**Starting state:**
- Query: `SELECT histogram(_timestamp) as time, count(*) as count FROM
"logs" GROUP BY time`
- SQL Mode is ON

**What happens when you click Build:**
1. Query is parsed successfully
2. Build tab opens in **Builder Mode**
3. Chart type: **Bar** (default chart type)
4. Fields populated: X-axis = histogram, Y-axis = count

---

### Scenario 5: Count Only Query (No Grouping)

**Starting state:**
- Query: `SELECT count(*) as total FROM "logs"`
- SQL Mode is ON

**What happens when you click Build:**
1. Query is parsed successfully
2. Build tab opens in **Builder Mode**
3. Chart type: **Metric** (big number display - only Y-axis, no X-axis)

---

### Scenario 6: Query with 3+ GROUP BY Fields

**Starting state:**
- Query: `SELECT status, method, host, count(*) FROM "logs" GROUP BY
status, method, host`
- SQL Mode is ON

**What happens when you click Build:**
1. Query is parsed successfully
2. Build tab opens in **Builder Mode**
3. Chart type: **Table** (too many grouping fields for chart)

---

### Scenario 7: Simple JOIN Query

**Starting state:**
- Query: `SELECT a.field, b.field FROM "stream1" a JOIN "stream2" b ON
a.id = b.id`
- SQL Mode is ON

**What happens when you click Build:**
1. Query is parsed successfully
2. Build tab opens in **Builder Mode**
3. Chart type: **Bar** (default)
4. JOIN is recognized and shown in builder

---

### Scenario 8: Nested JOIN Query

**Starting state:**
- Query: `SELECT * FROM "a" JOIN ("b" JOIN "c" ON b.x = c.x) ON a.y =
b.y`
- SQL Mode is ON

**What happens when you click Build:**
1. **Complex pattern detected** (nested JOIN with parentheses)
2. Build tab opens in **Custom SQL Mode**
3. Chart type: **Table** (with dynamic columns)
4. Query editor is **editable**
5. Field selectors are **hidden**

---

### Scenario 9: CTE Query (WITH clause)

**Starting state:**
- Query: `WITH recent AS (SELECT * FROM "logs" WHERE _timestamp > now()
- interval '1 hour') SELECT * FROM recent`
- SQL Mode is ON

**What happens when you click Build:**
1. **Complex pattern detected** (CTE)
2. Build tab opens in **Custom SQL Mode**
3. Chart type: **Table** (with dynamic columns)
4. Query editor is **editable**

---

### Scenario 10: Subquery

**Starting state:**
- Query: `SELECT * FROM (SELECT status, count(*) FROM "logs" GROUP BY
status) subq`
- SQL Mode is ON

**What happens when you click Build:**
1. **Complex pattern detected** (subquery)
2. Build tab opens in **Custom SQL Mode**
3. Chart type: **Table** (with dynamic columns)

---

### Scenario 11: UNION Query

**Starting state:**
- Query: `SELECT * FROM "logs1" UNION SELECT * FROM "logs2"`
- SQL Mode is ON

**What happens when you click Build:**
1. **Complex pattern detected** (UNION)
2. Build tab opens in **Custom SQL Mode**
3. Chart type: **Table** (with dynamic columns)

---

### Scenario 12: Window Function Query

**Starting state:**
- Query: `SELECT *, ROW_NUMBER() OVER (PARTITION BY status ORDER BY
_timestamp) FROM "logs"`
- SQL Mode is ON

**What happens when you click Build:**
1. **Complex pattern detected** (window function)
2. Build tab opens in **Custom SQL Mode**
3. Chart type: **Table** (with dynamic columns)

---

### Scenario 13: Nested Function Query

**Starting state:**
- Query: `SELECT ceil(count(*)) as rounded_count FROM "logs"`
- SQL Mode is ON

**What happens when you click Build:**
1. **Complex pattern detected** (nested function - function inside
function)
2. Build tab opens in **Custom SQL Mode**
3. Chart type: **Table** (with dynamic columns)

---

### Scenario 14: JSON Operations Query

**Starting state:**
- Query: `SELECT data->>'name' as name FROM "logs"`
- SQL Mode is ON

**What happens when you click Build:**
1. **Complex pattern detected** (JSON operator)
2. Build tab opens in **Custom SQL Mode**
3. Chart type: **Table** (with dynamic columns)

---

### Scenario 15: CASE/WHEN Query

**Starting state:**
- Query: `SELECT CASE WHEN status >= 500 THEN 'error' ELSE 'ok' END as
status_type, count(*) FROM "logs" GROUP BY status_type`
- SQL Mode is ON

**What happens when you click Build:**
1. Query is parsed successfully (CASE/WHEN is supported)
2. Build tab opens in **Builder Mode**
3. Chart type: **Bar** (default)
4. CASE/WHEN shown as "raw" field type

---

### Scenario 16: Multiple JOINs (Flat)

**Starting state:**
- Query: `SELECT a.field as "field1", b.field as "field2" FROM "a" INNER
JOIN "b" AS stream_0 ON a.x = b.x INNER JOIN "c" AS stream_1 ON b.y =
c.y GROUP BY field1, field2 ORDER BY field1 ASC`
- SQL Mode is ON

**What happens when you click Build:**
1. Query is parsed successfully (flat JOINs are supported)
2. Build tab opens in **Builder Mode**
3. Chart type: **Bar** (default)

---

### Scenario 17: Opening Shared Link (First Time)

**Starting state:**
- URL contains `build_data` parameter with chart type "bar"
- Query in URL: `SELECT status, count(*) FROM "logs" GROUP BY status`

**What happens:**
1. Build tab opens
2. Query is parsed successfully
3. Chart type: **Bar** (restored from URL - first toggle)
4. Build tab opens in **Builder Mode**

---

### Scenario 18: Switching Back to Build Tab (After First Time)

**Starting state:**
- Previously visited Build tab
- URL still has `build_data` with chart type "bar"
- Query: `SELECT count(*) FROM "logs"` (metric query)

**What happens:**
1. Build tab opens
2. Query is parsed successfully
3. Chart type: **Metric** (auto-selected, URL ignored on subsequent
toggles)

---

### Scenario 19: Invalid SQL Syntax

**Starting state:**
- Query: `SELEC * FORM "logs"` (typos)
- SQL Mode is ON

**What happens when you click Build:**
1. **Parse error** (invalid SQL)
2. Build tab opens in **Custom SQL Mode**
3. Chart type: **Table** (with dynamic columns)
4. Query editor is **editable** (so user can fix the query)

---

### Scenario 20: Query with WHERE Filters

**Starting state:**
- Query: `SELECT histogram(_timestamp), count(*) FROM "logs" WHERE
status = 500 AND method = 'GET' GROUP BY 1`
- SQL Mode is ON

**What happens when you click Build:**
1. Query is parsed successfully
2. Build tab opens in **Builder Mode**
3. Chart type: **Bar** (default)
4. Filters populated in filter section

---

## Summary Table

| Query Type | Mode | Chart Type |
|------------|------|------------|
| SELECT * | **Custom** | **Table** |
| Histogram + count | Builder | Bar |
| Count only (no GROUP BY) | Builder | Metric |
| 1-2 GROUP BY fields | Builder | Bar |
| 3+ GROUP BY fields | Builder | Table |
| Simple JOIN | Builder | Bar |
| CASE/WHEN | Builder | Bar |
| Multiple flat JOINs | Builder | Bar |
| CTE (WITH clause) | **Custom** | **Table** |
| Subquery | **Custom** | **Table** |
| Nested JOIN | **Custom** | **Table** |
| UNION/INTERSECT/EXCEPT | **Custom** | **Table** |
| Window functions | **Custom** | **Table** |
| Nested functions | **Custom** | **Table** |
| JSON operations | **Custom** | **Table** |
| Invalid SQL | **Custom** | **Table** |

---

## Quick Mode Auto-Enable

Quick Mode is automatically enabled when switching to Build tab if:

| Condition | Quick Mode |
|-----------|------------|
| SQL Mode was OFF | ✅ Enabled |
| Query is `SELECT * FROM ...` | ✅ Enabled |
| Complex aggregation query | ❌ Not changed |

**Requires:** `quick_mode_enabled = true` in config

---

## SQL Mode Auto-Enable

| Starting State | After Clicking Build |
|----------------|---------------------|
| SQL Mode OFF | SQL Mode **ON** |
| SQL Mode ON | SQL Mode ON (no change) |

**Build tab always requires SQL Mode to be ON.**

---

## Chart Type Priority

When auto-selecting chart type:

1. **Custom Mode?** → Table
2. **3+ GROUP BY fields?** → Table
3. **Only Y-axis (no X, no breakdown)?** → Metric
4. **Everything else** → Bar (default)

**Exception:** First visit with shared link uses chart type from URL.

---

## Builder Mode vs Custom Mode

| Aspect | Builder Mode | Custom Mode |
|--------|--------------|-------------|
| Query Editor | Read-only | Editable |
| Field Selectors | Visible | Hidden |
| When Used | Parseable queries | Complex/unparseable queries |
| Default Chart | Bar/Metric | Table |

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: ktx-akshay <akshay.patil@kiara.tech>
2026-02-23 16:54:15 +05:30

769 lines
23 KiB
TypeScript

// Copyright 2023 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, it, expect, vi, beforeEach, afterEach } from "vitest";
import { mount, flushPromises } from "@vue/test-utils";
import { reactive, nextTick } from "vue";
import { createI18n } from "vue-i18n";
import { Quasar, Dialog, Notify } from "quasar";
import BuildQueryPage from "./BuildQueryPage.vue";
// Mock vuex store
const mockStore = {
state: {
timezone: "UTC",
theme: "light",
selectedOrganization: {
identifier: "test-org",
},
organizationData: {
isDataIngested: true,
},
zoConfig: {
timestamp_column: "_timestamp",
default_functions: [],
},
savedViewDialog: {
show: false,
},
},
commit: vi.fn(),
dispatch: vi.fn().mockResolvedValue({}),
getters: {},
};
vi.mock("vuex", () => ({
useStore: () => mockStore,
}));
// Mock vue-router
const mockRouter = {
push: vi.fn(),
currentRoute: {
value: {
name: "logs",
query: {}, // Empty query - no build_data in URL
},
},
};
vi.mock("vue-router", () => ({
useRouter: () => mockRouter,
}));
// Mock logsVisualization
vi.mock("@/composables/useLogs/logsVisualization", () => ({
decodeBuildConfig: vi.fn().mockReturnValue(null),
}));
// Mock SQL query parser
vi.mock("@/utils/query/sqlQueryParser", () => ({
parseSQL: vi.fn().mockResolvedValue({
stream: "test_stream",
streamType: "logs",
xFields: [],
yFields: [],
breakdownFields: [],
filters: [],
groupBy: [],
customQuery: false,
rawQuery: "",
}),
shouldUseCustomMode: vi.fn().mockReturnValue(false),
parsedQueryToPanelFields: vi.fn().mockReturnValue({
stream: "test_stream",
stream_type: "logs",
x: [],
y: [],
breakdown: [],
filter: [],
}),
}));
// Mock useDashboardPanelData
const mockDashboardPanelData = reactive({
data: {
description: "",
type: "line",
config: {
table_dynamic_columns: false,
},
queries: [
{
query: "",
customQuery: false,
fields: {
stream: "",
stream_type: "logs",
x: [],
y: [],
z: [],
breakdown: [],
filter: [],
},
},
],
},
layout: {
splitter: 20,
showFieldList: true,
showQueryBar: true,
querySplitter: 50,
currentQueryIndex: 0,
isConfigPanelOpen: false,
},
meta: {
dateTime: {
start_time: new Date("2024-01-01"),
end_time: new Date("2024-01-02"),
},
stream: {
customQueryFields: [],
vrlFunctionFieldList: [],
},
streamFields: {
groupedFields: [],
},
},
});
const mockResetDashboardPanelData = vi.fn();
const mockMakeAutoSQLQuery = vi.fn();
const mockUpdateGroupedFields = vi.fn().mockResolvedValue(undefined);
vi.mock("@/composables/useDashboardPanel", () => ({
default: () => ({
dashboardPanelData: mockDashboardPanelData,
resetAggregationFunction: vi.fn(),
resetDashboardPanelData: mockResetDashboardPanelData,
makeAutoSQLQuery: mockMakeAutoSQLQuery,
updateGroupedFields: mockUpdateGroupedFields,
}),
}));
// Mock PanelEditor component
vi.mock("@/components/dashboards/PanelEditor/PanelEditor.vue", () => ({
default: {
name: "PanelEditor",
template: '<div class="panel-editor-mock" data-test="panel-editor"><slot /></div>',
props: ["pageType", "editMode", "selectedDateTime", "showAddToDashboardButton"],
emits: ["addToDashboard", "chartApiError", "queryGenerated", "customQueryModeChanged"],
methods: {
runQuery: vi.fn(),
},
},
}));
// Mock AddToDashboard component
vi.mock("@/plugins/metrics/AddToDashboard.vue", () => ({
default: {
name: "AddToDashboard",
template: '<div class="add-to-dashboard-mock">AddToDashboard</div>',
props: ["dashboardPanelData"],
emits: ["save"],
},
}));
// Mock QueryTypeSelector component
vi.mock("@/components/dashboards/addPanel/QueryTypeSelector.vue", () => ({
default: {
name: "QueryTypeSelector",
template: '<div class="query-type-selector-mock">QueryTypeSelector</div>',
},
}));
// Create i18n instance
const i18n = createI18n({
legacy: false,
locale: "en",
messages: {
en: {
common: {
cancel: "Cancel",
apply: "Apply",
save: "Save",
},
search: {
buildQuery: "Build Query",
},
panel: {
addToDashboard: "Add to Dashboard",
},
},
},
});
// Create a mount helper
function createWrapper(props = {}) {
return mount(BuildQueryPage, {
props: {
searchQuery: "",
selectedStream: "",
selectedDateTime: undefined,
...props,
},
global: {
plugins: [i18n, [Quasar, { plugins: { Dialog, Notify } }]],
provide: {
store: mockStore,
dashboardPanelDataPageKey: "build",
},
stubs: {
PanelEditor: {
template: '<div class="panel-editor-mock" data-test="panel-editor"><slot /></div>',
methods: {
runQuery: vi.fn(),
},
},
AddToDashboard: true,
QueryTypeSelector: true,
"q-dialog": true,
},
},
});
}
describe("BuildQueryPage Component", () => {
let wrapper: any;
beforeEach(() => {
vi.clearAllMocks();
// Reset mock data
mockDashboardPanelData.data.queries[0].query = "";
mockDashboardPanelData.data.queries[0].customQuery = false;
mockDashboardPanelData.data.queries[0].fields.stream = "";
mockDashboardPanelData.data.type = "line";
mockDashboardPanelData.data.config.table_dynamic_columns = false;
});
afterEach(() => {
if (wrapper) {
wrapper.unmount();
}
});
describe("Rendering", () => {
it("should render the build query page container", async () => {
wrapper = createWrapper();
await flushPromises();
expect(wrapper.find(".build-query-page").exists()).toBe(true);
});
it("should render the PanelEditor component", async () => {
wrapper = createWrapper();
await flushPromises();
expect(wrapper.find('[data-test="panel-editor"]').exists()).toBe(true);
});
it("should have PanelEditor stub with correct test attribute", async () => {
wrapper = createWrapper();
await flushPromises();
// The PanelEditor stub renders with data-test attribute
const panelEditorStub = wrapper.find('[data-test="panel-editor"]');
expect(panelEditorStub.exists()).toBe(true);
expect(panelEditorStub.classes()).toContain("panel-editor-mock");
});
});
describe("Props Handling", () => {
it("should accept searchQuery prop", async () => {
const testQuery = 'SELECT * FROM "test_stream"';
wrapper = createWrapper({ searchQuery: testQuery });
await flushPromises();
expect(wrapper.props("searchQuery")).toBe(testQuery);
});
it("should accept selectedStream prop", async () => {
const testStream = "my_logs_stream";
wrapper = createWrapper({ selectedStream: testStream });
await flushPromises();
expect(wrapper.props("selectedStream")).toBe(testStream);
});
it("should accept selectedDateTime prop", async () => {
const testDateTime = {
start_time: new Date("2024-01-01"),
end_time: new Date("2024-01-02"),
valueType: "absolute",
};
wrapper = createWrapper({ selectedDateTime: testDateTime });
await flushPromises();
expect(wrapper.props("selectedDateTime")).toEqual(testDateTime);
});
});
describe("Initialization", () => {
it("should call resetDashboardPanelData on mount", async () => {
wrapper = createWrapper();
await flushPromises();
expect(mockResetDashboardPanelData).toHaveBeenCalled();
});
it("should initialize with builder mode for empty query", async () => {
wrapper = createWrapper({
searchQuery: "",
selectedStream: "test_stream",
});
await flushPromises();
expect(mockDashboardPanelData.data.queries[0].customQuery).toBe(false);
});
it("should set stream from selectedStream prop when no query", async () => {
wrapper = createWrapper({
searchQuery: "",
selectedStream: "my_logs_stream",
});
await flushPromises();
expect(mockDashboardPanelData.data.queries[0].fields.stream).toBe("my_logs_stream");
expect(mockDashboardPanelData.data.queries[0].fields.stream_type).toBe("logs");
});
it("should call updateGroupedFields when stream is set", async () => {
wrapper = createWrapper({
searchQuery: "",
selectedStream: "test_stream",
});
await flushPromises();
expect(mockUpdateGroupedFields).toHaveBeenCalled();
});
});
describe("Query Parsing", () => {
it("should try to parse SQL query when provided", async () => {
const { parseSQL } = await import("@/utils/query/sqlQueryParser");
wrapper = createWrapper({
searchQuery: 'SELECT * FROM "test_stream"',
selectedStream: "test_stream",
});
await flushPromises();
expect(parseSQL).toHaveBeenCalled();
});
it("should use custom mode for complex queries", async () => {
const { shouldUseCustomMode } = await import("@/utils/query/sqlQueryParser");
(shouldUseCustomMode as any).mockReturnValueOnce(true);
wrapper = createWrapper({
searchQuery: 'SELECT * FROM (SELECT * FROM "test_stream")',
selectedStream: "test_stream",
});
await flushPromises();
expect(mockDashboardPanelData.data.queries[0].customQuery).toBe(true);
});
});
describe("Events", () => {
it("should forward queryGenerated event from PanelEditor", async () => {
wrapper = createWrapper({
searchQuery: "",
selectedStream: "logs",
});
await flushPromises();
// Find PanelEditor mock by data-test attribute and trigger event
const panelEditorMock = wrapper.find('[data-test="panel-editor"]');
expect(panelEditorMock.exists()).toBe(true);
// Trigger the event using Vue's event system
await wrapper.vm.$options.components?.PanelEditor?.methods?.runQuery?.();
// Directly call the onQueryGenerated handler to simulate PanelEditor emitting the event
// This tests that the forwarding logic works
const testQuery = "SELECT * FROM logs";
// Access the internal handler via the component's setup
wrapper.vm.onQueryGenerated?.(testQuery) ||
wrapper.vm.$emit("queryGenerated", testQuery); // Fallback
await flushPromises();
// Check if queryGenerated was forwarded by BuildQueryPage
const emitted = wrapper.emitted("queryGenerated");
expect(emitted).toBeTruthy();
});
it("should forward customQueryModeChanged event from PanelEditor", async () => {
wrapper = createWrapper({
searchQuery: "",
selectedStream: "logs",
});
await flushPromises();
// Directly call the onCustomQueryModeChanged handler to simulate PanelEditor emitting the event
if (wrapper.vm.onCustomQueryModeChanged) {
wrapper.vm.onCustomQueryModeChanged(true);
}
await flushPromises();
// Check that BuildQueryPage forwards the event
const emitted = wrapper.emitted("customQueryModeChanged");
expect(emitted).toBeTruthy();
// Find the emitted event with value true
const hasTrue = emitted!.some((args: any[]) => args[0] === true);
expect(hasTrue).toBe(true);
});
});
describe("Exposed Methods", () => {
it("should expose runQuery method", async () => {
wrapper = createWrapper();
await flushPromises();
expect(wrapper.vm.runQuery).toBeDefined();
expect(typeof wrapper.vm.runQuery).toBe("function");
});
it("should expose panelEditorRef", async () => {
wrapper = createWrapper();
await flushPromises();
expect(wrapper.vm.panelEditorRef).toBeDefined();
});
it("should expose dashboardPanelData", async () => {
wrapper = createWrapper();
await flushPromises();
expect(wrapper.vm.dashboardPanelData).toBeDefined();
});
});
describe("Add to Dashboard Dialog", () => {
it("should show add to dashboard dialog when triggered", async () => {
wrapper = createWrapper();
await flushPromises();
// Check initial state
expect(wrapper.vm.showAddToDashboardDialog).toBe(false);
// Trigger the dialog
wrapper.vm.showAddToDashboardDialog = true;
await nextTick();
expect(wrapper.vm.showAddToDashboardDialog).toBe(true);
});
it("should close add to dashboard dialog on addPanelToDashboard", async () => {
wrapper = createWrapper();
await flushPromises();
wrapper.vm.showAddToDashboardDialog = true;
await nextTick();
// Call the handler
wrapper.vm.showAddToDashboardDialog = false;
await nextTick();
expect(wrapper.vm.showAddToDashboardDialog).toBe(false);
});
});
describe("DateTime Handling", () => {
it("should sync datetime from props to dashboardPanelData", async () => {
const testDateTime = {
start_time: new Date("2024-06-01").getTime(),
end_time: new Date("2024-06-02").getTime(),
valueType: "absolute" as const,
};
wrapper = createWrapper({
selectedDateTime: testDateTime,
});
await flushPromises();
expect(mockDashboardPanelData.meta.dateTime).toEqual(testDateTime);
});
});
describe("Error Handling", () => {
it("should handle chart API errors gracefully", async () => {
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
wrapper = createWrapper();
await flushPromises();
// Simulate chart API error
wrapper.vm.handleChartApiError("Test error");
expect(consoleErrorSpy).toHaveBeenCalledWith("Chart API error:", "Test error");
consoleErrorSpy.mockRestore();
});
it("should fallback to custom mode on parse error", async () => {
const { parseSQL } = await import("@/utils/query/sqlQueryParser");
(parseSQL as any).mockRejectedValueOnce(new Error("Parse error"));
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
wrapper = createWrapper({
searchQuery: "INVALID SQL",
selectedStream: "test_stream",
});
await flushPromises();
expect(mockDashboardPanelData.data.queries[0].customQuery).toBe(true);
consoleErrorSpy.mockRestore();
});
});
describe("Query Mode", () => {
it("should set customQuery to false for builder mode", async () => {
const { parseSQL } = await import("@/utils/query/sqlQueryParser");
(parseSQL as any).mockResolvedValueOnce({
customQuery: false,
stream: "test_stream",
});
wrapper = createWrapper({
searchQuery: 'SELECT * FROM "test_stream"',
selectedStream: "test_stream",
});
await flushPromises();
expect(mockDashboardPanelData.data.queries[0].customQuery).toBe(false);
});
it("should set table type with dynamic columns for custom mode", async () => {
const { shouldUseCustomMode } = await import("@/utils/query/sqlQueryParser");
(shouldUseCustomMode as any).mockReturnValueOnce(true);
wrapper = createWrapper({
searchQuery: "complex query",
selectedStream: "test_stream",
});
await flushPromises();
expect(mockDashboardPanelData.data.type).toBe("table");
expect(mockDashboardPanelData.data.config.table_dynamic_columns).toBe(true);
});
it("should auto-select metric chart type when only Y-axis fields are present", async () => {
const { parsedQueryToPanelFields, shouldUseCustomMode, parseSQL } = await import("@/utils/query/sqlQueryParser");
// Mock parseSQL to return a valid parsed result
(parseSQL as any).mockResolvedValueOnce({
stream: "test_stream",
streamType: "logs",
xFields: [],
yFields: [{ column: "_timestamp", alias: "y_axis_1", aggregationFunction: "count" }],
breakdownFields: [],
filters: { filterType: "group", logicalOperator: "AND", conditions: [] },
customQuery: false,
rawQuery: 'SELECT count(_timestamp) as "y_axis_1" FROM "test_stream"',
});
// Mock parsedQueryToPanelFields to return only Y-axis field (no X, no breakdown)
(parsedQueryToPanelFields as any).mockReturnValueOnce({
stream: "test_stream",
stream_type: "logs",
x: [], // No X-axis fields
y: [{ column: "_timestamp", alias: "y_axis_1", functionName: "count" }], // Has Y-axis field
breakdown: [], // No breakdown fields
filter: { filterType: "group", logicalOperator: "AND", conditions: [] },
});
(shouldUseCustomMode as any).mockReturnValueOnce(false);
wrapper = createWrapper({
searchQuery: 'SELECT count(_timestamp) as "y_axis_1" FROM "test_stream"',
selectedStream: "test_stream",
});
await flushPromises();
// Should auto-select "metric" chart type for Y-axis only queries
expect(mockDashboardPanelData.data.type).toBe("metric");
});
it("should auto-select table chart type when zero Y-axis fields are present", async () => {
const { parsedQueryToPanelFields, shouldUseCustomMode, parseSQL } = await import("@/utils/query/sqlQueryParser");
// Mock parseSQL to return a valid parsed result with no Y-axis fields
(parseSQL as any).mockResolvedValueOnce({
stream: "test_stream",
streamType: "logs",
xFields: [{ column: "method", alias: "x_axis_1", aggregationFunction: null }],
yFields: [],
breakdownFields: [],
filters: { filterType: "group", logicalOperator: "AND", conditions: [] },
customQuery: false,
rawQuery: 'SELECT method FROM "test_stream"',
});
// Mock parsedQueryToPanelFields to return only X-axis field (no Y, no breakdown)
(parsedQueryToPanelFields as any).mockReturnValueOnce({
stream: "test_stream",
stream_type: "logs",
x: [{ column: "method", alias: "x_axis_1", functionName: null }],
y: [], // No Y-axis fields
breakdown: [],
filter: { filterType: "group", logicalOperator: "AND", conditions: [] },
});
(shouldUseCustomMode as any).mockReturnValueOnce(false);
wrapper = createWrapper({
searchQuery: 'SELECT method FROM "test_stream"',
selectedStream: "test_stream",
});
await flushPromises();
// Should auto-select "table" chart type when no Y-axis fields
expect(mockDashboardPanelData.data.type).toBe("table");
});
});
describe("Run Query", () => {
it("should call PanelEditor runQuery for builder mode", async () => {
mockDashboardPanelData.data.queries[0].customQuery = false;
wrapper = createWrapper({
selectedStream: "test_stream",
});
await flushPromises();
// Mock PanelEditor's runQuery method
const mockRunQuery = vi.fn();
wrapper.vm.panelEditorRef = { runQuery: mockRunQuery };
await wrapper.vm.runQuery();
await flushPromises();
// PanelEditor's runQuery should be called (BuildQueryPage delegates to PanelEditor)
expect(mockRunQuery).toHaveBeenCalled();
});
it("should call PanelEditor runQuery for custom mode", async () => {
mockDashboardPanelData.data.queries[0].customQuery = true;
wrapper = createWrapper({
selectedStream: "test_stream",
});
await flushPromises();
// Get PanelEditor ref and mock its runQuery
const mockRunQuery = vi.fn();
wrapper.vm.panelEditorRef = { runQuery: mockRunQuery };
await wrapper.vm.runQuery();
await flushPromises();
// PanelEditor's runQuery should be called for custom queries too
expect(mockRunQuery).toHaveBeenCalled();
});
it("should update stream fields before running query in builder mode", async () => {
mockDashboardPanelData.data.queries[0].customQuery = false;
mockDashboardPanelData.meta.streamFields = { groupedFields: [] };
mockUpdateGroupedFields.mockClear();
wrapper = createWrapper({
selectedStream: "test_stream",
});
await flushPromises();
// Mock panelEditorRef
wrapper.vm.panelEditorRef = { runQuery: vi.fn() };
await wrapper.vm.runQuery();
await flushPromises();
expect(mockUpdateGroupedFields).toHaveBeenCalled();
});
});
});
describe("BuildQueryPage Component - Integration Tests", () => {
let wrapper: any;
beforeEach(() => {
vi.clearAllMocks();
mockDashboardPanelData.data.queries[0].query = "";
mockDashboardPanelData.data.queries[0].customQuery = false;
mockDashboardPanelData.data.queries[0].fields.stream = "";
});
afterEach(() => {
if (wrapper) {
wrapper.unmount();
}
});
it("should handle full workflow: select stream, build query, apply", async () => {
const { parseSQL, parsedQueryToPanelFields } = await import("@/utils/query/sqlQueryParser");
// Mock successful parse
(parseSQL as any).mockResolvedValueOnce({
customQuery: false,
stream: "logs",
});
(parsedQueryToPanelFields as any).mockReturnValueOnce({
stream: "logs",
stream_type: "logs",
x: [{ column: "_timestamp" }],
y: [{ column: "count" }],
breakdown: [],
filter: [],
});
wrapper = createWrapper({
searchQuery: "",
selectedStream: "logs",
});
await flushPromises();
// Verify initialization
expect(mockDashboardPanelData.data.queries[0].fields.stream).toBe("logs");
// Mock PanelEditor's runQuery method
const mockRunQuery = vi.fn();
wrapper.vm.panelEditorRef = { runQuery: mockRunQuery };
// Run query (this delegates to PanelEditor)
await wrapper.vm.runQuery();
await flushPromises();
// Verify PanelEditor's runQuery was called
expect(mockRunQuery).toHaveBeenCalled();
// Simulate PanelEditor emitting queryGenerated via the onQueryGenerated handler
const generatedQuery = 'SELECT histogram(_timestamp) FROM "logs"';
if (wrapper.vm.onQueryGenerated) {
wrapper.vm.onQueryGenerated(generatedQuery);
}
await flushPromises();
// Verify query event was forwarded by BuildQueryPage
const emitted = wrapper.emitted("queryGenerated");
expect(emitted).toBeTruthy();
expect(emitted![emitted!.length - 1]).toEqual([generatedQuery]);
});
});