mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-26 20:00:33 +01:00
Compare commits
1 Commits
tvats-fix-
...
fix/tansta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e46f61a036 |
@@ -54,12 +54,5 @@
|
||||
"ROLES_SETTINGS": "SigNoz | Roles",
|
||||
"MEMBERS_SETTINGS": "SigNoz | Members",
|
||||
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts",
|
||||
"MCP_SERVER": "SigNoz | MCP Server",
|
||||
"AI_ASSISTANT": "SigNoz | AI Assistant",
|
||||
"TRACE_DETAIL_OLD": "SigNoz | Trace Detail",
|
||||
"SERVICE_TOP_LEVEL_OPERATIONS": "SigNoz | Service Operations",
|
||||
"ROLE_DETAILS": "SigNoz | Role Details",
|
||||
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
|
||||
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
|
||||
"PUBLIC_DASHBOARD": "SigNoz | Dashboard"
|
||||
"MCP_SERVER": "SigNoz | MCP Server"
|
||||
}
|
||||
|
||||
@@ -77,12 +77,5 @@
|
||||
"ROLES_SETTINGS": "SigNoz | Roles",
|
||||
"MEMBERS_SETTINGS": "SigNoz | Members",
|
||||
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts",
|
||||
"MCP_SERVER": "SigNoz | MCP Server",
|
||||
"AI_ASSISTANT": "SigNoz | AI Assistant",
|
||||
"TRACE_DETAIL_OLD": "SigNoz | Trace Detail",
|
||||
"SERVICE_TOP_LEVEL_OPERATIONS": "SigNoz | Service Operations",
|
||||
"ROLE_DETAILS": "SigNoz | Role Details",
|
||||
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
|
||||
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
|
||||
"PUBLIC_DASHBOARD": "SigNoz | Dashboard"
|
||||
"MCP_SERVER": "SigNoz | MCP Server"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
|
||||
import cx from 'classnames';
|
||||
|
||||
import tableStyles from './TanStackTable.module.scss';
|
||||
@@ -22,21 +22,19 @@ type WithDangerousHtml = BaseProps & {
|
||||
|
||||
export type TanStackTableTextProps = WithChildren | WithDangerousHtml;
|
||||
|
||||
function TanStackTableText({
|
||||
children,
|
||||
className,
|
||||
dangerouslySetInnerHTML,
|
||||
...rest
|
||||
}: TanStackTableTextProps): JSX.Element {
|
||||
return (
|
||||
const TanStackTableText = forwardRef<HTMLSpanElement, TanStackTableTextProps>(
|
||||
({ children, className, dangerouslySetInnerHTML, ...rest }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
className={cx(tableStyles.tableCellText, className)}
|
||||
dangerouslySetInnerHTML={dangerouslySetInnerHTML}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
|
||||
TanStackTableText.displayName = 'TanStackTableText';
|
||||
|
||||
export default TanStackTableText;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
@@ -10,17 +9,7 @@ import {
|
||||
CommandShortcut,
|
||||
} from '@signozhq/ui/command';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
AIAssistantEvents,
|
||||
AIAssistantOpenSource,
|
||||
} from 'container/AIAssistant/events';
|
||||
import { normalizePage } from 'container/AIAssistant/hooks/useAIAssistantAnalyticsContext';
|
||||
import {
|
||||
openAIAssistantModal,
|
||||
useAIAssistantStore,
|
||||
} from 'container/AIAssistant/store/useAIAssistantStore';
|
||||
import { useThemeMode } from 'hooks/useDarkMode';
|
||||
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
|
||||
import history from 'lib/history';
|
||||
import { ROLES as UserRole } from 'types/roles';
|
||||
|
||||
@@ -48,11 +37,6 @@ export function CmdKPalette({
|
||||
const { open, setOpen } = useCmdK();
|
||||
|
||||
const { setAutoSwitch, setTheme, theme } = useThemeMode();
|
||||
const location = useLocation();
|
||||
const isAIAssistantEnabled = useIsAIAssistantEnabled();
|
||||
const startNewConversation = useAIAssistantStore(
|
||||
(s) => s.startNewConversation,
|
||||
);
|
||||
|
||||
// toggle palette with ⌘/Ctrl+K
|
||||
function handleGlobalCmdK(
|
||||
@@ -94,21 +78,9 @@ export function CmdKPalette({
|
||||
history.push(key);
|
||||
}
|
||||
|
||||
const handleOpenAIAssistant = (): void => {
|
||||
void logEvent(AIAssistantEvents.Opened, {
|
||||
source: AIAssistantOpenSource.Cmdk,
|
||||
currentPage: normalizePage(location.pathname),
|
||||
});
|
||||
startNewConversation();
|
||||
openAIAssistantModal();
|
||||
};
|
||||
|
||||
const actions = createShortcutActions({
|
||||
navigate: onClickHandler,
|
||||
handleThemeChange,
|
||||
aiAssistant: isAIAssistantEnabled
|
||||
? { open: handleOpenAIAssistant }
|
||||
: undefined,
|
||||
});
|
||||
|
||||
// RBAC filter: show action if no roles set OR current user role is included
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
ListMinus,
|
||||
ScrollText,
|
||||
Settings,
|
||||
Sparkles,
|
||||
TowerControl,
|
||||
Workflow,
|
||||
} from '@signozhq/icons';
|
||||
@@ -35,20 +34,12 @@ export type CmdAction = {
|
||||
type ActionDeps = {
|
||||
navigate: (path: string) => void;
|
||||
handleThemeChange: (mode: string) => void;
|
||||
/**
|
||||
* Provided only when the AI Assistant feature is available for the current
|
||||
* tenant. When present, the palette surfaces an "Open AI Assistant" entry
|
||||
* at the top; when absent, the action is omitted entirely.
|
||||
*/
|
||||
aiAssistant?: {
|
||||
open: () => void;
|
||||
};
|
||||
};
|
||||
|
||||
export function createShortcutActions(deps: ActionDeps): CmdAction[] {
|
||||
const { navigate, handleThemeChange, aiAssistant } = deps;
|
||||
const { navigate, handleThemeChange } = deps;
|
||||
|
||||
const actions: CmdAction[] = [
|
||||
return [
|
||||
{
|
||||
id: 'home',
|
||||
name: 'Go to Home',
|
||||
@@ -288,19 +279,4 @@ export function createShortcutActions(deps: ActionDeps): CmdAction[] {
|
||||
perform: (): void => navigate(ROUTES.MEMBERS_SETTINGS),
|
||||
},
|
||||
];
|
||||
|
||||
if (aiAssistant) {
|
||||
actions.unshift({
|
||||
id: 'ai-assistant',
|
||||
name: 'Open AI Assistant',
|
||||
shortcut: ['cmd+j'],
|
||||
keywords: 'ai assistant chat ask sparkles copilot',
|
||||
section: 'AI Assistant',
|
||||
icon: <Sparkles size={14} />,
|
||||
roles: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
perform: aiAssistant.open,
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import logEvent from 'api/common/logEvent';
|
||||
|
||||
import HistorySidebar from '../components/ConversationsList';
|
||||
import ConversationView from '../ConversationView';
|
||||
import { AIAssistantEvents, AIAssistantOpenSource } from '../events';
|
||||
import { AIAssistantEvents } from '../events';
|
||||
import {
|
||||
normalizePage,
|
||||
useAIAssistantAnalyticsContext,
|
||||
@@ -65,7 +65,7 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
startNewConversation();
|
||||
setShowHistory(false);
|
||||
void logEvent(AIAssistantEvents.Opened, {
|
||||
source: AIAssistantOpenSource.Shortcut,
|
||||
source: 'shortcut',
|
||||
currentPage: normalizePage(pathname),
|
||||
});
|
||||
openModal();
|
||||
@@ -162,57 +162,57 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
onClick={(): void => setShowHistory((v) => !v)}
|
||||
aria-label="Toggle conversations"
|
||||
className={showHistory ? styles.toggleBtnActive : ''}
|
||||
prefix={<History size={14} />}
|
||||
/>
|
||||
>
|
||||
<History size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
|
||||
<TooltipSimple title="New conversation">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
onClick={handleNew}
|
||||
aria-label="New conversation"
|
||||
prefix={<Plus size={14} />}
|
||||
/>
|
||||
>
|
||||
<Plus size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
|
||||
<TooltipSimple title="Open full screen">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
onClick={handleExpand}
|
||||
disabled={!activeConversationId}
|
||||
aria-label="Open full screen"
|
||||
prefix={<Maximize2 size={14} />}
|
||||
/>
|
||||
>
|
||||
<Maximize2 size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
|
||||
<TooltipSimple title="Minimize to side panel">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
onClick={handleMinimize}
|
||||
aria-label="Minimize to side panel"
|
||||
prefix={<Minus size={14} />}
|
||||
/>
|
||||
>
|
||||
<Minus size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
|
||||
<TooltipSimple title="Close">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
onClick={closeModal}
|
||||
aria-label="Close"
|
||||
prefix={<X size={14} />}
|
||||
/>
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -150,8 +150,9 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
color="secondary"
|
||||
onClick={(): void => setShowHistory((v) => !v)}
|
||||
aria-label="Toggle conversations"
|
||||
prefix={<History size={14} />}
|
||||
/>
|
||||
>
|
||||
<History size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
|
||||
<TooltipSimple title="New conversation">
|
||||
@@ -161,8 +162,9 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
color="secondary"
|
||||
onClick={handleNew}
|
||||
aria-label="New conversation"
|
||||
prefix={<Plus size={14} />}
|
||||
/>
|
||||
>
|
||||
<Plus size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
|
||||
<TooltipSimple title="Open full screen">
|
||||
@@ -173,8 +175,9 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
onClick={handleExpand}
|
||||
disabled={!activeConversationId}
|
||||
aria-label="Open full screen"
|
||||
prefix={<Maximize2 size={14} />}
|
||||
/>
|
||||
>
|
||||
<Maximize2 size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
|
||||
<TooltipSimple title="Close">
|
||||
@@ -184,8 +187,9 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
color="secondary"
|
||||
onClick={closeDrawer}
|
||||
aria-label="Close panel"
|
||||
prefix={<X size={14} />}
|
||||
/>
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import logEvent from 'api/common/logEvent';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { Bot } from '@signozhq/icons';
|
||||
|
||||
import { AIAssistantEvents, AIAssistantOpenSource } from '../events';
|
||||
import { AIAssistantEvents } from '../events';
|
||||
import { normalizePage } from '../hooks/useAIAssistantAnalyticsContext';
|
||||
import {
|
||||
openAIAssistant,
|
||||
@@ -31,7 +31,7 @@ export default function AIAssistantTrigger(): JSX.Element | null {
|
||||
|
||||
const handleOpen = useCallback((): void => {
|
||||
void logEvent(AIAssistantEvents.Opened, {
|
||||
source: AIAssistantOpenSource.Icon,
|
||||
source: 'icon',
|
||||
currentPage: normalizePage(pathname),
|
||||
});
|
||||
openAIAssistant();
|
||||
|
||||
@@ -159,7 +159,6 @@ export default function ConversationView({
|
||||
<ConversationSkeleton />
|
||||
<div className={inputWrapperClass}>
|
||||
<ChatInput
|
||||
key={conversationId}
|
||||
onSend={handleSend}
|
||||
disabled
|
||||
autoContexts={autoContexts}
|
||||
@@ -173,7 +172,6 @@ export default function ConversationView({
|
||||
return (
|
||||
<div className={styles.conversation}>
|
||||
<VirtualizedMessages
|
||||
key={conversationId}
|
||||
conversationId={conversationId}
|
||||
messages={messages}
|
||||
isStreaming={isStreamingHere}
|
||||
@@ -186,7 +184,6 @@ export default function ConversationView({
|
||||
)}
|
||||
<div className={inputWrapperClass}>
|
||||
<ChatInput
|
||||
key={conversationId}
|
||||
onSend={handleSend}
|
||||
onCancel={handleCancel}
|
||||
disabled={inputDisabled}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
DialogTitle,
|
||||
} from '@signozhq/ui/dialog';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import type {
|
||||
ApprovalEventDTO,
|
||||
ApprovalEventDTODiff,
|
||||
@@ -101,16 +100,16 @@ export default function ApprovalCard({
|
||||
<div className={styles.diffSection}>
|
||||
<div className={styles.diffHeader}>
|
||||
<span className={styles.diffHeaderLabel}>Diff</span>
|
||||
<TooltipSimple title="Expand diff">
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={(): void => setDiffExpanded(true)}
|
||||
aria-label="Expand diff"
|
||||
prefix={<Maximize2 size={12} />}
|
||||
/>
|
||||
</TooltipSimple>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={(): void => setDiffExpanded(true)}
|
||||
title="Expand diff"
|
||||
aria-label="Expand diff"
|
||||
>
|
||||
<Maximize2 size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
<DiffView diff={approval.diff} />
|
||||
</div>
|
||||
@@ -120,8 +119,6 @@ export default function ApprovalCard({
|
||||
<DialogContent
|
||||
className={styles.diffDialog}
|
||||
style={{ width: '80vw', maxWidth: '80vw', height: '70vh' }}
|
||||
// Skip auto-focus — otherwise the first Copy button opens its tooltip on dialog open.
|
||||
onOpenAutoFocus={(e): void => e.preventDefault()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Approval diff</DialogTitle>
|
||||
@@ -137,22 +134,19 @@ export default function ApprovalCard({
|
||||
size="sm"
|
||||
value={viewMode}
|
||||
onChange={(next): void => {
|
||||
// Radix `single` group can emit '' when the active item is clicked again.
|
||||
// Radix `single` group can emit '' when the active item
|
||||
// is clicked again — preserve the current mode.
|
||||
if (next === 'split' || next === 'unified') {
|
||||
setViewMode(next);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TooltipSimple title="Split view">
|
||||
<ToggleGroupItem value="split" aria-label="Split view">
|
||||
<Columns2 size={12} />
|
||||
</ToggleGroupItem>
|
||||
</TooltipSimple>
|
||||
<TooltipSimple title="Unified view">
|
||||
<ToggleGroupItem value="unified" aria-label="Unified view">
|
||||
<List size={12} />
|
||||
</ToggleGroupItem>
|
||||
</TooltipSimple>
|
||||
<ToggleGroupItem value="split" aria-label="Split view">
|
||||
<Columns2 size={12} />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="unified" aria-label="Unified view">
|
||||
<List size={12} />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<ToggleGroup
|
||||
type="multiple"
|
||||
@@ -160,16 +154,12 @@ export default function ApprovalCard({
|
||||
value={wrapText ? ['wrap'] : []}
|
||||
onChange={(next): void => setWrapText(next.includes('wrap'))}
|
||||
>
|
||||
<TooltipSimple
|
||||
title={wrapText ? 'Disable text wrap' : 'Wrap long lines'}
|
||||
<ToggleGroupItem
|
||||
value="wrap"
|
||||
aria-label={wrapText ? 'Disable text wrap' : 'Wrap long lines'}
|
||||
>
|
||||
<ToggleGroupItem
|
||||
value="wrap"
|
||||
aria-label={wrapText ? 'Disable text wrap' : 'Wrap long lines'}
|
||||
>
|
||||
<WrapText size={12} />
|
||||
</ToggleGroupItem>
|
||||
</TooltipSimple>
|
||||
<WrapText size={12} />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
{approval.diff && (
|
||||
@@ -467,16 +457,15 @@ function CopyButton({ text, label }: CopyButtonProps): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipSimple title={copied ? `Copied ${label}` : `Copy ${label}`}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={handleCopy}
|
||||
aria-label={copied ? `Copied ${label}` : `Copy ${label}`}
|
||||
>
|
||||
{copied ? <Check size={12} /> : <Copy size={12} />}
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={handleCopy}
|
||||
title={copied ? `Copied ${label}` : `Copy ${label}`}
|
||||
aria-label={copied ? `Copied ${label}` : `Copy ${label}`}
|
||||
>
|
||||
{copied ? <Check size={12} /> : <Copy size={12} />}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,12 @@
|
||||
border-radius: var(--radius-2);
|
||||
padding: 8px;
|
||||
border: 1px solid var(--l1-border);
|
||||
transition: border-color 0.15s;
|
||||
position: relative;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--l1-border);
|
||||
}
|
||||
}
|
||||
|
||||
.attachments {
|
||||
@@ -124,18 +129,6 @@
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: var(--radius-2);
|
||||
padding: 4px;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
|
||||
// Scope the focus ring to the textarea row only — the surrounding
|
||||
// chrome (context chips, "Add Context", mic, send) sits outside this
|
||||
// element and stays unaffected when the cursor enters the textarea.
|
||||
&:focus-within {
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 1px
|
||||
color-mix(in srgb, var(--accent-primary), transparent 70%);
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
@@ -251,24 +244,16 @@
|
||||
}
|
||||
|
||||
.contextPopoverCategoryItem {
|
||||
// Override DS Button's centered layout.
|
||||
--button-justify-content: flex-start;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 0 8px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
border: 1px solid color-mix(in srgb, var(--l1-foreground), transparent 96%);
|
||||
border-radius: var(--radius-2);
|
||||
font-size: 12px;
|
||||
font-weight: 550;
|
||||
text-align: left;
|
||||
border: 1px solid color-mix(in srgb, var(--l1-foreground), transparent 96%);
|
||||
border-radius: var(--radius-2);
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
@@ -324,24 +309,17 @@
|
||||
}
|
||||
|
||||
.contextPopoverEntityItem {
|
||||
// Override DS Button's centered layout.
|
||||
--button-justify-content: flex-start;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
background: transparent;
|
||||
border: 1px solid color-mix(in srgb, var(--l1-foreground), transparent 96%);
|
||||
border-radius: var(--radius-2);
|
||||
color: var(--l1-foreground);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
text-align: left;
|
||||
border: 1px solid color-mix(in srgb, var(--l1-foreground), transparent 96%);
|
||||
border-radius: var(--radius-2);
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
// Required for the inner span's `text-overflow: ellipsis` to engage —
|
||||
// flex items default to `min-width: auto` (intrinsic width) and would
|
||||
@@ -407,11 +385,6 @@
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
// Reset native <button> defaults so the 24px circle isn't inflated by
|
||||
// browser-default padding / font metrics.
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.micDiscard {
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import cx from 'classnames';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
@@ -32,11 +26,7 @@ import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import {
|
||||
AIAssistantEvents,
|
||||
VoiceInputSource,
|
||||
getBrowserInfo,
|
||||
} from '../../events';
|
||||
import { AIAssistantEvents, getBrowserInfo } from '../../events';
|
||||
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useSpeechRecognition } from '../../hooks/useSpeechRecognition';
|
||||
import { MessageAttachment } from '../../types';
|
||||
@@ -152,10 +142,6 @@ function autoContextCategory(ctx: MessageContext): string {
|
||||
|
||||
const MAX_INPUT_LENGTH = 20000;
|
||||
const WARNING_THRESHOLD = 15000;
|
||||
// Cap for the auto-growing composer. Past this, the textarea stops growing
|
||||
// and starts scrolling internally so the message list above doesn't get
|
||||
// squeezed in tighter container variants (e.g. the floating panel).
|
||||
const TEXTAREA_MAX_HEIGHT_PX = 200;
|
||||
const HOME_SERVICES_INTERVAL = 30 * 60 * 1000;
|
||||
/** sessionStorage key for the "voice input failed this tab" flag. */
|
||||
const VOICE_UNAVAILABLE_KEY = 'ai-assistant-voice-unavailable';
|
||||
@@ -238,18 +224,6 @@ export default function ChatInput({
|
||||
const [activeContextCategory, setActiveContextCategory] =
|
||||
useState<ContextCategory>('Dashboards');
|
||||
const [pickerSearchQuery, setPickerSearchQuery] = useState('');
|
||||
// Refs to each category tab so we can move DOM focus to the newly-active
|
||||
// tab on ArrowUp/ArrowDown. Without this the roving-tabindex pattern
|
||||
// stalls: focus stays on the original button (whose closure has the old
|
||||
// category), so subsequent arrow keys never advance past the second tab.
|
||||
const categoryTabRefs = useRef(
|
||||
new Map<ContextCategory, HTMLButtonElement | null>(),
|
||||
);
|
||||
// Refs to each entity row in the active tab panel, so we can cross from
|
||||
// the category tablist (ArrowRight) into the panel and step through
|
||||
// entities with ArrowUp/Down. Array is rewritten each render — there's
|
||||
// only ever one tab panel mounted so stale indices clear naturally.
|
||||
const entityRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// When the picker was opened by typing `@` in the textarea, this holds the
|
||||
@@ -329,92 +303,11 @@ export default function ChatInput({
|
||||
[mentionRange, selectedContexts, text],
|
||||
);
|
||||
|
||||
const focusCategory = useCallback((category: ContextCategory) => {
|
||||
setActiveContextCategory(category);
|
||||
setPickerSearchQuery('');
|
||||
categoryTabRefs.current.get(category)?.focus();
|
||||
}, []);
|
||||
|
||||
const handleCategoryKeyDown = useCallback(
|
||||
(
|
||||
e: React.KeyboardEvent<HTMLButtonElement>,
|
||||
category: ContextCategory,
|
||||
): void => {
|
||||
const total = CONTEXT_CATEGORIES.length;
|
||||
const idx = CONTEXT_CATEGORIES.indexOf(category);
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
focusCategory(CONTEXT_CATEGORIES[(idx + 1) % total]);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
focusCategory(CONTEXT_CATEGORIES[(idx - 1 + total) % total]);
|
||||
} else if (e.key === 'Home') {
|
||||
e.preventDefault();
|
||||
focusCategory(CONTEXT_CATEGORIES[0]);
|
||||
} else if (e.key === 'End') {
|
||||
e.preventDefault();
|
||||
focusCategory(CONTEXT_CATEGORIES[total - 1]);
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
// Cross from tablist into entity panel.
|
||||
e.preventDefault();
|
||||
entityRefs.current[0]?.focus();
|
||||
}
|
||||
},
|
||||
[focusCategory],
|
||||
);
|
||||
|
||||
const handleEntityKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLButtonElement>, index: number): void => {
|
||||
const count = entityRefs.current.length;
|
||||
if (count === 0) {
|
||||
return;
|
||||
}
|
||||
const focusAt = (i: number): void => {
|
||||
e.preventDefault();
|
||||
entityRefs.current[i]?.focus();
|
||||
};
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
focusAt((index + 1) % count);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
focusAt((index - 1 + count) % count);
|
||||
break;
|
||||
case 'Home':
|
||||
focusAt(0);
|
||||
break;
|
||||
case 'End':
|
||||
focusAt(count - 1);
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
// Cross back to tablist.
|
||||
e.preventDefault();
|
||||
categoryTabRefs.current.get(activeContextCategory)?.focus();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
},
|
||||
[activeContextCategory],
|
||||
);
|
||||
|
||||
// Focus the textarea when this component mounts (panel/modal open)
|
||||
useEffect(() => {
|
||||
textareaRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
// Auto-grow the textarea so long prompts aren't trapped in a 2-line
|
||||
// scrolling porthole. Reset to `auto` first to let the field shrink back
|
||||
// down when the user deletes content, then snap to scrollHeight capped at
|
||||
// TEXTAREA_MAX_HEIGHT_PX (overflow-y: auto in CSS handles the rest).
|
||||
useLayoutEffect(() => {
|
||||
const el = textareaRef.current;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
el.style.height = 'auto';
|
||||
el.style.height = `${Math.min(el.scrollHeight, TEXTAREA_MAX_HEIGHT_PX)}px`;
|
||||
}, [text]);
|
||||
|
||||
const handleSend = useCallback(async () => {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed && pendingFiles.length === 0) {
|
||||
@@ -489,7 +382,7 @@ export default function ChatInput({
|
||||
// start time so we can attribute `durationMs` on the Voice input used
|
||||
// event regardless of which control ended the session.
|
||||
const voiceStartedAtRef = useRef<number | null>(null);
|
||||
const voiceSourceRef = useRef<VoiceInputSource | null>(null);
|
||||
const voiceSourceRef = useRef<'button' | 'shortcut' | null>(null);
|
||||
// Set to true after a `network`, `not-allowed`, or `not-supported` failure
|
||||
// so we hide the mic button for the rest of the tab session — silent
|
||||
// retries don't help, and Chromium derivatives without the Google Speech
|
||||
@@ -566,7 +459,7 @@ export default function ChatInput({
|
||||
const showMic = isSupported && micPermission !== 'denied' && !voiceUnavailable;
|
||||
|
||||
const startVoiceInput = useCallback(
|
||||
(source: VoiceInputSource) => {
|
||||
(source: 'button' | 'shortcut') => {
|
||||
// Defense in depth: the button is hidden when `voiceUnavailable` is
|
||||
// true, but the PTT shortcut listener can still call us. Bailing here
|
||||
// keeps a single source of truth and prevents repeat `Voice input
|
||||
@@ -643,7 +536,7 @@ export default function ChatInput({
|
||||
return; // ignore auto-repeat
|
||||
}
|
||||
pttActiveRef.current = true;
|
||||
startVoiceInput(VoiceInputSource.Shortcut);
|
||||
startVoiceInput('shortcut');
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent): void => {
|
||||
@@ -831,12 +724,6 @@ export default function ChatInput({
|
||||
entity.value.toLowerCase().includes(activeQuery),
|
||||
)
|
||||
: contextEntitiesByCategory[activeContextCategory];
|
||||
// Truncate the ref array to match the current entity count so that
|
||||
// switching from a large category (e.g. 100 dashboards) to a smaller one
|
||||
// doesn't leave stale `null` slots from earlier renders. Keyboard nav math
|
||||
// already uses `filteredContextOptions.length` for the modulo, so stale
|
||||
// slots wouldn't be reached — this is purely housekeeping.
|
||||
entityRefs.current.length = filteredContextOptions.length;
|
||||
const { isLoading: isActiveContextLoading, isError: isActiveContextError } =
|
||||
contextCategoryStateByCategory[activeContextCategory];
|
||||
const currentLength = text.length;
|
||||
@@ -943,7 +830,7 @@ export default function ChatInput({
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={disabled}
|
||||
maxLength={MAX_INPUT_LENGTH}
|
||||
rows={3}
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
{showTextWarning && (
|
||||
@@ -990,37 +877,15 @@ export default function ChatInput({
|
||||
sideOffset={8}
|
||||
>
|
||||
<div className={styles.contextPopoverContent}>
|
||||
<div
|
||||
className={styles.contextPopoverCategories}
|
||||
role="tablist"
|
||||
aria-orientation="vertical"
|
||||
aria-label="Context categories"
|
||||
>
|
||||
<div className={styles.contextPopoverCategories}>
|
||||
{CONTEXT_CATEGORIES.map((category) => {
|
||||
const CategoryIcon = CONTEXT_CATEGORY_ICONS[category];
|
||||
const isActive = activeContextCategory === category;
|
||||
return (
|
||||
<Button
|
||||
<div
|
||||
key={category}
|
||||
ref={(el): void => {
|
||||
categoryTabRefs.current.set(category, el);
|
||||
}}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
role="tab"
|
||||
id={`ai-context-tab-${category}`}
|
||||
// Single stable panel id shared by every tab: only the
|
||||
// active category's tabpanel is rendered, so per-category
|
||||
// `aria-controls` ids would point at nonexistent nodes
|
||||
// for the two inactive tabs. APG allows a single
|
||||
// dynamic panel whose `aria-labelledby` swaps to the
|
||||
// active tab.
|
||||
aria-controls="ai-context-tabpanel"
|
||||
// Roving tabindex: only the active tab participates in
|
||||
// the Tab sequence; arrow keys move between tabs.
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
tabIndex={0}
|
||||
aria-selected={isActive}
|
||||
className={cx(styles.contextPopoverCategoryItem, {
|
||||
[styles.active]: isActive,
|
||||
@@ -1029,21 +894,22 @@ export default function ChatInput({
|
||||
setActiveContextCategory(category);
|
||||
setPickerSearchQuery('');
|
||||
}}
|
||||
onKeyDown={(e): void => handleCategoryKeyDown(e, category)}
|
||||
prefix={<CategoryIcon size={13} />}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
setActiveContextCategory(category);
|
||||
setPickerSearchQuery('');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CategoryIcon size={13} />
|
||||
<span>{category}</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles.contextPopoverRight}
|
||||
role="tabpanel"
|
||||
id="ai-context-tabpanel"
|
||||
aria-labelledby={`ai-context-tab-${activeContextCategory}`}
|
||||
>
|
||||
<div className={styles.contextPopoverRight}>
|
||||
<div className={styles.contextPopoverSearch}>
|
||||
<Input
|
||||
type="text"
|
||||
@@ -1073,7 +939,7 @@ export default function ChatInput({
|
||||
No matching entities
|
||||
</div>
|
||||
) : (
|
||||
filteredContextOptions.map((option, index) => {
|
||||
filteredContextOptions.map((option) => {
|
||||
const isSelected = selectedContexts.some(
|
||||
(item) =>
|
||||
item.category === activeContextCategory &&
|
||||
@@ -1081,16 +947,8 @@ export default function ChatInput({
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
<div
|
||||
key={option.id}
|
||||
ref={(el): void => {
|
||||
entityRefs.current[index] = el;
|
||||
}}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
aria-pressed={isSelected}
|
||||
className={cx(styles.contextPopoverEntityItem, {
|
||||
[styles.selected]: isSelected,
|
||||
})}
|
||||
@@ -1101,12 +959,11 @@ export default function ChatInput({
|
||||
option.value,
|
||||
)
|
||||
}
|
||||
onKeyDown={(e): void => handleEntityKeyDown(e, index)}
|
||||
>
|
||||
<span className={styles.contextPopoverEntityItemText}>
|
||||
{option.value}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
@@ -1120,24 +977,14 @@ export default function ChatInput({
|
||||
<div className={styles.rightActions}>
|
||||
{showMic &&
|
||||
(isListening ? (
|
||||
<div
|
||||
className={styles.micRecording}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label="Recording voice input"
|
||||
>
|
||||
<TooltipSimple title="Discard recording">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
className={cx(styles.micDiscard, styles.secondary)}
|
||||
onClick={handleDiscard}
|
||||
aria-label="Discard recording"
|
||||
prefix={<X size={12} />}
|
||||
/>
|
||||
</TooltipSimple>
|
||||
<div className={styles.micRecording}>
|
||||
<div
|
||||
className={cx(styles.micDiscard, styles.secondary)}
|
||||
onClick={handleDiscard}
|
||||
aria-label="Discard recording"
|
||||
>
|
||||
<X size={12} />
|
||||
</div>
|
||||
<span className={styles.micWaves} aria-hidden="true">
|
||||
<span />
|
||||
<span />
|
||||
@@ -1148,30 +995,26 @@ export default function ChatInput({
|
||||
<span />
|
||||
<span />
|
||||
</span>
|
||||
<TooltipSimple title="Stop and send">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="destructive"
|
||||
className={cx(styles.micStop, styles.destructive)}
|
||||
onClick={handleStopAndSend}
|
||||
aria-label="Stop and send"
|
||||
prefix={<Square size={9} fill="currentColor" strokeWidth={0} />}
|
||||
/>
|
||||
</TooltipSimple>
|
||||
<div
|
||||
className={cx(styles.micStop, styles.destructive)}
|
||||
onClick={handleStopAndSend}
|
||||
aria-label="Stop and send"
|
||||
>
|
||||
<Square size={9} fill="currentColor" strokeWidth={0} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<TooltipSimple title="Voice input">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(): void => startVoiceInput(VoiceInputSource.Button)}
|
||||
onClick={(): void => startVoiceInput('button')}
|
||||
disabled={disabled}
|
||||
aria-label="Start voice input"
|
||||
className={styles.micBtn}
|
||||
prefix={<Mic size={14} />}
|
||||
/>
|
||||
>
|
||||
<Mic size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
))}
|
||||
|
||||
@@ -1183,21 +1026,21 @@ export default function ChatInput({
|
||||
color="destructive"
|
||||
onClick={onCancel}
|
||||
aria-label="Stop generating"
|
||||
prefix={<Square size={10} fill="currentColor" strokeWidth={0} />}
|
||||
/>
|
||||
>
|
||||
<Square size={10} fill="currentColor" strokeWidth={0} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
) : (
|
||||
<TooltipSimple title="Send message">
|
||||
<Button
|
||||
variant="solid"
|
||||
size="icon"
|
||||
color="primary"
|
||||
onClick={isListening ? handleStopAndSend : handleSend}
|
||||
disabled={disabled || (!text.trim() && pendingFiles.length === 0)}
|
||||
aria-label="Send message"
|
||||
prefix={<Send size={14} />}
|
||||
/>
|
||||
</TooltipSimple>
|
||||
<Button
|
||||
variant="solid"
|
||||
size="icon"
|
||||
color="primary"
|
||||
onClick={isListening ? handleStopAndSend : handleSend}
|
||||
disabled={disabled || (!text.trim() && pendingFiles.length === 0)}
|
||||
aria-label="Send message"
|
||||
>
|
||||
<Send size={14} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -64,19 +64,6 @@
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
// Mirrors `.field` for the multi_select group, but resets the browser's
|
||||
// default `<fieldset>` border/padding/margin so the visual matches the
|
||||
// `<div>`-based field rows.
|
||||
.multiSelectFieldset {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -63,14 +63,7 @@ export default function ClarificationForm({
|
||||
setAnswers((prev) => ({ ...prev, [id]: value }));
|
||||
};
|
||||
|
||||
const isFormValid = fields.every(
|
||||
(f) => !f.required || isFieldFilled(f, answers[f.id]),
|
||||
);
|
||||
|
||||
const handleSubmit = async (): Promise<void> => {
|
||||
if (!isFormValid) {
|
||||
return;
|
||||
}
|
||||
setSubmitted(true);
|
||||
// Approximate queryLength as the JSON encoding of the form answers — the
|
||||
// clarification API doesn't render a single user-visible string, but the
|
||||
@@ -143,7 +136,7 @@ export default function ClarificationForm({
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={isStreaming || !isFormValid}
|
||||
disabled={isStreaming}
|
||||
prefix={<Send />}
|
||||
>
|
||||
Submit
|
||||
@@ -169,9 +162,8 @@ export default function ClarificationForm({
|
||||
|
||||
/**
|
||||
* Per-type seed value. The DTO's `default` is `string | string[] | null`,
|
||||
* which doesn't fit boolean / number fields cleanly — we coerce 'true'/'false'
|
||||
* strings for booleans, parse number defaults out of the string form,
|
||||
* fall back to `[]` for multi_select, and the raw string otherwise.
|
||||
* which doesn't fit boolean fields cleanly — we coerce 'true'/'false' strings
|
||||
* for them, fall back to `[]` for multi_select, and the raw string otherwise.
|
||||
*/
|
||||
function initialAnswerFor(f: ClarificationFieldEventDTO): unknown {
|
||||
const raw = f.default;
|
||||
@@ -183,41 +175,9 @@ function initialAnswerFor(f: ClarificationFieldEventDTO): unknown {
|
||||
if (f.type === ClarificationFieldTypeDTO.multi_select) {
|
||||
return Array.isArray(raw) ? raw : [];
|
||||
}
|
||||
if (f.type === ClarificationFieldTypeDTO.number) {
|
||||
// Server sends number defaults as strings (e.g. `"5"`). Parse so the
|
||||
// stored value is a real `number` — otherwise `isFieldFilled` (which
|
||||
// requires `typeof === 'number'`) rejects a visibly-filled field and
|
||||
// Submit stays disabled.
|
||||
if (typeof raw !== 'string' || raw === '') {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number(raw);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
return raw ?? '';
|
||||
}
|
||||
|
||||
// Whether a required field has been answered. Booleans are always considered
|
||||
// filled (they're initialised to a concrete true/false). For other types we
|
||||
// reject empty strings, empty arrays, NaN numbers, and `null` (which the
|
||||
// number input emits when its raw value is `''` — `Number('')` would
|
||||
// otherwise silently coerce to `0` and read as a valid answer).
|
||||
function isFieldFilled(
|
||||
field: ClarificationFieldEventDTO,
|
||||
value: unknown,
|
||||
): boolean {
|
||||
switch (field.type) {
|
||||
case ClarificationFieldTypeDTO.multi_select:
|
||||
return Array.isArray(value) && value.length > 0;
|
||||
case ClarificationFieldTypeDTO.boolean:
|
||||
return true;
|
||||
case ClarificationFieldTypeDTO.number:
|
||||
return typeof value === 'number' && !Number.isNaN(value);
|
||||
default:
|
||||
return typeof value === 'string' && value.trim().length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
interface FieldInputProps {
|
||||
field: ClarificationFieldEventDTO;
|
||||
value: unknown;
|
||||
@@ -256,21 +216,13 @@ function FieldInput({ field, value, onChange }: FieldInputProps): JSX.Element {
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label} htmlFor={id}>
|
||||
{label}
|
||||
{required && (
|
||||
<span className={styles.required} aria-hidden="true">
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
{required && <span className={styles.required}>*</span>}
|
||||
</label>
|
||||
<Select
|
||||
value={isCustom ? CUSTOM_OPTION_SENTINEL : String(value ?? '')}
|
||||
onChange={handleSelectChange}
|
||||
>
|
||||
<SelectTrigger
|
||||
id={id}
|
||||
placeholder="Select…"
|
||||
aria-required={required || undefined}
|
||||
/>
|
||||
<SelectTrigger id={id} placeholder="Select…" />
|
||||
{/* Pin the dropdown width to the trigger via Radix's
|
||||
`--radix-select-trigger-width`; otherwise the popover
|
||||
sizes to its widest item and looks misaligned. */}
|
||||
@@ -315,11 +267,7 @@ function FieldInput({ field, value, onChange }: FieldInputProps): JSX.Element {
|
||||
onChange={(): void => onChange(!checked)}
|
||||
>
|
||||
{label}
|
||||
{required && (
|
||||
<span className={styles.required} aria-hidden="true">
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
{required && <span className={styles.required}>*</span>}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
@@ -364,21 +312,11 @@ function FieldInput({ field, value, onChange }: FieldInputProps): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
// `fieldset` + `legend` is the WCAG-recommended grouping for
|
||||
// related checkboxes (1.3.1). SRs announce the legend before each
|
||||
// option, so users hear the group label as context.
|
||||
<fieldset
|
||||
className={styles.multiSelectFieldset}
|
||||
aria-required={required || undefined}
|
||||
>
|
||||
<legend className={styles.label}>
|
||||
<div className={styles.field}>
|
||||
<span className={styles.label}>
|
||||
{label}
|
||||
{required && (
|
||||
<span className={styles.required} aria-hidden="true">
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
</legend>
|
||||
{required && <span className={styles.required}>*</span>}
|
||||
</span>
|
||||
<div className={styles.checkboxGroup}>
|
||||
{options?.map((opt) => (
|
||||
<Checkbox
|
||||
@@ -409,7 +347,7 @@ function FieldInput({ field, value, onChange }: FieldInputProps): JSX.Element {
|
||||
onChange={(e): void => updateCustomValue(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</fieldset>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -418,29 +356,16 @@ function FieldInput({ field, value, onChange }: FieldInputProps): JSX.Element {
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label} htmlFor={id}>
|
||||
{label}
|
||||
{required && (
|
||||
<span className={styles.required} aria-hidden="true">
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
{required && <span className={styles.required}>*</span>}
|
||||
</label>
|
||||
<Input
|
||||
id={id}
|
||||
type={type === 'number' ? 'number' : 'text'}
|
||||
className={styles.input}
|
||||
value={String(value ?? '')}
|
||||
aria-required={required || undefined}
|
||||
onChange={(e): void => {
|
||||
if (type === 'number') {
|
||||
const raw = e.target.value;
|
||||
// Map empty input to `null` instead of `Number('') === 0`
|
||||
// so a required numeric field cleared after typing doesn't
|
||||
// silently read as a valid `0` in `isFieldFilled`.
|
||||
onChange(raw === '' ? null : Number(raw));
|
||||
} else {
|
||||
onChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
onChange={(e): void =>
|
||||
onChange(type === 'number' ? Number(e.target.value) : e.target.value)
|
||||
}
|
||||
placeholder={label}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useTimezone } from 'providers/Timezone';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import { FeedbackRatingDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
|
||||
import { AIAssistantEvents } from '../../events';
|
||||
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
|
||||
@@ -18,22 +17,6 @@ import { FeedbackRating, Message } from '../../types';
|
||||
|
||||
import styles from './MessageFeedback.module.scss';
|
||||
|
||||
const FEEDBACK_ANALYTICS_RATING = {
|
||||
[FeedbackRatingDTO.positive]: 'up',
|
||||
[FeedbackRatingDTO.negative]: 'down',
|
||||
} as const;
|
||||
|
||||
const VOTE_LABEL = {
|
||||
[FeedbackRatingDTO.positive]: {
|
||||
tooltip: 'Good response',
|
||||
ariaLabel: 'Good response',
|
||||
},
|
||||
[FeedbackRatingDTO.negative]: {
|
||||
tooltip: 'Bad response',
|
||||
ariaLabel: 'Bad response',
|
||||
},
|
||||
} as const;
|
||||
|
||||
interface MessageFeedbackProps {
|
||||
message: Message;
|
||||
onRegenerate?: () => void;
|
||||
@@ -134,7 +117,7 @@ export default function MessageFeedback({
|
||||
if (vote === rating) {
|
||||
return;
|
||||
}
|
||||
if (rating === FeedbackRatingDTO.negative) {
|
||||
if (rating === 'negative') {
|
||||
setNegativeComment('');
|
||||
setIsNegativeDialogOpen(true);
|
||||
return;
|
||||
@@ -143,7 +126,7 @@ export default function MessageFeedback({
|
||||
void logEvent(AIAssistantEvents.FeedbackSubmitted, {
|
||||
messageId: message.id,
|
||||
threadId,
|
||||
rating: FEEDBACK_ANALYTICS_RATING[rating],
|
||||
rating: 'up',
|
||||
hasComment: false,
|
||||
commentLength: 0,
|
||||
});
|
||||
@@ -153,21 +136,17 @@ export default function MessageFeedback({
|
||||
);
|
||||
|
||||
const handleSubmitNegative = useCallback((): void => {
|
||||
setVote(FeedbackRatingDTO.negative);
|
||||
setVote('negative');
|
||||
setIsNegativeDialogOpen(false);
|
||||
const trimmed = negativeComment.trim();
|
||||
void logEvent(AIAssistantEvents.FeedbackSubmitted, {
|
||||
messageId: message.id,
|
||||
threadId,
|
||||
rating: FEEDBACK_ANALYTICS_RATING[FeedbackRatingDTO.negative],
|
||||
rating: 'down',
|
||||
hasComment: trimmed.length > 0,
|
||||
commentLength: trimmed.length,
|
||||
});
|
||||
submitMessageFeedback(
|
||||
message.id,
|
||||
FeedbackRatingDTO.negative,
|
||||
trimmed || undefined,
|
||||
);
|
||||
submitMessageFeedback(message.id, 'negative', trimmed || undefined);
|
||||
}, [message.id, negativeComment, submitMessageFeedback, threadId]);
|
||||
|
||||
return (
|
||||
@@ -181,39 +160,32 @@ export default function MessageFeedback({
|
||||
variant="ghost"
|
||||
onClick={handleCopy}
|
||||
color="secondary"
|
||||
aria-label={copied ? 'Copied' : 'Copy message'}
|
||||
>
|
||||
{copied ? <Check size={12} /> : <Copy size={12} />}
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
|
||||
<TooltipSimple title={VOTE_LABEL[FeedbackRatingDTO.positive].tooltip}>
|
||||
<TooltipSimple title="Good response">
|
||||
<Button
|
||||
className={cx(styles.btn, {
|
||||
[styles.votedUp]: vote === FeedbackRatingDTO.positive,
|
||||
})}
|
||||
className={cx(styles.btn, { [styles.votedUp]: vote === 'positive' })}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
onClick={(): void => handleVote(FeedbackRatingDTO.positive)}
|
||||
aria-label={VOTE_LABEL[FeedbackRatingDTO.positive].ariaLabel}
|
||||
aria-pressed={vote === FeedbackRatingDTO.positive}
|
||||
onClick={(): void => handleVote('positive')}
|
||||
>
|
||||
<ThumbsUp size={12} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
|
||||
<TooltipSimple title={VOTE_LABEL[FeedbackRatingDTO.negative].tooltip}>
|
||||
<TooltipSimple title="Bad response">
|
||||
<Button
|
||||
className={cx(styles.btn, {
|
||||
[styles.votedDown]: vote === FeedbackRatingDTO.negative,
|
||||
[styles.votedDown]: vote === 'negative',
|
||||
})}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
onClick={(): void => handleVote(FeedbackRatingDTO.negative)}
|
||||
aria-label={VOTE_LABEL[FeedbackRatingDTO.negative].ariaLabel}
|
||||
aria-pressed={vote === FeedbackRatingDTO.negative}
|
||||
onClick={(): void => handleVote('negative')}
|
||||
>
|
||||
<ThumbsDown size={12} />
|
||||
</Button>
|
||||
@@ -227,7 +199,6 @@ export default function MessageFeedback({
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
onClick={onRegenerate}
|
||||
aria-label="Regenerate response"
|
||||
>
|
||||
<RefreshCw size={12} />
|
||||
</Button>
|
||||
|
||||
@@ -47,7 +47,6 @@ export default function UserMessageActions({
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
onClick={handleCopy}
|
||||
aria-label={copied ? 'Copied' : 'Copy message'}
|
||||
>
|
||||
{copied ? <Check size={12} /> : <Copy size={12} />}
|
||||
</Button>
|
||||
|
||||
@@ -90,16 +90,6 @@ export default function VirtualizedMessages({
|
||||
|
||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||
const scrollerRef = useRef<HTMLElement | Window | null>(null);
|
||||
// Tracks whether the scroller is pinned to (or near) the bottom. Updated
|
||||
// via Virtuoso's `atBottomStateChange` so we can stop force-scrolling the
|
||||
// user back down when they've intentionally scrolled up to read earlier
|
||||
// content.
|
||||
const atBottomRef = useRef(true);
|
||||
// Id of the latest user message we've already anchored to. Used to detect
|
||||
// a fresh user send so we can re-anchor to the bottom regardless of where
|
||||
// the user was scrolled — sending a message and not seeing it is worse
|
||||
// than the anti-yank guarantee.
|
||||
const lastSeenUserMessageIdRef = useRef<string | null>(null);
|
||||
|
||||
const handleRegenerate = useCallback(
|
||||
(messageId: string): void => {
|
||||
@@ -121,25 +111,8 @@ export default function VirtualizedMessages({
|
||||
// align: 'end')` would only reach the last item's bottom and leave the
|
||||
// padding hidden below the fold. Use `auto` while streaming so the bottom
|
||||
// stays glued as text deltas arrive; `smooth` lags when triggered every
|
||||
// few ms. Bail out if the user has scrolled away from the bottom — that's
|
||||
// an explicit signal they want to read earlier content without being
|
||||
// yanked back.
|
||||
// few ms.
|
||||
useEffect(() => {
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
const isFreshUserSend =
|
||||
lastMessage?.role === 'user' &&
|
||||
lastMessage.id !== lastSeenUserMessageIdRef.current;
|
||||
if (isFreshUserSend) {
|
||||
lastSeenUserMessageIdRef.current = lastMessage.id;
|
||||
// Re-anchor so the user sees their own send (and the assistant's
|
||||
// follow-up streaming) even if they were reading history when they
|
||||
// hit Enter.
|
||||
atBottomRef.current = true;
|
||||
}
|
||||
|
||||
if (!atBottomRef.current) {
|
||||
return;
|
||||
}
|
||||
const scroller = scrollerRef.current;
|
||||
if (!(scroller instanceof HTMLElement)) {
|
||||
return;
|
||||
@@ -149,7 +122,7 @@ export default function VirtualizedMessages({
|
||||
behavior: isStreaming ? 'auto' : 'smooth',
|
||||
});
|
||||
}, [
|
||||
messages,
|
||||
messages.length,
|
||||
streamingEvents.length,
|
||||
streamingContentLength,
|
||||
isStreaming,
|
||||
@@ -159,18 +132,14 @@ export default function VirtualizedMessages({
|
||||
|
||||
const followOutput = useCallback(
|
||||
(atBottom: boolean): false | 'auto' | 'smooth' => {
|
||||
if (!atBottom) {
|
||||
return false;
|
||||
if (isStreaming) {
|
||||
return 'auto';
|
||||
}
|
||||
return isStreaming ? 'auto' : 'smooth';
|
||||
return atBottom ? 'smooth' : false;
|
||||
},
|
||||
[isStreaming],
|
||||
);
|
||||
|
||||
const handleAtBottomStateChange = useCallback((atBottom: boolean): void => {
|
||||
atBottomRef.current = atBottom;
|
||||
}, []);
|
||||
|
||||
const showStreamingSlot =
|
||||
isStreaming || Boolean(pendingApproval) || Boolean(pendingClarification);
|
||||
|
||||
@@ -219,8 +188,6 @@ export default function VirtualizedMessages({
|
||||
className={styles.messages}
|
||||
totalCount={totalCount}
|
||||
followOutput={followOutput}
|
||||
atBottomStateChange={handleAtBottomStateChange}
|
||||
atBottomThreshold={64}
|
||||
initialTopMostItemIndex={Math.max(0, totalCount - 1)}
|
||||
itemContent={(index): JSX.Element => {
|
||||
if (index < messages.length) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Check, Copy } from '@signozhq/icons';
|
||||
import SyntaxHighlighter, {
|
||||
a11yDark,
|
||||
@@ -127,17 +126,16 @@ function CopyButton({ text }: { text: string }): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipSimple title={copied ? 'Copied' : 'Copy code'}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
className={styles.copyBtn}
|
||||
onClick={handleCopy}
|
||||
aria-label={copied ? 'Copied' : 'Copy code'}
|
||||
>
|
||||
{copied ? <Check size={12} /> : <Copy size={12} />}
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
className={styles.copyBtn}
|
||||
onClick={handleCopy}
|
||||
title={copied ? 'Copied' : 'Copy code'}
|
||||
aria-label={copied ? 'Copied' : 'Copy code'}
|
||||
>
|
||||
{copied ? <Check size={12} /> : <Copy size={12} />}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,26 +63,6 @@ export const SuggestedPromptCategory = {
|
||||
export type SuggestedPromptCategory =
|
||||
(typeof SuggestedPromptCategory)[keyof typeof SuggestedPromptCategory];
|
||||
|
||||
// `source` attribute on the AI Assistant `Opened` event — describes which
|
||||
// surface triggered the open. Keep values stable: dashboards downstream
|
||||
// depend on the literal strings.
|
||||
export const AIAssistantOpenSource = {
|
||||
Icon: 'icon',
|
||||
Shortcut: 'shortcut',
|
||||
Cmdk: 'cmdk',
|
||||
} as const;
|
||||
export type AIAssistantOpenSource =
|
||||
(typeof AIAssistantOpenSource)[keyof typeof AIAssistantOpenSource];
|
||||
|
||||
// `source` attribute on the `VoiceInputUsed` event — which surface initiated
|
||||
// the recording.
|
||||
export const VoiceInputSource = {
|
||||
Button: 'button',
|
||||
Shortcut: 'shortcut',
|
||||
} as const;
|
||||
export type VoiceInputSource =
|
||||
(typeof VoiceInputSource)[keyof typeof VoiceInputSource];
|
||||
|
||||
export enum AIAssistantEvents {
|
||||
Opened = 'AI Assistant: Opened',
|
||||
MessageSent = 'AI Assistant: Message sent',
|
||||
|
||||
@@ -1,32 +1,9 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
const PARAM_SEGMENT = /:[^/]+/g;
|
||||
const REGEX_SPECIALS = /[.+*?^$()[\]{}|\\]/g;
|
||||
|
||||
function templateToRegex(template: string): RegExp {
|
||||
const pattern = template
|
||||
.replace(REGEX_SPECIALS, '\\$&')
|
||||
.replace(PARAM_SEGMENT, '[^/]+');
|
||||
return new RegExp(`^${pattern}$`);
|
||||
}
|
||||
|
||||
export function getRouteKey(pathname: string): string {
|
||||
const entries = Object.entries(ROUTES);
|
||||
const [routeKey] = Object.entries(ROUTES).find(
|
||||
([, value]) => value === pathname,
|
||||
) || ['DEFAULT'];
|
||||
|
||||
const exact = entries.find(([, value]) => value === pathname);
|
||||
if (exact) {
|
||||
return exact[0];
|
||||
}
|
||||
|
||||
// First template that matches wins, so declaration order in `ROUTES`
|
||||
// matters when templates can overlap. Today's set is unambiguous because
|
||||
// `[^/]+` is segment-bounded, but if you ever add a sibling like
|
||||
// `/services/list` next to `SERVICE_METRICS: '/services/:servicename'`,
|
||||
// list the more-specific (more-static-segments) entry first in `ROUTES`
|
||||
// — otherwise the param template will swallow the static path.
|
||||
const dynamic = entries.find(
|
||||
([, value]) => value.includes(':') && templateToRegex(value).test(pathname),
|
||||
);
|
||||
|
||||
return dynamic?.[0] ?? 'DEFAULT';
|
||||
return routeKey;
|
||||
}
|
||||
|
||||
@@ -364,7 +364,7 @@ func (m *module) buildFilterClause(ctx context.Context, filter *qbtypes.Filter,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if whereClause.IsEmpty() {
|
||||
if whereClause == nil || whereClause.WhereClause == nil {
|
||||
return sqlbuilder.NewWhereClause(), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -964,7 +964,7 @@ func (m *module) buildFilterClause(ctx context.Context, filter *qbtypes.Filter,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if whereClause.IsEmpty() {
|
||||
if whereClause == nil || whereClause.WhereClause == nil {
|
||||
return sqlbuilder.NewWhereClause(), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -510,7 +510,9 @@ func (s *store) buildFilterClause(ctx context.Context, filter qbtypes.Filter, st
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if prepared == nil || prepared.WhereClause == nil {
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
return prepared.WhereClause, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -206,6 +206,7 @@ func (v *exprVisitor) VisitFunctionExpr(fn *chparser.FunctionExpr) error {
|
||||
dataType = telemetrytypes.FieldDataTypeFloat64
|
||||
}
|
||||
|
||||
//
|
||||
bodyJSONEnabled := v.flagger.BooleanOrEmpty(v.ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
|
||||
|
||||
// Handle *If functions with predicate + values
|
||||
@@ -230,8 +231,8 @@ func (v *exprVisitor) VisitFunctionExpr(fn *chparser.FunctionExpr) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// not possible for whereClause to be empty here but still adding a check.
|
||||
if whereClause.IsEmpty() {
|
||||
// not possible for whereClause to be nil here but still adding a check.
|
||||
if whereClause == nil {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid predicate argument for %q: %q", name, origPred)
|
||||
}
|
||||
|
||||
|
||||
@@ -96,12 +96,8 @@ type PreparedWhereClause struct {
|
||||
WarningsDocURL string
|
||||
}
|
||||
|
||||
func (p PreparedWhereClause) IsEmpty() bool {
|
||||
return p.WhereClause == nil
|
||||
}
|
||||
|
||||
// PrepareWhereClause generates a ClickHouse compatible WHERE clause from the filter query.
|
||||
func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (PreparedWhereClause, error) {
|
||||
func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (*PreparedWhereClause, error) {
|
||||
|
||||
// Setup the ANTLR parsing pipeline
|
||||
input := antlr.NewInputStream(query)
|
||||
@@ -152,7 +148,7 @@ func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (PreparedWhere
|
||||
}
|
||||
}
|
||||
|
||||
return PreparedWhereClause{}, combinedErrors.WithAdditional(additionals...).WithUrl(searchTroubleshootingGuideURL)
|
||||
return nil, combinedErrors.WithAdditional(additionals...).WithUrl(searchTroubleshootingGuideURL)
|
||||
}
|
||||
|
||||
// Visit the parse tree with our ClickHouse visitor
|
||||
@@ -170,17 +166,18 @@ func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (PreparedWhere
|
||||
if url == "" {
|
||||
url = searchTroubleshootingGuideURL
|
||||
}
|
||||
return PreparedWhereClause{}, combinedErrors.WithAdditional(visitor.errors...).WithUrl(url)
|
||||
return nil, combinedErrors.WithAdditional(visitor.errors...).WithUrl(url)
|
||||
}
|
||||
|
||||
// Return empty where clause so callers can skip the WHERE clause
|
||||
// Return nil so callers can skip the
|
||||
// entire CTE/subquery rather than emitting WHERE clause that select all the rows
|
||||
if cond == "" || cond == SkipConditionLiteral {
|
||||
return PreparedWhereClause{WhereClause: nil, Warnings: visitor.warnings, WarningsDocURL: visitor.mainWarnURL}, nil
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
whereClause := sqlbuilder.NewWhereClause().AddWhereExpr(visitor.builder.Args, cond)
|
||||
|
||||
return PreparedWhereClause{WhereClause: whereClause, Warnings: visitor.warnings, WarningsDocURL: visitor.mainWarnURL}, nil
|
||||
return &PreparedWhereClause{WhereClause: whereClause, Warnings: visitor.warnings, WarningsDocURL: visitor.mainWarnURL}, nil
|
||||
}
|
||||
|
||||
// Visit dispatches to the specific visit method based on node type.
|
||||
|
||||
@@ -874,7 +874,7 @@ func TestVisitComparison_AND(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if !result.IsEmpty() {
|
||||
if result != nil {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr)
|
||||
@@ -883,7 +883,7 @@ func TestVisitComparison_AND(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if !result.IsEmpty() {
|
||||
if result != nil {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr)
|
||||
@@ -968,7 +968,7 @@ func TestVisitComparison_NOT(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if !result.IsEmpty() {
|
||||
if result != nil {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr)
|
||||
@@ -977,7 +977,7 @@ func TestVisitComparison_NOT(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if !result.IsEmpty() {
|
||||
if result != nil {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr)
|
||||
@@ -1070,7 +1070,7 @@ func TestVisitComparison_OR(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if !result.IsEmpty() {
|
||||
if result != nil {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr)
|
||||
@@ -1079,7 +1079,7 @@ func TestVisitComparison_OR(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if !result.IsEmpty() {
|
||||
if result != nil {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr)
|
||||
@@ -1151,7 +1151,7 @@ func TestVisitComparison_Precedence(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if !result.IsEmpty() {
|
||||
if result != nil {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr)
|
||||
@@ -1160,7 +1160,7 @@ func TestVisitComparison_Precedence(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if !result.IsEmpty() {
|
||||
if result != nil {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr)
|
||||
@@ -1254,7 +1254,7 @@ func TestVisitComparison_Parens(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if !result.IsEmpty() {
|
||||
if result != nil {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr)
|
||||
@@ -1263,7 +1263,7 @@ func TestVisitComparison_Parens(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if !result.IsEmpty() {
|
||||
if result != nil {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr)
|
||||
@@ -1409,7 +1409,7 @@ func TestVisitComparison_FullText(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if !result.IsEmpty() {
|
||||
if result != nil {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr)
|
||||
@@ -1418,7 +1418,7 @@ func TestVisitComparison_FullText(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if !result.IsEmpty() {
|
||||
if result != nil {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr)
|
||||
@@ -1518,7 +1518,7 @@ func TestVisitComparison_AllVariable(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if !result.IsEmpty() {
|
||||
if result != nil {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr)
|
||||
@@ -1527,7 +1527,7 @@ func TestVisitComparison_AllVariable(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if !result.IsEmpty() {
|
||||
if result != nil {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr)
|
||||
@@ -1597,7 +1597,7 @@ func TestVisitComparison_FunctionCalls(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if !result.IsEmpty() {
|
||||
if result != nil {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr)
|
||||
@@ -1606,7 +1606,7 @@ func TestVisitComparison_FunctionCalls(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if !result.IsEmpty() {
|
||||
if result != nil {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr)
|
||||
@@ -1666,7 +1666,7 @@ func TestVisitComparison_UnknownKeys(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if !result.IsEmpty() {
|
||||
if result != nil {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr)
|
||||
@@ -1675,7 +1675,7 @@ func TestVisitComparison_UnknownKeys(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if !result.IsEmpty() {
|
||||
if result != nil {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr)
|
||||
@@ -1758,7 +1758,7 @@ func TestVisitComparison_SkippableLiteralValues(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrRSB, err != nil, "resourceConditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if !result.IsEmpty() {
|
||||
if result != nil {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantRSB, expr, "resourceConditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantRSB, expr)
|
||||
@@ -1767,7 +1767,7 @@ func TestVisitComparison_SkippableLiteralValues(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErrSB, err != nil, "conditionBuilder: error expectation mismatch")
|
||||
if err == nil {
|
||||
var expr string
|
||||
if !result.IsEmpty() {
|
||||
if result != nil {
|
||||
expr, _ = result.WhereClause.Build()
|
||||
}
|
||||
assert.Equal(t, tt.wantSB, expr, "conditionBuilder SQL mismatch:\n want: %s\n got: %s", tt.wantSB, expr)
|
||||
|
||||
@@ -282,10 +282,12 @@ func (b *auditQueryStatementBuilder) buildListQuery(
|
||||
finalArgs := querybuilder.PrependArgs(cteArgs, mainArgs)
|
||||
|
||||
stmt := &qbtypes.Statement{
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
}
|
||||
if preparedWhereClause != nil {
|
||||
stmt.Warnings = preparedWhereClause.Warnings
|
||||
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -420,10 +422,12 @@ func (b *auditQueryStatementBuilder) buildTimeSeriesQuery(
|
||||
}
|
||||
|
||||
stmt := &qbtypes.Statement{
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
}
|
||||
if preparedWhereClause != nil {
|
||||
stmt.Warnings = preparedWhereClause.Warnings
|
||||
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -522,10 +526,12 @@ func (b *auditQueryStatementBuilder) buildScalarQuery(
|
||||
finalArgs := querybuilder.PrependArgs(cteArgs, mainArgs)
|
||||
|
||||
stmt := &qbtypes.Statement{
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
}
|
||||
if preparedWhereClause != nil {
|
||||
stmt.Warnings = preparedWhereClause.Warnings
|
||||
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -538,8 +544,8 @@ func (b *auditQueryStatementBuilder) addFilterCondition(
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation],
|
||||
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (querybuilder.PreparedWhereClause, error) {
|
||||
var preparedWhereClause querybuilder.PreparedWhereClause
|
||||
) (*querybuilder.PreparedWhereClause, error) {
|
||||
var preparedWhereClause *querybuilder.PreparedWhereClause
|
||||
var err error
|
||||
|
||||
if query.Filter != nil && query.Filter.Expression != "" {
|
||||
@@ -558,11 +564,11 @@ func (b *auditQueryStatementBuilder) addFilterCondition(
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return preparedWhereClause, err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if !preparedWhereClause.IsEmpty() {
|
||||
if preparedWhereClause != nil {
|
||||
sb.AddWhereClause(preparedWhereClause.WhereClause)
|
||||
}
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ func TestFilterExprLogsBodyJSON(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
if clause.IsEmpty() {
|
||||
if clause == nil {
|
||||
t.Errorf("Expected clause for query: %s\n", tc.query)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2403,7 +2403,7 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
if clause.IsEmpty() {
|
||||
if clause == nil {
|
||||
t.Errorf("Expected clause for query: %s\n", tc.query)
|
||||
return
|
||||
}
|
||||
@@ -2524,7 +2524,7 @@ func TestFilterExprLogsConflictNegation(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
if clause.IsEmpty() {
|
||||
if clause == nil {
|
||||
t.Errorf("Expected clause for query: %s\n", tc.query)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -348,10 +348,12 @@ func (b *logQueryStatementBuilder) buildListQuery(
|
||||
finalArgs := querybuilder.PrependArgs(cteArgs, mainArgs)
|
||||
|
||||
stmt := &qbtypes.Statement{
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
}
|
||||
if preparedWhereClause != nil {
|
||||
stmt.Warnings = preparedWhereClause.Warnings
|
||||
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -504,10 +506,12 @@ func (b *logQueryStatementBuilder) buildTimeSeriesQuery(
|
||||
}
|
||||
|
||||
stmt := &qbtypes.Statement{
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
}
|
||||
if preparedWhereClause != nil {
|
||||
stmt.Warnings = preparedWhereClause.Warnings
|
||||
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -623,10 +627,12 @@ func (b *logQueryStatementBuilder) buildScalarQuery(
|
||||
finalArgs := querybuilder.PrependArgs(cteArgs, mainArgs)
|
||||
|
||||
stmt := &qbtypes.Statement{
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
}
|
||||
if preparedWhereClause != nil {
|
||||
stmt.Warnings = preparedWhereClause.Warnings
|
||||
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -640,9 +646,9 @@ func (b *logQueryStatementBuilder) addFilterCondition(
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation],
|
||||
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (querybuilder.PreparedWhereClause, error) {
|
||||
) (*querybuilder.PreparedWhereClause, error) {
|
||||
|
||||
var preparedWhereClause querybuilder.PreparedWhereClause
|
||||
var preparedWhereClause *querybuilder.PreparedWhereClause
|
||||
var err error
|
||||
// TODO(Tushar): thread orgID here to evaluate correctly
|
||||
bodyJSONEnabled := b.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
|
||||
@@ -665,11 +671,11 @@ func (b *logQueryStatementBuilder) addFilterCondition(
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return preparedWhereClause, err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if !preparedWhereClause.IsEmpty() {
|
||||
if preparedWhereClause != nil {
|
||||
sb.AddWhereClause(preparedWhereClause.WhereClause)
|
||||
}
|
||||
|
||||
|
||||
@@ -237,10 +237,8 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
name string
|
||||
requestType qbtypes.RequestType
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
|
||||
variables map[string]qbtypes.VariableItem
|
||||
expected qbtypes.Statement
|
||||
expectedErr error
|
||||
expectWarn bool
|
||||
}{
|
||||
{
|
||||
name: "default list",
|
||||
@@ -314,22 +312,6 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "filter skips entirely but emits LIKE-without-wildcards warning",
|
||||
requestType: qbtypes.RequestTypeRaw,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "message LIKE 'plain' OR message IN $env",
|
||||
},
|
||||
Limit: 10,
|
||||
},
|
||||
variables: map[string]qbtypes.VariableItem{
|
||||
"env": {Type: qbtypes.DynamicVariableType, Value: "__all__"},
|
||||
},
|
||||
expectedErr: nil,
|
||||
expectWarn: true,
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -358,20 +340,16 @@ func TestStatementBuilderListQuery(t *testing.T) {
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
|
||||
q, err := statementBuilder.Build(ctx, 1747947419000, 1747983448000, c.requestType, c.query, c.variables)
|
||||
q, err := statementBuilder.Build(ctx, 1747947419000, 1747983448000, c.requestType, c.query, nil)
|
||||
|
||||
if c.expectedErr != nil {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), c.expectedErr.Error())
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
if c.expectWarn {
|
||||
require.NotEmpty(t, q.Warnings)
|
||||
} else {
|
||||
require.Equal(t, c.expected.Query, q.Query)
|
||||
require.Equal(t, c.expected.Args, q.Args)
|
||||
require.Equal(t, c.expected.Warnings, q.Warnings)
|
||||
}
|
||||
require.Equal(t, c.expected.Query, q.Query)
|
||||
require.Equal(t, c.expected.Args, q.Args)
|
||||
require.Equal(t, c.expected.Warnings, q.Warnings)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1424,7 +1424,7 @@ func (t *telemetryMetaStore) getRelatedValues(ctx context.Context, fieldValueSel
|
||||
if err != nil {
|
||||
t.logger.WarnContext(ctx, "error parsing existing query for related values", errors.Attr(err))
|
||||
}
|
||||
if !whereClause.IsEmpty() {
|
||||
if whereClause != nil {
|
||||
sb.AddWhereClause(whereClause.WhereClause)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ func (b *meterQueryStatementBuilder) buildTemporalAggDeltaFastPath(
|
||||
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (string, []any, error) {
|
||||
var filterWhere querybuilder.PreparedWhereClause
|
||||
var filterWhere *querybuilder.PreparedWhereClause
|
||||
var err error
|
||||
stepSec := int64(query.StepInterval.Seconds())
|
||||
|
||||
@@ -161,7 +161,7 @@ func (b *meterQueryStatementBuilder) buildTemporalAggDeltaFastPath(
|
||||
return "", nil, err
|
||||
}
|
||||
}
|
||||
if !filterWhere.IsEmpty() {
|
||||
if filterWhere != nil {
|
||||
sb.AddWhereClause(filterWhere.WhereClause)
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ func (b *meterQueryStatementBuilder) buildTemporalAggDelta(
|
||||
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (string, []any, error) {
|
||||
var filterWhere querybuilder.PreparedWhereClause
|
||||
var filterWhere *querybuilder.PreparedWhereClause
|
||||
var err error
|
||||
|
||||
stepSec := int64(query.StepInterval.Seconds())
|
||||
@@ -250,7 +250,7 @@ func (b *meterQueryStatementBuilder) buildTemporalAggDelta(
|
||||
return "", nil, err
|
||||
}
|
||||
}
|
||||
if !filterWhere.IsEmpty() {
|
||||
if filterWhere != nil {
|
||||
sb.AddWhereClause(filterWhere.WhereClause)
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ func (b *meterQueryStatementBuilder) buildTemporalAggCumulativeOrUnspecified(
|
||||
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (string, []any, error) {
|
||||
var filterWhere querybuilder.PreparedWhereClause
|
||||
var filterWhere *querybuilder.PreparedWhereClause
|
||||
var err error
|
||||
stepSec := int64(query.StepInterval.Seconds())
|
||||
|
||||
@@ -320,7 +320,7 @@ func (b *meterQueryStatementBuilder) buildTemporalAggCumulativeOrUnspecified(
|
||||
return "", nil, err
|
||||
}
|
||||
}
|
||||
if !filterWhere.IsEmpty() {
|
||||
if filterWhere != nil {
|
||||
baseSb.AddWhereClause(filterWhere.WhereClause)
|
||||
}
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@ func (b *MetricQueryStatementBuilder) buildTimeSeriesCTE(
|
||||
) (string, []any, error) {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
|
||||
var preparedWhereClause querybuilder.PreparedWhereClause
|
||||
var preparedWhereClause *querybuilder.PreparedWhereClause
|
||||
var err error
|
||||
|
||||
if query.Filter != nil && query.Filter.Expression != "" {
|
||||
@@ -311,7 +311,7 @@ func (b *MetricQueryStatementBuilder) buildTimeSeriesCTE(
|
||||
sb.EQ("__normalized", false),
|
||||
)
|
||||
|
||||
if !preparedWhereClause.IsEmpty() {
|
||||
if preparedWhereClause != nil {
|
||||
sb.AddWhereClause(preparedWhereClause.WhereClause)
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ func (b *resourceFilterStatementBuilder[T]) addConditions(
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if filterWhereClause.IsEmpty() {
|
||||
if filterWhereClause == nil {
|
||||
// this means all conditions evaluated to no-op (non-resource fields, unknown keys, skipped full-text/functions)
|
||||
// the CTE would select all fingerprints, so skip it entirely
|
||||
return true, nil
|
||||
|
||||
@@ -353,10 +353,12 @@ func (b *traceQueryStatementBuilder) buildListQuery(
|
||||
finalArgs := querybuilder.PrependArgs(cteArgs, mainArgs)
|
||||
|
||||
stmt := &qbtypes.Statement{
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
}
|
||||
if preparedWhereClause != nil {
|
||||
stmt.Warnings = preparedWhereClause.Warnings
|
||||
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -469,10 +471,12 @@ func (b *traceQueryStatementBuilder) buildTraceQuery(
|
||||
finalArgs := querybuilder.PrependArgs(cteArgs, mainArgs)
|
||||
|
||||
stmt := &qbtypes.Statement{
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
}
|
||||
if preparedWhereClause != nil {
|
||||
stmt.Warnings = preparedWhereClause.Warnings
|
||||
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -618,10 +622,12 @@ func (b *traceQueryStatementBuilder) buildTimeSeriesQuery(
|
||||
}
|
||||
|
||||
stmt := &qbtypes.Statement{
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
}
|
||||
if preparedWhereClause != nil {
|
||||
stmt.Warnings = preparedWhereClause.Warnings
|
||||
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -734,10 +740,12 @@ func (b *traceQueryStatementBuilder) buildScalarQuery(
|
||||
finalArgs := querybuilder.PrependArgs(cteArgs, mainArgs)
|
||||
|
||||
stmt := &qbtypes.Statement{
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
Warnings: preparedWhereClause.Warnings,
|
||||
WarningsDocURL: preparedWhereClause.WarningsDocURL,
|
||||
Query: finalSQL,
|
||||
Args: finalArgs,
|
||||
}
|
||||
if preparedWhereClause != nil {
|
||||
stmt.Warnings = preparedWhereClause.Warnings
|
||||
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
@@ -751,9 +759,9 @@ func (b *traceQueryStatementBuilder) addFilterCondition(
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation],
|
||||
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (querybuilder.PreparedWhereClause, error) {
|
||||
) (*querybuilder.PreparedWhereClause, error) {
|
||||
|
||||
var preparedWhereClause querybuilder.PreparedWhereClause
|
||||
var preparedWhereClause *querybuilder.PreparedWhereClause
|
||||
var err error
|
||||
|
||||
if query.Filter != nil && query.Filter.Expression != "" {
|
||||
@@ -771,11 +779,11 @@ func (b *traceQueryStatementBuilder) addFilterCondition(
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return preparedWhereClause, err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if !preparedWhereClause.IsEmpty() {
|
||||
if preparedWhereClause != nil {
|
||||
sb.AddWhereClause(preparedWhereClause.WhereClause)
|
||||
}
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@ func (b *traceOperatorCTEBuilder) buildQueryCTE(ctx context.Context, queryName s
|
||||
b.stmtBuilder.logger.ErrorContext(ctx, "Failed to prepare where clause", errors.Attr(err), slog.String("filter", query.Filter.Expression))
|
||||
return "", err
|
||||
}
|
||||
if !filterWhereClause.IsEmpty() {
|
||||
if filterWhereClause != nil {
|
||||
b.stmtBuilder.logger.DebugContext(ctx, "Adding where clause", slog.Any("where_clause", filterWhereClause.WhereClause))
|
||||
sb.AddWhereClause(filterWhereClause.WhereClause)
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user