mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 08:09:22 +00:00
- #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>
769 lines
23 KiB
TypeScript
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]);
|
|
});
|
|
|
|
});
|