diff --git a/frontend/src/stores/explore.ts b/frontend/src/stores/explore.ts index 1b46943b..fc1bdb26 100644 --- a/frontend/src/stores/explore.ts +++ b/frontend/src/stores/explore.ts @@ -19,6 +19,7 @@ import type { DateValue } from "@internationalized/date"; import { now, getLocalTimeZone, CalendarDateTime } from "@internationalized/date"; import { useSourcesStore } from "./sources"; import { useTeamsStore } from "@/stores/teams"; +import { useLiveLogStore } from "@/stores/liveLog"; import { useBaseStore } from "./base"; import { QueryService } from '@/services/QueryService' import { parseRelativeTimeString } from "@/utils/time"; @@ -28,6 +29,8 @@ import { type TimeRange } from '@/types/query'; import { getErrorMessage } from '@/api/types'; import { HistogramService, type HistogramData } from '@/services/HistogramService'; +const liveLogStore = useLiveLogStore(); + // Helper function to get formatted table name export function getFormattedTableName(source: any): string { if (!source) { @@ -811,36 +814,7 @@ export const useExploreStore = defineStore("explore", () => { const response = await state.callApi({ apiCall: async () => exploreApi.getLogs(state.data.value.sourceId, params, currentTeamId), // Update results ONLY on successful API call with data - onSuccess: (data: QuerySuccessResponse | null) => { - if (data && data.logs) { - // We have new data, update the store - state.data.value.logs = data.logs; - state.data.value.columns = data.columns || []; - state.data.value.queryStats = data.stats || DEFAULT_QUERY_STATS; - // Check if query_id exists in params before accessing it - if (data.params && typeof data.params === 'object' && "query_id" in data.params) { - state.data.value.queryId = data.params.query_id as string; - } else { - state.data.value.queryId = null; // Reset if not present - } - } else { - // Query was successful but returned no logs or null data - console.warn("Query successful but received no logs or null data."); - // Clear the logs, columns, stats now that the API call is complete - state.data.value.logs = []; - state.data.value.columns = []; - state.data.value.queryStats = DEFAULT_QUERY_STATS; - state.data.value.queryId = null; - } - - // Update lastExecutedState after successful execution - _updateLastExecutedState(); - - // Restore the relative time if it was set before - if (relativeTime) { - state.data.value.selectedRelativeTime = relativeTime; - } - }, + onSuccess: (data: QuerySuccessResponse | null) => fetchLogData(data), operationKey: operationKey, }); @@ -1307,6 +1281,102 @@ export const useExploreStore = defineStore("explore", () => { return "1d"; // Default for very long time ranges } + function prepareQueryParams(sql: string) { + + // Prepare parameters for the API call + const params: QueryParams = { + raw_sql: '', // Will be set below + limit: state.data.value.limit, + query_timeout: state.data.value.queryTimeout + }; + + // Handle empty SQL for both modes + if (!sql || !sql.trim()) { + // Generate default SQL for both LogchefQL and SQL modes when SQL is empty + console.log(`Explore store: Generating default SQL for empty ${state.data.value.activeMode} query`); + + // Get source details to check they're fully loaded + const sourcesStore = useSourcesStore(); + const sourceDetails = sourcesStore.currentSourceDetails; + if (!sourceDetails) { + throw new Error("Source details not available"); + } + + const tsField = sourceDetails._meta_ts_field || 'timestamp'; + const tableName = sourcesStore.getCurrentSourceTableName || 'default.logs'; + + // Generate default SQL using SqlManager + const result = SqlManager.generateDefaultSql({ + tableName, + tsField, + timeRange: state.data.value.timeRange as TimeRange, + limit: state.data.value.limit, + timezone: state.data.value.selectedTimezoneIdentifier || undefined + }); + + if (!result.success) { + const operationKey = 'executeQuery'; + return state.handleError({ + status: "error", + message: "Failed to generate default SQL", + error_type: "ValidationError" + }, operationKey); + } + + // Use the generated SQL + sql = result.sql; + + // If in SQL mode, update the UI to show the generated SQL + if (state.data.value.activeMode === 'sql') { + state.data.value.rawSql = sql; + } + } + + // Set the SQL in the params + params.raw_sql = sql; + + return params; + } + + function fetchLogData(data: QuerySuccessResponse | null) { + if (data && data.logs) { + // check is live log or not + if(liveLogStore.isOn && !liveLogStore.isPause) { + // append data when live log + state.data.value.logs = [...state.data.value.logs, ...data.logs]; + } else { + // We have new data, update the store + state.data.value.logs = data.logs; + } + state.data.value.columns = data.columns || []; + state.data.value.queryStats = data.stats || DEFAULT_QUERY_STATS; + // Check if query_id exists in params before accessing it + if (data.params && typeof data.params === 'object' && "query_id" in data.params) { + state.data.value.queryId = data.params.query_id as string; + } else { + state.data.value.queryId = null; // Reset if not present + } + + } else { + // Query was successful but returned no logs or null data + console.warn("Query successful but received no logs or null data."); + // Clear the logs, columns, stats now that the API call is complete + state.data.value.logs = []; + state.data.value.columns = []; + state.data.value.queryStats = DEFAULT_QUERY_STATS; + state.data.value.queryId = null; + } + + // Update lastExecutedState after successful execution + _updateLastExecutedState(); + + // Restore the relative time if it was set before + const relativeTime = state.data.value.selectedRelativeTime; + if (relativeTime) { + state.data.value.selectedRelativeTime = relativeTime; + } + } + // Return the store return { // State - exposed as computed properties @@ -1375,7 +1445,9 @@ export const useExploreStore = defineStore("explore", () => { generateAiSql, clearAiSqlState, fetchHistogramData, - + prepareQueryParams, + fetchLogData, + getTimezoneIdentifier, // Loading state helpers isLoadingOperation: state.isLoadingOperation, }; diff --git a/frontend/src/stores/liveLog.ts b/frontend/src/stores/liveLog.ts new file mode 100644 index 00000000..583dde10 --- /dev/null +++ b/frontend/src/stores/liveLog.ts @@ -0,0 +1,64 @@ + +import {defineStore} from "pinia"; + +import {useBaseStore} from "@/stores/base.ts"; +import {computed} from "vue"; + + +interface LiveLogState { + isOn: boolean; // is live log on + start: number; // last request start + end: number; // last request end + sql: string | null; // last request sql + isPause: boolean; // is live log paused or not +} + +export const useLiveLogStore = defineStore("liveLog", () => { + + const state = useBaseStore({ + isOn: false, + start: Date.now(), + end: Date.now(), + sql: null, + isPause: false, + }); + + + + // Computed properties + const isOn = computed(() => state.data.value.isOn); + const start = computed(() => state.data.value.start); + const end = computed(() => state.data.value.end); + const sql = computed(() => state.data.value.sql); + const isPause = computed(() => state.data.value.isPause); + + // Set data + function setIsOn(isOn: boolean) { + state.data.value.isOn = isOn; + } + function setStart(start: number) { + state.data.value.start = start; + } + function setEnd(end: number) { + state.data.value.end = end; + } + function setSql(sql: string | null) { + state.data.value.sql = sql; + } + function setIsPause(isPause: boolean) { + state.data.value.isPause = isPause; + } + + return { + isOn, + start, + end, + sql, + isPause, + setIsOn, + setStart, + setEnd, + setSql, + setIsPause + }; +}); \ No newline at end of file diff --git a/frontend/src/views/explore/LogExplorer.vue b/frontend/src/views/explore/LogExplorer.vue index 3a7b2c7f..76b31fbb 100644 --- a/frontend/src/views/explore/LogExplorer.vue +++ b/frontend/src/views/explore/LogExplorer.vue @@ -17,6 +17,7 @@ import { useExploreStore } from "@/stores/explore"; import { useTeamsStore } from "@/stores/teams"; import { useSourcesStore } from "@/stores/sources"; import { useSavedQueriesStore } from "@/stores/savedQueries"; +import { useLiveLogStore } from "@/stores/liveLog" import { FieldSideBar } from "@/components/field-sidebar"; import { getErrorMessage } from "@/api/types"; import DataTable from "./table/data-table.vue"; @@ -145,6 +146,9 @@ const executingQueryId = ref(null); const lastQueryTime = ref(0); let lastExecutionKey = ""; +// live log paused or not +const isPaused = ref(false); + // Display related refs const displayTimezone = computed(() => localStorage.getItem("logchef_timezone") === "utc" ? "utc" : "local" @@ -891,6 +895,13 @@ const onSaveQueryModalSave = (formData: SaveQueryFormData) => { processSaveQueryFromComposable(formData); }; +// handle live log's pause on / off +const liveLogPauseOnOff = (isPause: boolean) => { + isPaused.value = isPause; + const liveLogStore = useLiveLogStore(); + liveLogStore.setIsPause(isPause); +}; + // Handle query_id changes from URL, especially when component is kept alive watch( () => route.query.query_id, @@ -1322,7 +1333,7 @@ onBeforeUnmount(() => { hasValidSource && exploreStore.timeRange "> - + diff --git a/frontend/src/views/explore/components/QueryControls.vue b/frontend/src/views/explore/components/QueryControls.vue index 3208bdab..46fb6146 100644 --- a/frontend/src/views/explore/components/QueryControls.vue +++ b/frontend/src/views/explore/components/QueryControls.vue @@ -1,8 +1,9 @@