fix: Add route Prefetching (#9109)

This commit is contained in:
Omkar Kesarkhane
2025-11-19 13:55:21 +05:30
committed by GitHub
parent a0133e6668
commit b682df28fc
8 changed files with 1268 additions and 709 deletions
+114
View File
@@ -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 <http://www.gnu.org/licenses/>.
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<Set<string>>(new Set());
/**
* Route to module path mapping
* Maps the route path to the corresponding lazy-loaded component import
*/
const routeModuleMap: Record<string, () => Promise<any>> = {
"/": () => 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,
};
}
+13
View File
@@ -524,6 +524,7 @@ class="padding-none" />
:link-name="nav.name"
:animation-index="index"
v-bind="{ ...nav, mini: miniMode }"
@mouseenter="handleMenuHover(nav.link)"
/>
</q-list>
</q-drawer>
@@ -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: {
+11 -10
View File
@@ -62,7 +62,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template #before>
<div class="relative-position tw-h-full tw-pl-[0.625rem]">
<index-list
v-show="searchObj.meta.showFields"
v-if="searchObj.meta.showFields"
data-test="logs-search-index-list"
class="card-container"
@setInterestingFieldInSQLQuery="
@@ -418,7 +418,6 @@ import MainLayoutCloudMixin from "@/enterprise/mixins/mainLayout.mixin";
import SanitizedHtmlRenderer from "@/components/SanitizedHtmlRenderer.vue";
import useLogs from "@/composables/useLogs";
import useStreamFields from "@/composables/useLogs/useStreamFields";
import VisualizeLogsQuery from "@/plugins/logs/VisualizeLogsQuery.vue";
import useDashboardPanelData from "@/composables/useDashboardPanel";
import { reactive } from "vue";
import { getConsumableRelativeTime } from "@/utils/date";
@@ -452,24 +451,26 @@ import { useHistogram } from "@/composables/useLogs/useHistogram";
import useStreams from "@/composables/useStreams";
import { contextRegistry } from "@/composables/contextProviders";
import { createLogsContextProvider } from "@/composables/contextProviders/logsContextProvider";
import IndexList from "@/plugins/logs/IndexList.vue";
export default defineComponent({
name: "PageSearch",
components: {
SearchBar,
SearchSchedulersList,
IndexList: defineAsyncComponent(
() => import("@/plugins/logs/IndexList.vue"),
),
IndexList,
SearchResult: defineAsyncComponent(
() => import("@/plugins/logs/SearchResult.vue"),
),
ConfirmDialog: defineAsyncComponent(
() => import("@/components/ConfirmDialog.vue"),
SearchSchedulersList: defineAsyncComponent(
() => import("@/plugins/logs/SearchSchedulersList.vue"),
),
SanitizedHtmlRenderer,
VisualizeLogsQuery,
SearchHistory,
VisualizeLogsQuery: defineAsyncComponent(
() => import("@/plugins/logs/VisualizeLogsQuery.vue"),
),
SearchHistory: defineAsyncComponent(
() => import("@/plugins/logs/SearchHistory.vue"),
),
},
mixins: [MainLayoutCloudMixin],
emits: ["sendToAiChat"],
+68 -699
View File
@@ -79,703 +79,53 @@ size="xs" /> No field found in
</h3>
</div>
<div v-else class="index-table q-mt-xs">
<q-table
<FieldList
ref="fieldListRef"
data-test="log-search-index-list-fields-table"
v-model="sortedStreamFields"
:visible-columns="['name']"
:rows="streamFieldsRows"
:row-key="
(row: any) => searchObj.data.stream.selectedStream[0] + row.name
"
:filter="searchObj.data.stream.filterField"
:filter-method="filterFieldFn"
v-model:pagination="pagination"
hide-header
:stream-fields-rows="streamFieldsRows"
:selected-stream="searchObj.data.stream.selectedStream[0]"
:filter-field="searchObj.data.stream.filterField"
:filter-field-fn="filterFieldFn"
:pagination="pagination"
@update:pagination="pagination = $event"
@update:filter-field="searchObj.data.stream.filterField = $event"
:wrap-cells="searchObj.meta.resultGrid.wrapCells"
class="field-table full-height"
:class="{ 'loading-fields': searchObj.loadingStream }"
id="fieldList"
:rows-per-page-options="[]"
>
<template #body-cell-name="props">
<q-tr
v-if="props.row.name === 'no-fields-found'"
class="tw-text-center tw-py-[0.725rem] tw-flex tw-items-center tw-justify-center"
>
<q-icon name="info" color="primary" size="xs" />
<span class="tw-pl-[0.375rem]">No matching fields found.</span>
</q-tr>
<q-tr
:props="props"
v-else-if="
props.row.label &&
(showOnlyInterestingFields
? searchObj.data.stream.interestingExpandedGroupRowsFieldCount[
props.row.group
]
: true)
"
@click="
searchObj.data.stream.expandGroupRows[props.row.group] =
!searchObj.data.stream.expandGroupRows[props.row.group]
"
class="cursor-pointer text-bold"
>
<q-td
class="field_list field-group-header"
:class="
store.state.theme === 'dark' ? 'text-grey-5' : 'bg-grey-3'
"
>
{{ props.row.name }} ({{
showOnlyInterestingFields
? searchObj.data.stream
.interestingExpandedGroupRowsFieldCount[props.row.group]
: searchObj.data.stream.expandGroupRowsFieldCount[
props.row.group
]
}})
<q-icon
v-if="
searchObj.data.stream.expandGroupRowsFieldCount[
props.row.group
] > 0
"
:name="
searchObj.data.stream.expandGroupRows[props.row.group]
? 'expand_less'
: 'expand_more'
"
size="20px"
class="float-right q-mt-xs"
></q-icon>
</q-td>
</q-tr>
<q-tr
:props="props"
v-else-if="
!showOnlyInterestingFields ||
(showOnlyInterestingFields &&
props.row.isInterestingField &&
searchObj.data.stream.interestingExpandedGroupRowsFieldCount[
props.row.group
])
"
v-show="searchObj.data.stream.expandGroupRows[props.row.group]"
>
<q-td
:props="props"
class="field_list"
:class="
searchObj.data.stream.selectedFields.includes(props.row.name)
? 'selected'
: ''
"
>
<!-- TODO OK : Repeated code make separate component to display field -->
<div
v-if="
props.row.ftsKey ||
!props.row.isSchemaField ||
!props.row.showValues
"
class="field-container flex content-center ellipsis q-pl-lg full-width hover:tw-bg-[var(--o2-hover-accent)] tw-rounded-[0.25rem]"
:title="props.row.name"
>
<div
class="field_label full-width"
:data-test="`logs-field-list-item-${props.row.name}`"
>
<div
class="ellipsis"
style="max-width: 90% !important; display: inline-block"
>
{{ props.row.name }}
</div>
<span class="float-right">
<q-icon
:data-test="`log-search-index-list-interesting-${props.row.name}-field-btn`"
v-if="
searchObj.meta.quickMode &&
props.row.name !== store.state.zoConfig.timestamp_column
"
:name="
props.row.isInterestingField ? 'info' : 'info_outline'
"
:class="
store.state.theme === 'dark' ? '' : 'light-dimmed'
"
style="margin-right: 0.375rem"
size="1.1rem"
:title="
props.row.isInterestingField
? 'Remove from interesting fields'
: 'Add to interesting fields'
"
/>
</span>
</div>
<div
class="field_overlay tw-rounded-[0.25rem]"
v-if="
props.row.name !== store.state.zoConfig.timestamp_column
"
>
<q-btn
v-if="
props.row.isSchemaField &&
props.row.name != store.state.zoConfig.timestamp_column
"
:icon="outlinedAdd"
:data-test="`log-search-index-list-filter-${props.row.name}-field-btn`"
style="margin-right: 0.375rem"
size="0.4rem"
class="q-mr-sm"
@click.stop="addToFilter(`${props.row.name}=''`)"
round
/>
<q-icon
:data-test="`log-search-index-list-add-${props.row.name}-field-btn`"
v-if="
!searchObj.data.stream.selectedFields.includes(
props.row.name,
) &&
props.row.name !== store.state.zoConfig.timestamp_column
"
:name="outlinedVisibility"
style="margin-right: 0.375rem"
size="1.1rem"
title="Add field to table"
@click.stop="clickFieldFn(props.row, props.pageIndex)"
/>
<q-icon
:data-test="`log-search-index-list-remove-${props.row.name}-field-btn`"
v-if="
searchObj.data.stream.selectedFields.includes(
props.row.name,
)
"
:name="outlinedVisibilityOff"
style="margin-right: 0.375rem"
size="1.1rem"
title="Remove field from table"
@click.stop="clickFieldFn(props.row, props.pageIndex)"
/>
<q-icon
:data-test="`log-search-index-list-interesting-${props.row.name}-field-btn`"
v-if="
searchObj.meta.quickMode &&
props.row.name !== store.state.zoConfig.timestamp_column
"
:name="
props.row.isInterestingField ? 'info' : 'info_outline'
"
size="1.1rem"
:title="
props.row.isInterestingField
? 'Remove from interesting fields'
: 'Add to interesting fields'
"
@click.stop="
addToInterestingFieldList(
props.row,
props.row.isInterestingField,
)
"
/>
</div>
</div>
<q-expansion-item
v-else
dense
switch-toggle-side
:label="props.row.name"
expand-icon-class="field-expansion-icon"
expand-icon="
expand_more
"
expanded-icon="
expand_less
"
class="hover:tw-bg-[var(--o2-hover-accent)] tw-rounded-[0.25rem]"
@before-show="
(event: any) => openFilterCreator(event, props.row)
"
@before-hide="(event: any) => cancelFilterCreator(props.row)"
>
<template v-slot:header>
<div
class="flex content-center ellipsis full-width"
:title="props.row.name"
:data-test="`log-search-expand-${props.row.name}-field-btn`"
>
<div
class="field_label full-width"
:data-test="`logs-field-list-item-${props.row.name}`"
>
<div
class="ellipsis"
style="max-width: 90% !important; display: inline-block"
>
{{ props.row.name }}
</div>
<span class="float-right">
<q-icon
:data-test="`log-search-index-list-interesting-${props.row.name}-field-btn`"
v-if="searchObj.meta.quickMode"
:name="
props.row.isInterestingField
? 'info'
: 'info_outline'
"
:class="
store.state.theme === 'dark' ? '' : 'light-dimmed'
"
style="margin-right: 0.375rem"
size="1.1rem"
:title="
props.row.isInterestingField
? 'Remove from interesting fields'
: 'Add to interesting fields'
"
/>
</span>
</div>
<div
class="field_overlay tw-rounded-[0.25rem] tw-overflow-hidden"
>
<q-btn
v-if="props.row.isSchemaField"
:data-test="`log-search-index-list-filter-${props.row.name}-field-btn`"
:icon="outlinedAdd"
style="margin-right: 0.375rem"
size="0.4rem"
class="q-mr-sm"
@click.stop="addToFilter(`${props.row.name}=''`)"
round
/>
<q-icon
:data-test="`log-search-index-list-add-${props.row.name}-field-btn`"
v-if="
!searchObj.data.stream.selectedFields.includes(
props.row.name,
)
"
:name="outlinedVisibility"
style="margin-right: 0.375rem"
size="1.1rem"
title="Add field to table"
@click.stop="clickFieldFn(props.row, props.pageIndex)"
/>
<q-icon
:data-test="`log-search-index-list-remove-${props.row.name}-field-btn`"
v-if="
searchObj.data.stream.selectedFields.includes(
props.row.name,
)
"
:name="outlinedVisibilityOff"
style="margin-right: 0.375rem"
title="Remove field from table"
size="1.1rem"
@click.stop="clickFieldFn(props.row, props.pageIndex)"
/>
<q-icon
:data-test="`log-search-index-list-interesting-${props.row.name}-field-btn`"
v-if="searchObj.meta.quickMode"
:name="
props.row.isInterestingField ? 'info' : 'info_outline'
"
size="1.1rem"
:title="
props.row.isInterestingField
? 'Remove from interesting fields'
: 'Add to interesting fields'
"
@click.stop="
addToInterestingFieldList(
props.row,
props.row.isInterestingField,
)
"
/>
</div>
</div>
</template>
<q-card>
<q-card-section class="q-pl-md q-pr-xs q-py-xs">
<div class="filter-values-container">
<div
v-show="fieldValues[props.row.name]?.isLoading"
class="q-pl-md q-py-xs"
style="height: 60px"
>
<q-inner-loading
size="xs"
:showing="fieldValues[props.row.name]?.isLoading"
label="Fetching values..."
label-style="font-size: 1.1em"
/>
</div>
<div
v-show="
!fieldValues[props.row.name]?.values?.length &&
!fieldValues[props.row.name]?.isLoading
"
class="q-pl-md q-py-xs text-subtitle2"
>
{{
fieldValues[props.row.name]?.errMsg ||
"No values found"
}}
</div>
<div
v-for="value in fieldValues[props.row.name]?.values ||
[]"
:key="value.key"
>
<q-list dense>
<q-item
tag="label"
class="q-pr-none"
:data-test="`logs-search-subfield-add-${props.row.name}-${value.key}`"
>
<div
class="flex row wrap justify-between"
:style="
searchObj.data.stream.selectedStream.length ==
props.row.streams.length
? 'width: calc(100% - 42px)'
: 'width: calc(100% - 0px)'
"
>
<div
:title="value.key"
class="ellipsis q-pr-xs"
style="width: calc(100% - 50px)"
>
{{ value.key }}
</div>
<div
:title="value.count.toString()"
class="ellipsis text-right q-pr-sm"
style="display: contents"
:style="
searchObj.data.stream.selectedStream.length ==
props.row.streams.length
? 'width: 50px'
: ''
"
>
{{ formatLargeNumber(value.count) }}
</div>
</div>
<div
v-if="
searchObj.data.stream.selectedStream.length ==
props.row.streams.length
"
class="flex row tw-ml-[0.125rem]"
:class="
store.state.theme === 'dark'
? 'text-white'
: 'text-black'
"
>
<q-btn
class="o2-custom-button-hover tw-ml-[0.25rem] !tw-border !tw-border-solid !tw-border-[var(--o2-border-color)]"
size="5px"
style="margin-right: 4px;"
@click="
addSearchTerm(
props.row.name,
value.key,
'include',
)
"
title="Include Term"
round
:data-test="`log-search-subfield-list-equal-${props.row.name}-field-btn`"
>
<q-icon style="height: 8px; width: 8px;">
<EqualIcon></EqualIcon>
</q-icon>
</q-btn>
<q-btn
class="o2-custom-button-hover !tw-border !tw-border-solid !tw-border-[var(--o2-border-color)]"
size="5px"
@click="
addSearchTerm(
props.row.name,
value.key,
'exclude',
)
"
title="Exclude Term"
round
:data-test="`log-search-subfield-list-not-equal-${props.row.name}-field-btn`"
>
<q-icon style="height: 8px; width: 8px;">
<NotEqualIcon></NotEqualIcon>
</q-icon>
</q-btn>
</div>
</q-item>
</q-list>
</div>
</div>
</q-card-section>
</q-card>
</q-expansion-item>
</q-td>
</q-tr>
</template>
<template #top-right>
<q-input
data-test="log-search-index-list-field-search-input"
v-model="searchObj.data.stream.filterField"
data-cy="index-field-search-input"
borderless
dense
clearable
debounce="1"
:placeholder="t('search.searchField')"
class="!tw-mb-[0.375rem] indexlist-search-input"
>
<template #prepend>
<q-icon name="search" size="20px"
class="o2-search-input-icon" />
</template>
</q-input>
<q-tr v-if="searchObj.loadingStream == true">
<q-td colspan="100%" class="text-bold"
style="opacity: 0.7">
<div class="text-subtitle2 text-weight-bold">
<q-spinner-hourglass size="20px" />
{{ t("confirmDialog.loading") }}
</div>
</q-td>
</q-tr>
</template>
<template v-slot:pagination="scope">
<div
class="tw-pt-[0.375rem] tw-justify-between tw-w-full"
:class="
showUserDefinedSchemaToggle || searchObj.meta.quickMode
? 'tw-flex'
: ''
"
>
<div v-if="showUserDefinedSchemaToggle">
<q-btn-toggle
no-caps
v-model="searchObj.meta.useUserDefinedSchemas"
data-test="logs-page-field-list-user-defined-schema-toggle"
class="schema-field-toggle q-mr-xs tw-p-0"
toggle-color="primary"
bordered
size="8px"
text-color="primary"
bg-color="primary"
@update:model-value="toggleSchema"
:options="userDefinedSchemaBtnGroupOption"
>
<template v-slot:user_defined_slot>
<div data-test="logs-user-defined-fields-btn">
<q-icon name="person" class="!tw-text-[12px]"></q-icon>
<q-icon name="schema" class="!tw-text-[12px]"></q-icon>
<q-tooltip
data-test="logs-page-fields-list-user-defined-fields-warning-tooltip"
anchor="center right"
self="center left"
max-width="300px"
class="text-body2"
>
<span class="text-bold" color="white">{{
t("search.userDefinedSchemaLabel")
}}</span>
</q-tooltip>
</div>
</template>
<template v-slot:all_fields_slot>
<div data-test="logs-all-fields-btn">
<q-icon name="schema" class="!tw-text-[12px]"></q-icon>
<q-tooltip
data-test="logs-page-fields-list-all-fields-warning-tooltip"
anchor="center right"
self="center left"
max-width="300px"
class="text-body2"
>
<span class="text-bold" color="white">{{
t("search.allFieldsLabel")
}}</span>
<q-separator color="white" class="q-mt-xs q-mb-xs" />
{{ t("search.allFieldsWarningMsg") }}
</q-tooltip>
</div>
</template>
<template
v-slot:interesting_fields_slot
v-if="searchObj.meta.quickMode"
>
<div data-test="logs-interesting-fields-btn">
<q-icon name="info" class="!tw-text-[12px]" />
<q-icon name="schema" class="!tw-text-[12px]"></q-icon>
<q-tooltip
anchor="center right"
self="center left"
max-width="300px"
class="text-body2"
>
<span class="text-bold" color="white">{{
t("search.showOnlyInterestingFields")
}}</span>
</q-tooltip>
</div>
</template>
</q-btn-toggle>
</div>
<div v-else-if="searchObj.meta.quickMode">
<q-btn-toggle
no-caps
v-model="showOnlyInterestingFields"
data-test="logs-page-field-list-user-defined-schema-toggle"
class="schema-field-toggle q-mr-xs"
toggle-color="primary"
bordered
size="8px"
text-color="primary"
:options="selectedFieldsBtnGroupOption"
@update:model-value="toggleInterestingFields"
>
<template v-slot:all_fields_slot>
<div data-test="logs-all-fields-btn">
<q-icon name="schema" class="!tw-text-[12px]"></q-icon>
<q-tooltip
data-test="logs-page-fields-list-all-fields-warning-tooltip"
anchor="center right"
self="center left"
max-width="300px"
class="text-body2"
>
<span class="text-bold" color="white">{{
t("search.allFieldsLabel")
}}</span>
<q-separator color="white" class="q-mt-xs q-mb-xs" />
{{ t("search.allFieldsWarningMsg") }}
</q-tooltip>
</div>
</template>
<template
v-slot:interesting_fields_slot
v-if="searchObj.meta.quickMode"
>
<div data-test="logs-interesting-fields-btn">
<q-icon name="info" class="!tw-text-[12px]" />
<q-icon name="schema" class="!tw-text-[12px]"></q-icon>
<q-tooltip
anchor="center right"
self="center left"
max-width="300px"
class="text-body2"
>
<span class="text-bold" color="white">{{
t("search.showOnlyInterestingFields")
}}</span>
</q-tooltip>
</div>
</template>
</q-btn-toggle>
</div>
<div class="tw-flex tw-items-center tw-justify-end tw-gap-2">
<div v-if="scope.pagesNumber > 1" class="field-list-pagination">
<q-tooltip
data-test="logs-page-fields-list-pagination-tooltip"
anchor="center left"
self="center right"
max-width="300px"
class="text-body2"
>
Total Fields:
{{
searchObj.data.stream.selectedStream.length > 1
? searchObj.data.stream.selectedStreamFields.length -
(searchObj.data.stream.selectedStream.length + 1)
: searchObj.data.stream.selectedStreamFields.length
}}
</q-tooltip>
<!-- First page button -->
<q-btn
data-test="logs-page-fields-list-pagination-firstpage-button"
icon="fast_rewind"
color="primary"
flat
:disable="scope.isFirstPage"
@click="scope.firstPage"
class="pagination-nav-btn"
aria-label="First page"
/>
<!-- Page number buttons (3 at a time) -->
<template
v-for="page in getPageNumbers(
scope.pagination.page,
scope.pagesNumber,
)"
:key="page"
>
<q-btn
flat
:data-test="`logs-page-fields-list-pagination-page-${page}-button`"
:class="[
'pagination-page-btn',
scope.pagination.page === page
? 'pagination-page-active'
: '',
]"
@click="setPage(page)"
>{{ page }}</q-btn
>
</template>
<!-- Last page button -->
<q-btn
data-test="logs-page-fields-list-pagination-lastpage-button"
icon="fast_forward"
color="primary"
flat
:disable="scope.isLastPage"
@click="scope.lastPage"
class="pagination-nav-btn"
aria-label="Last page"
/>
</div>
<div class="field-list-reset">
<q-icon
name="restart_alt"
data-test="logs-page-fields-list-reset-icon"
class="cursor-pointer reset-icon"
@click="resetSelectedFileds"
/>
<q-tooltip
data-test="logs-page-fields-list-reset-tooltip"
anchor="center left"
self="center right"
max-width="300px"
class="text-body2"
>
<span class="text-bold" color="white">{{
t("search.resetFields")
}}</span>
</q-tooltip>
</div>
</div>
</div>
</template>
</q-table>
:loading-stream="searchObj.loadingStream"
:show-only-interesting-fields="showOnlyInterestingFields"
:interesting-expanded-group-rows-field-count="
searchObj.data.stream.interestingExpandedGroupRowsFieldCount
"
:expand-group-rows-field-count="
searchObj.data.stream.expandGroupRowsFieldCount
"
:expand-group-rows="searchObj.data.stream.expandGroupRows"
:theme="store.state.theme"
:selected-fields="searchObj.data.stream.selectedFields"
:timestamp-column="store.state.zoConfig.timestamp_column"
:show-quick-mode="searchObj.meta.quickMode"
:field-values="fieldValues"
:selected-streams-count="searchObj.data.stream.selectedStream.length"
:show-user-defined-schema-toggle="showUserDefinedSchemaToggle"
:use-user-defined-schemas="searchObj.meta.useUserDefinedSchemas"
:user-defined-schema-btn-group-option="userDefinedSchemaBtnGroupOption"
:selected-fields-btn-group-option="selectedFieldsBtnGroupOption"
:total-fields-count="
searchObj.data.stream.selectedStream.length > 1
? searchObj.data.stream.selectedStreamFields.length -
(searchObj.data.stream.selectedStream.length + 1)
: searchObj.data.stream.selectedStreamFields.length
"
@add-to-filter="addToFilter"
@toggle-field="clickFieldFn"
@toggle-interesting="addToInterestingFieldList"
@add-search-term="addSearchTerm"
@before-show="openFilterCreator"
@before-hide="cancelFilterCreator"
@toggle-group="toggleFieldGroup"
@toggle-schema="toggleSchema"
@toggle-interesting-fields="toggleInterestingFields"
@set-page="setPage"
@reset-fields="resetSelectedFileds"
/>
</div>
</div>
</template>
@@ -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,
@@ -0,0 +1,281 @@
<!-- 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/>.
-->
<template>
<q-expansion-item
dense
switch-toggle-side
:label="field.name"
expand-icon-class="field-expansion-icon"
expand-icon="expand_more"
expanded-icon="expand_less"
class="hover:tw-bg-[var(--o2-hover-accent)] tw-rounded-[0.25rem]"
@before-show="(event: any) => handleBeforeShow(event)"
@before-hide="() => handleBeforeHide()"
>
<template v-slot:header>
<div
class="flex content-center ellipsis full-width"
:title="field.name"
:data-test="`log-search-expand-${field.name}-field-btn`"
>
<div
class="field_label full-width"
:data-test="`logs-field-list-item-${field.name}`"
>
<div
class="ellipsis"
style="max-width: 90% !important; display: inline-block"
>
{{ field.name }}
</div>
<span class="float-right">
<q-icon
:data-test="`log-search-index-list-interesting-${field.name}-field-btn`"
v-if="showQuickMode"
:name="field.isInterestingField ? 'info' : 'info_outline'"
:class="theme === 'dark' ? '' : 'light-dimmed'"
style="margin-right: 0.375rem"
size="1.1rem"
:title="
field.isInterestingField
? 'Remove from interesting fields'
: 'Add to interesting fields'
"
/>
</span>
</div>
<div class="field_overlay tw-rounded-[0.25rem] tw-overflow-hidden">
<q-btn
v-if="field.isSchemaField"
:data-test="`log-search-index-list-filter-${field.name}-field-btn`"
:icon="outlinedAdd"
style="margin-right: 0.375rem"
size="0.4rem"
class="q-mr-sm"
@click.stop="$emit('add-to-filter', `${field.name}=''`)"
round
/>
<q-icon
:data-test="`log-search-index-list-add-${field.name}-field-btn`"
v-if="!isFieldSelected"
:name="outlinedVisibility"
style="margin-right: 0.375rem"
size="1.1rem"
title="Add field to table"
@click.stop="$emit('toggle-field', field)"
/>
<q-icon
:data-test="`log-search-index-list-remove-${field.name}-field-btn`"
v-if="isFieldSelected"
:name="outlinedVisibilityOff"
style="margin-right: 0.375rem"
title="Remove field from table"
size="1.1rem"
@click.stop="$emit('toggle-field', field)"
/>
<q-icon
:data-test="`log-search-index-list-interesting-${field.name}-field-btn`"
v-if="showQuickMode"
:name="field.isInterestingField ? 'info' : 'info_outline'"
size="1.1rem"
:title="
field.isInterestingField
? 'Remove from interesting fields'
: 'Add to interesting fields'
"
@click.stop="
$emit('toggle-interesting', field, field.isInterestingField)
"
/>
</div>
</div>
</template>
<q-card>
<q-card-section class="q-pl-md q-pr-xs q-py-xs">
<div class="filter-values-container">
<!-- Loading state -->
<div
v-show="fieldValues?.isLoading"
class="q-pl-md q-py-xs"
style="height: 3.75rem"
>
<q-inner-loading
size="xs"
:showing="fieldValues?.isLoading"
label="Fetching values..."
label-style="font-size: 1.1em"
/>
</div>
<!-- No values found -->
<div
v-show="!fieldValues?.values?.length && !fieldValues?.isLoading"
class="q-pl-md q-py-xs text-subtitle2"
>
{{ fieldValues?.errMsg || "No values found" }}
</div>
<!-- Field values list -->
<div v-for="value in fieldValues?.values || []" :key="value.key">
<q-list dense>
<q-item
tag="label"
class="q-pr-none"
:data-test="`logs-search-subfield-add-${field.name}-${value.key}`"
>
<div
class="flex row wrap justify-between"
:style="
selectedStreamsCount == field.streams.length
? 'width: calc(100% - 2.625rem)'
: 'width: 100%'
"
>
<div
:title="value.key"
class="ellipsis q-pr-xs"
style="width: calc(100% - 3.125rem)"
>
{{ value.key }}
</div>
<div
:title="value.count.toString()"
class="ellipsis text-right q-pr-sm"
style="display: contents"
:style="
selectedStreamsCount == field.streams.length
? 'width: 3.125rem'
: ''
"
>
{{ formatLargeNumber(value.count) }}
</div>
</div>
<!-- Include/Exclude buttons -->
<div
v-if="selectedStreamsCount == field.streams.length"
class="flex row tw-ml-[0.125rem]"
:class="theme === 'dark' ? 'text-white' : 'text-black'"
>
<q-btn
class="o2-custom-button-hover tw-ml-[0.25rem] tw-mr-[0.25rem] !tw-border !tw-border-solid !tw-border-[var(--o2-border-color)]"
size="0.3125rem"
@click="handleAddSearchTerm(field.name, value.key, 'include')"
title="Include Term"
round
:data-test="`log-search-subfield-list-equal-${field.name}-field-btn`"
>
<q-icon class="tw-h-[0.5rem] tw-w-[0.5rem]">
<EqualIcon></EqualIcon>
</q-icon>
</q-btn>
<q-btn
class="o2-custom-button-hover !tw-border !tw-border-solid !tw-border-[var(--o2-border-color)]"
size="0.3125rem"
@click="handleAddSearchTerm(field.name, value.key, 'exclude')"
title="Exclude Term"
round
:data-test="`log-search-subfield-list-not-equal-${field.name}-field-btn`"
>
<q-icon class="tw-h-[0.5rem] tw-w-[0.5rem]">
<NotEqualIcon></NotEqualIcon>
</q-icon>
</q-btn>
</div>
</q-item>
</q-list>
</div>
</div>
</q-card-section>
</q-card>
</q-expansion-item>
</template>
<script setup lang="ts">
import { computed } from "vue";
import {
outlinedAdd,
outlinedVisibility,
outlinedVisibilityOff,
} from "@quasar/extras/material-icons-outlined";
import EqualIcon from "@/components/icons/EqualIcon.vue";
import NotEqualIcon from "@/components/icons/NotEqualIcon.vue";
import { formatLargeNumber } from "@/utils/zincutils";
interface Props {
field: any;
fieldValues?: {
isLoading: boolean;
values: { key: string; count: number }[];
errMsg?: string;
};
selectedFields: string[];
selectedStreamsCount: number;
theme: string;
showQuickMode: boolean;
}
const props = defineProps<Props>();
const emit = defineEmits<{
"add-to-filter": [value: string];
"toggle-field": [field: any];
"toggle-interesting": [field: any, isInteresting: boolean];
"add-search-term": [fieldName: string, value: string, action: string];
"before-show": [event: any, field: any];
"before-hide": [field: any];
}>();
const isFieldSelected = computed(() =>
props.selectedFields.includes(props.field.name),
);
const handleBeforeShow = (event: any) => {
emit("before-show", event, props.field);
};
const handleBeforeHide = () => {
emit("before-hide", props.field);
};
const handleAddSearchTerm = (
fieldName: string,
value: string,
action: string,
) => {
emit("add-search-term", fieldName, value, action);
};
</script>
<style scoped lang="scss">
.field_overlay {
position: absolute;
right: 0;
top: 0;
bottom: 0;
display: none;
align-items: center;
padding: 0 0.25rem;
background: var(--q-dark);
}
:deep(.q-expansion-item):hover .field_overlay {
display: flex;
}
</style>
@@ -0,0 +1,305 @@
<!-- 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/>.
-->
<template>
<q-table
ref="fieldListRef"
data-test="log-search-index-list-fields-table"
:visible-columns="['name']"
:rows="streamFieldsRows"
:row-key="(row: any) => selectedStream + row.name"
:filter="filterField"
:filter-method="filterFieldFn"
:pagination="pagination"
@update:pagination="$emit('update:pagination', $event)"
hide-header
:wrap-cells="wrapCells"
class="field-table full-height"
:class="{ 'loading-fields': loadingStream }"
id="fieldList"
:rows-per-page-options="[]"
>
<template #body-cell-name="props">
<!-- Field Group Header -->
<q-tr
v-if="props.row.name === 'no-fields-found'"
class="tw-text-center tw-py-[0.725rem] tw-flex tw-items-center tw-justify-center"
>
<q-icon name="info" color="primary" size="xs" />
<span class="tw-pl-[0.375rem]">No matching fields found.</span>
</q-tr>
<!-- Field Group Header -->
<q-tr
:props="props"
v-else-if="
props.row.label &&
(showOnlyInterestingFields
? interestingExpandedGroupRowsFieldCount[props.row.group]
: true)
"
@click="$emit('toggle-group', props.row.group)"
class="cursor-pointer text-bold"
>
<q-td
class="field_list field-group-header !tw-flex tw-justify-between tw-items-center tw-rounded-[0.25rem]"
:class="[theme === 'dark' ? 'text-grey-5' : 'bg-grey-3']"
>
<div class="tw-w-[calc(100%-1.25rem)] ellipsis">
{{ props.row.name }} ({{
showOnlyInterestingFields
? interestingExpandedGroupRowsFieldCount[props.row.group]
: expandGroupRowsFieldCount[props.row.group]
}})
</div>
<q-icon
v-if="expandGroupRowsFieldCount[props.row.group] > 0"
:name="
expandGroupRows[props.row.group] ? 'expand_less' : 'expand_more'
"
size="1.25rem"
class="float-right q-mt-xs"
></q-icon>
</q-td>
</q-tr>
<!-- Field Row -->
<q-tr
:props="props"
v-else-if="
!showOnlyInterestingFields ||
(showOnlyInterestingFields &&
props.row.isInterestingField &&
interestingExpandedGroupRowsFieldCount[props.row.group])
"
v-show="expandGroupRows[props.row.group]"
>
<q-td
:props="props"
class="field_list"
:class="selectedFields.includes(props.row.name) ? 'selected' : ''"
>
<FieldRow
:field="props.row"
:selected-fields="selectedFields"
:timestamp-column="timestampColumn"
:theme="theme"
:show-quick-mode="showQuickMode"
@add-to-filter="$emit('add-to-filter', $event)"
@toggle-field="$emit('toggle-field', $event)"
@toggle-interesting="
(field, isInteresting) =>
$emit('toggle-interesting', field, isInteresting)
"
>
<template #expansion="{ field }">
<FieldExpansion
:field="field"
:field-values="fieldValues[field.name]"
:selected-fields="selectedFields"
:selected-streams-count="selectedStreamsCount"
:theme="theme"
:show-quick-mode="showQuickMode"
@add-to-filter="$emit('add-to-filter', $event)"
@toggle-field="$emit('toggle-field', $event)"
@toggle-interesting="
(field, isInteresting) =>
$emit('toggle-interesting', field, isInteresting)
"
@add-search-term="
(fieldName, value, action) =>
$emit('add-search-term', fieldName, value, action)
"
@before-show="
(event, field) => $emit('before-show', event, field)
"
@before-hide="(field) => $emit('before-hide', field)"
/>
</template>
</FieldRow>
</q-td>
</q-tr>
</template>
<template #top-right>
<div class="field-list-search-container">
<q-input
data-test="log-search-index-list-field-search-input"
:model-value="filterField"
@update:model-value="$emit('update:filter-field', $event)"
data-cy="index-field-search-input"
borderless
dense
clearable
debounce="1"
:placeholder="t('search.searchField')"
class="indexlist-search-input tw-mb-[0.25rem]"
>
<template #prepend>
<q-icon name="search" size="1.25rem" class="o2-search-input-icon" />
</template>
</q-input>
<q-tr v-if="loadingStream == true">
<q-td colspan="100%" class="text-bold" style="opacity: 0.7">
<div class="text-subtitle2 text-weight-bold">
<q-spinner-hourglass size="1.25rem" />
{{ t("confirmDialog.loading") }}
</div>
</q-td>
</q-tr>
</div>
</template>
<template v-slot:pagination="scope">
<FieldListPagination
:show-user-defined-schema-toggle="showUserDefinedSchemaToggle"
:show-quick-mode="showQuickMode"
:use-user-defined-schemas="useUserDefinedSchemas"
:show-only-interesting-fields="showOnlyInterestingFields"
:user-defined-schema-btn-group-option="userDefinedSchemaBtnGroupOption"
:selected-fields-btn-group-option="selectedFieldsBtnGroupOption"
:current-page="scope.pagination.page"
:pages-number="scope.pagesNumber"
:is-first-page="scope.isFirstPage"
:is-last-page="scope.isLastPage"
:total-fields-count="totalFieldsCount"
@toggle-schema="$emit('toggle-schema', $event)"
@toggle-interesting-fields="$emit('toggle-interesting-fields', $event)"
@first-page="scope.firstPage"
@last-page="scope.lastPage"
@set-page="$emit('set-page', $event)"
@reset-fields="$emit('reset-fields')"
/>
</template>
</q-table>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import FieldRow from "./FieldRow.vue";
import FieldExpansion from "./FieldExpansion.vue";
import FieldListPagination from "./FieldListPagination.vue";
const { t } = useI18n();
interface Props {
streamFieldsRows: any[];
selectedStream: string;
filterField: string;
filterFieldFn: (rows: any, terms: any) => any[];
pagination: { page: number; rowsPerPage: number };
wrapCells: boolean;
loadingStream: boolean;
showOnlyInterestingFields: boolean;
interestingExpandedGroupRowsFieldCount: Record<string, number>;
expandGroupRowsFieldCount: Record<string, number>;
expandGroupRows: Record<string, boolean>;
theme: string;
selectedFields: string[];
timestampColumn: string;
showQuickMode: boolean;
fieldValues: Record<string, any>;
selectedStreamsCount: number;
showUserDefinedSchemaToggle: boolean;
useUserDefinedSchemas: string;
userDefinedSchemaBtnGroupOption: any[];
selectedFieldsBtnGroupOption: any[];
totalFieldsCount: number;
}
defineProps<Props>();
defineEmits<{
"update:pagination": [value: { page: number; rowsPerPage: number }];
"update:filter-field": [value: string];
"add-to-filter": [value: string];
"toggle-field": [field: any];
"toggle-interesting": [field: any, isInteresting: boolean];
"add-search-term": [fieldName: string, value: string, action: string];
"before-show": [event: any, field: any];
"before-hide": [field: any];
"toggle-group": [group: string];
"toggle-schema": [value: string];
"toggle-interesting-fields": [value: boolean];
"set-page": [page: number];
"reset-fields": [];
}>();
// Refs
const fieldListRef = ref<HTMLElement | null>(null);
// Methods
const scrollToTop = () => {
if (fieldListRef.value) {
const scrollContainer = fieldListRef.value.querySelector(
".q-table__middle.scroll",
);
if (scrollContainer) {
scrollContainer.scrollTop = 0;
}
}
};
// Expose methods and refs to parent
defineExpose({
fieldListRef,
scrollToTop,
});
</script>
<style scoped lang="scss">
.field-list-search-container {
width: 100%;
padding: 0;
margin: 0;
}
.indexlist-search-input {
height: 2.25rem;
.q-field__control {
height: 2.25rem;
display: flex;
align-items: center;
font-size: 0.8125rem;
padding: 0 0.375rem !important;
font-weight: 500;
}
.q-field__prepend {
height: 2.25rem !important;
padding-bottom: 0.25rem !important;
}
.q-field__append {
padding-top: 0.5rem !important;
}
.q-icon {
height: 1rem;
width: 1rem;
margin-right: 0.625rem;
}
}
.field_list {
padding: 0;
&.field-group-header {
font-weight: 600;
font-size: 0.75rem;
padding: 0.25rem 0.325rem;
}
}
</style>
@@ -0,0 +1,323 @@
<!-- 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/>.
-->
<template>
<div
class="tw-pt-[0.375rem] tw-justify-between tw-w-full"
:class="showUserDefinedSchemaToggle || showQuickMode ? 'tw-flex' : ''"
>
<!-- Schema Toggle Buttons -->
<div v-if="showUserDefinedSchemaToggle">
<q-btn-toggle
no-caps
:model-value="useUserDefinedSchemas"
@update:model-value="$emit('toggle-schema', $event)"
data-test="logs-page-field-list-user-defined-schema-toggle"
class="schema-field-toggle q-mr-xs tw-p-0"
toggle-color="primary"
bordered
size="0.5rem"
text-color="primary"
bg-color="primary"
:options="userDefinedSchemaBtnGroupOption"
>
<template v-slot:user_defined_slot>
<div data-test="logs-user-defined-fields-btn">
<q-icon name="person" class="!tw-text-[12px]"></q-icon>
<q-icon name="schema" class="!tw-text-[12px]"></q-icon>
<q-tooltip
data-test="logs-page-fields-list-user-defined-fields-warning-tooltip"
anchor="center right"
self="center left"
max-width="18.75rem"
class="text-body2"
>
<span class="text-bold" color="white">{{
t("search.userDefinedSchemaLabel")
}}</span>
</q-tooltip>
</div>
</template>
<template v-slot:all_fields_slot>
<div data-test="logs-all-fields-btn">
<q-icon name="schema" class="!tw-text-[12px]"></q-icon>
<q-tooltip
data-test="logs-page-fields-list-all-fields-warning-tooltip"
anchor="center right"
self="center left"
max-width="18.75rem"
class="text-body2"
>
<span class="text-bold" color="white">{{
t("search.allFieldsLabel")
}}</span>
<q-separator color="white" class="q-mt-xs q-mb-xs" />
{{ t("search.allFieldsWarningMsg") }}
</q-tooltip>
</div>
</template>
<template v-slot:interesting_fields_slot v-if="showQuickMode">
<div data-test="logs-interesting-fields-btn">
<q-icon name="info" class="!tw-text-[12px]" />
<q-icon name="schema" class="!tw-text-[12px]"></q-icon>
<q-tooltip
anchor="center right"
self="center left"
max-width="18.75rem"
class="text-body2"
>
<span class="text-bold" color="white">{{
t("search.showOnlyInterestingFields")
}}</span>
</q-tooltip>
</div>
</template>
</q-btn-toggle>
</div>
<!-- Interesting Fields Toggle (when no user defined schema) -->
<div v-else-if="showQuickMode">
<q-btn-toggle
no-caps
:model-value="showOnlyInterestingFields"
@update:model-value="$emit('toggle-interesting-fields', $event)"
data-test="logs-page-field-list-user-defined-schema-toggle"
class="schema-field-toggle q-mr-xs"
toggle-color="primary"
bordered
size="0.5rem"
text-color="primary"
:options="selectedFieldsBtnGroupOption"
>
<template v-slot:all_fields_slot>
<div data-test="logs-all-fields-btn">
<q-icon name="schema" class="!tw-text-[12px]"></q-icon>
<q-tooltip
data-test="logs-page-fields-list-all-fields-warning-tooltip"
anchor="center right"
self="center left"
max-width="18.75rem"
class="text-body2"
>
<span class="text-bold" color="white">{{
t("search.allFieldsLabel")
}}</span>
<q-separator color="white" class="q-mt-xs q-mb-xs" />
{{ t("search.allFieldsWarningMsg") }}
</q-tooltip>
</div>
</template>
<template v-slot:interesting_fields_slot v-if="showQuickMode">
<div data-test="logs-interesting-fields-btn">
<q-icon name="info" class="!tw-text-[12px]" />
<q-icon name="schema" class="!tw-text-[12px]"></q-icon>
<q-tooltip
anchor="center right"
self="center left"
max-width="18.75rem"
class="text-body2"
>
<span class="text-bold" color="white">{{
t("search.showOnlyInterestingFields")
}}</span>
</q-tooltip>
</div>
</template>
</q-btn-toggle>
</div>
<!-- Pagination and Reset Controls -->
<div class="tw-flex tw-items-center tw-justify-end tw-gap-2">
<!-- Pagination -->
<div v-if="pagesNumber > 1" class="field-list-pagination">
<q-tooltip
data-test="logs-page-fields-list-pagination-tooltip"
anchor="center left"
self="center right"
max-width="18.75rem"
class="text-body2"
>
Total Fields: {{ totalFieldsCount }}
</q-tooltip>
<!-- First page button -->
<q-btn
data-test="logs-page-fields-list-pagination-firstpage-button"
icon="fast_rewind"
color="primary"
flat
:disable="isFirstPage"
@click="$emit('first-page')"
class="pagination-nav-btn"
aria-label="First page"
/>
<!-- Page number buttons (3 at a time) -->
<template v-for="page in visiblePages" :key="page">
<q-btn
flat
:data-test="`logs-page-fields-list-pagination-page-${page}-button`"
:class="[
'pagination-page-btn',
currentPage === page ? 'pagination-page-active' : '',
]"
@click="$emit('set-page', page)"
>{{ page }}</q-btn
>
</template>
<!-- Last page button -->
<q-btn
data-test="logs-page-fields-list-pagination-lastpage-button"
icon="fast_forward"
color="primary"
flat
:disable="isLastPage"
@click="$emit('last-page')"
class="pagination-nav-btn"
aria-label="Last page"
/>
</div>
<!-- Reset Fields Icon -->
<div class="field-list-reset">
<q-icon
name="restart_alt"
data-test="logs-page-fields-list-reset-icon"
class="cursor-pointer reset-icon"
@click="$emit('reset-fields')"
/>
<q-tooltip
data-test="logs-page-fields-list-reset-tooltip"
anchor="center left"
self="center right"
max-width="18.75rem"
class="text-body2"
>
<span class="text-bold" color="white">{{
t("search.resetFields")
}}</span>
</q-tooltip>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
interface Props {
showUserDefinedSchemaToggle: boolean;
showQuickMode: boolean;
useUserDefinedSchemas: string;
showOnlyInterestingFields: boolean;
userDefinedSchemaBtnGroupOption: any[];
selectedFieldsBtnGroupOption: any[];
currentPage: number;
pagesNumber: number;
isFirstPage: boolean;
isLastPage: boolean;
totalFieldsCount: number;
}
const props = defineProps<Props>();
defineEmits<{
"toggle-schema": [value: string];
"toggle-interesting-fields": [value: boolean];
"first-page": [];
"last-page": [];
"set-page": [page: number];
"reset-fields": [];
}>();
// Get page numbers to display (3 at a time)
const visiblePages = computed(() => {
const pages: number[] = [];
const { currentPage, pagesNumber } = props;
if (pagesNumber <= 3) {
// If 3 or fewer pages, show all
for (let i = 1; i <= pagesNumber; i++) {
pages.push(i);
}
} else {
// Show 3 pages centered around current page
let startPage = Math.max(1, currentPage - 1);
let endPage = Math.min(pagesNumber, startPage + 2);
// Adjust if we're near the end
if (endPage === pagesNumber) {
startPage = Math.max(1, endPage - 2);
}
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
}
return pages;
});
</script>
<style scoped lang="scss">
.field-list-pagination {
display: flex;
align-items: center;
gap: 0.25rem;
}
.pagination-nav-btn {
min-width: 2rem;
height: 2rem;
}
.pagination-page-btn {
min-width: 2rem;
height: 2rem;
font-size: 0.8125rem;
}
.pagination-page-active {
background-color: var(--q-primary);
color: white;
}
.field-list-reset {
display: flex;
align-items: center;
}
.reset-icon {
font-size: 1.25rem;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
&:hover {
opacity: 1;
}
}
.schema-field-toggle {
:deep(.q-btn) {
padding: 0.25rem 0.5rem;
}
}
</style>
@@ -0,0 +1,153 @@
<!-- 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/>.
-->
<template>
<!-- Simple field without expansion (FTS keys or fields without values) -->
<div
v-if="field.ftsKey || !field.isSchemaField || !field.showValues"
class="field-container flex content-center ellipsis q-pl-lg full-width hover:tw-bg-[var(--o2-hover-accent)] tw-rounded-[0.25rem]"
:title="field.name"
>
<div
class="field_label full-width !tw-flex !tw-items-center !tw-justify-between"
:data-test="`logs-field-list-item-${field.name}`"
>
<div
class="ellipsis tw-flex tw-items-center"
style="max-width: 90% !important; display: inline-block"
>
{{ field.name }}
</div>
<span class="float-right">
<q-icon
:data-test="`log-search-index-list-interesting-${field.name}-field-btn`"
v-if="showQuickMode && field.name !== timestampColumn"
:name="field.isInterestingField ? 'info' : 'info_outline'"
:class="theme === 'dark' ? '' : 'light-dimmed'"
style="margin-right: 0.375rem"
size="1.1rem"
:title="
field.isInterestingField
? 'Remove from interesting fields'
: 'Add to interesting fields'
"
/>
</span>
</div>
<div
class="field_overlay tw-rounded-[0.25rem] tw-overflow-hidden"
v-if="field.name !== timestampColumn"
>
<q-btn
v-if="field.isSchemaField && field.name != timestampColumn"
:icon="outlinedAdd"
:data-test="`log-search-index-list-filter-${field.name}-field-btn`"
style="margin-right: 0.375rem"
size="0.4rem"
class="q-mr-sm"
@click.stop="$emit('add-to-filter', `${field.name}=''`)"
round
/>
<q-icon
:data-test="`log-search-index-list-add-${field.name}-field-btn`"
v-if="!isFieldSelected && field.name !== timestampColumn"
:name="outlinedVisibility"
style="margin-right: 0.375rem"
size="1.1rem"
title="Add field to table"
@click.stop="$emit('toggle-field', field)"
/>
<q-icon
:data-test="`log-search-index-list-remove-${field.name}-field-btn`"
v-if="isFieldSelected"
:name="outlinedVisibilityOff"
style="margin-right: 0.375rem"
size="1.1rem"
title="Remove field from table"
@click.stop="$emit('toggle-field', field)"
/>
<q-icon
:data-test="`log-search-index-list-interesting-${field.name}-field-btn`"
v-if="showQuickMode && field.name !== timestampColumn"
:name="field.isInterestingField ? 'info' : 'info_outline'"
size="1.1rem"
:title="
field.isInterestingField
? 'Remove from interesting fields'
: 'Add to interesting fields'
"
@click.stop="$emit('toggle-interesting', field, field.isInterestingField)"
/>
</div>
</div>
<!-- Field with expansion for values -->
<slot v-else name="expansion" :field="field"></slot>
</template>
<script setup lang="ts">
import { computed } from "vue";
import {
outlinedAdd,
outlinedVisibility,
outlinedVisibilityOff,
} from "@quasar/extras/material-icons-outlined";
interface Props {
field: any;
selectedFields: string[];
timestampColumn: string;
theme: string;
showQuickMode: boolean;
}
const props = defineProps<Props>();
defineEmits<{
"add-to-filter": [value: string];
"toggle-field": [field: any];
"toggle-interesting": [field: any, isInteresting: boolean];
}>();
const isFieldSelected = computed(() =>
props.selectedFields.includes(props.field.name),
);
</script>
<style scoped lang="scss">
.field-container {
position: relative;
.field_overlay {
position: absolute;
right: 0;
top: 0;
bottom: 0;
display: none;
align-items: center;
padding: 0 0.25rem;
background: var(--q-dark);
}
&:hover .field_overlay {
display: flex;
}
}
.field_label {
padding: 0.25rem 0;
}
</style>