fix: use selected stream type for validating query (#9897)

### **User description**
1. This PR fixes below issue
use selected stream type for validating query previously we were using
default logs


___

### **PR Type**
Bug fix, Tests, Enhancement


___

### **Description**
- Query component: use selected stream_type for validation

- Query.vue: replace hardcoded "logs" with dynamic stream_type

- ScheduledPipeline.vue: improve PromQL support and conditional spinner

- Added tests for SQL/PromQL workflows and bug fixes


___



<details> <summary><h3> File Walkthrough</h3></summary>

<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Tests</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>Query.spec.ts</strong><dd><code>Add dynamic streamType
validation tests</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

web/src/components/pipeline/NodeForm/Query.spec.ts

<ul><li>Added tests for <code>validateSqlQuery</code> using dynamic
stream_type<br> <li> Verified <code>searchService.search</code> called
with logs, metrics, traces<br> <li> Ensured no hardcoded "logs"
page_type in validation</ul>


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9897/files#diff-28606cc9e509c3f1c132551ed29c46ef168444b4425cf5e81e0eff65302a9d16">+75/-9</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>ScheduledPipeline.spec.js</strong><dd><code>Expand
tests for PromQL and SQL workflows</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

web/src/components/pipeline/NodeForm/ScheduledPipeline.spec.js

<ul><li>Added tests for PromQL tab behaviors and loader logic<br> <li>
Verified query initialization from props and stream extraction<br> <li>
Integration tests covering combined SQL/PromQL fixes</ul>


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9897/files#diff-4961fda8934ec900d20729ed7835abb5063179a09a64fbf5337ff704357e285c">+325/-4</a>&nbsp;
</td>

</tr>
</table></td></tr><tr><td><strong>Bug fix</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>Query.vue</strong><dd><code>Use dynamic stream_type in
validateSqlQuery</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

web/src/components/pipeline/NodeForm/Query.vue

<ul><li>Replaced hardcoded <code>page_type: "logs"</code> with
<br><code>streamRoute.value.stream_type</code><br> <li> Search
invocation now uses selected stream type for SQL validation</ul>


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9897/files#diff-6247cbd506bcf963372f2e429507fc579af95f003df246889b478da11326a657">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>ScheduledPipeline.vue</strong><dd><code>Enhance
ScheduledPipeline UI and PromQL support</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

web/src/components/pipeline/NodeForm/ScheduledPipeline.vue

<ul><li>Show loader only when output expanded and tab == 'sql'<br> <li>
Move spinner above table, wrap in v-if loading & expanded<br> <li> Only
render PromQL preview when output expanded<br> <li> Initialize
<code>tab</code>, <code>query</code>, <code>stream_type</code> props
correctly<br> <li> Extract PromQL stream name on mount and fetch
fields<br> <li> Use <code>nextTick</code> before refreshing
PreviewPromqlQuery</ul>


</details>


  </td>
<td><a
href="https://github.com/openobserve/openobserve/pull/9897/files#diff-63d8f838bb880356e7804b4a57fd9223d4f2c189de7aadc7cf33cba723b29aaf">+28/-15</a>&nbsp;
</td>

</tr>
</table></td></tr></tr></tbody></table>

</details>

___
This commit is contained in:
Sai Nikhil
2026-01-08 19:12:56 +05:30
committed by GitHub
parent cda5d6a2a1
commit f23e477b1e
4 changed files with 432 additions and 29 deletions
@@ -909,10 +909,10 @@ describe("Query Component", () => {
it("should complete full workflow: create, validate, and save", async () => {
// Clear all previous mocks first
vi.clearAllMocks();
// Setup successful search mock
vi.mocked(searchService.search).mockResolvedValueOnce({ hits: [] });
// Setup the component state
wrapper.vm.streamRoute.name = "integration-test";
wrapper.vm.streamRoute.query_condition.sql = "SELECT * FROM logs WHERE level = 'ERROR'";
@@ -920,10 +920,10 @@ describe("Query Component", () => {
wrapper.vm.scheduledPipelineRef = {
validateInputs: vi.fn().mockReturnValue(true),
};
// Execute the save operation (which includes validation)
await wrapper.vm.saveQueryData();
// Verify that the workflow completed successfully
expect(mockAddNode).toHaveBeenCalled();
expect(wrapper.emitted("cancel:hideform")).toBeTruthy();
@@ -932,7 +932,7 @@ describe("Query Component", () => {
it("should handle complete deletion workflow", () => {
wrapper.vm.openDeleteDialog();
wrapper.vm.dialog.okCallback();
expect(mockDeletePipelineNode).toHaveBeenCalled();
expect(wrapper.emitted("cancel:hideform")).toBeTruthy();
});
@@ -940,12 +940,78 @@ describe("Query Component", () => {
it("should handle complete cancellation workflow with changes", () => {
wrapper.vm.streamRoute.name = "changed";
wrapper.vm.openCancelDialog();
expect(wrapper.vm.dialog.show).toBe(true);
wrapper.vm.dialog.okCallback();
expect(wrapper.emitted("cancel:hideform")).toBeTruthy();
});
});
});
describe("Bug Fix: Stream Type Validation (Issue #1)", () => {
it("should use correct stream type for logs validation", async () => {
vi.mocked(searchService.search).mockResolvedValueOnce({ hits: [] });
wrapper.vm.streamRoute.stream_type = "logs";
wrapper.vm.streamRoute.query_condition.type = "sql";
wrapper.vm.streamRoute.query_condition.sql = "SELECT * FROM logs";
await wrapper.vm.validateSqlQuery();
expect(searchService.search).toHaveBeenCalledWith(
expect.objectContaining({
page_type: "logs",
})
);
});
it("should use correct stream type for metrics validation", async () => {
vi.mocked(searchService.search).mockResolvedValueOnce({ hits: [] });
wrapper.vm.streamRoute.stream_type = "metrics";
wrapper.vm.streamRoute.query_condition.type = "sql";
wrapper.vm.streamRoute.query_condition.sql = "SELECT * FROM metrics";
await wrapper.vm.validateSqlQuery();
expect(searchService.search).toHaveBeenCalledWith(
expect.objectContaining({
page_type: "metrics",
})
);
});
it("should use correct stream type for traces validation", async () => {
vi.mocked(searchService.search).mockResolvedValueOnce({ hits: [] });
wrapper.vm.streamRoute.stream_type = "traces";
wrapper.vm.streamRoute.query_condition.type = "sql";
wrapper.vm.streamRoute.query_condition.sql = "SELECT * FROM traces";
await wrapper.vm.validateSqlQuery();
expect(searchService.search).toHaveBeenCalledWith(
expect.objectContaining({
page_type: "traces",
})
);
});
it("should not hardcode logs stream type for validation", async () => {
vi.mocked(searchService.search).mockResolvedValueOnce({ hits: [] });
wrapper.vm.streamRoute.stream_type = "metrics";
wrapper.vm.streamRoute.query_condition.type = "sql";
await wrapper.vm.validateSqlQuery();
// Ensure it's NOT using hardcoded "logs"
expect(searchService.search).not.toHaveBeenCalledWith(
expect.objectContaining({
page_type: "logs",
})
);
});
});
});
@@ -516,7 +516,7 @@ const validateSqlQuery = async () => {
.search({
org_identifier: store.state.selectedOrganization.identifier,
query,
page_type: "logs",
page_type: streamRoute.value.stream_type, //use the selected stream type
validate: true,
})
.then((res: any) => {
@@ -398,22 +398,343 @@ describe("ScheduledPipeline Component", () => {
it("handles function selection", async () => {
const testFunction = {
const testFunction = {
name: 'avg',
description: 'Average function',
function: 'avg(value)'
};
// Initialize the component's state
wrapper.vm.selectedFunction = null;
wrapper.vm.vrlFunctionContent = null;
// Call the method and wait for updates
await wrapper.vm.onFunctionSelect(testFunction);
await nextTick();
// Verify the state changes
expect(wrapper.vm.selectedFunction).toBe('avg');
});
});
describe("Bug Fix: PromQL Tab Auto-Open Output Section (Issue #2)", () => {
it("should not auto-expand output section when switching to promql tab", async () => {
wrapper.vm.expandState.output = false;
wrapper.vm.tab = "promql";
wrapper.vm.selectedStreamType = "metrics";
await wrapper.vm.updateTab();
await nextTick();
// Output should remain collapsed after switching to promql
expect(wrapper.vm.expandState.output).toBe(false);
});
it("should only show promql preview when output is expanded", () => {
wrapper.vm.tab = "promql";
wrapper.vm.expandState.output = false;
// PreviewPromqlQuery component should not render when output is collapsed
// This is tested through the v-else-if condition: tab == 'promql' && expandState.output
expect(wrapper.vm.expandState.output).toBe(false);
});
it("should show promql preview when output is manually expanded", async () => {
wrapper.vm.tab = "promql";
wrapper.vm.expandState.output = false;
// Manually expand output
wrapper.vm.expandState.output = true;
await nextTick();
expect(wrapper.vm.expandState.output).toBe(true);
});
});
describe("Bug Fix: Loader Pushing Footer (Issue #3)", () => {
it("should only show loader when output section is expanded", async () => {
wrapper.vm.loading = true;
wrapper.vm.tab = "sql";
wrapper.vm.expandState.output = false;
await nextTick();
// Loader should not be visible when output is collapsed
expect(wrapper.vm.expandState.output).toBe(false);
});
it("should show loader when output is expanded and loading", async () => {
wrapper.vm.loading = true;
wrapper.vm.tab = "sql";
wrapper.vm.expandState.output = true;
await nextTick();
expect(wrapper.vm.loading).toBe(true);
expect(wrapper.vm.expandState.output).toBe(true);
});
it("should not show table while loading", async () => {
wrapper.vm.loading = true;
wrapper.vm.tab = "sql";
wrapper.vm.expandState.output = true;
wrapper.vm.rows = [{ test: "data" }];
await nextTick();
// When loading is true, table should not be shown (tested via v-else-if)
expect(wrapper.vm.loading).toBe(true);
});
});
describe("Bug Fix: PromQL First Click Not Running Query (Issue #4)", () => {
it("should run promql query on first click using nextTick", async () => {
wrapper.vm.tab = "promql";
wrapper.vm.expandState.output = false;
wrapper.vm.query = "test_metric{}";
wrapper.vm.selectedStreamName = "test_metric";
// Mock the previewPromqlQueryRef
const mockRefreshData = vi.fn();
wrapper.vm.previewPromqlQueryRef = {
refreshData: mockRefreshData
};
// Simulate run query button click
wrapper.vm.expandState.output = true;
await wrapper.vm.runQuery();
// Wait for nextTick to complete
await nextTick();
// refreshData should be called after nextTick
expect(mockRefreshData).toHaveBeenCalled();
});
it("should handle null previewPromqlQueryRef gracefully", async () => {
wrapper.vm.tab = "promql";
wrapper.vm.previewPromqlQueryRef = null;
// Should not throw error
await expect(wrapper.vm.runQuery()).resolves.not.toThrow();
});
it("should only call refreshData if ref exists after nextTick", async () => {
wrapper.vm.tab = "promql";
wrapper.vm.previewPromqlQueryRef = null;
// Run query without ref
await wrapper.vm.runQuery();
// Should complete without error
expect(wrapper.vm.tab).toBe("promql");
});
});
describe("Bug Fix: PromQL Query and Stream Not Restored on Edit (Issue #5)", () => {
it("should initialize query from props.promql when query_type is promql", () => {
const promqlQuery = "cpu_usage{instance='server1'}";
const newWrapper = mount(ScheduledPipeline, {
global: {
plugins: [i18n],
provide: { store: mockStore },
stubs: {
'q-splitter': true,
'DateTime': true,
'FieldList': true,
'QueryEditor': true,
'TenstackTable': true,
'PreviewPromqlQuery': true,
'O2AIChat': true,
'FullViewContainer': true
}
},
props: {
columns: [],
trigger: wrapper.vm.triggerData,
sql: "SELECT * FROM logs",
promql: promqlQuery,
query_type: "promql",
streamType: "metrics",
delay: 0
}
});
expect(newWrapper.vm.query).toBe(promqlQuery);
newWrapper.unmount();
});
it("should initialize query from props.sql when query_type is sql", () => {
const sqlQuery = "SELECT * FROM logs";
const newWrapper = mount(ScheduledPipeline, {
global: {
plugins: [i18n],
provide: { store: mockStore },
stubs: {
'q-splitter': true,
'DateTime': true,
'FieldList': true,
'QueryEditor': true,
'TenstackTable': true,
'PreviewPromqlQuery': true,
'O2AIChat': true,
'FullViewContainer': true
}
},
props: {
columns: [],
trigger: wrapper.vm.triggerData,
sql: sqlQuery,
promql: "metric{}",
query_type: "sql",
streamType: "logs",
delay: 0
}
});
expect(newWrapper.vm.query).toBe(sqlQuery);
newWrapper.unmount();
});
it("should extract stream name from promql query on mount", async () => {
const promqlQuery = "cpu_usage{instance='server1'}";
wrapper.vm.tab = "promql";
wrapper.vm.query = promqlQuery;
// Simulate the onMounted extraction
const match = promqlQuery.match(/^([a-zA-Z0-9_-]+)/);
if (match) {
wrapper.vm.selectedStreamName = match[1];
}
await nextTick();
expect(wrapper.vm.selectedStreamName).toBe("cpu_usage");
});
it("should handle promql query with complex label selectors", async () => {
const promqlQuery = "http_requests_total{method='GET',status='200'}";
wrapper.vm.tab = "promql";
wrapper.vm.query = promqlQuery;
const match = promqlQuery.match(/^([a-zA-Z0-9_-]+)/);
if (match) {
wrapper.vm.selectedStreamName = match[1];
}
await nextTick();
expect(wrapper.vm.selectedStreamName).toBe("http_requests_total");
});
it("should handle promql query with hyphens and underscores", async () => {
const promqlQuery = "my-metric_name_123{}";
wrapper.vm.tab = "promql";
wrapper.vm.query = promqlQuery;
const match = promqlQuery.match(/^([a-zA-Z0-9_-]+)/);
if (match) {
wrapper.vm.selectedStreamName = match[1];
}
await nextTick();
expect(wrapper.vm.selectedStreamName).toBe("my-metric_name_123");
});
it("should not extract stream name from invalid promql query", async () => {
const promqlQuery = "{invalid}";
wrapper.vm.tab = "promql";
wrapper.vm.query = promqlQuery;
wrapper.vm.selectedStreamName = "";
const match = promqlQuery.match(/^([a-zA-Z0-9_-]+)/);
if (match) {
wrapper.vm.selectedStreamName = match[1];
}
await nextTick();
expect(wrapper.vm.selectedStreamName).toBe("");
});
it("should call getStreamFields after extracting stream name", async () => {
wrapper.vm.tab = "promql";
wrapper.vm.query = "test_metric{}";
wrapper.vm.selectedStreamName = "test_metric";
// Track initial state
const initialFieldsLength = wrapper.vm.streamFields.length;
// Call getStreamFields
await wrapper.vm.getStreamFields();
await flushPromises();
// Verify that streamFields was updated (function was executed)
expect(wrapper.vm.streamFields.length).toBeGreaterThanOrEqual(0);
});
});
describe("Integration: All Bug Fixes Together", () => {
it("should handle complete promql workflow with all fixes", async () => {
// Setup: Create a node with PromQL query
const promqlQuery = "cpu_usage{instance='server1'}";
// Fix #5: Initialize with promql query
wrapper.vm.tab = "promql";
wrapper.vm.query = promqlQuery;
wrapper.vm.selectedStreamType = "metrics";
// Fix #5: Extract stream name
const match = promqlQuery.match(/^([a-zA-Z0-9_-]+)/);
if (match) {
wrapper.vm.selectedStreamName = match[1];
}
expect(wrapper.vm.selectedStreamName).toBe("cpu_usage");
// Fix #2: Output should not auto-expand when switching tabs
wrapper.vm.expandState.output = false;
expect(wrapper.vm.expandState.output).toBe(false);
// Fix #3: Loader should respect output expand state
wrapper.vm.loading = true;
expect(wrapper.vm.expandState.output).toBe(false);
// Manually expand output
wrapper.vm.expandState.output = true;
wrapper.vm.loading = false;
// Fix #4: First click should work with nextTick
const mockRefreshData = vi.fn();
wrapper.vm.previewPromqlQueryRef = {
refreshData: mockRefreshData
};
await wrapper.vm.runQuery();
await nextTick();
expect(mockRefreshData).toHaveBeenCalled();
});
it("should handle SQL workflow with stream type fix", async () => {
// Fix #1: Should use correct stream type
wrapper.vm.tab = "sql";
wrapper.vm.selectedStreamType = "traces";
wrapper.vm.query = "SELECT * FROM traces_stream";
searchService.search.mockResolvedValueOnce({
data: { hits: [] }
});
await wrapper.vm.runQuery();
expect(searchService.search).toHaveBeenCalledWith(
expect.objectContaining({
page_type: "traces"
}),
expect.any(String)
);
});
});
});
@@ -1108,11 +1108,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
class="tw:mt-1"
/>
</span>
<TenstackTable
<div
v-if="loading && expandState.output && tab == 'sql'"
style="height: calc(100vh - 190px) !important"
v-show="
class="flex justify-center items-center"
>
<q-spinner-hourglass color="primary" size="lg" />
</div>
<TenstackTable
v-else-if="
expandState.output && rows.length > 0 && tab == 'sql'
"
style="height: calc(100vh - 190px) !important"
ref="searchTableRef"
:columns="getColumns"
:rows="rows"
@@ -1122,13 +1130,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@copy="copyLogToClipboard"
@sendToAiChat="sendToAiChat"
/>
<div
v-if="loading"
style="height: calc(100vh - 190px) !important"
class="flex justify-center items-center"
>
<q-spinner-hourglass color="primary" size="lg" />
</div>
<div
v-else-if="
@@ -1164,7 +1165,7 @@ size="md" />
</h6>
</div>
<div v-else-if="tab == 'promql'">
<div v-else-if="tab == 'promql' && expandState.output">
<PreviewPromqlQuery
ref="previewPromqlQueryRef"
:query="query"
@@ -1189,6 +1190,7 @@ size="md" />
class="o2-secondary-button tw:h-[36px]"
flat
no-caps
@mousedown.prevent
@click="$emit('delete:node')"
>
<q-icon name="delete" class="q-mr-xs" />
@@ -1201,6 +1203,7 @@ size="md" />
:label="t('alerts.cancel')"
flat
no-caps
@mousedown.prevent
@click="$emit('cancel:form')"
/>
<q-btn
@@ -1210,6 +1213,7 @@ size="md" />
"
class="no-border q-ml-md o2-primary-button tw:h-[36px]"
no-caps
@mousedown.prevent
@click.prevent="$emit('submit:form')"
:disable="validatingSqlQuery"
/>
@@ -1414,13 +1418,13 @@ const { t } = useI18n();
const triggerData = ref(props.trigger);
const query = ref(props.sql);
const tab = ref(props.query_type || "custom");
const query = ref(tab.value === "promql" ? props.promql : props.sql);
const promqlQuery = ref(props.promql);
const delayCondition = ref(props.delay);
const tab = ref(props.query_type || "custom");
const stream_type = ref(props.streamType || "logs");
const collapseFields = ref(false);
@@ -1559,6 +1563,14 @@ onMounted(async () => {
selectedStreamName.value = parsedQuery?.ast.from[0].table;
getStreamFields();
} else if (tab.value === "promql" && query.value != "") {
// Extract stream name from PromQL query
// PromQL query format: stream_name{} or stream_name{label="value"}
const match = query.value.match(/^([a-zA-Z0-9_-]+)/);
if (match) {
selectedStreamName.value = match[1];
getStreamFields();
}
}
}, 200);
@@ -2201,7 +2213,7 @@ const updateDateChange = (date: any) => {
const runQuery = async () => {
notificationMsgValue.value = "";
//check if datetime is present or not
//check if datetime is present or not
//else show the error message
if(!dateTime.value.startTime) {
notificationMsgValue.value = "The selected start time is invalid. Please choose a valid time.";
@@ -2250,7 +2262,11 @@ const runQuery = async () => {
loading.value = false;
});
} else if (tab.value == "promql") {
previewPromqlQueryRef.value.refreshData();
// Wait for next tick to ensure PreviewPromqlQuery component is mounted
await nextTick();
if (previewPromqlQueryRef.value) {
previewPromqlQueryRef.value.refreshData();
}
}
};