1282 lines
47 KiB
Vue
1282 lines
47 KiB
Vue
<!-- Copyright 2026 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>
|
|
<!-- Editor Dialog -->
|
|
<ODrawer
|
|
data-test="query-editor-dialog"
|
|
bleed
|
|
v-model:open="isOpen"
|
|
size="full"
|
|
:show-close="false"
|
|
>
|
|
<template #header-left>
|
|
<!-- Left: back + title + stream info -->
|
|
<div class="flex items-center gap-2.5">
|
|
<div
|
|
data-test="add-alert-back-btn"
|
|
class="flex size-5 flex-shrink-0 cursor-pointer items-center justify-center rounded-full border-[1.5px] opacity-60 transition-opacity hover:opacity-100"
|
|
:title="t('common.goBack')"
|
|
@click="closeDialog"
|
|
>
|
|
<OIcon name="arrow-back-ios-new" size="xs" />
|
|
</div>
|
|
<span class="text-dialog-header-text block truncate text-lg font-semibold">{{
|
|
t("alerts.addConditions")
|
|
}}</span>
|
|
|
|
<!-- Separator -->
|
|
<div class="bg-separator h-4 w-px opacity-30" />
|
|
|
|
<!-- Stream Type + Stream Name -->
|
|
<div class="flex items-center gap-2">
|
|
<div
|
|
v-if="streamType"
|
|
class="rounded-default inline-flex flex-row items-center gap-1.25 border border-[color-mix(in_srgb,var(--color-info)_28%,transparent)] bg-[color-mix(in_srgb,var(--color-info)_10%,transparent)] px-2.5 py-0.75"
|
|
>
|
|
<span class="text-2xs text-text-label font-semibold">{{ t("alerts.streamType") }}</span>
|
|
<span class="text-2xs text-text-label opacity-30">:</span>
|
|
<span class="text-text-link text-xs font-bold">{{ streamType }}</span>
|
|
</div>
|
|
<span v-if="streamType && streamName" class="opacity-20 select-none">|</span>
|
|
<div
|
|
class="rounded-default inline-flex flex-row items-center gap-1.25 border border-[color-mix(in_srgb,var(--color-sql-accent)_28%,transparent)] bg-[color-mix(in_srgb,var(--color-sql-accent)_10%,transparent)] px-2.5 py-0.75"
|
|
>
|
|
<span class="text-2xs text-text-label font-semibold">{{
|
|
t("alerts.stream_name")
|
|
}}</span>
|
|
<span class="text-2xs text-text-label opacity-30">:</span>
|
|
<span v-if="streamName" class="text-sql-accent text-xs font-bold">{{
|
|
streamName
|
|
}}</span>
|
|
<span v-else class="text-sql-accent text-xs font-bold italic opacity-40">{{
|
|
t("alerts.queryEditor.noStream")
|
|
}}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #header-right>
|
|
<!-- Right: AI -->
|
|
<div class="flex items-center gap-3">
|
|
<!-- AI button -->
|
|
<OButton
|
|
v-if="config.isEnterprise == 'true' && store.state.zoConfig.ai_enabled"
|
|
:ripple="false"
|
|
@click="toggleAIChat"
|
|
data-test="menu-link-ai-item"
|
|
variant="ghost"
|
|
size="icon-toolbar"
|
|
class="group transition-[background,box-shadow] duration-300 ease-[ease] [background:var(--color-gradient-ai-subtle)]! hover:shadow-[0_0.25rem_0.75rem_0_rgba(139,92,246,0.35)] hover:[background:var(--color-gradient-ai)]!"
|
|
@mouseenter="isHovered = true"
|
|
@mouseleave="isHovered = false"
|
|
>
|
|
<img
|
|
:src="getBtnLogo"
|
|
class="transition-transform duration-[600ms] ease-[ease] group-hover:rotate-180"
|
|
style="width: 18px; height: 18px"
|
|
/>
|
|
</OButton>
|
|
</div>
|
|
</template>
|
|
<div
|
|
data-test="query-editor-dialog-card"
|
|
class="bg-card-glass-bg flex h-[calc(100vh-3.5rem)] overflow-hidden"
|
|
>
|
|
<div class="flex h-full flex-1 overflow-hidden">
|
|
<div class="flex h-full w-full flex-col">
|
|
<!-- Main Content Grid: field browser | editors | output -->
|
|
<div
|
|
class="grid min-h-0 w-full flex-1 grid-cols-[20fr_45fr_35fr] gap-x-2 overflow-hidden px-2 py-2 pr-2"
|
|
>
|
|
<!-- Left Section (25%) — Field Browser -->
|
|
<div
|
|
class="rounded-default border-border-default bg-surface-base h-full overflow-hidden border p-2.5"
|
|
>
|
|
<SearchFieldList
|
|
:fields="fieldListItems"
|
|
:stream-name="streamName"
|
|
:stream-type="streamType"
|
|
:query="fieldListWhereClause"
|
|
:time-stamp="fieldListTimeStamp"
|
|
:hide-copy-value="true"
|
|
@event-emitted="handleFieldListEvent"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Input Section (40%) -->
|
|
<div class="flex h-full min-h-0 w-full overflow-y-auto">
|
|
<div ref="editorsColumnRef" class="flex min-h-full w-full flex-col gap-y-2">
|
|
<!-- SQL/PromQL Editor Pane + Status Bar wrapper -->
|
|
<div class="flex min-h-55 w-full flex-[3] flex-col overflow-visible">
|
|
<!-- Editor Pane (no overflow:hidden bottom clip issue for status bar) -->
|
|
<div
|
|
class="rounded-default border-border-default flex w-full flex-1 flex-col overflow-hidden rounded-b-none border border-b-0"
|
|
>
|
|
<!-- Pane Header -->
|
|
<div
|
|
class="bg-surface-subtle border-border-default flex min-h-12 shrink-0 items-center justify-between border-b px-3 py-2"
|
|
>
|
|
<div class="flex items-center gap-2">
|
|
<div class="rounded-default bg-theme-accent h-3.5 w-0.75 shrink-0" />
|
|
<span class="text-xs font-semibold">{{
|
|
localTab === "sql" ? t("alerts.sqlEditor") : t("alerts.promqlEditor")
|
|
}}</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<OSwitch
|
|
v-if="localTab !== 'promql'"
|
|
:model-value="!sqlEditorMaximized"
|
|
class="o2-toggle-button-xs"
|
|
@update:model-value="
|
|
(val) => (val ? restoreVrlEditor() : (sqlEditorMaximized = true))
|
|
"
|
|
>
|
|
<OTooltip
|
|
:delay="400"
|
|
:content="
|
|
sqlEditorMaximized
|
|
? t('alerts.queryEditor.showVrlEditor')
|
|
: t('alerts.queryEditor.hideVrlEditor')
|
|
"
|
|
/>
|
|
</OSwitch>
|
|
<OButton
|
|
data-test="alert-run-query-btn"
|
|
variant="primary"
|
|
size="sm"
|
|
class="rounded-default h-7 px-3! text-xs"
|
|
:disabled="
|
|
localTab == 'sql' ? localSqlQuery == '' : localPromqlQuery == ''
|
|
"
|
|
@click="localTab === 'sql' ? runSqlQuery() : runPromqlQuery()"
|
|
>
|
|
<span class="text-xs font-semibold">{{ t("alerts.runQuery") }}</span>
|
|
</OButton>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Unified Query Editor -->
|
|
<div class="relative min-h-0 flex-1">
|
|
<UnifiedQueryEditor
|
|
ref="queryEditorRef"
|
|
:languages="availableLanguages"
|
|
:default-language="localTab"
|
|
:query="localTab === 'sql' ? localSqlQuery : localPromqlQuery"
|
|
:disable-ai="!streamName"
|
|
:disable-ai-reason="t('search.selectStreamForAI')"
|
|
@update:query="handleQueryUpdate"
|
|
@language-change="handleLanguageChange"
|
|
@ask-ai="handleAskAI"
|
|
@run-query="handleRunQuery(localTab)"
|
|
@focus="onQueryEditorFocus"
|
|
@blur="onBlurQueryEditor"
|
|
editor-height="100%"
|
|
data-test-prefix="alert"
|
|
:keywords="effectiveKeywords"
|
|
:suggestions="effectiveSuggestions"
|
|
:field-value-resolver="resolveFieldValues"
|
|
/>
|
|
<div
|
|
v-if="
|
|
(localTab === 'sql' ? !localSqlQuery : !localPromqlQuery) &&
|
|
queryEditorPlaceholderFlag
|
|
"
|
|
class="pointer-events-none absolute top-0 right-0 bottom-0 left-0 z-1 flex items-start [padding:0.1875rem_0.5rem_0_2.15rem] select-none"
|
|
>
|
|
<span
|
|
class="text-text-placeholder overflow-hidden font-mono [line-height:1.3125rem] text-ellipsis whitespace-nowrap text-[var(--text-sm)]"
|
|
>{{ fullEditorPlaceholder }}</span
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status bar: outside overflow:hidden pane so border-bottom is never clipped -->
|
|
<div
|
|
class="text-compact relative h-5.5 shrink-0 cursor-default font-medium"
|
|
:class="[
|
|
sqlStatusBarClasses,
|
|
'border-border-default rounded-bl-default rounded-br-default border-r border-b border-l',
|
|
]"
|
|
>
|
|
<div class="absolute inset-0 flex items-center gap-1.25 overflow-hidden px-2.5">
|
|
<template v-if="sqlStatusState === 'sql-status-bar--error'">
|
|
<OIcon name="error-outline" size="xs" class="shrink-0" />
|
|
<span
|
|
class="min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap"
|
|
>{{ localSqlQueryErrorMsg || sqlQueryErrorMsg }}</span
|
|
>
|
|
</template>
|
|
<template v-else-if="sqlStatusState === 'sql-status-bar--loading'">
|
|
<OSpinner size="xs" class="shrink-0" />
|
|
<span>{{ t("alerts.queryEditor.fetchingResults") }}</span>
|
|
</template>
|
|
<template v-else-if="sqlStatusState === 'sql-status-bar--hint'">
|
|
<OIcon name="edit" size="xs" class="shrink-0 opacity-60" />
|
|
<span>{{ t("alerts.queryEditor.writeQueryHint") }}</span>
|
|
</template>
|
|
<template v-else-if="sqlStatusState === 'sql-status-bar--idle'">
|
|
<OIcon name="play-arrow" size="xs" class="shrink-0 opacity-70" />
|
|
<span>{{ t("alerts.queryEditor.runQueryHint") }}</span>
|
|
</template>
|
|
<template v-else-if="sqlStatusState === 'sql-status-bar--empty'">
|
|
<OIcon name="search-off" size="xs" class="shrink-0" />
|
|
<span>{{ t("alerts.queryEditor.noMatchingEvents") }}</span>
|
|
</template>
|
|
<template v-else-if="sqlStatusState === 'sql-status-bar--success'">
|
|
<OIcon name="check-circle" size="xs" class="shrink-0" />
|
|
<span>{{ t("alerts.queryEditor.eventsFound", sqlResultCount) }}</span>
|
|
</template>
|
|
</div>
|
|
<OTooltip
|
|
v-if="sqlStatusState === 'sql-status-bar--error'"
|
|
side="top"
|
|
align="center"
|
|
:max-width="'520px'"
|
|
:content="localSqlQueryErrorMsg || sqlQueryErrorMsg"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- VRL Editor Pane -->
|
|
<div
|
|
v-if="localTab !== 'promql'"
|
|
class="rounded-default flex w-full flex-col overflow-hidden"
|
|
:class="[
|
|
'border-border-default border',
|
|
sqlEditorMaximized ? 'flex-none' : 'min-h-40 flex-[2]',
|
|
]"
|
|
>
|
|
<!-- Pane Header -->
|
|
<div
|
|
class="bg-surface-subtle border-border-default flex min-h-12 shrink-0 items-center justify-between border-b px-3 py-2"
|
|
>
|
|
<div class="flex items-center gap-2">
|
|
<div
|
|
class="rounded-default bg-section-accent-secondary h-3.5 w-0.75 shrink-0"
|
|
/>
|
|
<span class="text-xs font-semibold">{{ t("alerts.vrlEditor") }}</span>
|
|
</div>
|
|
<div v-if="!sqlEditorMaximized" class="flex items-center gap-2">
|
|
<!-- Saved functions -->
|
|
<OSelect
|
|
v-model="selectedFunction"
|
|
:options="functionOptions"
|
|
data-test="alert-saved-vrl-function-select"
|
|
labelKey="name"
|
|
valueKey="name"
|
|
@update:modelValue="onFunctionSelect"
|
|
class="mini-select alert-v3-select w-35!"
|
|
clearable
|
|
@clear="onFunctionClear"
|
|
:placeholder="t('alerts.placeholders.savedFunctions')"
|
|
>
|
|
<template #empty>
|
|
<div class="text-muted-foreground px-3 py-2">
|
|
{{ t("search.noResult") }}
|
|
</div>
|
|
</template>
|
|
</OSelect>
|
|
<!-- Apply VRL -->
|
|
<OButton
|
|
data-test="alert-apply-vrl-btn"
|
|
variant="primary"
|
|
size="sm"
|
|
class="rounded-default h-7 px-3! text-xs"
|
|
:disabled="vrlFunctionContent == ''"
|
|
@click="runTestFunction"
|
|
>
|
|
<span class="text-xs font-semibold">{{ t("alerts.applyVRL") }}</span>
|
|
</OButton>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- VRL Editor -->
|
|
<div
|
|
v-if="!sqlEditorMaximized && vrlContentMounted"
|
|
class="relative min-h-0 flex-1"
|
|
>
|
|
<UnifiedQueryEditor
|
|
data-test="scheduled-alert-vrl-function-editor"
|
|
data-test-prefix="alert-dialog-vrl"
|
|
ref="fnEditorRef"
|
|
:languages="['vrl']"
|
|
:default-language="'vrl'"
|
|
:query="vrlFunctionContent"
|
|
:hide-nl-toggle="false"
|
|
:disable-ai="false"
|
|
:disable-ai-reason="''"
|
|
:ai-placeholder="t('search.askAIFunctionPlaceholder')"
|
|
:ai-tooltip="t('search.enterFunctionPrompt')"
|
|
:debounce-time="300"
|
|
editor-height="100%"
|
|
class="h-full w-full"
|
|
@update:query="updateVrlFunction"
|
|
@focus="functionEditorPlaceholderFlag = false"
|
|
@blur="onBlurFunctionEditor"
|
|
@toggle-nlp-mode="handleAlertFunctionEditorToggleNlpMode"
|
|
@generation-start="handleAlertFunctionEditorGenerationStart"
|
|
@generation-end="handleAlertFunctionEditorGenerationEnd"
|
|
@generation-success="handleAlertFunctionEditorGenerationSuccess"
|
|
/>
|
|
<div
|
|
v-if="!vrlFunctionContent && functionEditorPlaceholderFlag"
|
|
class="pointer-events-none absolute top-0 right-0 bottom-0 left-0 z-1 flex items-start [padding:0.1875rem_0.5rem_0_2.15rem] select-none"
|
|
>
|
|
<span
|
|
class="text-text-placeholder overflow-hidden font-mono [line-height:1.3125rem] text-ellipsis whitespace-nowrap text-[var(--text-sm)]"
|
|
>{{ vrlPlaceholder }}</span
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Output Section (35%) -->
|
|
<div class="flex h-full min-h-0 flex-col gap-y-2 overflow-y-auto">
|
|
<!-- Query Result Pane -->
|
|
<div
|
|
class="rounded-default border-border-default flex min-h-55 flex-1 flex-col overflow-hidden border"
|
|
>
|
|
<!-- Pane Header -->
|
|
<div
|
|
class="bg-surface-subtle border-border-default flex min-h-12 shrink-0 items-center justify-between border-b px-3 py-2"
|
|
>
|
|
<div class="flex items-center gap-2">
|
|
<div class="rounded-default bg-theme-accent h-3.5 w-0.75 shrink-0" />
|
|
<span class="text-xs font-semibold">{{
|
|
t("alerts.queryEditor.queryResult")
|
|
}}</span>
|
|
<span
|
|
v-if="multiTimeRange && multiTimeRange.length > 0"
|
|
class="text-3xs rounded-default bg-status-info-bg border-banner-info-border text-text-link border px-1.75 py-px font-bold tracking-[0.04em]"
|
|
>{{ t("alerts.queryEditor.resultsAcrossWindows") }}</span
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="min-h-0 flex-1 overflow-hidden">
|
|
<!-- Idle: not yet run -->
|
|
<div
|
|
v-if="!tempRunQuery && outputEvents == ''"
|
|
class="bg-card-glass-bg flex h-full w-full flex-col items-center justify-center"
|
|
>
|
|
<div class="flex flex-col items-center gap-2">
|
|
<OIcon name="table-chart" class="size-12! opacity-[0.18]" />
|
|
<span class="text-xs opacity-[0.45]">{{
|
|
t("alerts.runQueryForOutput")
|
|
}}</span>
|
|
</div>
|
|
</div>
|
|
<!-- No results after run -->
|
|
<div
|
|
v-else-if="outputEvents == '' && !runQueryLoading"
|
|
class="bg-card-glass-bg flex h-full flex-col items-center justify-center"
|
|
>
|
|
<div class="flex flex-col items-center gap-2">
|
|
<OIcon name="warning" size="xl" class="text-warning opacity-60" />
|
|
</div>
|
|
</div>
|
|
<!-- Loading -->
|
|
<div
|
|
v-else-if="runQueryLoading"
|
|
class="flex h-full flex-col items-center justify-center gap-2"
|
|
>
|
|
<OSpinner size="md" />
|
|
<span class="text-sm opacity-60">{{ t("search.fetchingResults") }}</span>
|
|
</div>
|
|
<!-- Results -->
|
|
<QueryEditor
|
|
v-else
|
|
class="h-full w-full overflow-y-auto"
|
|
data-test="sql-output-editor"
|
|
ref="outputEventsEditorRef"
|
|
editor-id="sql-output-editor"
|
|
language="json"
|
|
:read-only="true"
|
|
v-model:query="outputEvents"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Combined Output Pane (SQL + VRL only) -->
|
|
<div
|
|
v-if="localTab !== 'promql'"
|
|
class="rounded-default border-border-default flex min-h-50 flex-1 flex-col overflow-hidden border"
|
|
>
|
|
<!-- Pane Header -->
|
|
<div
|
|
class="bg-surface-subtle border-border-default flex min-h-12 shrink-0 items-center justify-between border-b px-3 py-2"
|
|
>
|
|
<div class="flex items-center gap-2">
|
|
<div
|
|
class="rounded-default bg-section-accent-secondary h-3.5 w-0.75 shrink-0"
|
|
/>
|
|
<span class="text-xs font-semibold">{{
|
|
t("alerts.queryEditor.combinedOutput")
|
|
}}</span>
|
|
<span
|
|
class="text-3xs rounded-default bg-badge-purple-soft-bg border-badge-purple-ol-border text-badge-purple-ol-text border px-1.75 py-px font-bold tracking-[0.04em]"
|
|
>SQL + VRL</span
|
|
>
|
|
</div>
|
|
<!-- Running indicator -->
|
|
<div v-if="runFnQueryLoading" class="flex items-center gap-1">
|
|
<span
|
|
class="query-editor-run-dot bg-status-positive h-1.5 w-1.5 rounded-full"
|
|
/>
|
|
<span class="text-3xs text-status-positive font-semibold">{{
|
|
t("alerts.queryEditor.running")
|
|
}}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="min-h-0 flex-1 overflow-hidden">
|
|
<!-- Idle -->
|
|
<div
|
|
v-if="!tempTestFunction && !runFnQueryLoading"
|
|
class="bg-card-glass-bg flex h-full w-full flex-col items-center justify-center"
|
|
>
|
|
<div class="flex flex-col items-center gap-2">
|
|
<OIcon name="data-object" class="size-12! opacity-[0.18]" />
|
|
<span class="text-xs opacity-[0.45]">{{
|
|
t("alerts.applyVRLForOutput")
|
|
}}</span>
|
|
</div>
|
|
</div>
|
|
<!-- No results -->
|
|
<div
|
|
v-else-if="outputFnEvents == '' && !runFnQueryLoading && tempTestFunction"
|
|
class="bg-card-glass-bg flex h-full flex-col items-center justify-center"
|
|
>
|
|
<div class="flex flex-col items-center gap-2">
|
|
<OIcon name="warning" size="xl" class="text-warning opacity-60" />
|
|
</div>
|
|
</div>
|
|
<!-- Loading -->
|
|
<div
|
|
v-else-if="runFnQueryLoading"
|
|
class="flex h-full flex-col items-center justify-center gap-2"
|
|
>
|
|
<OSpinner size="md" />
|
|
<span class="text-sm opacity-60">{{ t("search.fetchingResults") }}</span>
|
|
</div>
|
|
<!-- Results -->
|
|
<QueryEditor
|
|
v-else
|
|
class="h-full w-full"
|
|
data-test="vrl-function-test-events-output-editor"
|
|
ref="outputFnEventsEditorRef"
|
|
editor-id="test-function-events-output-editor"
|
|
language="json"
|
|
:read-only="true"
|
|
v-model:query="outputFnEvents"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AI Chat Panel -->
|
|
<div
|
|
class="bg-surface-base ml-2 w-[24.5vw] max-w-full min-w-18.75"
|
|
v-if="store.state.isAiChatEnabled"
|
|
>
|
|
<O2AIChat
|
|
:header-height="48"
|
|
:is-open="store.state.isAiChatEnabled"
|
|
@close="store.state.isAiChatEnabled = false"
|
|
class="h-[calc(100vh-3.5rem)]!"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</ODrawer>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, watch, type PropType, onMounted, inject, type Ref } from "vue";
|
|
import { type SqlErrorRange } from "@/utils/query/sqlDiagnostics";
|
|
import { useI18n } from "vue-i18n";
|
|
import { useStore } from "vuex";
|
|
import useTheme from "@/composables/useTheme";
|
|
import OButton from "@/lib/core/Button/OButton.vue";
|
|
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
|
|
import { debounce } from "lodash-es";
|
|
import { b64EncodeUnicode, getImageURL } from "@/utils/zincutils";
|
|
import OIcon from "@/lib/core/Icon/OIcon.vue";
|
|
import searchService from "@/services/search";
|
|
import { defineAsyncComponent } from "vue";
|
|
const QueryEditor = defineAsyncComponent(() => import("@/components/CodeQueryEditor.vue"));
|
|
import UnifiedQueryEditor from "@/components/QueryEditor.vue";
|
|
import O2AIChat from "@/components/O2AIChat.vue";
|
|
import SearchFieldList from "@/components/common/sidebar/SearchFieldList.vue";
|
|
import config from "@/aws-exports";
|
|
import useQuery from "@/composables/useQuery";
|
|
import { getParser as getParserUtil, type SqlUtilsContext } from "@/utils/alerts/alertSqlUtils";
|
|
import useParser from "@/composables/useParser";
|
|
import useSqlSuggestions from "@/composables/useSuggestions";
|
|
import { useSqlEditorDiagnostics } from "@/composables/useSqlEditorDiagnostics";
|
|
import { useVrlPlaceholder } from "@/composables/useVrlPlaceholder";
|
|
import { useQueryPlaceholder } from "@/components/logs/useQueryPlaceholder";
|
|
import { applyFilterTerm, removeFieldCondition } from "@/utils/traces/filterUtils";
|
|
import OSpinner from "@/lib/feedback/Spinner/OSpinner.vue";
|
|
import OSwitch from "@/lib/forms/Switch/OSwitch.vue";
|
|
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
|
|
import OSelect from "@/lib/forms/Select/OSelect.vue";
|
|
import { toast } from "@/lib/feedback/Toast/useToast";
|
|
|
|
const props = defineProps({
|
|
modelValue: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
tab: {
|
|
type: String as PropType<"sql" | "promql">,
|
|
default: "sql",
|
|
},
|
|
sqlQuery: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
promqlQuery: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
vrlFunction: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
streamName: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
streamType: {
|
|
type: String,
|
|
default: "logs",
|
|
},
|
|
columns: {
|
|
type: Array as PropType<any[]>,
|
|
default: () => [],
|
|
},
|
|
period: {
|
|
type: Number,
|
|
default: 10,
|
|
},
|
|
multiTimeRange: {
|
|
type: Array as PropType<any[]>,
|
|
default: () => [],
|
|
},
|
|
savedFunctions: {
|
|
type: Array as PropType<any[]>,
|
|
default: () => [],
|
|
},
|
|
sqlQueryErrorMsg: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
});
|
|
|
|
const emit = defineEmits([
|
|
"update:modelValue",
|
|
"update:sqlQuery",
|
|
"update:promqlQuery",
|
|
"update:vrlFunction",
|
|
"validate-sql",
|
|
]);
|
|
|
|
const { t } = useI18n();
|
|
const store = useStore();
|
|
const { isDark } = useTheme();
|
|
const { buildQueryPayload } = useQuery();
|
|
const { sqlParser } = useParser();
|
|
|
|
// SQL Parser for validation
|
|
const parser: any = ref({});
|
|
const localSqlQueryErrorMsg = ref("");
|
|
// Suppresses the parent-prop sqlQueryErrorMsg while a fresh query is in flight
|
|
const suppressPropError = ref(false);
|
|
|
|
const sqlStatusState = computed(() => {
|
|
const hasError =
|
|
(localSqlQueryErrorMsg.value ||
|
|
(!suppressPropError.value && (props as any).sqlQueryErrorMsg)) &&
|
|
localTab.value === "sql";
|
|
if (hasError) return "sql-status-bar--error";
|
|
if (runQueryLoading.value) return "sql-status-bar--loading";
|
|
const currentQuery = localTab.value === "sql" ? localSqlQuery.value : localPromqlQuery.value;
|
|
if (!currentQuery) return "sql-status-bar--hint";
|
|
if (!tempRunQuery.value) return "sql-status-bar--idle";
|
|
if (outputEvents.value) return "sql-status-bar--success";
|
|
return "sql-status-bar--empty";
|
|
});
|
|
|
|
const sqlStatusBarClasses = computed(() => {
|
|
const neutralBg = "bg-surface-subtle text-text-secondary";
|
|
const map: Record<string, string> = {
|
|
"sql-status-bar--hint": neutralBg,
|
|
"sql-status-bar--idle": neutralBg,
|
|
"sql-status-bar--loading":
|
|
"bg-[color-mix(in_srgb,var(--color-sql-accent)_6%,transparent)] text-sql-accent",
|
|
"sql-status-bar--error": "bg-status-error-bg text-status-error-text cursor-pointer",
|
|
"sql-status-bar--empty": "bg-status-warning-bg text-status-warning-text",
|
|
"sql-status-bar--success": "bg-status-success-bg text-status-positive",
|
|
};
|
|
return map[sqlStatusState.value] ?? "";
|
|
});
|
|
|
|
const sqlResultCount = computed(() => queryHitCount.value);
|
|
|
|
watch(
|
|
() => (props as any).sqlQueryErrorMsg,
|
|
() => {
|
|
suppressPropError.value = false;
|
|
},
|
|
);
|
|
|
|
// Initialize SQL parser on mount
|
|
onMounted(async () => {
|
|
parser.value = await sqlParser();
|
|
});
|
|
|
|
// Get parser function to validate SQL queries (checks for SELECT * and reserved words)
|
|
const getParser = (sqlQuery: string) => {
|
|
const sqlUtilsContext: SqlUtilsContext = {
|
|
parser: parser.value,
|
|
sqlQueryErrorMsg: localSqlQueryErrorMsg,
|
|
};
|
|
return getParserUtil(sqlQuery, sqlUtilsContext);
|
|
};
|
|
|
|
// Dialog state
|
|
const isOpen = computed({
|
|
get: () => props.modelValue,
|
|
set: (val) => emit("update:modelValue", val),
|
|
});
|
|
|
|
const closeDialog = () => {
|
|
isOpen.value = false;
|
|
};
|
|
|
|
// Local state
|
|
// Default to SQL tab if no tab is provided, otherwise use the provided tab
|
|
const localTab = ref<"sql" | "promql">(props.tab || "sql");
|
|
const localSqlQuery = ref(props.sqlQuery);
|
|
const localPromqlQuery = ref(props.promqlQuery);
|
|
const vrlFunctionContent = ref(props.vrlFunction);
|
|
const sqlEditorMaximized = ref(false);
|
|
// Controls whether the VRL Monaco editor is mounted. Kept false briefly when
|
|
// restoring from maximized so Monaco mounts AFTER the flex container has
|
|
// already reached its final size (no automaticLayout animation).
|
|
const vrlContentMounted = ref(true);
|
|
const isHovered = ref(false);
|
|
|
|
// DOM refs
|
|
const fnEditorRef = ref<any>(null);
|
|
const editorsColumnRef = ref<HTMLElement | null>(null);
|
|
|
|
const restoreVrlEditor = () => {
|
|
// Step 1: grow the VRL pane (sqlEditorMaximized = false triggers CSS)
|
|
// but keep the Monaco editor unmounted
|
|
vrlContentMounted.value = false;
|
|
sqlEditorMaximized.value = false;
|
|
// Step 2: after two animation frames the browser has finished laying out
|
|
// the flex container — mount Monaco into the correctly-sized slot
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
vrlContentMounted.value = true;
|
|
});
|
|
});
|
|
};
|
|
|
|
// Editor state
|
|
const queryEditorPlaceholderFlag = ref(true);
|
|
const functionEditorPlaceholderFlag = ref(true);
|
|
const { placeholder: vrlPlaceholder } = useVrlPlaceholder();
|
|
|
|
// ─── Typewriter placeholder for the full query editor ────────────────
|
|
const streamFieldsForPlaceholder = computed(() =>
|
|
(props.columns as any[]).map((c: any) => ({
|
|
name: typeof c === "string" ? c : (c.value ?? c.label ?? ""),
|
|
dataType: typeof c === "string" ? "" : (c.type ?? ""),
|
|
})),
|
|
);
|
|
const noStreamForPlaceholder = computed(() => !props.streamName);
|
|
const isSqlModeForPlaceholder = computed(() => localTab.value === "sql");
|
|
const { placeholder: fullEditorPlaceholder } = useQueryPlaceholder(
|
|
streamFieldsForPlaceholder,
|
|
ref({}),
|
|
isSqlModeForPlaceholder,
|
|
noStreamForPlaceholder,
|
|
{ noStreamText: t("pipeline.queryEditorPlaceholder") },
|
|
);
|
|
|
|
// Field selection
|
|
const selectedFunction = ref<any>(null);
|
|
const functionOptions = ref<any[]>(props.savedFunctions);
|
|
|
|
// Output state
|
|
const outputEvents = ref("");
|
|
const outputFnEvents = ref("");
|
|
const queryHitCount = ref(0);
|
|
// Expand output sections by default so users can see query results immediately
|
|
const expandSqlOutput = ref(true);
|
|
const expandCombinedOutput = ref(true);
|
|
const runQueryLoading = ref(false);
|
|
const runFnQueryLoading = ref(false);
|
|
const tempRunQuery = ref(false);
|
|
const tempTestFunction = ref(false);
|
|
const runPromqlError = ref("");
|
|
|
|
// Watch props
|
|
watch(
|
|
() => props.tab,
|
|
(newVal) => {
|
|
localTab.value = newVal || "sql";
|
|
},
|
|
);
|
|
|
|
watch(
|
|
() => props.sqlQuery,
|
|
(newVal) => {
|
|
localSqlQuery.value = newVal;
|
|
},
|
|
);
|
|
|
|
watch(
|
|
() => props.promqlQuery,
|
|
(newVal) => {
|
|
localPromqlQuery.value = newVal;
|
|
},
|
|
);
|
|
|
|
// Clear local error message when SQL query changes
|
|
watch(
|
|
() => localSqlQuery.value,
|
|
() => {
|
|
localSqlQueryErrorMsg.value = "";
|
|
},
|
|
);
|
|
|
|
watch(
|
|
() => props.vrlFunction,
|
|
(newVal) => {
|
|
vrlFunctionContent.value = newVal;
|
|
},
|
|
);
|
|
|
|
watch(
|
|
() => props.savedFunctions,
|
|
(newVal) => {
|
|
functionOptions.value = [...newVal];
|
|
},
|
|
);
|
|
|
|
// Update handlers
|
|
const updateSqlQuery = (value: string) => {
|
|
localSqlQuery.value = value;
|
|
emit("update:sqlQuery", value);
|
|
};
|
|
|
|
const updatePromqlQuery = (value: string) => {
|
|
localPromqlQuery.value = value;
|
|
emit("update:promqlQuery", value);
|
|
};
|
|
|
|
const updateVrlFunction = (value: string) => {
|
|
vrlFunctionContent.value = value;
|
|
emit("update:vrlFunction", value);
|
|
};
|
|
|
|
const onBlurQueryEditor = debounce(async () => {
|
|
queryEditorPlaceholderFlag.value =
|
|
localTab.value === "sql" ? localSqlQuery.value === "" : localPromqlQuery.value === "";
|
|
if (localTab.value === "sql") {
|
|
await _sqlOnBlur();
|
|
emit("validate-sql");
|
|
}
|
|
}, 10);
|
|
|
|
const onBlurFunctionEditor = () => {
|
|
functionEditorPlaceholderFlag.value = vrlFunctionContent.value === "";
|
|
};
|
|
|
|
/**
|
|
* Handle NLP mode toggle from AI icon in alert function editor
|
|
*/
|
|
const handleAlertFunctionEditorToggleNlpMode = () => {
|
|
// UnifiedQueryEditor manages its own NLP mode state internally
|
|
};
|
|
|
|
/**
|
|
* Handle generation start event from alert function editor
|
|
*/
|
|
const handleAlertFunctionEditorGenerationStart = () => {
|
|
// Can add loading indicators here if needed
|
|
};
|
|
|
|
/**
|
|
* Handle generation end event from alert function editor
|
|
*/
|
|
const handleAlertFunctionEditorGenerationEnd = () => {
|
|
// Can remove loading indicators here if needed
|
|
};
|
|
|
|
/**
|
|
* Handle successful generation from alert function editor
|
|
*/
|
|
const handleAlertFunctionEditorGenerationSuccess = () => {
|
|
// Function code is already updated via @update:query handler
|
|
};
|
|
|
|
// Column and function selection.
|
|
// OSelect emits the resolved `valueKey` (the function name), not the option
|
|
// object — look the function up before reading its body.
|
|
const onFunctionSelect = (name: any) => {
|
|
const func = props.savedFunctions.find((f: any) => f.name === name);
|
|
if (func) {
|
|
updateVrlFunction(func.function || func.body || "");
|
|
}
|
|
};
|
|
|
|
const onFunctionClear = () => {
|
|
vrlFunctionContent.value = "";
|
|
updateVrlFunction("");
|
|
selectedFunction.value = null;
|
|
};
|
|
|
|
// Build multi-window query - includes all multi-windows automatically
|
|
const buildMultiWindowQuery = (sql: string, periodInMicroseconds: number) => {
|
|
const queryToSend: any[] = [];
|
|
|
|
// Guard: If multiTimeRange is null, undefined, or empty, return empty array
|
|
if (!props.multiTimeRange || props.multiTimeRange.length === 0) {
|
|
return queryToSend;
|
|
}
|
|
|
|
const regex = /^(\d+)([smhdwM])$/;
|
|
|
|
const unitToMicroseconds: Record<string, number> = {
|
|
s: 1 * 1_000_000, // seconds
|
|
m: 60 * 1_000_000, // minutes
|
|
h: 60 * 60 * 1_000_000, // hours
|
|
d: 24 * 60 * 60 * 1_000_000, // days
|
|
w: 7 * 24 * 60 * 60 * 1_000_000, // weeks
|
|
M: 30 * 24 * 60 * 60 * 1_000_000, // month
|
|
};
|
|
|
|
const now = Date.now() * 1000; // Current time in microseconds because we are using microseconds of unix timestamp
|
|
|
|
// Include all multi-windows (no selection needed)
|
|
props.multiTimeRange.forEach((date: any) => {
|
|
const individualQuery: any = {};
|
|
const match = date.offSet.match(regex);
|
|
if (match) {
|
|
const value = parseInt(match[1], 10);
|
|
const unit = match[2];
|
|
const offsetMicroseconds = value * unitToMicroseconds[unit];
|
|
|
|
const endTime = now;
|
|
const startTime = endTime - offsetMicroseconds;
|
|
individualQuery.start_time = startTime - periodInMicroseconds;
|
|
individualQuery.end_time = startTime;
|
|
individualQuery.sql = sql;
|
|
queryToSend.push(individualQuery);
|
|
} else {
|
|
console.warn("Invalid format:", date);
|
|
}
|
|
});
|
|
|
|
return queryToSend;
|
|
};
|
|
|
|
// Query execution
|
|
const triggerQuery = async (fn = false) => {
|
|
try {
|
|
const queryReq = buildQueryPayload({
|
|
sqlMode: true,
|
|
streamName: props.streamName,
|
|
});
|
|
queryReq.query.sql = localSqlQuery.value;
|
|
queryReq.query.size = 10;
|
|
|
|
const periodInMicroseconds = props.period * 60 * 1000000;
|
|
const endTime = new Date().getTime() * 1000; // ← Use 1000 to get microseconds
|
|
const startTime = endTime - periodInMicroseconds;
|
|
|
|
queryReq.query.query_fn = fn ? b64EncodeUnicode(vrlFunctionContent.value) : null;
|
|
queryReq.query.sql_mode = true;
|
|
|
|
const multiWindowQueries = buildMultiWindowQuery(queryReq.query.sql, periodInMicroseconds);
|
|
|
|
if (multiWindowQueries.length > 0) {
|
|
// Multi-window: combine primary window + extra windows → _search_multi
|
|
queryReq.query.per_query_response = true;
|
|
const queryToSend = [
|
|
{
|
|
start_time: startTime,
|
|
end_time: endTime,
|
|
sql: queryReq.query.sql,
|
|
},
|
|
...multiWindowQueries,
|
|
];
|
|
queryReq.query.sql = queryToSend;
|
|
} else {
|
|
// No multi-window: use normal _search API
|
|
queryReq.query.start_time = startTime;
|
|
queryReq.query.end_time = endTime;
|
|
}
|
|
|
|
delete queryReq.aggs;
|
|
|
|
const res = await searchService.search({
|
|
org_identifier: store.state.selectedOrganization.identifier,
|
|
query: queryReq,
|
|
page_type: props.streamType,
|
|
validate: true,
|
|
});
|
|
|
|
if (res.data.hits.length > 0) {
|
|
if (fn) {
|
|
outputFnEvents.value = JSON.stringify(res.data.hits, null, 2);
|
|
} else {
|
|
outputEvents.value = JSON.stringify(res.data.hits, null, 2);
|
|
queryHitCount.value = res.data.total ?? res.data.hits.flat().length;
|
|
}
|
|
}
|
|
} catch (err: any) {
|
|
console.error("[QueryEditorDialog] ERROR in triggerQuery:", err);
|
|
console.error("[QueryEditorDialog] Error details:", {
|
|
message: err.message,
|
|
response: err.response,
|
|
stack: err.stack,
|
|
});
|
|
toast({
|
|
variant: "error",
|
|
message: err.response?.data?.message ?? t("search.errorFetchingResults"),
|
|
timeout: 1500,
|
|
});
|
|
}
|
|
};
|
|
|
|
const runSqlQuery = async () => {
|
|
runPromqlError.value = "";
|
|
localSqlQueryErrorMsg.value = "";
|
|
suppressPropError.value = true;
|
|
queryHitCount.value = 0;
|
|
|
|
// Validate SQL query before running (checks for SELECT * and reserved words)
|
|
const parserResult = getParser(localSqlQuery.value);
|
|
|
|
if (!parserResult) {
|
|
// Parser validation failed - don't run the query
|
|
// Error message is already set by getParser via sqlQueryErrorMsg
|
|
return;
|
|
}
|
|
|
|
tempRunQuery.value = true;
|
|
expandSqlOutput.value = true;
|
|
try {
|
|
runQueryLoading.value = true;
|
|
await triggerQuery();
|
|
} catch (err) {
|
|
console.error("[QueryEditorDialog] Error in runSqlQuery:", err);
|
|
} finally {
|
|
runQueryLoading.value = false;
|
|
suppressPropError.value = false;
|
|
}
|
|
};
|
|
|
|
const runTestFunction = async () => {
|
|
runPromqlError.value = "";
|
|
tempTestFunction.value = true;
|
|
expandCombinedOutput.value = true;
|
|
runFnQueryLoading.value = true;
|
|
try {
|
|
await triggerQuery(true);
|
|
runFnQueryLoading.value = false;
|
|
} catch (err) {
|
|
runFnQueryLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const triggerPromqlQuery = async () => {
|
|
const queryReq = buildQueryPayload({
|
|
sqlMode: true,
|
|
streamName: props.streamName,
|
|
});
|
|
|
|
const periodInMicroseconds = props.period * 60 * 1000000;
|
|
const endTime = new Date().getTime() * 1000;
|
|
const startTime = endTime - periodInMicroseconds;
|
|
|
|
outputEvents.value = "";
|
|
queryReq.query = localPromqlQuery.value;
|
|
|
|
try {
|
|
const res = await searchService.metrics_query_range({
|
|
org_identifier: store.state.selectedOrganization.identifier,
|
|
query: queryReq.query,
|
|
start_time: startTime,
|
|
end_time: endTime,
|
|
step: "0",
|
|
});
|
|
|
|
const result = res?.data?.data?.result;
|
|
if (result?.length > 0) {
|
|
outputEvents.value = JSON.stringify(result, null, 2);
|
|
// Count total data points across all series so the status bar shows a meaningful number
|
|
queryHitCount.value = result.reduce(
|
|
(sum: number, series: any) =>
|
|
sum + (Array.isArray(series.values) ? series.values.length : 0),
|
|
0,
|
|
);
|
|
}
|
|
} catch (err: any) {
|
|
runPromqlError.value = err.response?.data?.error ?? t("search.somethingWentWrong");
|
|
}
|
|
};
|
|
|
|
const runPromqlQuery = async () => {
|
|
runPromqlError.value = "";
|
|
queryHitCount.value = 0;
|
|
tempRunQuery.value = true;
|
|
expandSqlOutput.value = true;
|
|
runQueryLoading.value = true;
|
|
try {
|
|
await triggerPromqlQuery();
|
|
runQueryLoading.value = false;
|
|
} catch (err) {
|
|
runQueryLoading.value = false;
|
|
}
|
|
};
|
|
|
|
// Unified Query Editor ref
|
|
const queryEditorRef = ref<any>(null);
|
|
|
|
// Server SQL-validation squiggle ranges, provided by AddAlert.vue.
|
|
const alertSqlErrorRanges = inject<Ref<SqlErrorRange[]>>(
|
|
"alertSqlErrorRanges",
|
|
ref<SqlErrorRange[]>([]),
|
|
);
|
|
|
|
const {
|
|
onFocus: _sqlOnFocus,
|
|
onBlur: _sqlOnBlur,
|
|
onQueryChange: _sqlOnQueryChange,
|
|
} = useSqlEditorDiagnostics({
|
|
queryEditorRef,
|
|
sqlMode: computed(() => localTab.value === "sql"),
|
|
query: computed(() => localSqlQuery.value ?? ""),
|
|
streamName: computed(() => props.streamName),
|
|
externalErrors: alertSqlErrorRanges,
|
|
});
|
|
|
|
const onQueryEditorFocus = () => {
|
|
queryEditorPlaceholderFlag.value = false;
|
|
_sqlOnFocus();
|
|
};
|
|
|
|
// ── Autocomplete ──────────────────────────────────────────────────────────
|
|
const {
|
|
autoCompleteData,
|
|
autoCompleteKeywords,
|
|
autoCompleteSuggestions,
|
|
// Context-aware views: these swap in stream names after FROM and field VALUES
|
|
// after an operator. Binding the raw lists above meant the value popup showed
|
|
// field names exactly where values belong.
|
|
effectiveKeywords,
|
|
effectiveSuggestions,
|
|
getSuggestions,
|
|
updateFieldKeywords,
|
|
resolveFieldValues,
|
|
} = useSqlSuggestions();
|
|
|
|
// Rebuild field keywords whenever columns prop changes
|
|
watch(
|
|
() => props.columns,
|
|
(cols) => {
|
|
if (cols?.length) {
|
|
const fields = (cols as any[]).map((c: any) => ({
|
|
name: typeof c === "string" ? c : (c.value ?? c.label ?? c),
|
|
type: c.type ?? "Utf8",
|
|
}));
|
|
updateFieldKeywords(fields);
|
|
}
|
|
},
|
|
{ immediate: true, deep: true },
|
|
);
|
|
|
|
// Transform columns (value/label pairs) → FieldList items ({ name })
|
|
// Extract WHERE clause from SQL so FieldList can use it to filter field values.
|
|
// FieldList.buildSql() expects a raw WHERE clause string (not a full SQL query).
|
|
const fieldListWhereClause = computed(() => {
|
|
const sql = localSqlQuery.value?.trim();
|
|
if (!sql) return "";
|
|
const match = sql.match(
|
|
/\bWHERE\b([\s\S]+?)(?:\bGROUP\s+BY\b|\bHAVING\b|\bORDER\s+BY\b|\bLIMIT\b|$)/i,
|
|
);
|
|
return match ? match[1].trim() : "";
|
|
});
|
|
|
|
const fieldListItems = computed(() =>
|
|
(props.columns as any[]).map((c: any) => ({
|
|
name: typeof c === "string" ? c : (c.value ?? c.label ?? c),
|
|
showValues: true,
|
|
})),
|
|
);
|
|
|
|
const fieldListTimeStamp = computed(() => {
|
|
const endTime = new Date().getTime() * 1000;
|
|
const startTime = endTime - props.period * 60 * 1000000;
|
|
return { startTime, endTime };
|
|
});
|
|
|
|
const rebuildSqlWithWhere = (newWhere: string): string => {
|
|
const sql = localSqlQuery.value?.trim() || "";
|
|
if (!sql) return newWhere ? `SELECT * FROM "${props.streamName}" WHERE ${newWhere}` : "";
|
|
|
|
// Split off any trailing GROUP BY / HAVING / ORDER BY / LIMIT
|
|
const trailingMatch = sql.match(/(\s+(?:GROUP\s+BY|HAVING|ORDER\s+BY|LIMIT)\b[\s\S]*)$/i);
|
|
const trailing = trailingMatch ? trailingMatch[1] : "";
|
|
const sqlBeforeTrailing = trailing ? sql.slice(0, sql.length - trailing.length) : sql;
|
|
|
|
// Now strip any existing WHERE clause from sqlBeforeTrailing
|
|
const whereIdx = sqlBeforeTrailing.search(/\bWHERE\b/i);
|
|
const beforeWhere =
|
|
whereIdx >= 0 ? sqlBeforeTrailing.slice(0, whereIdx).trimEnd() : sqlBeforeTrailing;
|
|
|
|
return newWhere ? `${beforeWhere} WHERE ${newWhere}${trailing}` : `${beforeWhere}${trailing}`;
|
|
};
|
|
|
|
const handleFieldListEvent = (event: string, value: any) => {
|
|
let newSql: string | null = null;
|
|
if (event === "add-field") {
|
|
const newWhere = applyFilterTerm(value, fieldListWhereClause.value);
|
|
newSql = rebuildSqlWithWhere(newWhere);
|
|
} else if (event === "remove-field") {
|
|
const newWhere = removeFieldCondition(fieldListWhereClause.value, value);
|
|
newSql = rebuildSqlWithWhere(newWhere);
|
|
}
|
|
if (newSql !== null) {
|
|
localSqlQuery.value = newSql;
|
|
// Force the editor to update even if it has focus
|
|
queryEditorRef.value?.setValue?.(newSql);
|
|
emit("update:sqlQuery", newSql);
|
|
}
|
|
};
|
|
|
|
// Determine available languages based on stream type
|
|
const availableLanguages = computed<("sql" | "promql")[]>(() => {
|
|
// For metrics streams, only PromQL is available
|
|
if (props.streamType === "metrics") {
|
|
return ["promql"];
|
|
}
|
|
// For logs streams, only SQL is available
|
|
if (props.streamType === "logs") {
|
|
return ["sql"];
|
|
}
|
|
// For other stream types, allow both SQL and PromQL
|
|
return ["sql", "promql"];
|
|
});
|
|
|
|
// Unified Query Editor handlers
|
|
const handleQueryUpdate = (newQuery: string) => {
|
|
_sqlOnQueryChange();
|
|
if (localTab.value === "sql") {
|
|
updateSqlQuery(newQuery);
|
|
} else {
|
|
updatePromqlQuery(newQuery);
|
|
}
|
|
// Feed autocomplete context and trigger suggestions
|
|
autoCompleteData.value.query = newQuery;
|
|
autoCompleteData.value.position.cursorIndex = queryEditorRef.value?.getCursorIndex?.() ?? 0;
|
|
autoCompleteData.value.org = store.state.selectedOrganization.identifier;
|
|
autoCompleteData.value.streamType = props.streamType;
|
|
autoCompleteData.value.streamName = props.streamName;
|
|
autoCompleteData.value.popup.open = queryEditorRef.value?.triggerAutoComplete;
|
|
getSuggestions();
|
|
};
|
|
|
|
const handleLanguageChange = (language: "sql" | "promql" | "vrl" | "javascript") => {
|
|
// Alert editor only offers sql/promql
|
|
const newLanguage = language as "sql" | "promql";
|
|
localTab.value = newLanguage;
|
|
|
|
// Explicitly sync the editor with the correct query after language change
|
|
// This ensures the editor shows the right query for the selected language
|
|
setTimeout(() => {
|
|
if (queryEditorRef.value && queryEditorRef.value.setValue) {
|
|
const currentQuery = newLanguage === "sql" ? localSqlQuery.value : localPromqlQuery.value;
|
|
queryEditorRef.value.setValue(currentQuery);
|
|
}
|
|
}, 50); // Small delay to ensure editor has switched language
|
|
};
|
|
|
|
const handleRunQuery = (language: "sql" | "promql") => {
|
|
if (language === "sql") {
|
|
runSqlQuery();
|
|
} else {
|
|
runPromqlQuery();
|
|
}
|
|
};
|
|
|
|
const handleAskAI = async () => {
|
|
// The unified component handles AI generation internally
|
|
// This event is just for parent components that may need to react
|
|
};
|
|
|
|
// AI Chat
|
|
const toggleAIChat = () => {
|
|
const isEnabled = !store.state.isAiChatEnabled;
|
|
store.dispatch("setIsAiChatEnabled", isEnabled);
|
|
};
|
|
|
|
const getBtnLogo = computed(() => {
|
|
if (isHovered.value || store.state.isAiChatEnabled) {
|
|
return getImageURL("images/common/ai_icon_dark.svg");
|
|
}
|
|
|
|
return isDark.value
|
|
? getImageURL("images/common/ai_icon_dark.svg")
|
|
: getImageURL("images/common/ai_icon_gradient.svg");
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* keep(keyframes): the "Running" status dot is used only by this dialog. The
|
|
`animation` is declared here, not as a template `[animation:…]` utility, so
|
|
Vue's scoped compiler renames the keyframe and this reference together. */
|
|
.query-editor-run-dot {
|
|
animation: dot-pulse 1.5s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes dot-pulse {
|
|
0%,
|
|
100% {
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
opacity: 0.3;
|
|
}
|
|
}
|
|
</style>
|