mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-06 00:09:22 +00:00
fix: Add route Prefetching (#9109)
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user