mirror of
https://github.com/SigNoz/signoz.git
synced 2026-11-13 03:41:20 +00:00
Compare commits
5 Commits
fix/safe-n
...
feat/noz-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8ef6d478a | ||
|
|
4770657167 | ||
|
|
b5a8e7fb0a | ||
|
|
5a9bbc0e64 | ||
|
|
09784eec6c |
@@ -11,8 +11,6 @@ import axios, { AxiosError, AxiosRequestConfig } from 'axios';
|
||||
// generated API Instance
|
||||
const generatedAPIAxiosInstance = axios.create({
|
||||
baseURL: ENVIRONMENT.baseURL,
|
||||
// The backend binds repeated keys (`states=a&states=b`); axios' default sends `states[]=`.
|
||||
paramsSerializer: { indexes: null },
|
||||
});
|
||||
|
||||
let generatedAPIQueryKeyHeaderContext: Record<string, unknown> | undefined;
|
||||
|
||||
@@ -1,37 +1,5 @@
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
border-left: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.resizeHandle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
z-index: 10;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 1px;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
background: var(--accent-primary);
|
||||
}
|
||||
.panel:global(.floating-panel) {
|
||||
box-shadow: var(--shadow-dialog);
|
||||
}
|
||||
|
||||
.header {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useLayoutEffect, useState } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { matchRoute } from 'lib/router/matchRoute';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
@@ -7,6 +7,8 @@ import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { History, Maximize2, Plus, X } from '@signozhq/icons';
|
||||
import Noz from 'components/Noz/Noz';
|
||||
import { BOTTOM_STRIP_HEIGHT } from 'container/BottomStrip';
|
||||
import { FloatingPanel } from 'periscope/components/FloatingPanel';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
@@ -22,6 +24,22 @@ import styles from './AIAssistantPanel.module.scss';
|
||||
const AI_ASSISTANT_PANEL_OPEN_CLASS = 'ai-assistant-panel-open';
|
||||
const AI_ASSISTANT_PANEL_WIDTH_VAR = '--ai-assistant-panel-width';
|
||||
|
||||
const PANEL_WIDTH = 420;
|
||||
const PANEL_HEIGHT = 601;
|
||||
const PANEL_MIN_WIDTH = 380;
|
||||
const PANEL_MIN_HEIGHT = 500;
|
||||
const PANEL_MARGIN = 8;
|
||||
const PANEL_RESIZE_EDGES = {
|
||||
top: true,
|
||||
right: true,
|
||||
bottom: true,
|
||||
left: true,
|
||||
topRight: false,
|
||||
bottomRight: false,
|
||||
bottomLeft: false,
|
||||
topLeft: false,
|
||||
};
|
||||
|
||||
export default function AIAssistantPanel(): JSX.Element | null {
|
||||
const { pathname } = useAppLocation();
|
||||
const [showHistory, setShowHistory] = useState(false);
|
||||
@@ -72,11 +90,6 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
setShowHistory(false);
|
||||
}, []);
|
||||
|
||||
// ── Resize logic ──────────────────────────────────────────────────────────
|
||||
const [panelWidth, setPanelWidth] = useState(380);
|
||||
const dragStartX = useRef(0);
|
||||
const dragStartWidth = useRef(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const shouldOffsetChatSupport = isOpen && !isFullScreenPage;
|
||||
|
||||
@@ -88,7 +101,7 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
if (shouldOffsetChatSupport) {
|
||||
document.body.style.setProperty(
|
||||
AI_ASSISTANT_PANEL_WIDTH_VAR,
|
||||
`${panelWidth}px`,
|
||||
`${PANEL_WIDTH}px`,
|
||||
);
|
||||
} else {
|
||||
document.body.style.removeProperty(AI_ASSISTANT_PANEL_WIDTH_VAR);
|
||||
@@ -98,46 +111,40 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
document.body.classList.remove(AI_ASSISTANT_PANEL_OPEN_CLASS);
|
||||
document.body.style.removeProperty(AI_ASSISTANT_PANEL_WIDTH_VAR);
|
||||
};
|
||||
}, [isFullScreenPage, isOpen, panelWidth]);
|
||||
|
||||
const handleResizeMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
dragStartX.current = e.clientX;
|
||||
dragStartWidth.current = panelWidth;
|
||||
|
||||
const onMouseMove = (ev: MouseEvent): void => {
|
||||
// Panel is on the right; dragging left (lower clientX) increases width
|
||||
const delta = dragStartX.current - ev.clientX;
|
||||
const next = Math.min(Math.max(dragStartWidth.current + delta, 380), 800);
|
||||
setPanelWidth(next);
|
||||
};
|
||||
|
||||
const onMouseUp = (): void => {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
|
||||
document.body.style.cursor = 'col-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
},
|
||||
[panelWidth],
|
||||
);
|
||||
}, [isFullScreenPage, isOpen]);
|
||||
|
||||
if (!isOpen || isFullScreenPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const panelHeight = Math.max(
|
||||
PANEL_MIN_HEIGHT,
|
||||
Math.min(
|
||||
PANEL_HEIGHT,
|
||||
window.innerHeight - BOTTOM_STRIP_HEIGHT - 2 * PANEL_MARGIN,
|
||||
),
|
||||
);
|
||||
|
||||
return (
|
||||
<VariantContext.Provider value="panel">
|
||||
<div className={styles.panel} style={{ width: panelWidth }}>
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||
<div className={styles.resizeHandle} onMouseDown={handleResizeMouseDown} />
|
||||
<div className={styles.header}>
|
||||
<FloatingPanel
|
||||
isOpen
|
||||
className={styles.panel}
|
||||
width={PANEL_WIDTH}
|
||||
height={panelHeight}
|
||||
minWidth={PANEL_MIN_WIDTH}
|
||||
minHeight={PANEL_MIN_HEIGHT}
|
||||
enableResizing={PANEL_RESIZE_EDGES}
|
||||
defaultPosition={{
|
||||
x: window.innerWidth - PANEL_WIDTH - PANEL_MARGIN,
|
||||
// Never above the viewport, where the header could not be dragged back.
|
||||
y: Math.max(
|
||||
0,
|
||||
window.innerHeight - BOTTOM_STRIP_HEIGHT - PANEL_MARGIN - panelHeight,
|
||||
),
|
||||
}}
|
||||
>
|
||||
<div className={`${styles.header} floating-panel__drag-handle`}>
|
||||
<div className={`${styles.title} noz-wave`}>
|
||||
<Noz size={18} />
|
||||
<span>Noz</span>
|
||||
@@ -207,7 +214,7 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
<ConversationView conversationId={activeConversationId} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</FloatingPanel>
|
||||
</VariantContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
border-top: 1px solid var(--l1-border);
|
||||
|
||||
&.compact {
|
||||
padding: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,24 +3,32 @@
|
||||
.input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
background: var(--l1-background);
|
||||
border-radius: var(--radius-2);
|
||||
padding: 8px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: var(--radius-3);
|
||||
box-shadow: var(--shadow-dialog);
|
||||
position: relative;
|
||||
|
||||
// The panel already frames it.
|
||||
&.compact {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.attachments {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 8px 10px 0;
|
||||
}
|
||||
|
||||
.contextTags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 8px 10px 0;
|
||||
}
|
||||
|
||||
.contextTag {
|
||||
@@ -91,72 +99,53 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.composer {
|
||||
background: var(--l2-background);
|
||||
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%);
|
||||
}
|
||||
padding: 9px 16px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
padding: 4px 10px 8px;
|
||||
}
|
||||
|
||||
.leftActions,
|
||||
.rightActions {
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
--button-height: 28px;
|
||||
--button-width: 28px;
|
||||
--button-max-width: 28px;
|
||||
--button-border-radius: 9999px;
|
||||
// Send rests grey and turns primary on hover.
|
||||
--button-primary-background-color: var(--l3-background);
|
||||
--button-primary-hover-background-color: var(--primary-background);
|
||||
--button-primary-solid-foreground: var(--l2-foreground);
|
||||
|
||||
.attachBtn {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
.contextBtn {
|
||||
flex-shrink: 0;
|
||||
gap: 6px;
|
||||
border-style: dashed !important;
|
||||
padding-inline: 12px !important;
|
||||
[data-color='primary']:hover:not([aria-disabled='true']) {
|
||||
--button-primary-solid-foreground: var(--primary-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
color: var(--l1-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.005em;
|
||||
overflow-y: auto;
|
||||
font-family: inherit;
|
||||
@include scrollbar(0.2rem);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
color: var(--l2-foreground);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@@ -170,7 +159,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
margin: 0 10px;
|
||||
padding: 6px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
@@ -180,24 +169,9 @@
|
||||
border-radius: var(--radius-2);
|
||||
}
|
||||
|
||||
.sendBtn {
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--radius-2);
|
||||
|
||||
&.stop {
|
||||
background: var(--accent-cherry) !important;
|
||||
border-color: var(--accent-cherry) !important;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contextPopover {
|
||||
width: 480px !important;
|
||||
max-width: min(92vw, 480px);
|
||||
margin-left: 16px;
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: var(--radius-2);
|
||||
background: var(--l1-background);
|
||||
@@ -342,9 +316,8 @@
|
||||
.micRecording {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--l2-background);
|
||||
background: var(--l3-background);
|
||||
border-radius: 999px;
|
||||
padding: 2px;
|
||||
gap: 0;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -46,12 +46,13 @@ import {
|
||||
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useSpeechRecognition } from '../../hooks/useSpeechRecognition';
|
||||
import { MessageAttachment } from '../../types';
|
||||
import { useVariant } from '../../VariantContext';
|
||||
import { MessageContext } from '../../../../api/ai-assistant/chat';
|
||||
import {
|
||||
ArrowUp,
|
||||
AtSign,
|
||||
Mic,
|
||||
Plus,
|
||||
Search,
|
||||
Send,
|
||||
Square,
|
||||
TriangleAlert,
|
||||
X,
|
||||
@@ -249,6 +250,7 @@ export default function ChatInput({
|
||||
const { selectedTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const isCompact = useVariant() === 'panel';
|
||||
const [text, setText] = useState('');
|
||||
const [pendingFiles, setPendingFiles] = useState<UploadFile[]>([]);
|
||||
const [selectedContexts, setSelectedContexts] = useState<
|
||||
@@ -900,7 +902,10 @@ export default function ChatInput({
|
||||
const showTextWarning = currentLength >= WARNING_THRESHOLD;
|
||||
|
||||
return (
|
||||
<div className={styles.input} ref={inputRootRef}>
|
||||
<div
|
||||
className={cx(styles.input, { [styles.compact]: isCompact })}
|
||||
ref={inputRootRef}
|
||||
>
|
||||
{pendingFiles.length > 0 && (
|
||||
<div className={styles.attachments}>
|
||||
{pendingFiles.map((f) => (
|
||||
@@ -983,7 +988,7 @@ export default function ChatInput({
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className={styles.textarea}
|
||||
placeholder="Ask anything… (Shift+Enter for new line)"
|
||||
placeholder="Ask Noz…"
|
||||
value={text}
|
||||
onChange={(e): void => {
|
||||
const next = capText(e.target.value);
|
||||
@@ -995,7 +1000,7 @@ export default function ChatInput({
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={disabled}
|
||||
maxLength={MAX_INPUT_LENGTH}
|
||||
rows={3}
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
{showTextWarning && (
|
||||
@@ -1009,7 +1014,69 @@ export default function ChatInput({
|
||||
)}
|
||||
|
||||
<div className={styles.footer}>
|
||||
<div className={styles.leftActions}>
|
||||
<div className={styles.actions}>
|
||||
{showMic &&
|
||||
(isListening ? (
|
||||
<div
|
||||
className={styles.micRecording}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label="Recording voice input"
|
||||
>
|
||||
<Tooltip title="Discard recording">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon
|
||||
color="secondary"
|
||||
onClick={handleDiscard}
|
||||
aria-label="Discard recording"
|
||||
>
|
||||
<X size={12} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<span className={styles.micWaves} aria-hidden="true">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</span>
|
||||
<Tooltip title="Stop and send">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon
|
||||
color="secondary"
|
||||
onClick={handleStopAndSend}
|
||||
aria-label="Stop and send"
|
||||
>
|
||||
<Square size={9} fill="currentColor" strokeWidth={0} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : (
|
||||
<Tooltip title="Voice input">
|
||||
<Button
|
||||
disabledTooltip={disabledTooltip}
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon
|
||||
onClick={(): void => startVoiceInput(VoiceInputSource.Button)}
|
||||
disabled={disabled}
|
||||
aria-label="Start voice input"
|
||||
>
|
||||
<Mic size={14} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
))}
|
||||
|
||||
<Popover
|
||||
open={isContextPickerOpen}
|
||||
onOpenChange={(open): void => {
|
||||
@@ -1020,22 +1087,25 @@ export default function ChatInput({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
disabledTooltip={disabledTooltip}
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="md"
|
||||
disabled={disabled}
|
||||
onClick={(): void => {
|
||||
setActiveContextCategory('Dashboards');
|
||||
setPickerSearchQuery('');
|
||||
}}
|
||||
prefix={<Plus size={10} />}
|
||||
>
|
||||
Add Context
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<Tooltip title="Add context">
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
disabledTooltip={disabledTooltip}
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
icon
|
||||
disabled={disabled}
|
||||
onClick={(): void => {
|
||||
setActiveContextCategory('Dashboards');
|
||||
setPickerSearchQuery('');
|
||||
}}
|
||||
aria-label="Add context"
|
||||
>
|
||||
<AtSign size={14} />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
</Tooltip>
|
||||
<PopoverContent
|
||||
className={styles.contextPopover}
|
||||
side="top"
|
||||
@@ -1164,82 +1234,18 @@ export default function ChatInput({
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className={styles.rightActions}>
|
||||
{showMic &&
|
||||
(isListening ? (
|
||||
<div
|
||||
className={styles.micRecording}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label="Recording voice input"
|
||||
>
|
||||
<Tooltip title="Discard recording">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="md"
|
||||
icon
|
||||
color="secondary"
|
||||
onClick={handleDiscard}
|
||||
aria-label="Discard recording"
|
||||
>
|
||||
<X size={12} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<span className={styles.micWaves} aria-hidden="true">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</span>
|
||||
<Tooltip title="Stop and send">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="md"
|
||||
icon
|
||||
color="secondary"
|
||||
onClick={handleStopAndSend}
|
||||
aria-label="Stop and send"
|
||||
>
|
||||
<Square size={9} fill="currentColor" strokeWidth={0} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : (
|
||||
<Tooltip title="Voice input">
|
||||
<Button
|
||||
disabledTooltip={disabledTooltip}
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
size="md"
|
||||
icon
|
||||
onClick={(): void => startVoiceInput(VoiceInputSource.Button)}
|
||||
disabled={disabled}
|
||||
aria-label="Start voice input"
|
||||
>
|
||||
<Mic size={14} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
))}
|
||||
|
||||
{isStreaming && onCancel ? (
|
||||
<Tooltip title="Stop generating">
|
||||
<Button
|
||||
variant="solid"
|
||||
size="md"
|
||||
size="sm"
|
||||
icon
|
||||
color="danger"
|
||||
onClick={onCancel}
|
||||
aria-label="Stop generating"
|
||||
>
|
||||
<Square size={10} fill="currentColor" strokeWidth={0} />
|
||||
<Square size={14} fill="currentColor" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : (
|
||||
@@ -1247,14 +1253,14 @@ export default function ChatInput({
|
||||
<Button
|
||||
disabledTooltip={disabled ? disabledTooltip : 'Type a message first'}
|
||||
variant="solid"
|
||||
size="md"
|
||||
size="sm"
|
||||
icon
|
||||
color="primary"
|
||||
onClick={isListening ? handleStopAndSend : handleSend}
|
||||
disabled={disabled || (!text.trim() && pendingFiles.length === 0)}
|
||||
aria-label="Send message"
|
||||
>
|
||||
<Send size={14} />
|
||||
<ArrowUp size={14} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { render, screen, userEvent } from 'tests/test-utils';
|
||||
import ChatInput from '../ChatInput';
|
||||
|
||||
function getComposer(): HTMLTextAreaElement {
|
||||
return screen.getByPlaceholderText(/Ask anything/i) as HTMLTextAreaElement;
|
||||
return screen.getByPlaceholderText(/Ask Noz/i) as HTMLTextAreaElement;
|
||||
}
|
||||
|
||||
describe('ChatInput — @ mention trigger', () => {
|
||||
|
||||
@@ -58,7 +58,7 @@ function renderChatInput(): void {
|
||||
}
|
||||
|
||||
function getComposer(): HTMLTextAreaElement {
|
||||
return screen.getByPlaceholderText(/Ask anything/i) as HTMLTextAreaElement;
|
||||
return screen.getByPlaceholderText(/Ask Noz/i) as HTMLTextAreaElement;
|
||||
}
|
||||
|
||||
describe('ChatInput — empty-state CTA prefill flow', () => {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useListRulesV3 } from 'api/generated/services/rules';
|
||||
import {
|
||||
type ListRulesV3200,
|
||||
type ListRulesV3Params,
|
||||
RuletypesAlertStateDTO,
|
||||
type RuletypesListableRuleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { GeneratedAPIInstance } from 'api/generatedAPIInstance';
|
||||
|
||||
const HOME_ALERT_RULES_LIMIT = 5;
|
||||
|
||||
@@ -21,7 +23,18 @@ interface HomeAlertRules {
|
||||
|
||||
export function useHomeAlertRules(): HomeAlertRules {
|
||||
const firing = useListRulesV3(FIRING_RULES_PARAMS, {
|
||||
query: { cacheTime: 0 },
|
||||
query: {
|
||||
cacheTime: 0,
|
||||
// The backend binds repeated `states=` keys; axios' default sends `states[]=`.
|
||||
queryFn: ({ signal }) =>
|
||||
GeneratedAPIInstance<ListRulesV3200>({
|
||||
url: '/api/v3/rules',
|
||||
method: 'GET',
|
||||
params: FIRING_RULES_PARAMS,
|
||||
paramsSerializer: { indexes: null },
|
||||
signal,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const firingRules = firing.data?.data.rules;
|
||||
const latest = useListRulesV3(
|
||||
|
||||
@@ -31,8 +31,6 @@ jest.mock('@floating-ui/dom', () => ({
|
||||
Promise.resolve({ x: 0, y: 0, placement, strategy, middlewareData: {} }),
|
||||
}));
|
||||
|
||||
jest.setTimeout(15_000);
|
||||
|
||||
const VIEW_1_ORDER_BY = JSON.stringify({ columnName: 'name', order: 'asc' });
|
||||
const VIEW_1_QUERY = "severity = 'critical'";
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@ import logEvent from 'api/common/logEvent';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { useListRulesV3 } from 'api/generated/services/rules';
|
||||
import type {
|
||||
ListRulesV3200,
|
||||
ListRulesV3Params,
|
||||
RenderErrorResponseDTO,
|
||||
RuletypesLabelPairDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { AxiosError } from 'axios';
|
||||
import { GeneratedAPIInstance } from 'api/generatedAPIInstance';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
|
||||
import type { AlertRule } from './types';
|
||||
@@ -33,7 +35,18 @@ export function useAlertRulesData(
|
||||
const hasLoggedEvent = useRef(false);
|
||||
|
||||
const rulesResponse = useListRulesV3(params, {
|
||||
query: { keepPreviousData: true },
|
||||
query: {
|
||||
keepPreviousData: true,
|
||||
// The backend binds repeated `states=` keys; axios' default sends `states[]=`.
|
||||
queryFn: ({ signal }) =>
|
||||
GeneratedAPIInstance<ListRulesV3200>({
|
||||
url: '/api/v3/rules',
|
||||
method: 'GET',
|
||||
params,
|
||||
paramsSerializer: { indexes: null },
|
||||
signal,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const rules = useMemo(
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { back, getCurrentLocation } from 'lib/router/navigation';
|
||||
import { act, AllTheProviders, renderHook, waitFor } from 'tests/test-utils';
|
||||
|
||||
// jest.config.ts maps `hooks/useSafeNavigate` to a mock; the `.ts` path loads the real hook.
|
||||
const { useSafeNavigate } = jest.requireActual<
|
||||
typeof import('hooks/useSafeNavigate')
|
||||
>('hooks/useSafeNavigate.ts');
|
||||
|
||||
describe('useSafeNavigate', () => {
|
||||
it('adds a history entry for replace: false even when the target adds params', async () => {
|
||||
const initialSearch = `?${QueryParams.relativeTime}=15m`;
|
||||
const { result } = renderHook(() => useSafeNavigate(), {
|
||||
wrapper: ({ children }): JSX.Element => (
|
||||
<AllTheProviders initialRoute={`${ROUTES.LOGS_EXPLORER}${initialSearch}`}>
|
||||
{children}
|
||||
</AllTheProviders>
|
||||
),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.safeNavigate(
|
||||
`${ROUTES.LOGS_EXPLORER}?${QueryParams.startTime}=1000`,
|
||||
{ replace: false },
|
||||
);
|
||||
});
|
||||
act(() => back());
|
||||
|
||||
await waitFor(() => expect(getCurrentLocation().search).toBe(initialSearch));
|
||||
});
|
||||
});
|
||||
@@ -140,7 +140,7 @@ export const useSafeNavigate = (
|
||||
|
||||
const navigationOptions = {
|
||||
...options,
|
||||
replace: options?.replace ?? isDefaultParamsNavigation,
|
||||
replace: isDefaultParamsNavigation || options?.replace,
|
||||
};
|
||||
|
||||
if (typeof to === 'string') {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { screen, userEvent } from 'storybook/test';
|
||||
import { screen, userEvent, within } from 'storybook/test';
|
||||
|
||||
import type { GlobalMockArgs } from '../globals';
|
||||
|
||||
@@ -72,6 +72,19 @@ export const NozAwaitingYou: Story = {
|
||||
args: { noz: true, nozAwaiting: 2, support: 'pylon' },
|
||||
};
|
||||
|
||||
/** The Noz chat opened from Ask Noz, floating above the strip; its header drags it. */
|
||||
export const NozPanel: Story = {
|
||||
args: { noz: true, support: 'pylon' },
|
||||
play: async (): Promise<void> => {
|
||||
await userEvent.click(
|
||||
within(
|
||||
await screen.findByTestId('bottom-strip-ask-noz', undefined, untilLoaded),
|
||||
).getByRole('button'),
|
||||
);
|
||||
await screen.findByRole('button', { name: 'Close panel' });
|
||||
},
|
||||
};
|
||||
|
||||
/** Every tooltip the strip carries, held open. */
|
||||
export const Tooltips: Story = {
|
||||
args: { noz: true, support: 'pylon', tooltipsOpen: true },
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AI_API_PATH } from 'api/AIAPIInstance';
|
||||
import type { ThreadListResponse } from 'api/ai-assistant/chat';
|
||||
import { useAIAssistantStore } from 'container/AIAssistant/store/useAIAssistantStore';
|
||||
import type { ConversationStreamState } from 'container/AIAssistant/types';
|
||||
import { chipsResponse } from 'pages/AIAssistantPage/stories/__story_mockdata__/aiAssistant';
|
||||
|
||||
import { countControl, toggleControl } from '../controls/controls';
|
||||
import { defineStoryMocks } from '../controls/defineStoryMocks';
|
||||
@@ -57,6 +58,10 @@ export const nozMocks = defineStoryMocks({
|
||||
`${nozGlobalConfigResponse.data.ai_assistant_url}${AI_API_PATH}/threads`,
|
||||
(_req, res, ctx) => res(ctx.status(200), ctx.json(noThreads)),
|
||||
),
|
||||
rest.get(
|
||||
`${nozGlobalConfigResponse.data.ai_assistant_url}${AI_API_PATH}/empty-state/chips`,
|
||||
(_req, res, ctx) => res(ctx.status(200), ctx.json(chipsResponse())),
|
||||
),
|
||||
]
|
||||
: [],
|
||||
effect: ({ nozAwaiting }) => {
|
||||
@@ -72,6 +77,9 @@ export const nozMocks = defineStoryMocks({
|
||||
for (let index = 0; index < nozAwaiting; index += 1) {
|
||||
state.streams[`${AWAITING_PREFIX}${index}`] = awaitingStream();
|
||||
}
|
||||
|
||||
// Persisted, so a panel one story opened would hide Ask Noz in the next.
|
||||
state.isDrawerOpen = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user