Files
openobserve/web/src/plugins/logs/SearchBar.vue
T
Omkar Kesarkhane 91c52ea318 fix: Saved View preserves loadingHistogram State, leading to track total hits call on applying. (#9084)
#9079 
1. A view was saved while the histogram was being loaded, which
incorrectly hardcoded the loadingHistogram flag as true in the saved
view data.
2. When this saved view was later loaded, the true value of
loadingHistogram was retrieved.
3. This pre-existing true state bypassed the histogram loading logic and
resulted in an incorrect call to the get page count.
2025-11-17 23:45:08 +05:30

4767 lines
163 KiB
Vue

<!-- 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="store.state.theme === 'dark' ? 'dark-theme' : ''"
class="logs-search-bar-component"
id="searchBarComponent"
>
<div class="row !tw-m-0 !tw-p-[0.375rem] !tw-items-start">
<div class="float-right col flex">
<div class="button-group logs-visualize-toggle element-box-shadow">
<div class="row">
<div>
<q-btn
data-test="logs-logs-toggle"
:class="
searchObj.meta.logsVisualizeToggle === 'logs'
? 'selected'
: ''
"
@click="onLogsVisualizeToggleUpdate('logs')"
no-caps
size="sm"
icon="search"
class="button button-left tw-flex tw-justify-center tw-items-center no-border no-outline !tw-rounded-r-none q-px-sm btn-height-32"
>
<q-tooltip>
{{ t("common.search") }}
</q-tooltip>
</q-btn>
</div>
<div>
<q-btn
data-test="logs-visualize-toggle"
:class="[
searchObj.meta.logsVisualizeToggle === 'visualize' ? 'selected' : '',
config.isEnterprise == 'true' ? 'button button-center tw-rounded-none' : 'button button-right !tw-rounded-l-none',
'tw-flex tw-justify-center tw-items-center no-border no-outline q-px-sm btn-height-32'
]"
@click="onLogsVisualizeToggleUpdate('visualize')"
:disable="isVisualizeDisabled"
no-caps
size="sm"
:icon="outlinedShowChart"
>
<q-tooltip v-if="isVisualizeDisabled">
{{ t("search.enableSqlModeOrSelectSingleStream") }}
</q-tooltip>
<q-tooltip v-else>
{{ t("search.visualize") }}
</q-tooltip>
</q-btn>
</div>
<div v-if="config.isEnterprise == 'true'">
<q-btn
data-test="logs-patterns-toggle"
:class="
searchObj.meta.logsVisualizeToggle === 'patterns'
? 'selected'
: ''
"
class="button button-right tw-flex tw-justify-center tw-items-center no-border no-outline !tw-rounded-l-none q-px-sm btn-height-32"
@click="onLogsVisualizeToggleUpdate('patterns')"
no-caps
size="sm"
icon="pattern"
>
<q-tooltip>
{{ t("search.showPatternsLabel") }}
</q-tooltip>
</q-btn>
</div>
</div>
</div>
<!-- moved to dropdown if ai chat is enabled -->
<div
class="toolbar-toggle-container element-box-shadow"
v-if="!store.state.isAiChatEnabled"
>
<q-toggle
data-test="logs-search-bar-show-histogram-toggle-btn"
v-model="searchObj.meta.showHistogram"
class="o2-toggle-button-xs"
size="xs"
flat
:class="
store.state.theme === 'dark'
? 'o2-toggle-button-xs-dark'
: 'o2-toggle-button-xs-light'
"
>
</q-toggle>
<img :src="histogramIcon" alt="Histogram" class="toolbar-icon" />
<q-tooltip class="toolbar-icon" />
<q-tooltip>
{{ t("search.showHistogramLabel") }}
</q-tooltip>
</div>
<div class="toolbar-toggle-container element-box-shadow">
<q-toggle
data-test="logs-search-bar-sql-mode-toggle-btn"
v-model="searchObj.meta.sqlMode"
:disable="isSqlModeDisabled"
class="o2-toggle-button-xs"
size="xs"
flat
:class="
store.state.theme === 'dark'
? 'o2-toggle-button-xs-dark'
: 'o2-toggle-button-xs-light'
"
>
<img :src="sqlIcon"
alt="SQL Mode" class="toolbar-icon" />
<q-tooltip v-if="isSqlModeDisabled">
{{ t("search.sqlModeDisabledForVisualization") }}
</q-tooltip>
<q-tooltip v-else>
{{ t("search.sqlModeLabel") }}
</q-tooltip>
</q-toggle>
</div>
<!-- Explain Query Button -->
<q-btn
v-if="!store.state.isAiChatEnabled && searchObj.meta.sqlMode"
data-test="logs-search-bar-explain-query-btn"
no-caps
flat
dense
icon="lightbulb"
class="toolbar-reset-btn"
:disable="!searchObj.data.query || searchObj.data.query.trim() === ''"
@click="openExplainDialog"
>
<q-tooltip>
{{ t("search.explainTooltip") }}
</q-tooltip>
</q-btn>
<!-- moved to dropdown if ai chat is enabled -->
<q-btn
v-if="!store.state.isAiChatEnabled"
data-test="logs-search-bar-reset-filters-btn"
no-caps
flat
dense
icon="restart_alt"
class="toolbar-reset-btn element-box-shadow"
@click="resetFilters"
>
<q-tooltip>
{{ t("search.resetFilters") }}
</q-tooltip>
</q-btn>
<!-- moved to dropdown if ai chat is enabled -->
<syntax-guide
v-if="!store.state.isAiChatEnabled"
data-test="logs-search-bar-sql-mode-toggle-btn"
:sqlmode="searchObj.meta.sqlMode"
class="syntax-guide-in-toolbar element-box-shadow"
>
</syntax-guide>
<q-btn-group class="q-ml-xs q-pa-none element-box-shadow el-border">
<q-btn-dropdown
data-test="logs-search-saved-views-btn"
v-model="savedViewDropdownModel"
@click="fnSavedView"
@show="loadSavedView"
split
icon="save"
icon-right="saved_search"
class="saved-views-dropdown no-border"
content-class="saved-views-dropdown-menu"
>
<q-list
:style="
localSavedViews.length > 0 ? 'width: 500px' : 'width: 250px'
"
data-test="logs-search-saved-view-list"
>
<q-item style="padding: 0px 0px 0px 0px">
<q-item-section
class="column"
no-hover
style="width: 60%; border-right: 1px solid lightgray"
>
<q-table
data-test="log-search-saved-view-list-fields-table"
:visible-columns="['view_name']"
:rows="searchObj.data.savedViews"
:row-key="(row) => 'saved_view_' + row.view_id"
:filter="searchObj.data.savedViewFilterFields"
:filter-method="filterSavedViewFn"
:pagination="{ rowsPerPage }"
hide-header
:wrap-cells="searchObj.meta.resultGrid.wrapCells"
class="saved-view-table full-height"
no-hover
id="savedViewList"
:rows-per-page-options="[]"
:hide-bottom="
searchObj.data.savedViews.length <= rowsPerPage ||
searchObj.data.savedViews.length == 0
"
>
<template #top-right>
<div class="full-width">
<q-input
data-test="log-search-saved-view-field-search-input"
v-model="searchObj.data.savedViewFilterFields"
data-cy="index-field-search-input"
borderless
dense
clearable
debounce="1"
class="tw-mx-2 tw-my-2"
:placeholder="t('search.searchSavedView')"
>
<template #prepend>
<q-icon name="search" />
</template>
</q-input>
</div>
<div
v-if="searchObj.loadingSavedView == true"
class="full-width float-left"
>
<div class="text-subtitle2 text-weight-bold float-left">
<q-spinner-hourglass size="20px" />
{{ t("confirmDialog.loading") }}
</div>
</div>
<q-tr>
<q-td
v-if="
searchObj.data.savedViews.length == 0 &&
searchObj.loadingSavedView == false
"
>
<q-item-label class="q-pl-sm q-pt-sm">{{
t("search.savedViewsNotFound")
}}</q-item-label>
</q-td>
</q-tr>
</template>
<template v-slot:body-cell-view_name="props">
<q-td :props="props"
class="field_list" no-hover>
<q-item
class="q-pa-xs saved-view-item"
clickable
v-close-popup
>
<q-item-section
@click.stop="applySavedView(props.row)"
v-close-popup
:title="props.row.view_name"
>
<q-item-label
class="ellipsis"
style="max-width: 140px"
>{{ props.row.view_name }}</q-item-label
>
</q-item-section>
<q-item-section
:data-test="`logs-search-bar-favorite-${props.row.view_name}-saved-view-btn`"
side
@click.stop="
handleFavoriteSavedView(
props.row,
favoriteViews.includes(props.row.view_id),
)
"
>
<q-btn
:icon="
favoriteViews.includes(props.row.view_id)
? 'favorite'
: 'favorite_border'
"
:title="t('common.favourite')"
class="logs-saved-view-icon"
padding="xs"
unelevated
size="xs"
round
flat
></q-btn>
</q-item-section>
<q-item-section
:data-test="`logs-search-bar-update-${props.row.view_name}-saved-view-btn`"
side
@click.stop="handleUpdateSavedView(props.row)"
>
<q-btn
icon="edit"
:title="t('common.edit')"
class="logs-saved-view-icon"
padding="xs"
unelevated
size="xs"
round
flat
></q-btn>
</q-item-section>
<q-item-section
:data-test="`logs-search-bar-delete-${props.row.view_name}-saved-view-btn`"
side
@click.stop="handleDeleteSavedView(props.row)"
>
<q-btn
icon="delete"
:title="t('common.delete')"
class="logs-saved-view-icon"
padding="xs"
unelevated
size="xs"
round
flat
></q-btn>
</q-item-section>
</q-item> </q-td
></template>
</q-table>
</q-item-section>
<q-item-section
class="column"
style="width: 40%; margin-left: 0px"
v-if="localSavedViews.length > 0"
>
<q-table
data-test="log-search-saved-view-favorite-list-fields-table"
:visible-columns="['view_name']"
:rows="localSavedViews"
:row-key="(row) => 'favorite_saved_view_' + row.view_name"
hide-header
hide-bottom
:wrap-cells="searchObj.meta.resultGrid.wrapCells"
class="saved-view-table full-height"
id="savedViewFavoriteList"
:rows-per-page-options="[0]"
>
<template #top-right>
<q-item style="padding: 0px"
><q-item-label
header
class="q-pa-sm text-bold favorite-label"
>Favorite Views</q-item-label
></q-item
>
<q-separator horizontal inset></q-separator>
</template>
<template v-slot:body-cell-view_name="props">
<q-td :props="props" class="field_list q-pa-xs">
<q-item
class="q-pa-xs saved-view-item"
clickable
v-close-popup
>
<q-item-section
@click.stop="applySavedView(props.row)"
v-close-popup
>
<q-item-label
class="ellipsis"
style="max-width: 90px"
>{{ props.row.view_name }}</q-item-label
>
</q-item-section>
<q-item-section
:data-test="`logs-search-bar-favorite-${props.row.view_name}-saved-view-btn`"
side
@click.stop="
handleFavoriteSavedView(
props.row,
favoriteViews.includes(props.row.view_id),
)
"
>
<q-icon
:name="
favoriteViews.includes(props.row.view_id)
? 'favorite'
: 'favorite_border'
"
color="grey"
size="xs"
/>
</q-item-section>
<q-item-section
:data-test="`logs-search-bar-update-${props.row.view_name}-favorite-saved-view-btn`"
side
@click.stop="handleUpdateSavedView(props.row)"
>
<q-btn
icon="edit"
:title="t('common.edit')"
class="logs-saved-view-icon"
padding="xs"
unelevated
size="xs"
round
flat
></q-btn>
</q-item-section>
<q-item-section
:data-test="`logs-search-bar-delete-${props.row.view_name}-favorite-saved-view-btn`"
side
@click.stop="handleDeleteSavedView(props.row)"
>
<q-btn
icon="delete"
:title="t('common.delete')"
class="logs-saved-view-icon"
padding="xs"
unelevated
size="xs"
round
flat
></q-btn>
</q-item-section>
</q-item> </q-td
></template>
</q-table>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<q-tooltip>
{{ t("search.savedViewsLabel") }}
</q-tooltip>
</q-btn-group>
<!-- this is the button group responsible for showing all the utilities when ai chat is enabled -->
<q-btn
v-if="store.state.isAiChatEnabled"
class="group-menu-btn element-box-shadow"
no-caps
menu-anchor="bottom left"
menu-self="top left"
icon="menu"
flat
>
<q-menu>
<q-list>
<!-- Histogram Toggle -->
<q-item
clickable
@click="
searchObj.meta.showHistogram = !searchObj.meta.showHistogram
"
data-test="logs-search-bar-show-histogram-toggle-btn"
class="q-pa-sm saved-view-item"
>
<q-item-section>
<q-item-label class="tw-flex tw-items-center">
<div
style="
width: 28px;
display: flex;
align-items: center;
margin-right: 12px;
"
>
<q-toggle
v-model="searchObj.meta.showHistogram"
size="xs"
flat
:class="
store.state.theme === 'dark'
? 'o2-toggle-button-xs-dark'
: 'o2-toggle-button-xs-light'
"
@click.stop
/>
</div>
{{ t("search.showHistogramLabel") }}
</q-item-label>
</q-item-section>
</q-item>
<q-separator />
<!-- Wrap Content Toggle -->
<q-item
clickable
@click="
searchObj.meta.toggleSourceWrap =
!searchObj.meta.toggleSourceWrap
"
data-test="logs-search-bar-wrap-table-content-toggle-btn"
class="q-pa-sm saved-view-item"
>
<q-item-section>
<q-item-label class="tw-flex tw-items-center">
<div
style="
width: 28px;
display: flex;
align-items: center;
margin-right: 12px;
"
>
<q-toggle
v-model="searchObj.meta.toggleSourceWrap"
size="xs"
flat
:class="
store.state.theme === 'dark'
? 'o2-toggle-button-xs-dark'
: 'o2-toggle-button-xs-light'
"
@click.stop
/>
</div>
Wrap Content
</q-item-label>
</q-item-section>
</q-item>
<q-separator />
<!-- Quick Mode Toggle -->
<q-item
clickable
@click="
searchObj.meta.quickMode = !searchObj.meta.quickMode;
handleQuickMode();
"
data-test="logs-search-bar-quick-mode-toggle-btn"
class="q-pa-sm saved-view-item"
>
<q-item-section>
<q-item-label class="tw-flex tw-items-center">
<div
style="
width: 28px;
display: flex;
align-items: center;
margin-right: 12px;
"
>
<q-toggle
v-model="searchObj.meta.quickMode"
size="xs"
flat
:class="
store.state.theme === 'dark'
? 'o2-toggle-button-xs-dark'
: 'o2-toggle-button-xs-light'
"
@click.stop="handleQuickMode"
/>
</div>
{{ t("search.quickModeLabel") }}
</q-item-label>
</q-item-section>
</q-item>
<q-separator />
<!-- Syntax Guide -->
<q-item clickable class="q-pa-sm saved-view-item">
<q-item-section>
<q-item-label class="tw-flex tw-items-center">
<div
style="
width: 28px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
"
>
<syntax-guide
data-test="logs-search-bar-sql-mode-toggle-btn"
:sqlmode="searchObj.meta.sqlMode"
size="0.875rem"
class="syntax-guide-in-menu"
/>
</div>
Syntax Guide
</q-item-label>
</q-item-section>
</q-item>
<q-separator />
<!-- Reset Filters -->
<q-item
clickable
@click="resetFilters"
data-test="logs-search-bar-reset-filters-btn"
class="q-pa-sm saved-view-item"
>
<q-item-section>
<q-item-label class="tw-flex tw-items-center">
<div
style="
width: 28px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 8px;
margin-left: 3px;
"
>
<q-icon name="restart_alt" size="20px" />
</div>
{{ t("search.resetFilters") }}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
<!-- moved to dropdown if ai chat is enabled -->
<div
class="toolbar-toggle-container element-box-shadow"
v-if="!store.state.isAiChatEnabled"
>
<q-toggle
data-test="logs-search-bar-quick-mode-toggle-btn"
v-model="searchObj.meta.quickMode"
@click="handleQuickMode"
class="o2-toggle-button-xs"
size="xs"
flat
:class="
store.state.theme === 'dark'
? 'o2-toggle-button-xs-dark'
: 'o2-toggle-button-xs-light'
"
>
<img :src="quickModeIcon"
alt="Quick Mode" class="toolbar-icon" />
<q-tooltip>
{{ t("search.quickModeLabel") }}
</q-tooltip>
</q-toggle>
</div>
</div>
<div class="float-right col-auto">
<!-- this is moved to dropdown if ai chat is enabled -->
<div
v-if="!store.state.isAiChatEnabled"
class="toolbar-toggle-container float-left"
>
<q-toggle
data-test="logs-search-bar-wrap-table-content-toggle-btn"
v-model="searchObj.meta.toggleSourceWrap"
class="o2-toggle-button-xs element-box-shadow"
size="xs"
flat
:class="
store.state.theme === 'dark'
? 'o2-toggle-button-xs-dark'
: 'o2-toggle-button-xs-light'
"
:disable="searchObj.meta.logsVisualizeToggle === 'visualize'"
>
<q-icon name="wrap_text" class="toolbar-icon-in-toggle" />
<q-tooltip>
{{
searchObj.meta.logsVisualizeToggle === "visualize"
? "Not supported for visualization"
: t("search.messageWrapContent")
}}
</q-tooltip>
</q-toggle>
</div>
<transform-selector
v-if="isActionsEnabled"
:function-options="functionOptions"
@select:function="populateFunctionImplementation"
@save:function="fnSavedFunctionDialog"
/>
<function-selector
v-else
:function-options="functionOptions"
@select:function="populateFunctionImplementation"
@save:function="fnSavedFunctionDialog"
/>
<q-btn
data-test="logs-search-bar-share-link-btn"
class="q-mr-xs download-logs-btn q-px-sm element-box-shadow el-border"
size="xs"
@click="shareLink.execute()"
:loading="shareLink.isLoading.value"
icon="share"
>
<q-tooltip>
{{ t("search.shareLink") }}
</q-tooltip>
</q-btn>
<q-btn
data-test="logs-search-bar-more-options-btn"
class="q-mr-xs download-logs-btn q-px-sm element-box-shadow el-border"
>
<Menu size="1rem" />
<q-menu>
<q-list>
<q-item
data-test="search-history-item-btn"
class="q-pa-sm saved-view-item"
clickable
v-close-popup
>
<q-item-section @click.stop="showSearchHistoryfn">
<q-item-label class="tw-flex tw-items-center tw-gap-2">
<img
:src="searchHistoryIcon"
alt="Search History"
style="width: 20px; height: 20px"
/>
Search History</q-item-label
>
</q-item-section>
</q-item>
<q-separator />
<q-item
style="min-width: 150px"
class="q-pa-sm saved-view-item download-menu-parent"
clickable
v-close-popup
v-bind:disable="
searchObj.data.queryResults &&
searchObj.data.queryResults.hasOwnProperty('hits') &&
!searchObj.data.queryResults.hits.length
"
@mouseenter="showDownloadMenu = true"
>
<q-item-section class="cursor-pointer">
<q-item-label class="tw-flex tw-items-center tw-gap-2">
<img
:src="downloadTableIcon"
alt="Download Table"
style="width: 20px; height: 20px"
/>
{{ t("search.downloadTable") }}</q-item-label
>
</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu
v-model="showDownloadMenu"
anchor="top end"
self="top start"
:offset="[0, 0]"
@mouseenter="showDownloadMenu = true"
@mouseleave="showDownloadMenu = false"
>
<q-list>
<q-item
data-test="search-download-csv-btn"
class="q-pa-sm saved-view-item"
clickable
v-close-popup
@click="
downloadLogs(searchObj.data.queryResults.hits, 'csv')
"
>
<q-icon name="grid_on" size="14px"
class="q-pr-sm q-pt-xs" />
<q-item-section>
<q-item-label
class="tw-flex tw-items-center tw-gap-2 q-mr-md"
>
{{ t("search.downloadCSV") }}
</q-item-label>
</q-item-section>
</q-item>
<q-item
data-test="search-download-json-btn"
class="q-pa-sm saved-view-item"
clickable
v-close-popup
@click="
downloadLogs(searchObj.data.queryResults.hits, 'json')
"
>
<q-icon name="data_object" size="14px"
class="q-pr-sm q-pt-xs" />
<q-item-section>
<q-item-label
class="tw-flex tw-items-center tw-gap-2 q-mr-md"
>
{{ t("search.downloadJSON") }}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-item>
<q-item
class="q-pa-sm saved-view-item"
style="min-width: 150px"
clickable
v-close-popup
>
<q-item-section
@click.stop="toggleCustomDownloadDialog"
v-close-popup
>
<q-item-label class="tw-flex tw-items-center tw-gap-2">
<img
:src="customRangeIcon"
alt="Custom Range"
style="width: 20px; height: 20px"
/>
{{ t("search.customRange") }}</q-item-label
>
</q-item-section>
</q-item>
<q-separator />
<q-item
v-if="searchObj.meta.sqlMode"
data-test="logs-search-bar-explain-query-menu-btn"
class="q-pa-sm saved-view-item"
clickable
v-close-popup
:disable="!searchObj.data.query || searchObj.data.query.trim() === ''"
@click="openExplainDialog"
>
<q-item-section v-close-popup>
<q-item-label class="tw-flex tw-items-center tw-gap-2">
<q-icon name="lightbulb" size="20px" />
{{ t("search.explainQuery") }}</q-item-label
>
</q-item-section>
</q-item>
<q-separator v-if="searchObj.meta.sqlMode" />
<q-item
v-if="config.isEnterprise == 'true'"
data-test="search-scheduler-create-new-btn"
class="q-pa-sm saved-view-item"
clickable
v-close-popup
@click="createScheduleJob"
>
<q-item-section v-close-popup>
<q-item-label
class="tw-flex tw-items-center tw-gap-2"
data-test="search-scheduler-create-new-label"
>
<img
:src="createScheduledSearchIcon"
alt="Create Scheduled Search"
style="width: 20px; height: 20px"
/>
Create Scheduled Search</q-item-label
>
</q-item-section>
</q-item>
<q-item
v-if="config.isEnterprise == 'true'"
data-test="search-scheduler-list-btn"
class="q-pa-sm saved-view-item"
clickable
v-close-popup
@click="routeToSearchSchedule"
>
<q-item-section v-close-popup>
<q-item-label
class="tw-flex tw-items-center tw-gap-2"
data-test="search-scheduler-list-label"
>
<img
:src="listScheduledSearchIcon"
alt="List Scheduled Search"
style="width: 20px; height: 20px"
/>
List Scheduled Search</q-item-label
>
</q-item-section>
</q-item>
</q-list>
</q-menu>
<q-tooltip style="width: 80px">
{{ t("search.moreActions") }}
</q-tooltip>
</q-btn>
<div class="float-left tw-mr-[-4px]">
<date-time
ref="dateTimeRef"
auto-apply
:default-type="searchObj.data.datetime.type"
:default-absolute-time="{
startTime: searchObj.data.datetime.startTime,
endTime: searchObj.data.datetime.endTime,
}"
:default-relative-time="searchObj.data.datetime.relativeTimePeriod"
data-test="logs-search-bar-date-time-dropdown"
:queryRangeRestrictionMsg="
searchObj.data.datetime.queryRangeRestrictionMsg
"
:queryRangeRestrictionInHour="
searchObj.data.datetime.queryRangeRestrictionInHour
"
@on:date-change="updateDateTime"
@on:timezone-change="updateTimezone"
:disable="disable"
class="element-box-shadow"
/>
</div>
<div class="search-time float-left q-mr-xs">
<div class="flex">
<auto-refresh-interval
class="q-mr-xs q-px-none logs-auto-refresh-interval"
v-model="searchObj.meta.refreshInterval"
:trigger="true"
:min-refresh-interval="
store.state?.zoConfig?.min_auto_refresh_interval ?? 0
"
@update:model-value="onRefreshIntervalUpdate"
@trigger="$emit('onAutoIntervalTrigger')"
/>
<q-btn-group
class="q-pa-none q-mr-xs element-box-shadow el-border"
v-if="
config.isEnterprise == 'true' &&
Object.keys(store.state.regionInfo).length > 0 &&
store.state.zoConfig.super_cluster_enabled
"
>
<q-btn-dropdown
data-test="logs-search-bar-region-btn"
class="region-dropdown-btn q-px-xs"
:title="t('search.regionTitle')"
label="Region"
>
<q-input
ref="reginFilterRef"
borderless
dense
clearable
class="!tw-mb-[0.375rem] indexlist-search-input q-mx-sm q-mt-sm"
v-model="regionFilter"
:label="t('search.regionFilterMsg')"
>
</q-input>
<q-tree
class="col-12 col-sm-6 q-mx-sm q-mb-sm"
:nodes="store.state.regionInfo"
node-key="label"
:filter="regionFilter"
:filter-method="regionFilterMethod"
tick-strategy="leaf"
v-model:ticked="searchObj.meta.clusters"
/>
</q-btn-dropdown>
</q-btn-group>
<div v-if="searchObj.meta.logsVisualizeToggle === 'visualize'">
<div v-if="config.isEnterprise == 'true'" class="tw-flex">
<q-btn
v-if="
visualizeSearchRequestTraceIds.length > 0
"
data-test="logs-search-bar-visualize-cancel-btn"
dense
flat
:title="t('search.cancel')"
class="q-pa-none o2-run-query-button o2-color-cancel element-box-shadow"
:class="
config.isEnterprise == 'true'
? 'search-button-enterprise-border-radius'
: 'search-button-normal-border-radius'
"
@click="cancelVisualizeQueries"
>{{ t("search.cancel") }}</q-btn
>
<q-btn
v-else
data-test="logs-search-bar-visualize-refresh-btn"
dense
flat
:title="t('search.runQuery')"
class="q-pa-none o2-run-query-button o2-color-primary tw-h-[30px] element-box-shadow"
:class="
config.isEnterprise == 'true'
? 'search-button-enterprise-border-radius'
: 'search-button-normal-border-radius'
"
no-caps
@click="handleRunQueryFn"
>{{ t("search.runQuery") }}</q-btn
>
<q-separator class="tw-h-[29px] tw-w-[1px]" />
<q-btn-dropdown
flat
class="tw-h-[29px] search-button-dropdown"
:class="[
config.isEnterprise == 'true' &&
visualizeSearchRequestTraceIds.length
? 'o2-color-cancel'
: 'o2-color-primary',
config.isEnterprise == 'true'
? 'search-button-dropdown-enterprise-border-radius'
: 'search-button-normal-border-radius',
]"
unelevated
dense
>
<q-btn
data-test="logs-search-bar-refresh-btn"
data-cy="search-bar-visuzlie-hard-refresh-button"
dense
flat
no-caps
:title="'Refresh Cache & Run Query'"
class="q-pa-sm search-button-dropdown tw-text-[12px]"
v-close-popup
@click="handleRunQueryFn(true)"
:disable="
config.isEnterprise == 'true' &&
!!visualizeSearchRequestTraceIds.length
"
>
<q-icon name="refresh" class="q-mr-xs" />
Refresh Cache & Run Query</q-btn
>
</q-btn-dropdown>
</div>
<div v-else class="tw-flex">
<q-btn
data-test="logs-search-bar-visualize-refresh-btn"
dense
flat
:title="t('search.runQuery')"
class="q-pa-none o2-run-query-button o2-color-primary tw-h-[30px] element-box-shadow"
:class="
config.isEnterprise == 'true'
? 'search-button-enterprise-border-radius'
: 'search-button-normal-border-radius'
"
no-caps
@click="handleRunQueryFn"
:disable="disable"
>{{ t("search.runQuery") }}</q-btn
>
</div>
</div>
<div v-else class="tw-flex">
<q-btn
v-if="
config.isEnterprise == 'true' &&
(!!searchObj.data.searchRequestTraceIds.length ||
!!searchObj.data.searchWebSocketTraceIds.length) &&
(searchObj.loading == true ||
searchObj.loadingHistogram == true)
"
data-test="logs-search-bar-refresh-btn"
data-cy="search-bar-refresh-button"
dense
:title="t('search.cancel')"
class="q-pa-none o2-run-query-button o2-color-cancel element-box-shadow"
:class="config.isEnterprise == 'true' ? 'search-button-enterprise-border-radius' : 'search-button-normal-border-radius'"
@click="cancelQuery"
>{{ t("search.cancel") }}</q-btn
>
<q-btn
v-else
data-test="logs-search-bar-refresh-btn"
data-cy="search-bar-refresh-button"
dense
:title="t('search.runQuery')"
class="q-pa-none o2-run-query-button o2-color-primary tw-h-[30px] element-box-shadow"
:class="config.isEnterprise == 'true' ? 'search-button-enterprise-border-radius' : 'search-button-normal-border-radius'"
no-caps
@click="handleRunQueryFn"
:disable="
searchObj.loading == true ||
searchObj.loadingHistogram == true
"
>{{ t("search.runQuery") }}</q-btn
>
<q-separator class="tw-h-[29px] tw-w-[1px]" />
<q-btn-dropdown v-if="config.isEnterprise == 'true'" flat class="tw-h-[29px]"
:class="[
config.isEnterprise == 'true' &&
(!!searchObj.data.searchRequestTraceIds.length ||
!!searchObj.data.searchWebSocketTraceIds.length) &&
(searchObj.loading == true ||
searchObj.loadingHistogram == true) ? 'o2-color-cancel' : 'o2-color-primary',
config.isEnterprise == 'true' ? 'search-button-dropdown-enterprise-border-radius' : 'search-button-normal-border-radius'
]"
unelevated dense >
<q-btn
data-test="logs-search-bar-refresh-btn"
data-cy="search-bar-refresh-button"
dense
flat
no-caps
:title="'Refresh Cache & Run Query'"
class="q-pa-sm tw-text-[12px] "
v-close-popup
@click="handleRunQueryFn(true)"
:disable="
searchObj.loading == true ||
searchObj.loadingHistogram == true
"
>
<q-icon name="refresh" class="q-mr-xs" />
Refresh Cache & Run Query</q-btn>
</q-btn-dropdown>
</div>
</div>
</div>
</div>
</div>
<div class="row query-editor-container">
<div
class="col tw-h-full"
:class="{ 'expand-on-focus': isFocused }"
:style="backgroundColorStyle"
>
<q-splitter
class="logs-search-splitter !tw-h-full"
no-scroll
v-model="searchObj.config.fnSplitterModel"
:limits="searchObj.config.fnSplitterLimit"
>
<template #before>
<div
class="col tw-border tw-solid tw-border-[var(--o2-border-color)] tw-mb-[0.375rem] tw-rounded-[0.375rem] tw-overflow-hidden tw-h-full"
:class="searchObj.data.transformType && searchObj.meta.showTransformEditor ? 'tw-ml-[0.375rem]' : 'tw-mx-[0.375rem]'"
>
<code-query-editor
v-if="router.currentRoute.value.name === 'logs'"
data-test="logs-search-bar-query-editor"
editor-id="logsQueryEditor"
ref="queryEditorRef"
class="monaco-editor tw-px-[0.325rem] tw-py-[0.125rem]"
:style="editorWidthToggleFunction"
v-model:query="searchObj.data.query"
:keywords="autoCompleteKeywords"
:suggestions="autoCompleteSuggestions"
:debounceTime="100"
@update:query="updateQueryValue"
@run-query="handleRunQueryFn"
@keydown="handleKeyDown"
:class="
searchObj.data.editorValue == '' &&
searchObj.meta.queryEditorPlaceholderFlag
? 'empty-query'
: ''
"
language="sql"
@focus="searchObj.meta.queryEditorPlaceholderFlag = false"
@blur="searchObj.meta.queryEditorPlaceholderFlag = true"
/>
</div>
</template>
<template #after>
<div
data-test="logs-vrl-function-editor"
v-if="searchObj.data.transformType"
style="width: 100%; height: 100%"
>
<template v-if="showFunctionEditor">
<div class="tw-relative tw-h-full tw-w-full">
<div
class="tw-border tw-solid tw-border-[var(--o2-border-color)] tw-mr-[0.375rem] tw-mb-[0.375rem] tw-rounded-[0.375rem] tw-overflow-hidden tw-h-full"
>
<code-query-editor
v-if="router.currentRoute.value.name === 'logs'"
data-test="logs-vrl-function-editor"
ref="fnEditorRef"
editor-id="fnEditor"
class="monaco-editor tw-px-[0.325rem] tw-py-[0.125rem]"
v-model:query="searchObj.data.tempFunctionContent"
:class="
searchObj.data.tempFunctionContent == '' &&
searchObj.meta.functionEditorPlaceholderFlag
? 'empty-function'
: ''
"
:readOnly="
searchObj.meta.logsVisualizeToggle === 'visualize'
"
@keydown="handleKeyDown"
language="vrl"
@focus="
searchObj.meta.functionEditorPlaceholderFlag = false
"
@blur="
searchObj.meta.functionEditorPlaceholderFlag = true
"
/>
</div>
</div>
<div
v-if="searchObj.meta.logsVisualizeToggle === 'visualize'"
:class="
store.state.theme == 'dark'
? 'tw-bg-white tw-bg-opacity-10'
: 'tw-bg-black tw-bg-opacity-10'
"
class="tw-absolute tw-bottom-0 tw-w-full"
style="margin-top: 12px; display: flex; align-items: center; flex"
>
<q-icon
name="warning"
color="warning"
size="20px"
class="q-mx-sm"
/>
<span
class="text-negative q-pa-sm"
style="font-weight: semibold; font-size: 14px"
>VRL Function Editor is not supported in visualize
mode.</span
>
</div>
</template>
<template v-else-if="searchObj.data.transformType === 'action'">
<code-query-editor
v-if="router.currentRoute.value.name === 'logs'"
data-test="logs-vrl-function-editor"
ref="fnEditorRef"
editor-id="fnEditor"
class="monaco-editor"
:query="actionEditorQuery"
read-only
language="markdown"
/>
</template>
</div>
</template>
</q-splitter>
</div>
<q-btn
data-test="logs-query-editor-full_screen-btn"
:title="isFocused ? 'Collapse' : 'Expand'"
dense
size="10px"
round
color="primary"
@click="isFocused = !isFocused"
class="tw-absolute tw-top-[3.1rem] tw-right-[1.2rem] tw-z-50"
>
<Maximize size='0.8rem' v-if="!isFocused" />
<Minimize size="0.8rem" v-else />
</q-btn>
</div>
<q-dialog ref="confirmDialog" v-model="confirmDialogVisible">
<q-card>
<q-card-section>
{{ confirmMessage }}
</q-card-section>
<q-card-actions align="right">
<q-btn
data-test="logs-search-bar-confirm-dialog-cancel-btn"
:label="t('confirmDialog.cancel')"
color="primary"
@click="cancelConfirmDialog"
/>
<q-btn
data-test="logs-search-bar-confirm-dialog-ok-btn"
:label="t('confirmDialog.ok')"
color="positive"
@click="confirmDialogOK"
/>
</q-card-actions>
</q-card>
</q-dialog>
<q-dialog
ref="confirmSavedViewDialog"
v-model="confirmSavedViewDialogVisible"
>
<q-card>
<q-card-section>
{{ confirmMessageSavedView }}
</q-card-section>
<q-card-actions align="right">
<q-btn
data-test="logs-search-bar-confirm-dialog-cancel-btn"
:label="t('confirmDialog.cancel')"
color="primary"
@click="cancelConfirmDialog"
/>
<q-btn
data-test="logs-search-bar-confirm-dialog-ok-btn"
:label="t('confirmDialog.ok')"
color="positive"
@click="confirmDialogOK"
/>
</q-card-actions>
</q-card>
</q-dialog>
<q-dialog v-model="customDownloadDialog">
<q-card>
<q-card-section>
{{ t("search.customDownloadMessage") }}
</q-card-section>
<q-card-section>
<q-input
type="number"
data-test="custom-download-initial-number-input"
v-model="downloadCustomInitialNumber"
:label="t('search.initialNumber')"
default-value="1"
color="input-border"
bg-color="input-bg"
class="showLabelOnTop"
stack-label
outlined
filled
dense
tabindex="0"
min="1"
/>
<q-select
data-test="custom-download-range-select"
v-model="downloadCustomRange"
:options="downloadCustomRangeOptions"
:label="t('search.range')"
color="input-border"
bg-color="input-bg"
class="q-py-sm showLabelOnTop"
stack-label
outlined
filled
dense
/>
<div class="q-py-sm file-type">
<label class="q-pr-sm">{{ t("search.fileType") }}</label
><br />
<q-btn-group
data-test="custom-download-file-type-button-group"
class="file-type-button-group q-mt-xs"
>
<q-btn
v-for="option in downloadCustomFileTypeOptions"
:key="option.value"
:data-test="`custom-download-file-type-${option.value}-btn`"
:class="
downloadCustomFileType === option.value ? 'selected' : ''
"
@click="downloadCustomFileType = option.value"
:label="option.label"
no-caps
size="sm"
outline
/>
</q-btn-group>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn
unelevated
no-caps
class="q-mr-sm text-bold"
data-test="logs-search-bar-confirm-dialog-cancel-btn"
:label="t('confirmDialog.cancel')"
color="secondary"
v-close-popup
/>
<q-btn
unelevated
no-caps
class="q-mr-sm text-bold"
data-test="logs-search-bar-confirm-dialog-ok-btn"
:label="t('search.btnDownload')"
color="primary"
@click="downloadRangeData"
/>
</q-card-actions>
</q-card>
</q-dialog>
<q-dialog v-model="store.state.savedViewDialog">
<q-card style="width: 700px; max-width: 80vw">
<q-card-section>
<div class="text-h6">{{ t("search.savedViewsLabel") }}</div>
</q-card-section>
<q-card-section class="q-pt-none">
<div v-if="isSavedViewAction == 'create'">
<q-input
data-test="add-alert-name-input"
v-model="savedViewName"
:label="t('search.savedViewName')"
color="input-border"
bg-color="input-bg"
class="showLabelOnTop"
stack-label
borderless
dense
:rules="[
(val) => !!val.trim() || 'This field is required',
(val) =>
/^[-A-Za-z0-9 /@/_]+$/.test(val) ||
'Input must be alphanumeric',
]"
tabindex="0"
/>
</div>
<div v-else>
<q-select
data-test="saved-view-name-select"
v-model="savedViewSelectedName"
:options="searchObj.data.savedViews"
option-label="view_name"
option-value="view_id"
:label="t('search.savedViewName')"
:popup-content-style="{ textTransform: 'capitalize' }"
color="input-border"
bg-color="input-bg"
class="q-py-sm showLabelOnTop"
stack-label
borderless
dense
:rules="[(val: any) => !!val || 'Field is required!']"
/>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn
data-test="saved-view-dialog-cancel-btn"
unelevated
no-caps
class="q-mr-sm o2-secondary-button"
:label="t('confirmDialog.cancel')"
v-close-popup
/>
<q-btn
data-test="saved-view-dialog-save-btn"
v-if="!saveViewLoader"
unelevated
no-caps
:label="t('common.save')"
class="o2-primary-button"
@click="handleSavedView"
/>
<q-btn
data-test="saved-view-dialog-loading-btn"
v-if="saveViewLoader"
unelevated
no-caps
:label="t('confirmDialog.loading')"
color="primary"
class="text-bold"
/>
</q-card-actions>
</q-card>
</q-dialog>
<q-dialog v-model="store.state.savedFunctionDialog">
<q-card style="width: 700px; max-width: 80vw">
<q-card-section>
<div class="text-h6">{{ t("search.functionPlaceholder") }}</div>
</q-card-section>
<q-card-section class="q-pt-none">
<div class="tw-flex tw-items-center ">
<span class="tw-mt-2">Update</span>
<q-toggle
data-test="saved-function-action-toggle"
v-bind:disable="functionOptions.length == 0"
name="saved_function_action"
v-model="isSavedFunctionAction"
true-value="create"
false-value="update"
label=""
size="lg"
class="o2-toggle-button-lg"
@change="savedFunctionName = ''"
/>
<span class="tw-mt-2">Create</span>
</div>
<div v-if="isSavedFunctionAction == 'create'">
<q-input
data-test="saved-function-name-input"
v-model="savedFunctionName"
:label="t('search.saveFunctionName')"
class="showLabelOnTop"
stack-label
borderless
dense
:rules="[
(val) => !!val.trim() || 'This field is required',
(val) =>
/^[-A-Za-z0-9/_]+$/.test(val) || 'Input must be alphanumeric',
]"
tabindex="0"
/>
</div>
<div v-else>
<q-select
data-test="saved-function-name-select"
v-model="savedFunctionSelectedName"
:options="functionOptions"
option-label="name"
option-value="name"
:label="t('search.saveFunctionName')"
placeholder="Select Function Name"
:popup-content-style="{ textTransform: 'capitalize' }"
class="q-py-sm showLabelOnTop"
stack-label
borderless
dense
:rules="[(val: any) => !!val || 'Field is required!']"
/>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn
data-test="saved-function-dialog-cancel-btn"
unelevated
no-caps
class="q-mr-sm o2-secondary-button"
:label="t('confirmDialog.cancel')"
v-close-popup
/>
<q-btn
data-test="saved-view-dialog-save-btn"
v-if="!saveFunctionLoader"
unelevated
no-caps
:label="t('confirmDialog.ok')"
class="o2-primary-button"
@click="saveFunction"
/>
<q-btn
data-test="saved-function-dialog-loading-btn"
v-if="saveFunctionLoader"
unelevated
no-caps
:label="t('confirmDialog.loading')"
class="o2-primary-button"
/>
</q-card-actions>
</q-card>
</q-dialog>
<q-dialog v-model="searchSchedulerJob">
<q-card style="width: 700px; max-width: 80vw">
<q-card-section>
<div class="text-h6">Schedule Search Job</div>
</q-card-section>
<q-card-section class="q-pt-none">
<div>
<div class="text-left q-mb-xs">
No of Records:
<q-icon name="info"
size="17px" class="q-ml-xs cursor-pointer">
<q-tooltip
anchor="center right"
self="center left"
max-width="300px"
>
<span style="font-size: 14px"
>Number of records can be specified eg: if the no. of
records is 1000 then user can get maximum of 1000
records</span
>
</q-tooltip>
</q-icon>
</div>
<q-input
type="number"
data-test="search-scheuduler-max-number-of-records-input"
v-model="searchObj.meta.jobRecords"
default-value="100"
color="input-border"
bg-color="input-bg"
class="showLabelOnTop"
stack-label
borderless
dense
tabindex="0"
min="100"
/>
</div>
<div class="text-left">
Maximum 100000 events can be returned in schedule job
</div>
<div
style="opacity: 0.8"
class="text-left mapping-warning-msg q-mt-md"
>
<q-icon name="warning"
color="red" class="q-mr-sm" />
<span>Histogram will be disabled for the schedule job</span>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn
data-test="search-scheduler-max-records-cancel-btn"
unelevated
no-caps
class="q-mr-sm o2-secondary-button"
:label="t('confirmDialog.cancel')"
v-close-popup
@click="
{
searchSchedulerJob = false;
searchObj.meta.showSearchScheduler = false;
}
"
/>
<q-btn
data-test="search-scheduler-max-records-submit-btn"
unelevated
no-caps
:label="t('confirmDialog.ok')"
class="o2-primary-button"
@click="addJobScheduler"
v-close-popup
/>
</q-card-actions>
</q-card>
</q-dialog>
<ConfirmDialog
title="Delete Saved View"
message="Are you sure you want to delete saved view?"
@update:ok="confirmDeleteSavedViews"
@update:cancel="confirmDelete = false"
v-model="confirmDelete"
/>
<ConfirmDialog
title="Update Saved View"
message="Are you sure you want to update the saved view? This action will overwrite the existing one."
@update:ok="confirmUpdateSavedViews"
@update:cancel="confirmUpdate = false"
v-model="confirmUpdate"
/>
<!-- Query Plan Dialog -->
<QueryPlanDialog
v-model="showExplainDialog"
:searchObj="searchObj"
/>
</div>
</template>
<script lang="ts">
// @ts-nocheck
import {
defineComponent,
ref,
onMounted,
nextTick,
watch,
toRaw,
onActivated,
onUnmounted,
onDeactivated,
defineAsyncComponent,
onBeforeMount,
onBeforeUnmount,
} from "vue";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import { useStore } from "vuex";
import { useQuasar, copyToClipboard, is, QTooltip } from "quasar";
import DateTime from "@/components/DateTime.vue";
import useLogs from "@/composables/useLogs";
import useStreams from "@/composables/useStreams";
import SyntaxGuide from "./SyntaxGuide.vue";
import jsTransformService from "@/services/jstransform";
import searchService from "@/services/search";
import shortURLService from "@/services/short_url";
import segment from "@/services/segment_analytics";
import config from "@/aws-exports";
import CodeQueryEditor from "@/components/CodeQueryEditor.vue";
import AutoRefreshInterval from "@/components/AutoRefreshInterval.vue";
import useSqlSuggestions from "@/composables/useSuggestions";
import { json2csv } from "json-2-csv";
import QueryPlanDialog from "@/components/QueryPlanDialog.vue";
import {
mergeDeep,
b64DecodeUnicode,
getImageURL,
useLocalInterestingFields,
useLocalSavedView,
queryIndexSplit,
timestampToTimezoneDate,
b64EncodeUnicode,
buildDateTimeObject,
} from "@/utils/zincutils";
import savedviewsService from "@/services/saved_views";
import ConfirmDialog from "@/components/ConfirmDialog.vue";
import { cloneDeep } from "lodash-es";
import useDashboardPanelData from "@/composables/useDashboardPanel";
import { inject } from "vue";
import useCancelQuery from "@/composables/dashboard/useCancelQuery";
import { computed } from "vue";
import { useLoading } from "@/composables/useLoading";
import TransformSelector from "./TransformSelector.vue";
import FunctionSelector from "./FunctionSelector.vue";
import useSearchWebSocket from "@/composables/useSearchWebSocket";
import useNotifications from "@/composables/useNotifications";
import histogram_svg from "../../assets/images/common/histogram_image.svg";
import { allSelectionFieldsHaveAlias } from "@/utils/query/visualizationUtils";
import { logsUtils } from "@/composables/useLogs/logsUtils";
import { searchState } from "@/composables/useLogs/searchState";
import {
getVisualizationConfig,
encodeVisualizationConfig,
decodeVisualizationConfig,
} from "@/composables/useLogs/logsVisualization";
import useSearchBar from "@/composables/useLogs/useSearchBar";
import { useSearchStream } from "@/composables/useLogs/useSearchStream";
import useStreamFields from "@/composables/useLogs/useStreamFields";
import { Bookmark, ChartLine, ChartNoAxesColumn, RefreshCcw, ScanSearch, Share, Menu, Maximize, Minimize } from "lucide-vue-next";
import { outlinedShowChart } from "@quasar/extras/material-icons-outlined";
const defaultValue: any = () => {
return {
name: "",
function: "",
params: "row",
transType: "0",
};
};
export default defineComponent({
name: "ComponentSearchSearchBar",
components: {
DateTime,
SyntaxGuide,
AutoRefreshInterval,
ConfirmDialog,
TransformSelector,
FunctionSelector,
CodeQueryEditor,
QueryPlanDialog,
ScanSearch,
ChartLine,
ChartNoAxesColumn,
RefreshCcw,
Bookmark,
Share,
Menu,
Maximize,
Minimize,
},
emits: [
"searchdata",
"onChangeInterval",
"onChangeTimezone",
"handleQuickModeChange",
"handleRunQueryFn",
"onAutoIntervalTrigger",
"showSearchHistory",
"extractPatterns",
],
methods: {
searchData() {
if (this.searchObj.loading == false) {
// this.searchObj.runQuery = true;
this.$emit("searchdata");
}
},
changeFunctionName(value) {
// alert(value)
// console.log(value);
},
createNewValue(inputValue, doneFn) {
// Call the doneFn with the new value
doneFn(inputValue);
},
updateSelectedValue() {
// Update the selected value with the newly created value
if (
this.functionModel &&
!this.functionOptions.includes(this.functionModel)
) {
this.functionOptions.push(this.functionModel);
}
},
handleDeleteSavedView(item: any) {
this.savedViewDropdownModel = false;
this.deleteViewID = item.view_id;
this.confirmDelete = true;
},
handleUpdateSavedView(item: any) {
if (this.searchObj.data.stream.selectedStream.length == 0) {
this.$q.notify({
type: "negative",
message: "No stream available to update save view.",
});
return;
}
this.savedViewDropdownModel = false;
this.updateViewObj = item;
this.confirmUpdate = true;
},
confirmDeleteSavedViews() {
this.deleteSavedViews();
},
toggleCustomDownloadDialog() {
this.customDownloadDialog = true;
},
confirmUpdateSavedViews() {
this.updateSavedViews(
this.updateViewObj.view_id,
this.updateViewObj.view_name,
);
return;
},
downloadRangeData() {
let initNumber = parseInt(this.downloadCustomInitialNumber);
if (initNumber < 0) {
this.$q.notify({
message: "Initial number must be positive number.",
color: "negative",
position: "bottom",
timeout: 2000,
});
return;
}
// const queryReq = this.buildSearch();
this.searchObj.data.customDownloadQueryObj.query.from =
initNumber == 0 ? 0 : initNumber - 1;
this.searchObj.data.customDownloadQueryObj.query.size =
this.downloadCustomRange;
searchService
.search(
{
org_identifier: this.searchObj.organizationIdentifier,
query: this.searchObj.data.customDownloadQueryObj,
page_type: this.searchObj.data.stream.streamType,
},
"ui",
)
.then((res) => {
this.customDownloadDialog = false;
if (res.data.hits.length > 0) {
this.downloadLogs(res.data.hits, this.downloadCustomFileType);
} else {
this.$q.notify({
message: "No data found to download.",
color: "positive",
position: "top",
timeout: 2000,
});
}
})
.catch((err) => {
this.$q.notify({
message: err.message,
color: "negative",
position: "top",
timeout: 2000,
});
});
},
handleKeyDown(e) {
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
this.handleRunQueryFn();
}
},
},
props: {
fieldValues: {
type: Object,
default: () => ({}),
},
},
setup(props, { emit }) {
const router = useRouter();
const { t } = useI18n();
const $q = useQuasar();
const store = useStore();
const { showErrorNotification } = useNotifications();
const rowsPerPage = ref(10);
const regionFilter = ref();
const regionFilterRef = ref(null);
const { resetStreamData, searchObj } = searchState();
const { buildSearch } = useSearchStream();
const {
fnParsedSQL,
fnUnparsedSQL,
updatedLocalLogFilterField,
updateUrlQueryParams,
generateURLQuery,
isActionsEnabled,
} = logsUtils();
const {
getSavedViews,
setSelectedStreams,
onStreamChange,
getQueryData,
cancelQuery,
} = useSearchBar();
const { loadStreamLists, extractFields } = useStreamFields();
const {
refreshData,
handleRunQuery,
getJobData,
routeToSearchSchedule,
getHistogramTitle,
} = useLogs();
const { isStreamExists, isStreamFetched, getStreams, getStream } = useStreams();
const queryEditorRef = ref(null);
const formData: any = ref(defaultValue());
const functionOptions = ref(searchObj.data.transforms);
const { closeSocketWithError } = useSearchWebSocket();
const transformsExpandState = ref({
actions: false,
functions: false,
});
const functionModel: string = ref(null);
const fnEditorRef: any = ref(null);
const isSavedFunctionAction: string = ref("create");
const savedFunctionName: string = ref("");
const savedFunctionSelectedName: string = ref("");
const saveFunctionLoader = ref(false);
const isFocused = ref(false);
const confirmDialogVisible: boolean = ref(false);
const confirmSavedViewDialogVisible: boolean = ref(false);
const searchSchedulerJob = ref(false);
const autoSearchSchedulerJob = ref(false);
let confirmCallback;
let streamName = "";
const dateTimeRef = ref(null);
const saveViewLoader = ref(false);
const favoriteViews = ref([]);
const localSavedViews = ref([]);
let savedViews = useLocalSavedView();
if (savedViews.value != null) {
const favoriteValues = [];
Object.values(savedViews.value).forEach((view) => {
if (view.org_id === store.state.selectedOrganization.identifier) {
favoriteViews.value.push(view.view_id);
favoriteValues.push(view);
}
});
localSavedViews.value.push(...favoriteValues);
}
const {
autoCompleteData,
autoCompleteKeywords,
autoCompleteSuggestions,
getSuggestions,
updateFieldKeywords,
updateFunctionKeywords,
} = useSqlSuggestions();
const refreshTimeChange = (item) => {
searchObj.meta.refreshInterval = Number(item.value);
};
const isSavedViewAction = ref("create");
const savedViewName = ref("");
const savedViewSelectedName = ref("");
const showExplainDialog = ref(false);
const confirmDelete = ref(false);
const deleteViewID = ref("");
const savedViewDropdownModel = ref(false);
const moreOptionsDropdownModel = ref(false);
const searchTerm = ref("");
const filteredFunctionOptions = computed(() => {
if (searchObj.data.transformType !== "function") return [];
if (!searchTerm.value) return functionOptions.value;
return functionOptions.value.filter((item) =>
item.name.toLowerCase().includes(searchTerm.value.toLowerCase()),
);
});
const filteredActionOptions = computed(() => {
if (searchObj.data.transformType !== "action") return [];
if (!searchTerm.value) return searchObj.data.actions;
return searchObj.data.actions.filter((item) =>
item.name.toLowerCase().includes(searchTerm.value.toLowerCase()),
);
});
const filteredTransformOptions = computed(() => {
if (!searchObj.data.transformType) return [];
if (searchObj.data.transformType === "action")
return filteredActionOptions.value;
if (searchObj.data.transformType === "function")
return filteredFunctionOptions.value;
return [];
});
// const toggleHistogram = ref(false);
const toggleHistogram = computed({
get: () => {
return searchObj.meta.showHistogram;
},
set: (value) => {
searchObj.meta.showHistogram = value;
},
});
const confirmUpdate = ref(false);
const updateViewObj = ref({});
const transformTypes = computed(() => {
return [
{ label: "Function", value: "function" },
{ label: "Action", value: "action" },
];
});
const showFunctionEditor = computed(() => {
// IF actions are disabled, we are reverting to the old behavior of function editor
if (!isActionsEnabled.value) return searchObj.meta.showTransformEditor;
return searchObj.data.transformType === "function";
});
watch(
() => searchObj.data.transforms,
(newVal) => {
functionOptions.value = newVal;
},
);
watch(
() => searchObj.data.stream.selectedStreamFields,
(fields) => {
if (fields != undefined && fields.length) updateFieldKeywords(fields);
},
{ immediate: true, deep: true },
);
watch(
() => searchObj.meta.showSearchScheduler,
(showSearchScheduler) => {
if (showSearchScheduler) {
searchSchedulerJob.value = true;
}
},
{ immediate: true, deep: true },
);
watch(
() => searchObj.meta.functionEditorPlaceholderFlag,
(val) => {
if (
searchObj.meta.jobId != "" &&
val == true &&
(router.currentRoute.value.query.functionContent ||
searchObj.data.tempFunctionContent != "")
) {
if (!checkFnQuery(searchObj.data.tempFunctionContent)) {
$q.notify({
message: "Job Context have been removed",
color: "warning",
position: "bottom",
timeout: 2000,
});
searchObj.meta.jobId = "";
searchObj.data.queryResults.hits = [];
// getQueryData(false);
}
}
},
{ immediate: true, deep: true },
);
watch(
() => searchObj.meta.showHistogram,
(val) => {
if (val == true && searchObj.meta.jobId != "") {
$q.notify({
message: "Histogram is not available for scheduled search",
color: "negative",
position: "bottom",
timeout: 2000,
});
searchObj.meta.showHistogram = false;
searchObj.loadingHistogram = false;
}
},
{ immediate: true, deep: true },
);
watch(
() => searchObj.data.stream.functions,
(funs) => {
if (funs.length) updateFunctionKeywords(funs);
},
{ immediate: true, deep: true },
);
onBeforeUnmount(() => {
queryEditorRef.value = null;
fnEditorRef.value = null;
});
const transformsLabel = computed(() => {
if (
searchObj.data.selectedTransform?.type ===
searchObj.data.transformType &&
searchObj.data.transformType
) {
return searchObj.data.selectedTransform.name;
}
return searchObj.data.transformType === "action"
? "Action"
: searchObj.data.transformType === "function"
? "Function"
: "Transform";
});
const actionEditorQuery = computed(() => {
if (
searchObj.data.transformType === "action" &&
searchObj.data.selectedTransform?.type === "action" &&
searchObj.data.selectedTransform?.name
) {
return `${searchObj.data.selectedTransform?.name} action applied successfully. Run Query to see results.`;
}
return "Select an action to apply";
});
const updateAutoComplete = (value) => {
autoCompleteData.value.query = value;
autoCompleteData.value.cursorIndex =
queryEditorRef?.value?.getCursorIndex();
autoCompleteData.value.fieldValues = props.fieldValues;
autoCompleteData.value.popup.open =
queryEditorRef?.value?.triggerAutoComplete;
getSuggestions();
};
const transformIcon = computed(() => {
if (searchObj.data.transformType === "function")
return "img:" + getImageURL("images/common/function.svg");
if (searchObj.data.transformType === "action") return "code";
if (!searchObj.data.transformType)
return "img:" + getImageURL("images/common/transform.svg");
});
const getColumnNames = (parsedSQL: any) => {
const columnData = parsedSQL?.columns;
let columnNames = [];
for (const item of columnData) {
if (item.expr.type === "column_ref") {
columnNames.push(item.expr.column?.expr?.value);
} else if (item.expr.type === "aggr_func") {
if (item.expr.args?.expr?.hasOwnProperty("column")) {
columnNames.push(item.expr.args?.expr?.column?.value);
} else if (item.expr.args?.expr?.value) {
columnNames.push(item.expr.args?.expr?.value);
}
} else if (item.expr.type === "function") {
item.expr.args.value.map((val) => {
if (val.type === "column_ref") {
columnNames.push(val.column?.expr?.value);
}
});
}
}
if (parsedSQL?._next) {
columnNames = [
...new Set([...columnNames, ...getColumnNames(parsedSQL._next)]),
];
}
return columnNames;
};
const updateQueryValue = (value: string) => {
// if (searchObj.meta.jobId != "") {
// searchObj.meta.jobId = "";
// getQueryData(false);
// }
searchObj.data.editorValue = value;
if (searchObj.meta.quickMode === true) {
const parsedSQL = fnParsedSQL();
if (
searchObj.meta.sqlMode === true &&
Object.hasOwn(parsedSQL, "from") &&
isStreamFetched(searchObj.data.stream.streamType) &&
isStreamExists(value, searchObj.data.stream.streamType)
) {
setSelectedStreams(value);
// onStreamChange(value);
}
if (parsedSQL != undefined && parsedSQL?.columns?.length > 0) {
const columnNames = getColumnNames(parsedSQL);
searchObj.data.stream.interestingFieldList = [];
const defaultInterestingFields = new Set(
store.state?.zoConfig?.default_quick_mode_fields || [],
);
for (const col of columnNames) {
if (
!searchObj.data.stream.interestingFieldList.includes(col) &&
col != "*"
) {
// searchObj.data.stream.interestingFieldList.push(col);
const localInterestingFields: any = useLocalInterestingFields();
let localFields: any = {};
if (localInterestingFields.value != null) {
localFields = localInterestingFields.value;
}
for (const stream of searchObj.data.stream
?.selectedStreamFields || []) {
if (
stream.name == col &&
!searchObj.data.stream.interestingFieldList.includes(col) &&
col !== store.state.zoConfig?.timestamp_column
) {
const interestingFieldsCopy = [
...searchObj.data.stream.interestingFieldList,
];
searchObj.data.stream.interestingFieldList.push(col);
if (!defaultInterestingFields.has(col)) {
interestingFieldsCopy.push(col);
}
localFields[
searchObj.organizationIdentifier +
"_" +
searchObj.data.stream.selectedStream[0]
] = interestingFieldsCopy;
}
}
useLocalInterestingFields(localFields);
}
}
// Add timestamp column to the interesting field list, as it is default interesting field
searchObj.data.stream.interestingFieldList.unshift(
store.state.zoConfig?.timestamp_column,
);
for (const item of searchObj.data.stream?.selectedStreamFields ||
[]) {
if (
searchObj.data.stream.interestingFieldList.includes(item.name)
) {
item.isInterestingField = true;
} else {
item.isInterestingField = false;
}
}
}
}
if (
searchObj.meta.sqlMode === false &&
value.toLowerCase().includes("select") &&
value.toLowerCase().includes("from")
) {
searchObj.meta.sqlMode = true;
searchObj.meta.sqlModeManualTrigger = true;
}
if (value != "" && searchObj.meta.sqlMode === true) {
const parsedSQL = fnParsedSQL();
if (
(Object.hasOwn(parsedSQL, "from") ||
Object.hasOwn(parsedSQL, "select")) &&
isStreamFetched(searchObj.data.stream.streamType) &&
isStreamExists(value, searchObj.data.stream.streamType)
) {
setSelectedStreams(value);
// onStreamChange(value);
}
}
updateAutoComplete(value);
try {
if (searchObj.meta.sqlMode === true) {
let parsedQuery = null;
try {
parsedQuery = fnParsedSQL(value);
} catch (e) {
console.log(e, "Logs: Error while parsing query");
}
if (parsedQuery?.from?.length > 0) {
//this condition is to handle the with queries so for WITH queries the table name is not present in the from array it will be there in the with array
//the table which is there in from array is the temporary array
const tableName: string = !parsedQuery.with
? parsedQuery.from[0].table ||
parsedQuery.from[0].expr?.ast?.from?.[0]?.table
: "";
if (
!searchObj.data.stream.selectedStream.includes(tableName) &&
tableName !== streamName
) {
let streamFound = false;
searchObj.data.stream.selectedStream = [];
streamName = tableName;
searchObj.data.streamResults.list.forEach((stream) => {
if (stream.name == streamName) {
streamFound = true;
let itemObj = {
label: stream.name,
value: stream.name,
};
// searchObj.data.stream.selectedStream = itemObj;
searchObj.data.stream.selectedStream.push(itemObj.value);
onStreamChange(searchObj.data.query);
}
});
if (streamFound == false) {
// searchObj.data.stream.selectedStream = { label: "", value: "" };
searchObj.data.stream.selectedStream = [];
searchObj.data.stream.selectedStreamFields = [];
// $q.notify({
// message: "Stream not found",
// color: "info",
// position: "bottom",
// timeout: 2000,
// });
}
}
}
}
//here we reset the job id if user change the query and move outside of the editor
if (
searchObj.meta.jobId != "" &&
searchObj.meta.queryEditorPlaceholderFlag == true
) {
if (!checkQuery(value)) {
$q.notify({
message: "Job Context have been removed",
color: "warning",
position: "bottom",
timeout: 2000,
});
searchObj.meta.jobId = "";
searchObj.data.queryResults.hits = [];
// getQueryData(false);
}
}
} catch (e) {
console.log(e, "Logs: Error while updating query value");
}
};
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === "Escape") {
isFocused.value = false;
}
};
const updateDateTime = async (value: object) => {
if (
value.valueType == "absolute" &&
searchObj.data.stream.selectedStream.length > 0 &&
searchObj.data.datetime.queryRangeRestrictionInHour > 0 &&
value.hasOwnProperty("selectedDate") &&
value.hasOwnProperty("selectedTime") &&
value.selectedDate.hasOwnProperty("from") &&
value.selectedTime.hasOwnProperty("startTime")
) {
// Convert hours to microseconds
let newStartTime =
parseInt(value.endTime) -
searchObj.data.datetime.queryRangeRestrictionInHour *
60 *
60 *
1000000;
if (parseInt(newStartTime) > parseInt(value.startTime)) {
value.startTime = newStartTime;
value.selectedDate.from = timestampToTimezoneDate(
value.startTime / 1000,
store.state.timezone,
"yyyy/MM/DD",
);
value.selectedTime.startTime = timestampToTimezoneDate(
value.startTime / 1000,
store.state.timezone,
"HH:mm",
);
dateTimeRef.value.setAbsoluteTime(value.startTime, value.endTime);
dateTimeRef.value.setDateType("absolute");
}
}
searchObj.data.datetime = {
startTime: value.startTime,
endTime: value.endTime,
relativeTimePeriod: value.relativeTimePeriod
? value.relativeTimePeriod
: searchObj.data.datetime.relativeTimePeriod,
type: value.relativeTimePeriod ? "relative" : "absolute",
selectedDate: value?.selectedDate,
selectedTime: value?.selectedTime,
queryRangeRestrictionMsg:
searchObj.data.datetime?.queryRangeRestrictionMsg || "",
queryRangeRestrictionInHour:
searchObj.data.datetime?.queryRangeRestrictionInHour || 0,
};
await nextTick();
await nextTick();
await nextTick();
await nextTick();
if (
searchObj.loading == false &&
store.state.zoConfig.query_on_stream_selection == false
) {
searchObj.loading = true;
searchObj.runQuery = true;
}
if (config.isCloud == "true" && value.userChangedValue) {
segment.track("Button Click", {
button: "Date Change",
tab: value.tab,
value: value,
//user_org: this.store.state.selectedOrganization.identifier,
//user_id: this.store.state.userInfo.email,
stream_name: searchObj.data.stream.selectedStream.join(","),
page: "Search Logs",
});
}
if (
value.valueType === "relative" &&
store.state.zoConfig.query_on_stream_selection == false
) {
emit("searchdata");
}
};
const updateTimezone = () => {
if (store.state.zoConfig.query_on_stream_selection == false) {
emit("onChangeTimezone");
}
};
const updateQuery = () => {
if (queryEditorRef.value?.setValue)
queryEditorRef.value.setValue(searchObj.data.query);
};
const downloadLogs = async (data, format) => {
//here we are using a package json2csv which converts json to csv data
//why package because we faced one issue where user has , in some of the fields so
//it is treating it as seperate fields
//eg: {body:"hey this is the email body , with some info in it "}
//after converting it will treat hey this is the email body this as the body and remaining will be the next column
//to solve this issue we are using json2csv package
try {
let filename = "logs-data";
let dataobj;
const options = {
emptyFieldValue: "",
};
if (format === "csv") {
filename += ".csv";
dataobj = await json2csv(data, options);
} else {
filename += ".json";
dataobj = JSON.stringify(data, null, 2);
}
if (dataobj.length === 0) {
$q.notify({
type: "negative",
message: "No data available to download.",
});
return;
}
if (format === "csv") {
dataobj = new Blob([dataobj], { type: "text/csv" });
} else {
dataobj = new Blob([dataobj], { type: "application/json" });
}
const file = new File([dataobj], filename, {
type: format === "csv" ? "text/csv" : "application/json",
});
const url = URL.createObjectURL(file);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
showDownloadMenu.value = false;
} catch (error) {
showDownloadMenu.value = false;
$q.notify({
type: "negative",
message: "Error downloading logs",
timeout: 2000,
});
}
};
onMounted(async () => {
searchObj.data.transformType =
router.currentRoute.value.query.transformType || "function";
if (
router.currentRoute.value.query.transformType === "function" &&
(router.currentRoute.value.query.functionContent ||
searchObj.data.tempFunctionContent)
) {
const fnContent = router.currentRoute.value.query.functionContent
? b64DecodeUnicode(router.currentRoute.value.query.functionContent)
: searchObj.data.tempFunctionContent;
fnEditorRef?.value?.setValue(fnContent);
}
updateEditorWidth();
window.addEventListener("keydown", handleEscKey);
});
onUnmounted(() => {
window.removeEventListener("click", () => {
fnEditorRef?.value?.resetEditorLayout();
});
window.removeEventListener("keydown", handleEscKey);
});
onActivated(() => {
updateQuery();
updateEditorWidth();
if (
(router.currentRoute.value.query.functionContent ||
searchObj.data.tempFunctionContent) &&
searchObj.data.transformType === "function"
) {
const fnContent = router.currentRoute.value.query.functionContent
? b64DecodeUnicode(router.currentRoute.value.query.functionContent)
: searchObj.data.tempFunctionContent;
fnEditorRef?.value?.setValue(fnContent);
fnEditorRef?.value?.resetEditorLayout();
window.removeEventListener("click", () => {
fnEditorRef?.value?.resetEditorLayout();
});
}
fnEditorRef?.value?.resetEditorLayout();
});
onDeactivated(() => {
window.removeEventListener("click", () => {
fnEditorRef?.value?.resetEditorLayout();
});
});
const saveFunction = () => {
saveFunctionLoader.value = true;
let callTransform: Promise<{ data: any }>;
const content = searchObj.data.tempFunctionContent;
let fnName = "";
if (isSavedFunctionAction.value == "create") {
fnName = savedFunctionName.value;
} else {
fnName = savedFunctionSelectedName.value.name;
}
if (content.trim() == "") {
$q.notify({
type: "warning",
message:
"The function field must contain a value and cannot be left empty.",
});
saveFunctionLoader.value = false;
return;
}
const pattern = /^[a-zA-Z][a-zA-Z0-9_]*$/;
if (!pattern.test(fnName)) {
$q.notify({
type: "negative",
message: "Function name is not valid.",
});
saveFunctionLoader.value = false;
return;
}
formData.value.params = "row";
formData.value.function = content;
formData.value.transType = 0;
formData.value.name = fnName;
searchObj.data.tempFunctionContent = content;
// const result = functionOptions.value.find((obj) => obj.name === fnName);
if (isSavedFunctionAction.value == "create") {
callTransform = jsTransformService.create(
store.state.selectedOrganization.identifier,
formData.value,
);
callTransform
.then((res: { data: any }) => {
$q.notify({
type: "positive",
message: res.data.message,
});
functionModel.value = {
name: formData.value.name,
function: formData.value.function,
};
functionOptions.value.push({
name: formData.value.name,
function: formData.value.function,
transType: 0,
params: "row",
});
store.dispatch("setSavedFunctionDialog", false);
isSavedFunctionAction.value = "create";
savedFunctionName.value = "";
saveFunctionLoader.value = false;
savedFunctionSelectedName.value = "";
})
.catch((err) => {
saveFunctionLoader.value = false;
$q.notify({
type: "negative",
message:
JSON.stringify(err.response.data["message"]) ||
"Function creation failed",
timeout: 5000,
});
});
} else {
saveFunctionLoader.value = false;
showConfirmDialog(() => {
saveFunctionLoader.value = true;
callTransform = jsTransformService.update(
store.state.selectedOrganization.identifier,
formData.value,
);
callTransform
.then((res: { data: any }) => {
$q.notify({
type: "positive",
message: "Function updated successfully.",
});
const transformIndex = searchObj.data.transforms.findIndex(
(obj) => obj.name === formData.value.name,
);
if (transformIndex !== -1) {
searchObj.data.transforms[transformIndex].name =
formData.value.name;
searchObj.data.transforms[transformIndex].function =
formData.value.function;
}
functionOptions.value = searchObj.data.transforms;
store.dispatch("setSavedFunctionDialog", false);
isSavedFunctionAction.value = "create";
savedFunctionName.value = "";
saveFunctionLoader.value = false;
savedFunctionSelectedName.value = "";
})
.catch((err) => {
saveFunctionLoader.value = false;
$q.notify({
type: "negative",
message:
JSON.stringify(err.response.data["message"]) ||
"Function updation failed",
timeout: 5000,
});
});
});
}
};
const resetFunctionContent = () => {
fnEditorRef?.value?.setValue("");
store.dispatch("setSavedFunctionDialog", false);
isSavedFunctionAction.value = "create";
savedFunctionName.value = "";
saveFunctionLoader.value = false;
savedFunctionSelectedName.value = "";
};
const resetEditorLayout = () => {
setTimeout(() => {
queryEditorRef?.value?.resetEditorLayout();
fnEditorRef?.value?.resetEditorLayout();
}, 100);
};
const applyAction = (actionId) => {
searchObj.data.actionId = actionId.id;
};
const populateFunctionImplementation = (fnValue, flag = false) => {
if (flag) {
$q.notify({
type: "positive",
message: `${fnValue.name} function applied successfully.`,
timeout: 3000,
});
}
searchObj.config.fnSplitterModel = 60;
fnEditorRef?.value?.setValue(fnValue.function);
searchObj.data.tempFunctionName = fnValue.name;
searchObj.data.tempFunctionContent = fnValue.function;
};
const fnSavedFunctionDialog = () => {
const content = searchObj.data.tempFunctionContent;
if (content == "") {
$q.notify({
type: "negative",
message: "No function definition found.",
});
return;
}
store.dispatch("setSavedFunctionDialog", true);
isSavedFunctionAction.value = "create";
savedFunctionName.value = "";
saveFunctionLoader.value = false;
savedFunctionSelectedName.value = "";
};
const showConfirmDialog = (callback) => {
confirmDialogVisible.value = true;
confirmCallback = callback;
};
const showSavedViewConfirmDialog = (callback) => {
confirmSavedViewDialogVisible.value = true;
confirmCallback = callback;
};
const cancelConfirmDialog = () => {
confirmSavedViewDialogVisible.value = false;
confirmDialogVisible.value = false;
confirmCallback = null;
};
const confirmDialogOK = () => {
if (confirmCallback) {
confirmCallback();
}
confirmDialogVisible.value = false;
confirmCallback = null;
};
const filterFn = (val, update) => {
update(() => {
if (val === "") {
functionOptions.value = searchObj.data.transforms;
} else {
const needle = val.toLowerCase();
functionOptions.value = searchObj.data.transforms.filter(
(v) => v.name?.toLowerCase().indexOf(needle) > -1,
);
}
});
};
const onRefreshIntervalUpdate = () => {
emit("onChangeInterval");
};
const fnSavedView = () => {
if (searchObj.data.stream.selectedStream.length == 0) {
$q.notify({
type: "negative",
message: "No stream available to save view.",
});
return;
}
store.dispatch("setSavedViewDialog", true);
isSavedViewAction.value = "create";
savedViewName.value = "";
saveViewLoader.value = false;
savedViewSelectedName.value = "";
savedViewDropdownModel.value = false;
};
// Common function to restore visualization data and sync to URL
const restoreVisualizationData = async (visualizationData) => {
if (!visualizationData) return;
// Restore visualization config to dashboardPanelData
if (visualizationData.config) {
dashboardPanelData.data.config = visualizationData.config;
}
if (visualizationData.type) {
dashboardPanelData.data.type = visualizationData.type;
}
// Sync visualization data to URL
const currentVisualizationData =
getVisualizationConfig(dashboardPanelData);
if (currentVisualizationData) {
const encoded = encodeVisualizationConfig(currentVisualizationData);
if (encoded) {
const currentQuery = { ...router.currentRoute.value.query };
currentQuery.visualization_data = encoded;
await router.replace({
name: router.currentRoute.value.name,
query: currentQuery,
});
}
}
};
const applySavedView = async (item) => {
await cancelQuery();
searchObj.shouldIgnoreWatcher = true;
searchObj.meta.sqlMode = false;
savedviewsService
.getViewDetail(
store.state.selectedOrganization.identifier,
item.view_id,
)
.then(async (res) => {
if (res.status == 200) {
store.dispatch("setSavedViewFlag", true);
const extractedObj = res.data.data;
// Resetting columns as its not required in searchObj
// As we reassign columns from selectedFields and search results
extractedObj.data.resultGrid.columns = [];
// As in saved view, we observed field getting duplicated in selectedFields
// So, we are removing duplicates before applying saved view
if (extractedObj.data.stream.selectedFields?.length) {
extractedObj.data.stream.selectedFields = [
...new Set(extractedObj.data.stream.selectedFields),
];
}
if (extractedObj.data?.timezone) {
store.dispatch("setTimezone", extractedObj.data.timezone);
}
if (!extractedObj.data.stream.hasOwnProperty("streamType")) {
extractedObj.data.stream.streamType = "logs";
}
delete searchObj.data.queryResults.aggs;
if (
searchObj.data.stream.streamType ==
extractedObj.data.stream.streamType
) {
// if (
// extractedObj.data.stream.selectedStream.value !=
// searchObj.data.stream.selectedStream.value
// ) {
// extractedObj.data.stream.streamLists =
// searchObj.data.stream.streamLists;
// }
// ----- Here we are explicitly handling stream change for multistream -----
let selectedStreams = [];
const streamValues = searchObj.data.stream.streamLists.map(
(item) => item.value,
);
if (typeof extractedObj.data.stream.selectedStream == "object") {
if (
extractedObj.data.stream.selectedStream.hasOwnProperty(
"value",
)
) {
selectedStreams.push(
extractedObj.data.stream.selectedStream.value,
);
} else {
selectedStreams.push(
...extractedObj.data.stream.selectedStream,
);
}
} else {
selectedStreams.push(extractedObj.data.stream.selectedStream);
}
const streamNotExist = selectedStreams.filter(
(stream_str) => !streamValues.includes(stream_str),
);
if (streamNotExist.length > 0) {
let errMsg = t("search.streamNotExist").replace(
"[STREAM_NAME]",
streamNotExist,
);
throw new Error(errMsg);
return;
}
// extractedObj.data.stream.selectedStream = [];
// extractedObj.data.stream.selectedStream = selectedStreams;
delete extractedObj.data.stream.streamLists;
delete extractedObj.data.stream.selectedStream;
delete searchObj.data.stream.selectedStream;
delete searchObj.meta.regions;
if (extractedObj.meta.hasOwnProperty("regions")) {
searchObj.meta["regions"] = extractedObj.meta.regions;
} else {
searchObj.meta["regions"] = [];
}
delete searchObj.data.queryResults.aggs;
delete searchObj.data.stream.interestingFieldList;
searchObj.data.stream.selectedStream = [];
extractedObj.data.transforms = searchObj.data.transforms;
extractedObj.data.stream.functions =
searchObj.data.stream.functions;
extractedObj.data.histogram = {
xData: [],
yData: [],
chartParams: {},
};
extractedObj.data.savedViews = searchObj.data.savedViews;
extractedObj.data.queryResults = [];
extractedObj.meta.scrollInfo = {};
//here we are merging deep to the searchObj with the extractedObj
mergeDeep(searchObj, extractedObj);
searchObj.shouldIgnoreWatcher = true;
// Restore visualization data if available
if (extractedObj.data.visualizationData) {
await restoreVisualizationData(
extractedObj.data.visualizationData,
);
}
// await nextTick();
if (extractedObj.data.tempFunctionContent != "") {
populateFunctionImplementation(
{
name: "",
function: searchObj.data.tempFunctionContent,
},
false,
);
searchObj.data.tempFunctionContent =
extractedObj.data.tempFunctionContent;
searchObj.meta.functionEditorPlaceholderFlag = false;
searchObj.data.transformType = "function";
if (showFunctionEditor.value)
searchObj.meta.showTransformEditor = true;
} else {
populateFunctionImplementation(
{
name: "",
function: "",
},
false,
);
searchObj.data.tempFunctionContent = "";
searchObj.meta.functionEditorPlaceholderFlag = true;
}
//here we are getting data so we need to check here
//before we set the time to the dateTimeRef.value we need to check if the startTime and endTime difference is greater than maxQueryRange in hours
//solution will be we will do endTime - startTime and convert that difference into hours and check with both global and stream level maxQueryRange and if it is less than or equal to that we will keep that as it is
//if that exceeds the maxQueryRange we will convert that to relative type (because we cannot assume the start and end date) or we can do that by converting present time - maxQueryRange
// Validate and adjust time range based on maxQueryRange
//before we check all this we need to get the current selected stream max query range and also global max query range
//we need to compare so if max query range of stream is there we will use that otherwise we will use the global max query if both are not present
//we will skip this below process
// Get max query range for all selected streams and take the minimum
// Preference: stream max query range > global max query range
const globalMaxQueryRange = store.state.zoConfig.max_query_range || 0;
let effectiveMaxQueryRange = -1;
if (selectedStreams && selectedStreams.length > 0) {
// Fetch all stream data in parallel
const streamDataPromises = selectedStreams.map((streamName) =>
getStream(streamName, searchObj.data.stream.streamType, false)
);
try {
const streamDataList = await Promise.all(streamDataPromises);
// Extract max_query_range from each stream's settings
const streamMaxQueryRanges = streamDataList
.map((streamData) => streamData?.settings?.max_query_range || 0)
.filter((range) => range > 0); // Only consider positive values
// If we have stream-specific max query ranges, find the minimum (stream takes preference)
if (streamMaxQueryRanges.length > 0) {
effectiveMaxQueryRange = Math.min(...streamMaxQueryRanges);
} else if (globalMaxQueryRange > 0) {
// No stream-specific ranges, fall back to global max query range
effectiveMaxQueryRange = globalMaxQueryRange;
}
} catch (error) {
// On error, fall back to global max query range
effectiveMaxQueryRange = globalMaxQueryRange > 0 ? globalMaxQueryRange : -1;
}
} else if (globalMaxQueryRange > 0) {
// No selected streams, use global max query range
effectiveMaxQueryRange = globalMaxQueryRange;
}
// Validate and adjust time range if effective max query range exists
if (
effectiveMaxQueryRange > 0 &&
searchObj.data.datetime?.startTime &&
searchObj.data.datetime?.endTime
) {
// Calculate time difference in hours
const startTimeMicros = parseInt(searchObj.data.datetime.startTime);
const endTimeMicros = parseInt(searchObj.data.datetime.endTime);
const timeDiffInHours = (endTimeMicros - startTimeMicros) / (60 * 60 * 1000000);
// Check if time difference exceeds effective max query range
if (timeDiffInHours > effectiveMaxQueryRange) {
// Adjust to current time - maxQueryRange
const currentTimeMicros = Date.now() * 1000; // Convert milliseconds to microseconds
const maxQueryRangeMicros = effectiveMaxQueryRange * 60 * 60 * 1000000;
const adjustedStartTime = currentTimeMicros - maxQueryRangeMicros;
const adjustedEndTime = currentTimeMicros;
// Get the current datetime type
const currentType = searchObj.data.datetime.type || "relative";
// Build the complete datetime object with all required fields
const updatedDateTime = buildDateTimeObject(
adjustedStartTime,
adjustedEndTime,
currentType
);
// Update searchObj.data.datetime with all fields
searchObj.data.datetime.startTime = adjustedStartTime;
searchObj.data.datetime.endTime = adjustedEndTime;
if (currentType === "relative") {
// For relative type, update relativeTimePeriod
searchObj.data.datetime.relativeTimePeriod = updatedDateTime.relativeTimePeriod;
} else if (currentType === "absolute") {
// For absolute type, update selectedDate and selectedTime
searchObj.data.datetime.selectedDate = updatedDateTime.selectedDate;
searchObj.data.datetime.selectedTime = updatedDateTime.selectedTime;
searchObj.data.datetime.relativeTimePeriod = null;
}
}
}
dateTimeRef.value.setSavedDate(searchObj.data.datetime);
if (searchObj.meta.refreshInterval != "0") {
onRefreshIntervalUpdate();
} else {
clearInterval(store.state.refreshIntervalID);
}
searchObj.data.stream.selectedStream.push(...selectedStreams);
await updatedLocalLogFilterField();
await getStreams("logs", true);
} else {
// ----- Here we are explicitly handling stream change -----
resetStreamData();
searchObj.data.stream.streamType =
extractedObj.data.stream.streamType;
delete searchObj.meta.regions;
if (extractedObj.meta.hasOwnProperty("regions")) {
searchObj.meta["regions"] = extractedObj.meta.regions;
} else {
searchObj.meta["regions"] = [];
}
// Here copying selected stream object, as in loadStreamLists() we are setting selected stream object to empty object
// After loading stream list, we are setting selected stream object to copied object
// const selectedStream = cloneDeep(
// extractedObj.data.stream.selectedStream
// );
let selectedStreams = [];
if (typeof extractedObj.data.stream.selectedStream == "object") {
if (
extractedObj.data.stream.selectedStream.hasOwnProperty(
"value",
)
) {
selectedStreams.push(
extractedObj.data.stream.selectedStream.value,
);
} else {
selectedStreams.push(
...extractedObj.data.stream.selectedStream,
);
}
} else {
selectedStreams.push(extractedObj.data.stream.selectedStream);
}
extractedObj.data.transforms = searchObj.data.transforms;
extractedObj.data.histogram = {
xData: [],
yData: [],
chartParams: {},
};
extractedObj.data.savedViews = searchObj.data.savedViews;
extractedObj.data.queryResults = [];
extractedObj.meta.scrollInfo = {};
delete searchObj.data.queryResults.aggs;
mergeDeep(searchObj, extractedObj);
searchObj.data.streamResults = {};
// Restore visualization data if available
if (extractedObj.data.visualizationData) {
await restoreVisualizationData(
extractedObj.data.visualizationData,
);
}
const streamData = await getStreams(
searchObj.data.stream.streamType,
true,
);
searchObj.data.streamResults = streamData;
await loadStreamLists();
searchObj.data.stream.selectedStream = [selectedStreams];
const streamValues = searchObj.data.stream.streamLists.map(
(item) => item.value,
);
const streamNotExist = selectedStreams.filter(
(stream_str) => !streamValues.includes(stream_str),
);
if (streamNotExist.length > 0) {
let errMsg = t("search.streamNotExist").replace(
"[STREAM_NAME]",
streamNotExist,
);
throw new Error(errMsg);
return;
}
// await nextTick();
if (extractedObj.data.tempFunctionContent != "") {
populateFunctionImplementation(
{
name: "",
function: searchObj.data.tempFunctionContent,
},
false,
);
searchObj.data.tempFunctionContent =
extractedObj.data.tempFunctionContent;
searchObj.meta.functionEditorPlaceholderFlag = false;
} else {
populateFunctionImplementation(
{
name: "",
function: "",
},
false,
);
searchObj.data.tempFunctionContent = "";
searchObj.meta.functionEditorPlaceholderFlag = true;
}
dateTimeRef.value.setSavedDate(searchObj.data.datetime);
if (searchObj.meta.refreshInterval != "0") {
onRefreshIntervalUpdate();
} else {
clearInterval(store.state.refreshIntervalID);
}
await updatedLocalLogFilterField();
}
if (searchObj.meta.toggleFunction == false) {
searchObj.config.fnSplitterModel = 100;
resetFunctionContent();
}
updateEditorWidth();
$q.notify({
message: `${item.view_name} view applied successfully.`,
color: "positive",
position: "bottom",
timeout: 1000,
});
setTimeout(async () => {
try {
searchObj.loadingHistogram = false;
searchObj.loading = true;
searchObj.meta.refreshHistogram = true;
// TODO OK: Remove all the instances of communicationMethod and below assignment aswell
searchObj.communicationMethod = "streaming";
await extractFields();
await getQueryData();
store.dispatch("setSavedViewFlag", false);
updateUrlQueryParams();
searchObj.shouldIgnoreWatcher = false;
} catch (e) {
searchObj.shouldIgnoreWatcher = false;
console.log("Error while applying saved view", e);
}
}, 1000);
if (
extractedObj.data.resultGrid.colOrder &&
extractedObj.data.resultGrid.colOrder.hasOwnProperty(
searchObj.data.stream.selectedStream,
)
) {
searchObj.data.stream.selectedFields =
extractedObj.data.resultGrid.colOrder[
searchObj.data.stream.selectedStream
].filter((_field) => _field !== (store?.state?.zoConfig?.timestamp_column || '_timestamp'));
} else {
searchObj.data.stream.selectedFields =
extractedObj.data.stream.selectedFields;
}
if (
extractedObj.data.resultGrid.colSizes &&
extractedObj.data.resultGrid.colSizes.hasOwnProperty(
searchObj.data.stream.selectedStream,
)
) {
searchObj.data.resultGrid.colSizes[
searchObj.data.stream.selectedStream
] =
extractedObj.data.resultGrid.colSizes[
searchObj.data.stream.selectedStream
];
}
} else {
searchObj.shouldIgnoreWatcher = false;
store.dispatch("setSavedViewFlag", false);
$q.notify({
message: err.message || `Error while applying saved view.`,
color: "negative",
position: "bottom",
timeout: 3000,
});
}
})
.catch((err) => {
searchObj.shouldIgnoreWatcher = false;
store.dispatch("setSavedViewFlag", false);
$q.notify({
message: `Error while applying saved view.`,
color: "negative",
position: "bottom",
timeout: 1000,
});
console.log("Error while applying saved view", err);
});
};
const handleSavedView = () => {
if (isSavedViewAction.value == "create") {
if (
savedViewName.value == "" ||
/^[A-Za-z0-9 \-\_]+$/.test(savedViewName.value) == false
) {
$q.notify({
message: `Please provide valid view name.`,
color: "negative",
position: "bottom",
timeout: 1000,
});
} else {
saveViewLoader.value = true;
createSavedViews(savedViewName.value);
}
}
// else {
// if (savedViewSelectedName.value.view_id) {
// saveViewLoader.value = false;
// showSavedViewConfirmDialog(() => {
// saveViewLoader.value = true;
// updateSavedViews(
// savedViewSelectedName.value.view_id,
// savedViewSelectedName.value.view_name,
// );
// });
// } else {
// $q.notify({
// message: `Please select saved view to update.`,
// color: "negative",
// position: "bottom",
// timeout: 1000,
// });
// }
// }
};
const deleteSavedViews = async () => {
try {
savedviewsService
.delete(
store.state.selectedOrganization.identifier,
deleteViewID.value,
)
.then((res) => {
//remove it from localstorage as well
const localStoredSavedViews = JSON.parse(
localStorage.getItem("savedViews") || "[]",
);
delete localStoredSavedViews[deleteViewID.value];
favoriteViews.value.forEach((item: any) => {
//remove it from favorite views list because we dont need to show it in the favorite views list
if (item == deleteViewID.value) {
favoriteViews.value.splice(
favoriteViews.value.indexOf(item),
1,
);
}
});
//remove it from local saved views list because we dont need to show it in the local saved views list
localSavedViews.value = localSavedViews.value.filter(
(item: any) => item.view_id !== deleteViewID.value,
);
localStorage.setItem(
"savedViews",
JSON.stringify(localStoredSavedViews),
);
//we are deleting the local storage item and also we are removing the item from the favoriteViews array
if (res.status == 200) {
$q.notify({
message: `View deleted successfully.`,
color: "positive",
position: "bottom",
timeout: 1000,
});
getSavedViews();
} else {
$q.notify({
message: `Error while deleting saved view. ${res.data.error_detail}`,
color: "negative",
position: "bottom",
timeout: 1000,
});
}
})
.catch((err) => {
$q.notify({
message: `Error while deleting saved view.`,
color: "negative",
position: "bottom",
timeout: 1000,
});
console.log("Error while deleting saved view", err);
});
} catch (e: any) {
console.log("Error while getting saved views", e);
}
};
const getSearchObj = () => {
try {
delete searchObj.meta.scrollInfo;
delete searchObj?.value;
let savedSearchObj = toRaw(searchObj);
savedSearchObj = JSON.parse(JSON.stringify(savedSearchObj));
delete savedSearchObj.data.queryResults;
delete savedSearchObj.data.histogram;
delete savedSearchObj.data.sortedQueryResults;
delete savedSearchObj.data.stream.streamLists;
delete savedSearchObj.data.stream.functions;
delete savedSearchObj.data.streamResults;
delete savedSearchObj.data.savedViews;
delete savedSearchObj.data.transforms;
// Turn off all loaders before saving view
savedSearchObj.loading = false;
savedSearchObj.loadingHistogram = false;
savedSearchObj.loadingCounter = false;
savedSearchObj.loadingStream = false;
savedSearchObj.loadingSavedView = false;
savedSearchObj.data.timezone = store.state.timezone;
if (savedSearchObj.data.parsedQuery) {
delete savedSearchObj.data.parsedQuery;
}
// Include visualization data if in visualization mode
if (
searchObj.meta.logsVisualizeToggle === "visualize" &&
dashboardPanelData
) {
const visualizationData = getVisualizationConfig(dashboardPanelData);
if (visualizationData) {
savedSearchObj.data.visualizationData = visualizationData;
}
}
return savedSearchObj;
// return b64EncodeUnicode(JSON.stringify(savedSearchObj));
} catch (e) {
console.log("Error while encoding search obj", e);
}
};
const createSavedViews = (viewName: string) => {
try {
if (viewName.trim() == "") {
$q.notify({
message: `Please provide valid view name.`,
color: "negative",
position: "bottom",
timeout: 1000,
});
saveViewLoader.value = false;
return;
}
const viewObj: any = {
data: getSearchObj(),
view_name: viewName,
};
savedviewsService
.post(store.state.selectedOrganization.identifier, viewObj)
.then((res) => {
if (res.status == 200) {
store.dispatch("setSavedViewDialog", false);
if (searchObj.data.hasOwnProperty("savedViews") == false) {
searchObj.data.savedViews = [];
}
searchObj.data.savedViews.push({
org_id: res.data.org_id,
payload: viewObj.data,
view_id: res.data.view_id,
view_name: viewName,
});
$q.notify({
message: `View created successfully.`,
color: "positive",
position: "bottom",
timeout: 1000,
});
getSavedViews();
isSavedViewAction.value = "create";
savedViewName.value = "";
saveViewLoader.value = false;
} else {
saveViewLoader.value = false;
$q.notify({
message: `Error while creating saved view. ${res.data.error_detail}`,
color: "negative",
position: "bottom",
timeout: 1000,
});
}
})
.catch((err) => {
saveViewLoader.value = false;
$q.notify({
message: `Error while creating saved view.`,
color: "negative",
position: "bottom",
timeout: 1000,
});
console.log("Error while creating saved view", err);
});
} catch (e: any) {
isSavedViewAction.value = "create";
savedViewName.value = "";
saveViewLoader.value = false;
$q.notify({
message: `Error while saving view: ${e}`,
color: "negative",
position: "bottom",
timeout: 1000,
});
console.log("Error while saving view", e);
}
};
const updateSavedViews = (viewID: string, viewName: string) => {
try {
const viewObj: any = {
data: getSearchObj(),
view_name: viewName,
};
const dismiss = $q.notify({
message: "Updating saved view...",
position: "bottom",
timeout: 0,
});
savedviewsService
.put(store.state.selectedOrganization.identifier, viewID, viewObj)
.then((res) => {
dismiss();
if (res.status == 200) {
store.dispatch("setSavedViewDialog", false);
//update the payload and view_name in savedViews object based on id
searchObj.data.savedViews.forEach(
(item: { view_id: string }, index: string | number) => {
if (item.view_id == viewID) {
searchObj.data.savedViews[index].payload = viewObj.data;
searchObj.data.savedViews[index].view_name = viewName;
}
},
);
$q.notify({
message: `View updated successfully.`,
color: "positive",
position: "bottom",
timeout: 1000,
});
isSavedViewAction.value = "create";
savedViewSelectedName.value = "";
saveViewLoader.value = false;
confirmSavedViewDialogVisible.value = false;
} else {
saveViewLoader.value = false;
$q.notify({
message: `Error while updating saved view. ${res.data.error_detail}`,
color: "negative",
position: "bottom",
timeout: 1000,
});
}
})
.catch((err) => {
dismiss();
saveViewLoader.value = false;
$q.notify({
message: `Error while updating saved view.`,
color: "negative",
position: "bottom",
timeout: 1000,
});
console.log("Error while updating saved view", err);
});
} catch (e: any) {
isSavedViewAction.value = "create";
savedViewSelectedName.value = "";
saveViewLoader.value = false;
$q.notify({
message: `Error while saving view: ${e}`,
color: "negative",
position: "bottom",
timeout: 1000,
});
console.log("Error while saving view", e);
}
};
const shareLink = useLoading(async () => {
const queryObj = generateURLQuery(true, dashboardPanelData);
// Removed the 'type' property from the object to avoid issues when navigating from the stream to the logs page,
// especially when the user performs multi-select on streams and shares the URL.
delete queryObj?.type;
const queryString = Object.entries(queryObj)
.map(
([key, value]) =>
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
)
.join("&");
let shareURL = window.location.origin + window.location.pathname;
if (queryString != "") {
shareURL += "?" + queryString;
}
await shortURLService
.create(store.state.selectedOrganization.identifier, shareURL)
.then((res: any) => {
if (res.status == 200) {
shareURL = res.data.short_url;
copyToClipboard(shareURL)
.then(() => {
$q.notify({
type: "positive",
message: "Link Copied Successfully!",
timeout: 5000,
});
})
.catch(() => {
$q.notify({
type: "negative",
message: "Error while copy link.",
timeout: 5000,
});
});
}
})
.catch(() => {
$q.notify({
type: "negative",
message: "Error while shortening link.",
timeout: 5000,
});
});
});
const showSearchHistoryfn = () => {
emit("showSearchHistory");
};
const QUERY_TEMPLATE = 'SELECT [FIELD_LIST] FROM "[STREAM_NAME]"';
function getFieldList(
stream,
streamFields,
interestingFields,
isQuickMode,
) {
searchObj.data.streamResults.list.forEach((item) => {
if (
item.name == stream &&
Object.hasOwn(item, "schema") &&
item.schema.length > 0
) {
streamFields = item.schema;
}
});
return streamFields
.filter((item) => interestingFields.includes(item.name))
.map((item) => item.name);
}
function buildStreamQuery(stream, fieldList, isQuickMode) {
return QUERY_TEMPLATE.replace("[STREAM_NAME]", stream).replace(
"[FIELD_LIST]",
fieldList.length > 0 && isQuickMode ? fieldList.join(",") : "*",
);
}
const resetFilters = () => {
if (searchObj.meta.sqlMode == true) {
const parsedSQL = fnParsedSQL();
if (Object.hasOwn(parsedSQL, "from") && parsedSQL.from.length > 0) {
if (Object.hasOwn(parsedSQL, "where") && parsedSQL.where != "") {
parsedSQL.where = null;
}
if (Object.hasOwn(parsedSQL, "limit") && parsedSQL.limit != "") {
parsedSQL.limit = null;
}
if (Object.hasOwn(parsedSQL, "_next") && parsedSQL._next != "") {
parsedSQL._next.where = null;
parsedSQL._next.limit = null;
}
searchObj.data.query = fnUnparsedSQL(parsedSQL);
searchObj.data.query = searchObj.data.query.replaceAll("`", '"');
searchObj.data.editorValue = searchObj.data.query;
} else {
// Handle both single and multiple stream scenarios
const queries = searchObj.data.stream.selectedStream
.map((stream) => {
// Destructure for better readability
const { selectedStreamFields, interestingFieldList } =
searchObj.data.stream;
const { quickMode } = searchObj.meta;
// Generate the field list for the current stream
const fieldList = getFieldList(
stream,
selectedStreamFields,
interestingFieldList,
quickMode,
);
// Ensure fieldList is valid before building the query
if (!fieldList || fieldList.length === 0) {
console.warn(`No fields available for stream: ${stream}`);
return null;
}
// Build and return the query for the current stream
return buildStreamQuery(stream, fieldList, quickMode);
})
.filter(Boolean);
searchObj.data.query = queries.join(" UNION ALL BY NAME ");
searchObj.data.editorValue = searchObj.data.query;
}
} else {
searchObj.data.query = "";
searchObj.data.editorValue = "";
}
queryEditorRef.value?.setValue(searchObj.data.query);
if (store.state.zoConfig.query_on_stream_selection == false) {
handleRunQueryFn();
}
};
const customDownloadDialog = ref(false);
const showDownloadMenu = ref(false);
const downloadCustomInitialNumber = ref(1);
const downloadCustomRange = ref(100);
const downloadCustomRangeOptions = ref([100, 500, 1000, 5000, 10000]);
const downloadCustomFileName = ref("");
const downloadCustomFileType = ref("csv");
const downloadCustomFileTypeOptions = ref([
{ label: "CSV", value: "csv" },
{ label: "JSON", value: "json" },
]);
const loadSavedView = () => {
if (searchObj.data.savedViews.length == 0) {
getSavedViews();
}
};
const handleFavoriteSavedView = (row: any, flag: boolean) => {
let localSavedView: any = {};
let savedViews = useLocalSavedView();
if (savedViews.value != null) {
localSavedView = savedViews.value;
}
Object.keys(localSavedView).forEach((item, key) => {
if (item == row.view_id) {
if (flag) {
delete localSavedView[item];
useLocalSavedView(localSavedView);
const index = favoriteViews.value.indexOf(row.view_id);
if (index > -1) {
favoriteViews.value.splice(index, 1);
}
let favoriteViewsList = localSavedViews.value;
if (favoriteViewsList.length > 0) {
favoriteViewsList = favoriteViewsList.filter(
(item) => item.view_id != row.view_id,
);
// for (const [key, item] of favoriteViewsList.entries()) {
// console.log(item, key);
// if (item.view_id == row.view_id) {
// delete favoriteViewsList[key];
// }
// }
localSavedViews.value = favoriteViewsList;
}
}
}
});
if (!flag) {
if (favoriteViews.value.length >= 10) {
$q.notify({
message: "You can only save 10 views.",
color: "info",
position: "bottom",
timeout: 2000,
});
return;
}
localSavedView[row.view_id] = JSON.parse(JSON.stringify(row));
favoriteViews.value.push(row.view_id);
localSavedViews.value.push(row);
// moveItemsToTop(localSavedView, favoriteViews.value);
useLocalSavedView(localSavedView);
$q.notify({
message: "View added to favorites.",
color: "positive",
position: "bottom",
timeout: 2000,
});
} else {
// alert(favoriteViews.value.length)
// moveItemsToTop(localSavedView, favoriteViews.value);
$q.notify({
message: "View removed from favorites.",
color: "positive",
position: "bottom",
timeout: 2000,
});
}
};
const filterSavedViewFn = (rows: any, terms: any) => {
var filtered = [];
if (terms != "") {
terms = terms.toLowerCase();
for (var i = 0; i < rows.length; i++) {
if (rows[i]["view_name"].toLowerCase().includes(terms)) {
filtered.push(rows[i]);
}
}
}
return filtered;
};
const regionFilterMethod = (node, filter) => {
const filt = filter.toLowerCase();
return node.label && node.label.toLowerCase().indexOf(filt) > -1;
};
const resetRegionFilter = () => {
regionFilter.value = "";
};
const handleRegionsSelection = (item, isSelected) => {
if (isSelected) {
const index = searchObj.meta.regions.indexOf(item);
if (index > -1) {
searchObj.meta.regions.splice(index, 1);
}
} else {
searchObj.meta.regions.push(item);
}
};
const handleQuickMode = () => {
emit("handleQuickModeChange");
};
const handleHistogramMode = () => {};
const handleRunQueryFn = (clear_cache = false) => {
if (searchObj.meta.logsVisualizeToggle == "visualize" || searchObj.meta.logsVisualizeToggle == "patterns") {
emit("handleRunQueryFn", typeof clear_cache === 'boolean' ? clear_cache : false);
} else {
handleRunQuery(typeof clear_cache === 'boolean' ? clear_cache : false);
}
};
const onLogsVisualizeToggleUpdate = (value: any) => {
// prevent action if visualize is disabled (SQL mode disabled with multiple streams)
if (
value === "visualize" &&
!searchObj.meta.sqlMode &&
searchObj.data.stream.selectedStream.length > 1
) {
showErrorNotification(
"Please enable SQL mode or select a single stream to visualize",
);
return;
}
// confirm with user on toggle from visualize to logs
if (
value == "logs" &&
searchObj.meta.logsVisualizeToggle == "visualize"
) {
// cancel all the visualize queries
cancelVisualizeQueries();
if (
searchObj.meta.logsVisualizeDirtyFlag === true ||
!Object.hasOwn(searchObj.data?.queryResults, "hits") ||
searchObj.data?.queryResults?.hits?.length == 0
) {
searchObj.loading = true;
if (searchObj.meta.sqlMode) {
searchObj.data.queryResults.aggs = undefined;
}
searchObj.meta.refreshHistogram = true;
getQueryData();
searchObj.meta.logsVisualizeDirtyFlag = false;
}
} else if (
value == "logs" &&
searchObj.meta.logsVisualizeToggle == "patterns"
) {
// Switching from patterns to logs - check if we need to fetch logs
const hasLogs =
searchObj.data?.queryResults?.hits &&
searchObj.data.queryResults.hits.length > 0;
// console.log("[SearchBar] Switching patterns → logs, hasLogs:", hasLogs);
if (!hasLogs) {
// No logs data - fetch them
// console.log("[SearchBar] Fetching logs data");
searchObj.loading = true;
searchObj.meta.refreshHistogram = true;
getQueryData();
} else {
// Logs exist - just switch the view
// console.log("[SearchBar] Reusing existing logs data");
}
} else if (
value == "patterns" &&
(searchObj.meta.logsVisualizeToggle == "logs" ||
searchObj.meta.logsVisualizeToggle == "visualize")
) {
// Switching to patterns mode - this will be handled by a separate watcher in Index.vue
emit("extractPatterns");
// console.log("[SearchBar] Switching to patterns mode");
} else if (
value == "visualize" &&
searchObj.meta.logsVisualizeToggle == "logs"
) {
// validate query
// return if query is emptry and stream is not selected
if (
searchObj.data.query === "" &&
searchObj?.data?.stream?.selectedStream?.length === 0
) {
showErrorNotification(
"Query is empty, please write query to visualize",
);
return;
}
let logsPageQuery = searchObj.data.query;
// handle sql mode
if (!searchObj.data.sqlMode) {
const queryBuild = buildSearch();
logsPageQuery = queryBuild?.query?.sql ?? "";
}
// if multiple sql, then do not allow to visualize
if (
logsPageQuery &&
Array.isArray(logsPageQuery) &&
logsPageQuery.length > 1
) {
showErrorNotification(
"Multiple SQL queries are not allowed to visualize",
);
return;
}
// validate sql query that all fields have alias
if (!allSelectionFieldsHaveAlias(logsPageQuery)) {
showErrorNotification(
"Fields using aggregation functions must have aliases to visualize.",
);
return;
}
// cancel all the logs queries
cancelQuery();
}
searchObj.meta.logsVisualizeToggle = value;
updateUrlQueryParams();
if (searchObj.meta.logsVisualizeToggle === "logs") {
const hasLogs =
searchObj.data?.queryResults?.hits &&
searchObj.data.queryResults.hits.length > 0;
if (hasLogs) {
searchObj.data.histogram.chartParams.title = getHistogramTitle(false);
}
}
// dispatch resize event
window.dispatchEvent(new Event("resize"));
};
const dashboardPanelDataPageKey = inject(
"dashboardPanelDataPageKey",
"logs",
);
const { dashboardPanelData, resetDashboardPanelData } =
useDashboardPanelData(dashboardPanelDataPageKey);
// [START] cancel running queries
const variablesAndPanelsDataLoadingState =
inject("variablesAndPanelsDataLoadingState", {}) || {};
const visualizeSearchRequestTraceIds = computed(() => {
const searchIds = Object.values(
variablesAndPanelsDataLoadingState?.searchRequestTraceIds,
)
.filter((item: any) => item.length > 0)
.flat() as string[];
// If custom field extraction is in progress, push a dummy trace id so that cancel button is visible.
if (variablesAndPanelsDataLoadingState?.fieldsExtractionLoading) {
searchIds.push("fieldExtraction");
}
return searchIds;
});
const backgroundColorStyle = computed(() => {
const isDarkMode = store.state.theme === "dark";
return {
backgroundColor:
searchObj.data.transformType === "function" && isFocused.value
? isDarkMode
? "var(--o2-card-bg)"
: "white" // Dark mode: grey, Light mode: yellow (or any color)
: "",
borderBottom:
searchObj.data.transformType === "function" && isFocused.value
? isDarkMode
? "0.375rem solid var(--o2-card-bg)"
: "0.375rem solid var(--o2-card-bg)"
: "none",
};
});
const editorWidthToggleFunction = computed(() => {
const isDarkMode = store.state.theme === "dark";
if (!searchObj.data.transformType === "function" && isFocused.value) {
return {
width: `calc(100 - ${searchObj.config.fnSplitterModel})%`,
borderBottom: isDarkMode ? "2px solid #575A5A" : "2px solid #E0E0E0",
};
} else {
return {
width: "100%",
borderBottom: "none",
};
}
});
const { traceIdRef, cancelQuery: cancelVisualizeQuery } = useCancelQuery();
const cancelVisualizeQueries = () => {
// Filter out the dummy id before sending to backend cancel API
traceIdRef.value = visualizeSearchRequestTraceIds.value.filter(
(id: any) => id !== "fieldExtraction",
);
cancelVisualizeQuery();
};
const disable = ref(false);
watch(variablesAndPanelsDataLoadingState, () => {
const panelsValues = Object.values(
variablesAndPanelsDataLoadingState?.panels,
);
disable.value = panelsValues.some((item: any) => item === true);
});
const iconRight = computed(() => {
return (
"img:" +
getImageURL(
store.state.theme === "dark"
? "images/common/function_dark.svg"
: "images/common/function.svg",
)
);
});
const functionToggleIcon = computed(() => {
return (
"img:" +
getImageURL(
searchObj.data.transformType === "function"
? "images/common/function_dark.svg"
: "images/common/function.svg",
)
);
});
const addJobScheduler = async () => {
try {
// if(searchObj.meta.jobId != ""){
// searchObj.meta.jobId = "";
// }
if (searchObj.meta.jobId != "") {
$q.notify({
type: "negative",
message:
"Job Already Scheduled , please change some parameters to schedule new job",
timeout: 3000,
});
return;
}
if (
searchObj.meta.jobRecords > 100000 ||
searchObj.meta.jobRecords == 0 ||
searchObj.meta.jobRecords < 0
) {
$q.notify({
type: "negative",
message: "Job Scheduler should be between 1 and 100000",
timeout: 3000,
});
return;
}
searchSchedulerJob.value = false;
searchObj.meta.showSearchScheduler = false;
await getJobData();
} catch (e) {
if (e.response.status != 403) {
$q.notify({
type: "negative",
message: "Error while adding job",
timeout: 3000,
});
return;
}
}
};
const createScheduleJob = () => {
searchSchedulerJob.value = true;
searchObj.meta.jobRecords = 100;
};
const checkQuery = (query) => {
const jobQuery = router.currentRoute.value.query.query;
if (jobQuery == b64EncodeUnicode(query)) {
return true;
}
return false;
};
const checkFnQuery = (fnQuery) => {
const jobFnQuery = router.currentRoute.value.query.functionContent;
if (jobFnQuery == b64EncodeUnicode(fnQuery)) {
return true;
}
return false;
};
const updateTransforms = () => {
updateEditorWidth();
};
const selectTransform = (item: any, isSelected: boolean) => {
if (searchObj.data.transformType === "function") {
populateFunctionImplementation(item, isSelected);
}
// If action is selected notify the user
if (searchObj.data.transformType === "action") {
updateActionSelection(item);
}
if (typeof item === "object")
searchObj.data.selectedTransform = {
...item,
type: searchObj.data.transformType,
};
};
const updateActionSelection = (item: any) => {
$q.notify({
message: `${item?.name} action applied successfully`,
timeout: 3000,
color: "secondary",
});
};
const updateEditorWidth = () => {
if (searchObj.data.transformType) {
if (searchObj.meta.showTransformEditor) {
searchObj.config.fnSplitterModel = 60;
} else {
searchObj.config.fnSplitterModel = 100;
}
} else {
searchObj.config.fnSplitterModel = 100;
}
};
//so if it is active we need light this is fixed
//if it is inactive we will be having 2 conditions
//1. if dark mode show light color
//2.if light mode show dark color
const visualizeIcon = computed(() => {
return searchObj.meta.logsVisualizeToggle === "visualize"
? getImageURL("images/common/visualize_icon_light.svg")
: store.state.theme == 'dark' ? getImageURL("images/common/visualize_icon_light.svg") : getImageURL("images/common/visualize_icon_dark.svg");
});
const histogramIcon = computed(() => {
return store.state.theme === "dark"
? getImageURL("images/common/bar_chart_histogram_light.svg")
: getImageURL("images/common/bar_chart_histogram.svg");
});
const sqlIcon = computed(() => {
return store.state.theme === "dark"
? getImageURL("images/common/hugeicons_sql_light.svg")
: getImageURL("images/common/hugeicons_sql.svg");
});
const quickModeIcon = computed(() => {
return store.state.theme === "dark"
? getImageURL("images/common/quick_mode_light.svg")
: getImageURL("images/common/quick_mode.svg");
});
const searchHistoryIcon = computed(() => {
return store.state.theme === "dark"
? getImageURL("images/common/search_history_light.svg")
: getImageURL("images/common/search_history.svg");
});
const downloadTableIcon = computed(() => {
return store.state.theme === "dark"
? getImageURL("images/common/download_table_light.svg")
: getImageURL("images/common/download_table.svg");
});
const customRangeIcon = computed(() => {
return store.state.theme === "dark"
? getImageURL("images/common/custom_range_light.svg")
: getImageURL("images/common/custom_range.svg");
});
const createScheduledSearchIcon = computed(() => {
return store.state.theme === "dark"
? getImageURL("images/common/create_scheduled_search_light.svg")
: getImageURL("images/common/create_scheduled_search.svg");
});
const listScheduledSearchIcon = computed(() => {
return store.state.theme === "dark"
? getImageURL("images/common/list_scheduled_search_light.svg")
: getImageURL("images/common/list_scheduled_search.svg");
});
// [END] cancel running queries
// [START] explain query functionality
const openExplainDialog = () => {
if (searchObj.data.query && searchObj.data.query.trim() !== "") {
showExplainDialog.value = true;
}
};
// [END] explain query functionality
return {
$q,
t,
store,
router,
fnEditorRef,
searchObj,
queryEditorRef,
confirmDialogVisible,
confirmCallback,
refreshTimes: searchObj.config.refreshTimes,
refreshTimeChange,
updateQueryValue,
updateDateTime,
showConfirmDialog,
showSavedViewConfirmDialog,
cancelConfirmDialog,
confirmDialogOK,
updateQuery,
downloadLogs,
saveFunction,
resetFunctionContent,
resetEditorLayout,
populateFunctionImplementation,
functionModel,
functionOptions,
filterFn,
refreshData,
handleRunQuery,
handleRunQueryFn,
autoCompleteKeywords,
autoCompleteSuggestions,
onRefreshIntervalUpdate,
updateTimezone,
dateTimeRef,
fnSavedView,
applySavedView,
isSavedViewAction,
savedViewName,
savedViewSelectedName,
handleSavedView,
deleteSavedViews,
deleteViewID,
confirmDelete,
saveViewLoader,
savedViewDropdownModel,
moreOptionsDropdownModel,
fnSavedFunctionDialog,
isSavedFunctionAction,
savedFunctionName,
savedFunctionSelectedName,
saveFunctionLoader,
shareLink,
showSearchHistoryfn,
getImageURL,
resetFilters,
customDownloadDialog,
downloadCustomInitialNumber,
downloadCustomRange,
downloadCustomRangeOptions,
buildSearch,
confirmSavedViewDialogVisible,
rowsPerPage,
handleFavoriteSavedView,
favoriteViews,
localSavedViews,
loadSavedView,
filterSavedViewFn,
config,
handleRegionsSelection,
handleQuickMode,
handleHistogramMode,
regionFilterMethod,
regionFilterRef,
regionFilter,
resetRegionFilter,
cancelQuery,
onLogsVisualizeToggleUpdate,
visualizeSearchRequestTraceIds,
disable,
cancelVisualizeQueries,
isFocused,
backgroundColorStyle,
editorWidthToggleFunction,
fnParsedSQL,
iconRight,
functionToggleIcon,
searchSchedulerJob,
autoSearchSchedulerJob,
addJobScheduler,
routeToSearchSchedule,
createScheduleJob,
searchTerm,
filteredActionOptions,
filteredFunctionOptions,
confirmUpdate,
updateViewObj,
updateSavedViews,
checkQuery,
checkFnQuery,
transformsExpandState,
transformsLabel,
transformIcon,
transformTypes,
filteredTransformOptions,
updateTransforms,
selectTransform,
actionEditorQuery,
isActionsEnabled,
showFunctionEditor,
closeSocketWithError,
histogram_svg,
visualizeIcon,
histogramIcon,
sqlIcon,
quickModeIcon,
searchHistoryIcon,
downloadTableIcon,
customRangeIcon,
createScheduledSearchIcon,
listScheduledSearchIcon,
getColumnNames,
getSearchObj,
toggleHistogram,
createSavedViews,
downloadCustomFileName,
downloadCustomFileType,
downloadCustomFileTypeOptions,
showDownloadMenu,
// Expose additional functions for testing
updateAutoComplete,
handleEscKey,
applyAction,
getFieldList,
buildStreamQuery,
updateActionSelection,
updateEditorWidth,
showExplainDialog,
openExplainDialog,
outlinedShowChart,
};
},
computed: {
isVisualizeDisabled() {
return (
!this.searchObj.meta.sqlMode &&
this.searchObj.data.stream.selectedStream.length > 1
);
},
isSqlModeDisabled() {
return (
this.searchObj.meta.logsVisualizeToggle === "visualize" &&
this.searchObj.data.stream.selectedStream.length > 1
);
},
addSearchTerm() {
return this.searchObj.data.stream.addToFilter;
},
toggleTransformEditor() {
return this.searchObj.meta.showTransformEditor;
},
confirmMessage() {
return "Are you sure you want to update the function?";
},
confirmMessageSavedView() {
return "Are you sure you want to update the saved view?";
},
resetFunction() {
return this.searchObj.data.tempFunctionName;
},
resetFunctionDefinition() {
return this.searchObj.data.tempFunctionContent;
},
},
watch: {
addSearchTerm() {
if (this.searchObj.data.stream.addToFilter != "") {
let currentQuery = this.searchObj.data.query.split("|");
if (currentQuery.length > 1) {
if (currentQuery[1].trim() != "") {
currentQuery[1] += " and " + filter;
} else {
currentQuery[1] = filter;
}
this.searchObj.data.query = currentQuery.join("| ");
this.searchObj.data.editorValue = this.searchObj.data.query;
} else {
let unionType: string = "";
if (
currentQuery[0]
.replace("union all", "UNION ALL")
.includes("UNION ALL")
) {
unionType = "UNION ALL";
} else if (
currentQuery[0].replace("union", "UNION").includes("UNION")
) {
unionType = "UNION";
}
// Use regular expression to match "UNION" or "UNION ALL" (case insensitive)
const unionRegex = /\bUNION ALL\b|\bUNION\b/i;
// Split the string by "UNION" or "UNION ALL" if they are present
const queries = currentQuery[0].split(unionRegex);
// Iterate over each part
queries.forEach((query, index) => {
let filter = this.searchObj.data.stream.addToFilter;
const isFilterValueNull = filter.split(/=|!=/)[1] === "'null'";
if (isFilterValueNull) {
filter = filter
.replace(/=|!=/, (match) => {
return match === "=" ? " is " : " is not ";
})
.replace(/'null'/, "null");
}
if (this.searchObj.meta.sqlMode == true) {
if (
unionType == "" &&
this.searchObj.data.stream.selectedStream.length > 1
) {
const parsedSQL = this.fnParsedSQL();
const streamPrefix: string =
parsedSQL.from[0].as != null
? parsedSQL.from[0].as
: parsedSQL.from[0].table;
filter = `"${streamPrefix}".${filter}`;
}
// if query contains order by clause or limit clause then add where clause before that
// if query contains where clause then add filter after that with and operator and keep order by or limit after that
// if query does not contain where clause then add where clause before filter
if (query.toLowerCase().includes("where")) {
if (query.toLowerCase().includes("order by")) {
const [beforeOrderBy, afterOrderBy] = queryIndexSplit(
query,
"order by",
);
query =
beforeOrderBy.trim() +
" AND " +
filter +
" order by" +
afterOrderBy;
} else if (query.toLowerCase().includes("group by")) {
const [beforeGroupBy, afterGroupBy] = queryIndexSplit(
query,
"group by",
);
query =
beforeGroupBy.trim() +
" AND " +
filter +
" group by" +
afterGroupBy;
} else if (query.toLowerCase().includes("limit")) {
const [beforeLimit, afterLimit] = queryIndexSplit(
query,
"limit",
);
query =
beforeLimit.trim() +
" AND " +
filter +
" limit" +
afterLimit;
} else {
query = query + " AND " + filter;
}
} else {
if (query.toLowerCase().includes("order by")) {
const [beforeOrderBy, afterOrderBy] = queryIndexSplit(
query,
"order by",
);
query =
beforeOrderBy.trim() +
" where " +
filter +
" order by" +
afterOrderBy;
} else if (query.toLowerCase().includes("group by")) {
const [beforeGroupBy, afterGroupBy] = queryIndexSplit(
query,
"group by",
);
query =
beforeGroupBy.trim() +
" where " +
filter +
" group by" +
afterGroupBy;
} else if (query.toLowerCase().includes("limit")) {
const [beforeLimit, afterLimit] = queryIndexSplit(
query,
"limit",
);
query =
beforeLimit.trim() +
" where " +
filter +
" limit" +
afterLimit;
} else {
query = query + " where " + filter;
}
}
currentQuery[0] = query;
} else {
currentQuery[0].length == 0
? (currentQuery[0] = filter)
: (currentQuery[0] += " and " + filter);
}
// this.searchObj.data.query = currentQuery[0];
queries[index] = currentQuery[0];
});
if (unionType == "") {
this.searchObj.data.query = queries.join("");
} else {
this.searchObj.data.query = queries.join(` ${unionType} `);
}
this.searchObj.data.editorValue = this.searchObj.data.query;
this.searchObj.data.stream.addToFilter = "";
if (this.queryEditorRef?.setValue)
this.queryEditorRef.setValue(this.searchObj.data.query);
}
}
},
toggleTransformEditor(newVal) {
if (newVal == false) {
this.searchObj.config.fnSplitterModel = 100;
} else {
this.searchObj.config.fnSplitterModel = 60;
}
this.resetEditorLayout();
},
resetFunction(newVal) {
if (newVal == "" && store && !store?.state?.savedViewFlag) {
this.resetFunctionContent();
}
},
resetFunctionDefinition(newVal) {
if (newVal == "") this.resetFunctionContent();
},
},
});
</script>
<style scoped lang="scss">
.expand-on-focus {
height: calc(100vh - 12.5rem) !important; // 200px
z-index: 20 !important;
}
.file-type label {
transform: translate(-0.75rem, -175%);
font-weight: bold;
font-size: 0.875rem; // 14px
color: var(--o2-text-secondary);
}
.q-dark .q-btn {
font-weight: 600;
border: 0 solid rgba(255, 255, 255, 0.2);
}
.q-dark .file-type label,
.q-dark .file-type .q-btn {
color: var(--o2-text-secondary);
}
// Toolbar Icon and Toggle Styles
.toolbar-toggle-container {
padding: 0 0.175rem; // 0 ~2.8px
margin-left: 0.25rem; // 8px
display: flex;
align-items: center;
justify-content: center;
border: 0.0625rem solid var(--o2-border-color); // 1px
border-radius: 0.375rem; // 6px
transition: all 0.2s ease;
cursor: pointer;
&:hover {
background-color: var(--o2-hover-accent);
}
}
.dark-theme .toolbar-toggle-container {
border: 0.0625rem solid var(--o2-border-color);
}
.toolbar-icon {
width: 1rem; // 16px
height: 1rem; // 16px
object-fit: contain;
}
.q-dark .toolbar-icon {
filter: invert(1);
}
.toolbar-icon-in-toggle {
font-size: 0.9rem; // ~14.4px
}
.syntax-guide-in-menu {
:deep(.q-btn) {
border: none !important;
margin-left: 0 !important;
&:hover {
background-color: transparent !important;
}
&::before {
display: none !important;
}
}
}
.toolbar-reset-btn {
padding: 0.25rem 0.375rem; // 4px 6px
margin-left: 0.25rem; // 8px
border: 0.0625rem solid var(--o2-border-color); // 1px
border-radius: 0.375rem; // 6px
transition: all 0.2s ease;
min-height: 1.875rem; // 30px
.q-icon {
font-size: 1.215rem; // 16px
}
&:hover {
background-color: var(--o2-hover-accent);
}
&.theme-dark {
border-color: var(--o2-border-color) !important;
}
}
.q-dark .toolbar-reset-btn,
.dark-theme .toolbar-reset-btn {
border-color: var(--o2-border-color);
}
.group-menu-btn {
padding: 0.25rem 0.25rem !important; // 4px 8px
margin-left: 0.25rem; // 8px
border: 0.0625rem solid rgba(0, 0, 0, 0.12) !important; // 1px
border-radius: 0.375rem; // 6px
transition: all 0.2s ease;
min-height: 1.875rem !important; // 30px
font-size: 0.75rem; // 12px
font-weight: 500;
.q-icon {
font-size: 1.125rem; // 18px
}
&:hover {
background-color: var(--o2-hover-accent);
}
}
.dark-theme .group-menu-btn,
.q-dark .group-menu-btn {
border: 0.0625rem solid var(--o2-border-color) !important;
}
.o2-run-query-button{
font-size: 11px;
font-weight: 500 !important;
line-height: 16px !important;
padding: 0px 12px !important;
width: 92px !important;
transition: box-shadow 0.3s ease, opacity 0.2s ease;
/* subtle default glow */
// box-shadow: 0 0 8px color-mix(in srgb, var(--o2-primary-btn-bg), transparent 60%);
}
.o2-color-primary{
background-color: var(--o2-primary-btn-bg);
color: var(--o2-primary-btn-text);
&:hover {
opacity: 0.9;
box-shadow: 0 0 8px color-mix(in srgb, var(--o2-primary-btn-bg), transparent 30%);
}
}
.search-button-enterprise-border-radius{
border-radius: 0.375rem 0px 0px 0.375rem !important;
}
.search-button-normal-border-radius{
border-radius: 0.375rem;
}
.search-button-dropdown-enterprise-border-radius{
border-radius: 0px 0.375rem 0.375rem 0px !important;
}
.o2-color-cancel{
background-color:#f67a7a;
color: var(--o2-primary-btn-text);
}
</style>