From b682df28fc2fa7d538b20dd4ae69741a6882e145 Mon Sep 17 00:00:00 2001 From: Omkar Kesarkhane <127078473+omkarK06@users.noreply.github.com> Date: Wed, 19 Nov 2025 13:55:21 +0530 Subject: [PATCH] fix: Add route Prefetching (#9109) --- web/src/composables/useRoutePrefetch.ts | 114 +++ web/src/layouts/MainLayout.vue | 13 + web/src/plugins/logs/Index.vue | 21 +- web/src/plugins/logs/IndexList.vue | 767 ++---------------- .../logs/components/FieldExpansion.vue | 281 +++++++ web/src/plugins/logs/components/FieldList.vue | 305 +++++++ .../logs/components/FieldListPagination.vue | 323 ++++++++ web/src/plugins/logs/components/FieldRow.vue | 153 ++++ 8 files changed, 1268 insertions(+), 709 deletions(-) create mode 100644 web/src/composables/useRoutePrefetch.ts create mode 100644 web/src/plugins/logs/components/FieldExpansion.vue create mode 100644 web/src/plugins/logs/components/FieldList.vue create mode 100644 web/src/plugins/logs/components/FieldListPagination.vue create mode 100644 web/src/plugins/logs/components/FieldRow.vue diff --git a/web/src/composables/useRoutePrefetch.ts b/web/src/composables/useRoutePrefetch.ts new file mode 100644 index 0000000000..0dcf1628c6 --- /dev/null +++ b/web/src/composables/useRoutePrefetch.ts @@ -0,0 +1,114 @@ +// 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 . + +import { ref } from "vue"; + +/** + * Composable for prefetching route modules on hover + * This improves perceived performance by loading route components before navigation + */ +export default function useRoutePrefetch() { + // Track which routes have already been prefetched to avoid redundant loads + const prefetchedRoutes = ref>(new Set()); + + /** + * Route to module path mapping + * Maps the route path to the corresponding lazy-loaded component import + */ + const routeModuleMap: Record Promise> = { + "/": () => import("@/views/HomeView.vue"), + "/logs": () => { + return new Promise((resolve) => { + import("@/plugins/logs/Index.vue"); + import("@/plugins/logs/SearchResult.vue"); + resolve(true); + }); + }, + "/metrics": () => import("@/plugins/metrics/Index.vue"), + "/traces": () => import("@/plugins/traces/Index.vue"), + "/rum": () => import("@/views/RUM/RealUserMonitoring.vue"), + "/dashboards": () => import("@/views/Dashboards/Dashboards.vue"), + "/streams": () => import("@/views/StreamExplorer.vue"), + "/alerts": () => import("@/views/AppAlerts.vue"), + "/ingestion": () => import("@/views/Ingestion.vue"), + "/iam": () => import("@/views/IdentityAccessManagement.vue"), + "/reports": () => import("@/components/reports/ReportList.vue"), + "/actions": () => import("@/components/actionScripts/ActionScripts.vue"), + "/settings": () => import("@/components/settings/index.vue"), + }; + + /** + * Prefetch a route's module by path + * @param routePath - The route path to prefetch (e.g., "/logs", "/metrics") + */ + const prefetchRoute = async (routePath: string) => { + // Skip if already prefetched + if (prefetchedRoutes.value.has(routePath)) { + return; + } + + // Get the module loader for this route + const moduleLoader = routeModuleMap[routePath]; + + if (moduleLoader) { + try { + // Mark as being prefetched + prefetchedRoutes.value.add(routePath); + + // Trigger the dynamic import + // The browser will cache this module, making subsequent navigation instant + await moduleLoader(); + + console.debug( + `[Prefetch] Successfully prefetched module for route: ${routePath}`, + ); + } catch (error) { + // If prefetch fails, remove from cache so it can be retried + prefetchedRoutes.value.delete(routePath); + console.warn( + `[Prefetch] Failed to prefetch module for route: ${routePath}`, + error, + ); + } + } + }; + + /** + * Prefetch multiple routes at once + * @param routePaths - Array of route paths to prefetch + */ + const prefetchRoutes = async (routePaths: string[]) => { + const promises = routePaths + .filter((path) => !prefetchedRoutes.value.has(path)) + .map((path) => prefetchRoute(path)); + + await Promise.allSettled(promises); + }; + + /** + * Reset the prefetch cache + * Useful for testing or if you need to force re-prefetch + */ + const resetPrefetchCache = () => { + prefetchedRoutes.value.clear(); + }; + + return { + prefetchRoute, + prefetchRoutes, + prefetchedRoutes: prefetchedRoutes.value, + resetPrefetchCache, + }; +} diff --git a/web/src/layouts/MainLayout.vue b/web/src/layouts/MainLayout.vue index d7fc357e78..b47380ab65 100644 --- a/web/src/layouts/MainLayout.vue +++ b/web/src/layouts/MainLayout.vue @@ -524,6 +524,7 @@ class="padding-none" /> :link-name="nav.name" :animation-index="index" v-bind="{ ...nav, mini: miniMode }" + @mouseenter="handleMenuHover(nav.link)" /> @@ -654,6 +655,7 @@ import useStreams from "@/composables/useStreams"; import { openobserveRum } from "@openobserve/browser-rum"; import useSearchWebSocket from "@/composables/useSearchWebSocket"; import O2AIChat from "@/components/O2AIChat.vue"; +import useRoutePrefetch from "@/composables/useRoutePrefetch"; let mainLayoutMixin: any = null; if (config.isCloud == "true") { @@ -739,6 +741,7 @@ export default defineComponent({ const { getStreams, resetStreams } = useStreams(); const { closeSocket } = useSearchWebSocket(); const { isOpen: isPredefinedThemesOpen, toggleThemes } = usePredefinedThemes(); + const { prefetchRoute } = useRoutePrefetch(); const isMonacoEditorLoaded = ref(false); const showGetStarted = ref( @@ -1451,6 +1454,15 @@ export default defineComponent({ const openPredefinedThemes = () => { toggleThemes(); }; + + /** + * Prefetch route module on menu hover + * @param routePath - The route path from the menu link + */ + const handleMenuHover = (routePath: string) => { + prefetchRoute(routePath); + }; + //this is the used to set the selected org to the user clicked org because all the operations are happening on the selected org //to make sync with the user clicked org //we dont need search query after selectedOrg has been changed so resetting it @@ -1510,6 +1522,7 @@ export default defineComponent({ setRumUser, openPredefinedThemes, isPredefinedThemesOpen, + handleMenuHover, }; }, computed: { diff --git a/web/src/plugins/logs/Index.vue b/web/src/plugins/logs/Index.vue index c5c84b38bb..54e64bce89 100644 --- a/web/src/plugins/logs/Index.vue +++ b/web/src/plugins/logs/Index.vue @@ -62,7 +62,7 @@ along with this program. If not, see . @@ -790,6 +140,7 @@ import { onBeforeMount, onBeforeUnmount, nextTick, + defineAsyncComponent, } from "vue"; import { useI18n } from "vue-i18n"; import { useStore } from "vuex"; @@ -832,7 +183,13 @@ interface Filter { } export default defineComponent({ name: "ComponentSearchIndexSelect", - components: { EqualIcon, NotEqualIcon}, + components: { + EqualIcon, + NotEqualIcon, + FieldList: defineAsyncComponent( + () => import("@/plugins/logs/components/FieldList.vue"), + ), + }, emits: ["setInterestingFieldInSQLQuery"], methods: { handleMultiStreamSelection() { @@ -1147,7 +504,7 @@ export default defineComponent({ searchObj.data.stream.addToFilter = field; }; - function clickFieldFn(row: { name: never }, pageIndex: number) { + function clickFieldFn(row: { name: never }) { let selectedFields = reorderSelectedFields(); if (selectedFields.includes(row.name)) { @@ -1659,7 +1016,10 @@ export default defineComponent({ }; - const toggleSchema = async () => { + const toggleSchema = async (newValue: string) => { + // Update the schema type with the new value from the toggle + searchObj.meta.useUserDefinedSchemas = newValue; + // Reset pagination to page 1 before resetting fields resetPagination(); @@ -1676,12 +1036,20 @@ export default defineComponent({ }; - const toggleInterestingFields = () => { + const toggleInterestingFields = (newValue: boolean) => { + // Update the interesting fields toggle with the new value + showOnlyInterestingFields.value = newValue; + // Reset pagination to page 1 before resetting fields resetPagination(); resetFields(); }; + const toggleFieldGroup = (group: string) => { + searchObj.data.stream.expandGroupRows[group] = + !searchObj.data.stream.expandGroupRows[group]; + }; + const hasUserDefinedSchemas = () => { return searchObj.data.stream.selectedStream.some((stream: any) => { store.state.zoConfig.user_defined_schemas_enabled && @@ -1997,6 +1365,7 @@ export default defineComponent({ toggleSchema, toggleInterestingFields, fieldListRef, + toggleFieldGroup, streamFieldsRows: computed(() => { let expandKeys = Object.keys( searchObj.data.stream.expandGroupRows, diff --git a/web/src/plugins/logs/components/FieldExpansion.vue b/web/src/plugins/logs/components/FieldExpansion.vue new file mode 100644 index 0000000000..5fed807cc2 --- /dev/null +++ b/web/src/plugins/logs/components/FieldExpansion.vue @@ -0,0 +1,281 @@ + + + + + + + diff --git a/web/src/plugins/logs/components/FieldList.vue b/web/src/plugins/logs/components/FieldList.vue new file mode 100644 index 0000000000..8e5c2e18a4 --- /dev/null +++ b/web/src/plugins/logs/components/FieldList.vue @@ -0,0 +1,305 @@ + + + + + + + diff --git a/web/src/plugins/logs/components/FieldListPagination.vue b/web/src/plugins/logs/components/FieldListPagination.vue new file mode 100644 index 0000000000..1889ce5e16 --- /dev/null +++ b/web/src/plugins/logs/components/FieldListPagination.vue @@ -0,0 +1,323 @@ + + + + + + + diff --git a/web/src/plugins/logs/components/FieldRow.vue b/web/src/plugins/logs/components/FieldRow.vue new file mode 100644 index 0000000000..cf293241d1 --- /dev/null +++ b/web/src/plugins/logs/components/FieldRow.vue @@ -0,0 +1,153 @@ + + + + + + +