mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 15:36:48 +00:00
Merge branch 'main' into json
This commit is contained in:
commit
d1a27fc3cd
@ -176,7 +176,7 @@ services:
|
|||||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
signoz:
|
signoz:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz:v0.96.0
|
image: signoz/signoz:v0.96.1
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@ -117,7 +117,7 @@ services:
|
|||||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
signoz:
|
signoz:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz:v0.96.0
|
image: signoz/signoz:v0.96.1
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@ -179,7 +179,7 @@ services:
|
|||||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
signoz:
|
signoz:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz:${VERSION:-v0.96.0}
|
image: signoz/signoz:${VERSION:-v0.96.1}
|
||||||
container_name: signoz
|
container_name: signoz
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
|
|||||||
@ -111,7 +111,7 @@ services:
|
|||||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
signoz:
|
signoz:
|
||||||
!!merge <<: *db-depend
|
!!merge <<: *db-depend
|
||||||
image: signoz/signoz:${VERSION:-v0.96.0}
|
image: signoz/signoz:${VERSION:-v0.96.1}
|
||||||
container_name: signoz
|
container_name: signoz
|
||||||
command:
|
command:
|
||||||
- --config=/root/config/prometheus.yml
|
- --config=/root/config/prometheus.yml
|
||||||
|
|||||||
@ -87,7 +87,7 @@ function ChangelogModal({ changelog, onClose }: Props): JSX.Element {
|
|||||||
|
|
||||||
const onClickUpdateWorkspace = (): void => {
|
const onClickUpdateWorkspace = (): void => {
|
||||||
window.open(
|
window.open(
|
||||||
'https://github.com/SigNoz/signoz/releases',
|
'https://signoz.io/upgrade-path',
|
||||||
'_blank',
|
'_blank',
|
||||||
'noopener,noreferrer',
|
'noopener,noreferrer',
|
||||||
);
|
);
|
||||||
|
|||||||
@ -91,7 +91,7 @@ describe('ChangelogModal', () => {
|
|||||||
renderChangelog();
|
renderChangelog();
|
||||||
fireEvent.click(screen.getByText('Update my workspace'));
|
fireEvent.click(screen.getByText('Update my workspace'));
|
||||||
expect(window.open).toHaveBeenCalledWith(
|
expect(window.open).toHaveBeenCalledWith(
|
||||||
'https://github.com/SigNoz/signoz/releases',
|
'https://signoz.io/upgrade-path',
|
||||||
'_blank',
|
'_blank',
|
||||||
'noopener,noreferrer',
|
'noopener,noreferrer',
|
||||||
);
|
);
|
||||||
|
|||||||
@ -26,7 +26,7 @@ interface LogsFormatOptionsMenuProps {
|
|||||||
config: OptionsMenuConfig;
|
config: OptionsMenuConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LogsFormatOptionsMenu({
|
function OptionsMenu({
|
||||||
items,
|
items,
|
||||||
selectedOptionFormat,
|
selectedOptionFormat,
|
||||||
config,
|
config,
|
||||||
@ -49,7 +49,6 @@ export default function LogsFormatOptionsMenu({
|
|||||||
const [selectedValue, setSelectedValue] = useState<string | null>(null);
|
const [selectedValue, setSelectedValue] = useState<string | null>(null);
|
||||||
const listRef = useRef<HTMLDivElement>(null);
|
const listRef = useRef<HTMLDivElement>(null);
|
||||||
const initialMouseEnterRef = useRef<boolean>(false);
|
const initialMouseEnterRef = useRef<boolean>(false);
|
||||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(key: LogViewMode) => {
|
(key: LogViewMode) => {
|
||||||
@ -209,7 +208,7 @@ export default function LogsFormatOptionsMenu({
|
|||||||
};
|
};
|
||||||
}, [selectedValue]);
|
}, [selectedValue]);
|
||||||
|
|
||||||
const popoverContent = (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
'nested-menu-container',
|
'nested-menu-container',
|
||||||
@ -447,15 +446,30 @@ export default function LogsFormatOptionsMenu({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LogsFormatOptionsMenu({
|
||||||
|
items,
|
||||||
|
selectedOptionFormat,
|
||||||
|
config,
|
||||||
|
}: LogsFormatOptionsMenuProps): JSX.Element {
|
||||||
|
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
content={popoverContent}
|
content={
|
||||||
|
<OptionsMenu
|
||||||
|
items={items}
|
||||||
|
selectedOptionFormat={selectedOptionFormat}
|
||||||
|
config={config}
|
||||||
|
/>
|
||||||
|
}
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
arrow={false}
|
arrow={false}
|
||||||
open={isPopoverOpen}
|
open={isPopoverOpen}
|
||||||
onOpenChange={setIsPopoverOpen}
|
onOpenChange={setIsPopoverOpen}
|
||||||
rootClassName="format-options-popover"
|
rootClassName="format-options-popover"
|
||||||
|
destroyTooltipOnHide
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
className="periscope-btn ghost"
|
className="periscope-btn ghost"
|
||||||
@ -465,3 +479,5 @@ export default function LogsFormatOptionsMenu({
|
|||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default LogsFormatOptionsMenu;
|
||||||
|
|||||||
@ -0,0 +1,157 @@
|
|||||||
|
import { FontSize } from 'container/OptionsMenu/types';
|
||||||
|
import { fireEvent, render, waitFor } from 'tests/test-utils';
|
||||||
|
|
||||||
|
import LogsFormatOptionsMenu from '../LogsFormatOptionsMenu';
|
||||||
|
|
||||||
|
const mockUpdateFormatting = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
|
||||||
|
usePreferenceSync: (): any => ({
|
||||||
|
preferences: {
|
||||||
|
columns: [],
|
||||||
|
formatting: {
|
||||||
|
maxLines: 2,
|
||||||
|
format: 'table',
|
||||||
|
fontSize: 'small',
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
updateColumns: jest.fn(),
|
||||||
|
updateFormatting: mockUpdateFormatting,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('LogsFormatOptionsMenu (unit)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockUpdateFormatting.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
function setup(): {
|
||||||
|
getByTestId: ReturnType<typeof render>['getByTestId'];
|
||||||
|
findItemByLabel: (label: string) => Element | undefined;
|
||||||
|
formatOnChange: jest.Mock<any, any>;
|
||||||
|
maxLinesOnChange: jest.Mock<any, any>;
|
||||||
|
fontSizeOnChange: jest.Mock<any, any>;
|
||||||
|
} {
|
||||||
|
const items = [
|
||||||
|
{ key: 'raw', label: 'Raw', data: { title: 'max lines per row' } },
|
||||||
|
{ key: 'list', label: 'Default' },
|
||||||
|
{ key: 'table', label: 'Column', data: { title: 'columns' } },
|
||||||
|
];
|
||||||
|
|
||||||
|
const formatOnChange = jest.fn();
|
||||||
|
const maxLinesOnChange = jest.fn();
|
||||||
|
const fontSizeOnChange = jest.fn();
|
||||||
|
|
||||||
|
const { getByTestId } = render(
|
||||||
|
<LogsFormatOptionsMenu
|
||||||
|
items={items}
|
||||||
|
selectedOptionFormat="table"
|
||||||
|
config={{
|
||||||
|
format: { value: 'table', onChange: formatOnChange },
|
||||||
|
maxLines: { value: 2, onChange: maxLinesOnChange },
|
||||||
|
fontSize: { value: FontSize.SMALL, onChange: fontSizeOnChange },
|
||||||
|
addColumn: {
|
||||||
|
isFetching: false,
|
||||||
|
value: [],
|
||||||
|
options: [],
|
||||||
|
onFocus: jest.fn(),
|
||||||
|
onBlur: jest.fn(),
|
||||||
|
onSearch: jest.fn(),
|
||||||
|
onSelect: jest.fn(),
|
||||||
|
onRemove: jest.fn(),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Open the popover menu by default for each test
|
||||||
|
const formatButton = getByTestId('periscope-btn-format-options');
|
||||||
|
fireEvent.click(formatButton);
|
||||||
|
|
||||||
|
const getMenuItems = (): Element[] =>
|
||||||
|
Array.from(document.querySelectorAll('.menu-items .item'));
|
||||||
|
const findItemByLabel = (label: string): Element | undefined =>
|
||||||
|
getMenuItems().find((el) => (el.textContent || '').includes(label));
|
||||||
|
|
||||||
|
return {
|
||||||
|
getByTestId,
|
||||||
|
findItemByLabel,
|
||||||
|
formatOnChange,
|
||||||
|
maxLinesOnChange,
|
||||||
|
fontSizeOnChange,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Covers: opens menu, changes format selection, updates max-lines, changes font size
|
||||||
|
it('opens and toggles format selection', async () => {
|
||||||
|
const { findItemByLabel, formatOnChange } = setup();
|
||||||
|
|
||||||
|
// Assert initial selection
|
||||||
|
const columnItem = findItemByLabel('Column') as Element;
|
||||||
|
expect(document.querySelectorAll('.menu-items .item svg')).toHaveLength(1);
|
||||||
|
expect(columnItem.querySelector('svg')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Change selection to 'Raw'
|
||||||
|
const rawItem = findItemByLabel('Raw') as Element;
|
||||||
|
fireEvent.click(rawItem as HTMLElement);
|
||||||
|
await waitFor(() => {
|
||||||
|
const rawEl = findItemByLabel('Raw') as Element;
|
||||||
|
expect(document.querySelectorAll('.menu-items .item svg')).toHaveLength(1);
|
||||||
|
expect(rawEl.querySelector('svg')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(formatOnChange).toHaveBeenCalledWith('raw');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('increments max-lines and calls onChange', async () => {
|
||||||
|
const { maxLinesOnChange } = setup();
|
||||||
|
|
||||||
|
// Increment max lines
|
||||||
|
const input = document.querySelector(
|
||||||
|
'.max-lines-per-row-input input',
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const initial = Number(input.value);
|
||||||
|
const buttons = document.querySelectorAll(
|
||||||
|
'.max-lines-per-row-input .periscope-btn',
|
||||||
|
);
|
||||||
|
const incrementBtn = buttons[1] as HTMLElement;
|
||||||
|
fireEvent.click(incrementBtn);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(Number(input.value)).toBe(initial + 1);
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(maxLinesOnChange).toHaveBeenCalledWith(initial + 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('changes font size to MEDIUM and calls onChange', async () => {
|
||||||
|
const { fontSizeOnChange } = setup();
|
||||||
|
// Open font dropdown
|
||||||
|
const fontButton = document.querySelector(
|
||||||
|
'.font-size-container .value',
|
||||||
|
) as HTMLElement;
|
||||||
|
fireEvent.click(fontButton);
|
||||||
|
|
||||||
|
// Choose MEDIUM
|
||||||
|
const optionButtons = Array.from(
|
||||||
|
document.querySelectorAll('.font-size-dropdown .option-btn'),
|
||||||
|
);
|
||||||
|
const mediumBtn = optionButtons[1] as HTMLElement;
|
||||||
|
fireEvent.click(mediumBtn);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
document.querySelectorAll('.font-size-dropdown .option-btn .icon'),
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
(optionButtons[1] as Element).querySelector('.icon'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(fontSizeOnChange).toHaveBeenCalledWith(FontSize.MEDIUM);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -97,7 +97,7 @@ function QuerySearch({
|
|||||||
onRun?: (query: string) => void;
|
onRun?: (query: string) => void;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const [query, setQuery] = useState<string>('');
|
const [query, setQuery] = useState<string>(queryData.filter?.expression || '');
|
||||||
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
|
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
|
||||||
const [activeKey, setActiveKey] = useState<string>('');
|
const [activeKey, setActiveKey] = useState<string>('');
|
||||||
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
|
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
|
||||||
@ -108,10 +108,6 @@ function QuerySearch({
|
|||||||
errors: [],
|
errors: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
|
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
|
||||||
const [hasInteractedWithQB, setHasInteractedWithQB] = useState(false);
|
|
||||||
|
|
||||||
const handleQueryValidation = (newQuery: string): void => {
|
const handleQueryValidation = (newQuery: string): void => {
|
||||||
try {
|
try {
|
||||||
const validationResponse = validateQuery(newQuery);
|
const validationResponse = validateQuery(newQuery);
|
||||||
@ -131,28 +127,13 @@ function QuerySearch({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newQuery = queryData.filter?.expression || '';
|
const newQuery = queryData.filter?.expression || '';
|
||||||
// Only update query from external source when editor is not focused
|
// Only mark as external change if the query actually changed from external source
|
||||||
// When focused, just update the lastExternalQuery to track changes
|
|
||||||
if (newQuery !== lastExternalQuery) {
|
if (newQuery !== lastExternalQuery) {
|
||||||
setQuery(newQuery);
|
setQuery(newQuery);
|
||||||
setIsExternalQueryChange(true);
|
setIsExternalQueryChange(true);
|
||||||
setLastExternalQuery(newQuery);
|
setLastExternalQuery(newQuery);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [queryData.filter?.expression, lastExternalQuery]);
|
||||||
}, [queryData.filter?.expression]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Update the query when the editor is blurred and the query has changed
|
|
||||||
// Only call onChange if the editor has been focused before (not on initial mount)
|
|
||||||
if (
|
|
||||||
!isFocused &&
|
|
||||||
hasInteractedWithQB &&
|
|
||||||
query !== queryData.filter?.expression
|
|
||||||
) {
|
|
||||||
onChange(query);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [isFocused]);
|
|
||||||
|
|
||||||
// Validate query when it changes externally (from queryData)
|
// Validate query when it changes externally (from queryData)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -168,6 +149,9 @@ function QuerySearch({
|
|||||||
|
|
||||||
const [showExamples] = useState(false);
|
const [showExamples] = useState(false);
|
||||||
|
|
||||||
|
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
isFetchingCompleteValuesList,
|
isFetchingCompleteValuesList,
|
||||||
setIsFetchingCompleteValuesList,
|
setIsFetchingCompleteValuesList,
|
||||||
@ -181,9 +165,6 @@ function QuerySearch({
|
|||||||
const lastFetchedKeyRef = useRef<string>('');
|
const lastFetchedKeyRef = useRef<string>('');
|
||||||
const lastValueRef = useRef<string>('');
|
const lastValueRef = useRef<string>('');
|
||||||
const isMountedRef = useRef<boolean>(true);
|
const isMountedRef = useRef<boolean>(true);
|
||||||
const [shouldRunQueryPostUpdate, setShouldRunQueryPostUpdate] = useState(
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { handleRunQuery } = useQueryBuilder();
|
const { handleRunQuery } = useQueryBuilder();
|
||||||
|
|
||||||
@ -229,7 +210,6 @@ function QuerySearch({
|
|||||||
|
|
||||||
return (): void => clearTimeout(timeoutId);
|
return (): void => clearTimeout(timeoutId);
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
[isFocused],
|
[isFocused],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -575,6 +555,7 @@ function QuerySearch({
|
|||||||
|
|
||||||
const handleChange = (value: string): void => {
|
const handleChange = (value: string): void => {
|
||||||
setQuery(value);
|
setQuery(value);
|
||||||
|
onChange(value);
|
||||||
// Mark as internal change to avoid triggering external validation
|
// Mark as internal change to avoid triggering external validation
|
||||||
setIsExternalQueryChange(false);
|
setIsExternalQueryChange(false);
|
||||||
// Update lastExternalQuery to prevent external validation trigger
|
// Update lastExternalQuery to prevent external validation trigger
|
||||||
@ -1238,25 +1219,6 @@ function QuerySearch({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Effect to handle query run after update
|
|
||||||
useEffect(
|
|
||||||
() => {
|
|
||||||
// Only run the query post updating the filter expression.
|
|
||||||
// This runs the query in the next update cycle of react, when it's guaranteed that the query is updated.
|
|
||||||
// Because both the things are sequential and react batches the updates so it was still taking the old query.
|
|
||||||
if (shouldRunQueryPostUpdate) {
|
|
||||||
if (onRun && typeof onRun === 'function') {
|
|
||||||
onRun(query);
|
|
||||||
} else {
|
|
||||||
handleRunQuery();
|
|
||||||
}
|
|
||||||
setShouldRunQueryPostUpdate(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
[shouldRunQueryPostUpdate, handleRunQuery, onRun],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="code-mirror-where-clause">
|
<div className="code-mirror-where-clause">
|
||||||
{editingMode && (
|
{editingMode && (
|
||||||
@ -1331,7 +1293,6 @@ function QuerySearch({
|
|||||||
theme={isDarkMode ? copilot : githubLight}
|
theme={isDarkMode ? copilot : githubLight}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onUpdate={handleUpdate}
|
onUpdate={handleUpdate}
|
||||||
data-testid="query-where-clause-editor"
|
|
||||||
className={cx('query-where-clause-editor', {
|
className={cx('query-where-clause-editor', {
|
||||||
isValid: validation.isValid === true,
|
isValid: validation.isValid === true,
|
||||||
hasErrors: validation.errors.length > 0,
|
hasErrors: validation.errors.length > 0,
|
||||||
@ -1368,14 +1329,11 @@ function QuerySearch({
|
|||||||
// and instead run a custom action
|
// and instead run a custom action
|
||||||
// Mod-Enter is usually Ctrl-Enter or Cmd-Enter based on OS
|
// Mod-Enter is usually Ctrl-Enter or Cmd-Enter based on OS
|
||||||
run: (): boolean => {
|
run: (): boolean => {
|
||||||
if (
|
if (onRun && typeof onRun === 'function') {
|
||||||
onChange &&
|
onRun(query);
|
||||||
typeof onChange === 'function' &&
|
} else {
|
||||||
query !== queryData.filter?.expression
|
handleRunQuery();
|
||||||
) {
|
|
||||||
onChange(query);
|
|
||||||
}
|
}
|
||||||
setShouldRunQueryPostUpdate(true);
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1394,13 +1352,8 @@ function QuerySearch({
|
|||||||
}}
|
}}
|
||||||
onFocus={(): void => {
|
onFocus={(): void => {
|
||||||
setIsFocused(true);
|
setIsFocused(true);
|
||||||
setHasInteractedWithQB(true);
|
|
||||||
}}
|
}}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onCreateEditor={(view: EditorView): EditorView => {
|
|
||||||
editorRef.current = view;
|
|
||||||
return view;
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{query && validation.isValid === false && !isFocused && (
|
{query && validation.isValid === false && !isFocused && (
|
||||||
|
|||||||
@ -222,28 +222,6 @@ describe('QuerySearch', () => {
|
|||||||
expect(screen.getByPlaceholderText(PLACEHOLDER_TEXT)).toBeInTheDocument();
|
expect(screen.getByPlaceholderText(PLACEHOLDER_TEXT)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls onChange on blur after user edits', async () => {
|
|
||||||
const handleChange = jest.fn() as jest.MockedFunction<(v: string) => void>;
|
|
||||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
|
||||||
|
|
||||||
render(
|
|
||||||
<QuerySearch
|
|
||||||
onChange={handleChange}
|
|
||||||
queryData={initialQueriesMap.metrics.builder.queryData[0]}
|
|
||||||
dataSource={DataSource.METRICS}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const editor = screen.getByTestId(TESTID_EDITOR);
|
|
||||||
await user.click(editor);
|
|
||||||
await user.type(editor, SAMPLE_VALUE_TYPING_COMPLETE);
|
|
||||||
// Blur triggers validation + onChange (only if focused at least once and value changed)
|
|
||||||
editor.blur();
|
|
||||||
|
|
||||||
await waitFor(() => expect(handleChange).toHaveBeenCalledTimes(1));
|
|
||||||
expect(handleChange.mock.calls[0][0]).toContain("service.name = 'frontend'");
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fetches key suggestions when typing a key (debounced)', async () => {
|
it('fetches key suggestions when typing a key (debounced)', async () => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
const advance = (ms: number): void => {
|
const advance = (ms: number): void => {
|
||||||
|
|||||||
@ -61,8 +61,6 @@ function RouteTab({
|
|||||||
defaultActiveKey={currentRoute?.key || activeKey}
|
defaultActiveKey={currentRoute?.key || activeKey}
|
||||||
animated
|
animated
|
||||||
items={items}
|
items={items}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
|
||||||
{...rest}
|
|
||||||
tabBarExtraContent={
|
tabBarExtraContent={
|
||||||
showRightSection && (
|
showRightSection && (
|
||||||
<HeaderRightSection
|
<HeaderRightSection
|
||||||
@ -72,6 +70,8 @@ function RouteTab({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading ---- TODO: remove this once follow the linting rules
|
||||||
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import './LiveLogsContainer.styles.scss';
|
import './LiveLogsContainer.styles.scss';
|
||||||
|
|
||||||
import { Button, Switch, Typography } from 'antd';
|
import { Switch, Typography } from 'antd';
|
||||||
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
||||||
import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail';
|
import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
@ -8,10 +8,8 @@ import GoToTop from 'container/GoToTop';
|
|||||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import useClickOutside from 'hooks/useClickOutside';
|
|
||||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||||
import { useEventSourceEvent } from 'hooks/useEventSourceEvent';
|
import { useEventSourceEvent } from 'hooks/useEventSourceEvent';
|
||||||
import { Sliders } from 'lucide-react';
|
|
||||||
import { useEventSource } from 'providers/EventSource';
|
import { useEventSource } from 'providers/EventSource';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
@ -41,9 +39,6 @@ function LiveLogsContainer(): JSX.Element {
|
|||||||
|
|
||||||
const batchedEventsRef = useRef<ILiveLogsLog[]>([]);
|
const batchedEventsRef = useRef<ILiveLogsLog[]>([]);
|
||||||
|
|
||||||
const [showFormatMenuItems, setShowFormatMenuItems] = useState(false);
|
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const prevFilterExpressionRef = useRef<string | null>(null);
|
const prevFilterExpressionRef = useRef<string | null>(null);
|
||||||
|
|
||||||
const { options, config } = useOptionsMenu({
|
const { options, config } = useOptionsMenu({
|
||||||
@ -73,18 +68,6 @@ function LiveLogsContainer(): JSX.Element {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleToggleShowFormatOptions = (): void =>
|
|
||||||
setShowFormatMenuItems(!showFormatMenuItems);
|
|
||||||
|
|
||||||
useClickOutside({
|
|
||||||
ref: menuRef,
|
|
||||||
onClickOutside: () => {
|
|
||||||
if (showFormatMenuItems) {
|
|
||||||
setShowFormatMenuItems(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleStartOpenConnection,
|
handleStartOpenConnection,
|
||||||
handleCloseConnection,
|
handleCloseConnection,
|
||||||
@ -231,21 +214,11 @@ function LiveLogsContainer(): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="format-options-container" ref={menuRef}>
|
<LogsFormatOptionsMenu
|
||||||
<Button
|
items={formatItems}
|
||||||
className="periscope-btn ghost"
|
selectedOptionFormat={options.format}
|
||||||
onClick={handleToggleShowFormatOptions}
|
config={config}
|
||||||
icon={<Sliders size={14} />}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
{showFormatMenuItems && (
|
|
||||||
<LogsFormatOptionsMenu
|
|
||||||
items={formatItems}
|
|
||||||
selectedOptionFormat={options.format}
|
|
||||||
config={config}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showLiveLogsFrequencyChart && (
|
{showLiveLogsFrequencyChart && (
|
||||||
|
|||||||
@ -59,6 +59,7 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
TagFilter,
|
TagFilter,
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { Filter } from 'types/api/v5/queryRange';
|
||||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||||
import { DataSource, LogsAggregatorOperator } from 'types/common/queryBuilder';
|
import { DataSource, LogsAggregatorOperator } from 'types/common/queryBuilder';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
@ -171,6 +172,11 @@ function LogsExplorerViewsContainer({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let updatedFilterExpression = listQuery.filter?.expression || '';
|
||||||
|
if (activeLogId) {
|
||||||
|
updatedFilterExpression = `${updatedFilterExpression} id <= '${activeLogId}'`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
const modifiedQueryData: IBuilderQuery = {
|
const modifiedQueryData: IBuilderQuery = {
|
||||||
...listQuery,
|
...listQuery,
|
||||||
aggregateOperator: LogsAggregatorOperator.COUNT,
|
aggregateOperator: LogsAggregatorOperator.COUNT,
|
||||||
@ -183,6 +189,10 @@ function LogsExplorerViewsContainer({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
legend: '{{severity_text}}',
|
legend: '{{severity_text}}',
|
||||||
|
filter: {
|
||||||
|
...listQuery?.filter,
|
||||||
|
expression: updatedFilterExpression || '',
|
||||||
|
},
|
||||||
...(activeLogId && {
|
...(activeLogId && {
|
||||||
filters: {
|
filters: {
|
||||||
...listQuery?.filters,
|
...listQuery?.filters,
|
||||||
@ -286,6 +296,7 @@ function LogsExplorerViewsContainer({
|
|||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
filters: TagFilter;
|
filters: TagFilter;
|
||||||
|
filter: Filter;
|
||||||
},
|
},
|
||||||
): Query | null => {
|
): Query | null => {
|
||||||
if (!query) return null;
|
if (!query) return null;
|
||||||
@ -297,6 +308,7 @@ function LogsExplorerViewsContainer({
|
|||||||
|
|
||||||
// Add filter for activeLogId if present
|
// Add filter for activeLogId if present
|
||||||
let updatedFilters = params.filters;
|
let updatedFilters = params.filters;
|
||||||
|
let updatedFilterExpression = params.filter?.expression || '';
|
||||||
if (activeLogId) {
|
if (activeLogId) {
|
||||||
updatedFilters = {
|
updatedFilters = {
|
||||||
...params.filters,
|
...params.filters,
|
||||||
@ -315,6 +327,7 @@ function LogsExplorerViewsContainer({
|
|||||||
],
|
],
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
};
|
};
|
||||||
|
updatedFilterExpression = `${updatedFilterExpression} id <= '${activeLogId}'`.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create orderBy array based on orderDirection
|
// Create orderBy array based on orderDirection
|
||||||
@ -336,6 +349,9 @@ function LogsExplorerViewsContainer({
|
|||||||
...(listQuery || initialQueryBuilderFormValues),
|
...(listQuery || initialQueryBuilderFormValues),
|
||||||
...paginateData,
|
...paginateData,
|
||||||
...(updatedFilters ? { filters: updatedFilters } : {}),
|
...(updatedFilters ? { filters: updatedFilters } : {}),
|
||||||
|
filter: {
|
||||||
|
expression: updatedFilterExpression || '',
|
||||||
|
},
|
||||||
...(selectedView === ExplorerViews.LIST
|
...(selectedView === ExplorerViews.LIST
|
||||||
? { order: newOrderBy, orderBy: newOrderBy }
|
? { order: newOrderBy, orderBy: newOrderBy }
|
||||||
: { order: [] }),
|
: { order: [] }),
|
||||||
@ -368,7 +384,7 @@ function LogsExplorerViewsContainer({
|
|||||||
if (isLimit) return;
|
if (isLimit) return;
|
||||||
if (logs.length < pageSize) return;
|
if (logs.length < pageSize) return;
|
||||||
|
|
||||||
const { limit, filters } = listQuery;
|
const { limit, filters, filter } = listQuery;
|
||||||
|
|
||||||
const nextLogsLength = logs.length + pageSize;
|
const nextLogsLength = logs.length + pageSize;
|
||||||
|
|
||||||
@ -379,6 +395,7 @@ function LogsExplorerViewsContainer({
|
|||||||
|
|
||||||
const newRequestData = getRequestData(stagedQuery, {
|
const newRequestData = getRequestData(stagedQuery, {
|
||||||
filters: filters || { items: [], op: 'AND' },
|
filters: filters || { items: [], op: 'AND' },
|
||||||
|
filter: filter || { expression: '' },
|
||||||
page: page + 1,
|
page: page + 1,
|
||||||
pageSize: nextPageSize,
|
pageSize: nextPageSize,
|
||||||
});
|
});
|
||||||
@ -526,6 +543,7 @@ function LogsExplorerViewsContainer({
|
|||||||
|
|
||||||
const newRequestData = getRequestData(stagedQuery, {
|
const newRequestData = getRequestData(stagedQuery, {
|
||||||
filters: listQuery?.filters || initialFilters,
|
filters: listQuery?.filters || initialFilters,
|
||||||
|
filter: listQuery?.filter || { expression: '' },
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize,
|
pageSize,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||||
@ -261,6 +262,68 @@ describe('LogsExplorerViews -', () => {
|
|||||||
|
|
||||||
// Verify the total number of filters (original + 1 new activeLogId filter)
|
// Verify the total number of filters (original + 1 new activeLogId filter)
|
||||||
expect(firstQuery.filters?.items.length).toBe(expectedFiltersLength);
|
expect(firstQuery.filters?.items.length).toBe(expectedFiltersLength);
|
||||||
|
|
||||||
|
// Verify the filter expression
|
||||||
|
expect(firstQuery.filter?.expression).toBe(`id <= '${ACTIVE_LOG_ID}'`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update filter expression with activeLogId when present with existing filter expression', async () => {
|
||||||
|
// Mock useCopyLogLink to return an activeLogId
|
||||||
|
(useCopyLogLink as jest.Mock).mockReturnValue({
|
||||||
|
activeLogId: ACTIVE_LOG_ID,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a custom QueryBuilderContext with an existing filter expression
|
||||||
|
const customContext = {
|
||||||
|
...mockQueryBuilderContextValue,
|
||||||
|
panelType: PANEL_TYPES.LIST,
|
||||||
|
stagedQuery: {
|
||||||
|
...mockQueryBuilderContextValue.stagedQuery,
|
||||||
|
builder: {
|
||||||
|
...mockQueryBuilderContextValue.stagedQuery.builder,
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
...mockQueryBuilderContextValue.stagedQuery.builder.queryData[0],
|
||||||
|
filter: { expression: "service = 'frontend'" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
lodsQueryServerRequest();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<QueryBuilderContext.Provider value={customContext as any}>
|
||||||
|
<PreferenceContextProvider>
|
||||||
|
<LogsExplorerViews
|
||||||
|
selectedView={ExplorerViews.LIST}
|
||||||
|
setIsLoadingQueries={(): void => {}}
|
||||||
|
listQueryKeyRef={{ current: {} }}
|
||||||
|
chartQueryKeyRef={{ current: {} }}
|
||||||
|
setWarning={(): void => {}}
|
||||||
|
showLiveLogs={false}
|
||||||
|
/>
|
||||||
|
</PreferenceContextProvider>
|
||||||
|
</QueryBuilderContext.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// Find the call made for LIST panel type (main logs list request)
|
||||||
|
const listCall = (useGetExplorerQueryRange as jest.Mock).mock.calls.find(
|
||||||
|
(call) => call[1] === PANEL_TYPES.LIST && call[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(listCall).toBeDefined();
|
||||||
|
if (listCall) {
|
||||||
|
const queryArg = listCall[0];
|
||||||
|
const firstQuery = queryArg.builder.queryData[0];
|
||||||
|
// It should append the activeLogId condition to existing expression
|
||||||
|
expect(firstQuery.filter?.expression).toBe(
|
||||||
|
"service = 'frontend' id <= 'test-log-id'",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1675,7 +1675,7 @@ func (r *ClickHouseReader) SetTTLV2(ctx context.Context, orgID string, params *m
|
|||||||
queries = append(queries, fmt.Sprintf(`ALTER TABLE %s ON CLUSTER %s MODIFY COLUMN _retention_days_cold UInt16 DEFAULT %d`,
|
queries = append(queries, fmt.Sprintf(`ALTER TABLE %s ON CLUSTER %s MODIFY COLUMN _retention_days_cold UInt16 DEFAULT %d`,
|
||||||
tableNames[0], r.cluster, coldStorageDuration))
|
tableNames[0], r.cluster, coldStorageDuration))
|
||||||
|
|
||||||
queries = append(queries, fmt.Sprintf(`ALTER TABLE %s ON CLUSTER %s MODIFY TTL toDateTime(timestamp / 1000000000) + toIntervalDay(_retention_days) DELETE, toDateTime(timestamp / 1000000000) + toIntervalDay(_retention_days_cold) TO VOLUME '%s'`,
|
queries = append(queries, fmt.Sprintf(`ALTER TABLE %s ON CLUSTER %s MODIFY TTL toDateTime(timestamp / 1000000000) + toIntervalDay(_retention_days) DELETE, toDateTime(timestamp / 1000000000) + toIntervalDay(_retention_days_cold) TO VOLUME '%s' SETTINGS materialize_ttl_after_modify=0`,
|
||||||
tableNames[0], r.cluster, params.ColdStorageVolume))
|
tableNames[0], r.cluster, params.ColdStorageVolume))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1690,7 +1690,7 @@ func (r *ClickHouseReader) SetTTLV2(ctx context.Context, orgID string, params *m
|
|||||||
resourceQueries = append(resourceQueries, fmt.Sprintf(`ALTER TABLE %s ON CLUSTER %s MODIFY COLUMN _retention_days_cold UInt16 DEFAULT %d`,
|
resourceQueries = append(resourceQueries, fmt.Sprintf(`ALTER TABLE %s ON CLUSTER %s MODIFY COLUMN _retention_days_cold UInt16 DEFAULT %d`,
|
||||||
tableNames[1], r.cluster, coldStorageDuration))
|
tableNames[1], r.cluster, coldStorageDuration))
|
||||||
|
|
||||||
resourceQueries = append(resourceQueries, fmt.Sprintf(`ALTER TABLE %s ON CLUSTER %s MODIFY TTL toDateTime(seen_at_ts_bucket_start) + toIntervalSecond(1800) + toIntervalDay(_retention_days) DELETE, toDateTime(seen_at_ts_bucket_start) + toIntervalSecond(1800) + toIntervalDay(_retention_days_cold) TO VOLUME '%s'`,
|
resourceQueries = append(resourceQueries, fmt.Sprintf(`ALTER TABLE %s ON CLUSTER %s MODIFY TTL toDateTime(seen_at_ts_bucket_start) + toIntervalSecond(1800) + toIntervalDay(_retention_days) DELETE, toDateTime(seen_at_ts_bucket_start) + toIntervalSecond(1800) + toIntervalDay(_retention_days_cold) TO VOLUME '%s' SETTINGS materialize_ttl_after_modify=0`,
|
||||||
tableNames[1], r.cluster, params.ColdStorageVolume))
|
tableNames[1], r.cluster, params.ColdStorageVolume))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -481,6 +481,69 @@ func (r *QueryRangeRequest) Validate() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if all queries are disabled
|
||||||
|
if err := r.validateAllQueriesNotDisabled(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateAllQueriesNotDisabled validates that at least one query in the composite query is enabled
|
||||||
|
func (r *QueryRangeRequest) validateAllQueriesNotDisabled() error {
|
||||||
|
allDisabled := true
|
||||||
|
for _, envelope := range r.CompositeQuery.Queries {
|
||||||
|
switch envelope.Type {
|
||||||
|
case QueryTypeBuilder, QueryTypeSubQuery:
|
||||||
|
switch spec := envelope.Spec.(type) {
|
||||||
|
case QueryBuilderQuery[TraceAggregation]:
|
||||||
|
if !spec.Disabled {
|
||||||
|
allDisabled = false
|
||||||
|
}
|
||||||
|
case QueryBuilderQuery[LogAggregation]:
|
||||||
|
if !spec.Disabled {
|
||||||
|
allDisabled = false
|
||||||
|
}
|
||||||
|
case QueryBuilderQuery[MetricAggregation]:
|
||||||
|
if !spec.Disabled {
|
||||||
|
allDisabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case QueryTypeFormula:
|
||||||
|
if spec, ok := envelope.Spec.(QueryBuilderFormula); ok && !spec.Disabled {
|
||||||
|
allDisabled = false
|
||||||
|
}
|
||||||
|
case QueryTypeTraceOperator:
|
||||||
|
if spec, ok := envelope.Spec.(QueryBuilderTraceOperator); ok && !spec.Disabled {
|
||||||
|
allDisabled = false
|
||||||
|
}
|
||||||
|
case QueryTypeJoin:
|
||||||
|
if spec, ok := envelope.Spec.(QueryBuilderJoin); ok && !spec.Disabled {
|
||||||
|
allDisabled = false
|
||||||
|
}
|
||||||
|
case QueryTypePromQL:
|
||||||
|
if spec, ok := envelope.Spec.(PromQuery); ok && !spec.Disabled {
|
||||||
|
allDisabled = false
|
||||||
|
}
|
||||||
|
case QueryTypeClickHouseSQL:
|
||||||
|
if spec, ok := envelope.Spec.(ClickHouseQuery); ok && !spec.Disabled {
|
||||||
|
allDisabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Early exit if we find at least one enabled query
|
||||||
|
if !allDisabled {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allDisabled {
|
||||||
|
return errors.NewInvalidInputf(
|
||||||
|
errors.CodeInvalidInput,
|
||||||
|
"all queries are disabled - at least one query must be enabled",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,334 @@
|
|||||||
|
package querybuildertypesv5
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func contains(s, substr string) bool {
|
||||||
|
return strings.Contains(s, substr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueryRangeRequest_ValidateAllQueriesNotDisabled(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
request QueryRangeRequest
|
||||||
|
wantErr bool
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "all queries disabled should return error",
|
||||||
|
request: QueryRangeRequest{
|
||||||
|
Start: 1640995200000,
|
||||||
|
End: 1640998800000,
|
||||||
|
RequestType: RequestTypeTimeSeries,
|
||||||
|
CompositeQuery: CompositeQuery{
|
||||||
|
Queries: []QueryEnvelope{
|
||||||
|
{
|
||||||
|
Type: QueryTypeBuilder,
|
||||||
|
Spec: QueryBuilderQuery[MetricAggregation]{
|
||||||
|
Name: "A",
|
||||||
|
Disabled: true,
|
||||||
|
Signal: telemetrytypes.SignalMetrics,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: QueryTypeBuilder,
|
||||||
|
Spec: QueryBuilderQuery[LogAggregation]{
|
||||||
|
Name: "B",
|
||||||
|
Disabled: true,
|
||||||
|
Signal: telemetrytypes.SignalLogs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "all queries are disabled - at least one query must be enabled",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed disabled and enabled queries should pass",
|
||||||
|
request: QueryRangeRequest{
|
||||||
|
Start: 1640995200000,
|
||||||
|
End: 1640998800000,
|
||||||
|
RequestType: RequestTypeTimeSeries,
|
||||||
|
CompositeQuery: CompositeQuery{
|
||||||
|
Queries: []QueryEnvelope{
|
||||||
|
{
|
||||||
|
Type: QueryTypeBuilder,
|
||||||
|
Spec: QueryBuilderQuery[MetricAggregation]{
|
||||||
|
Name: "A",
|
||||||
|
Disabled: true,
|
||||||
|
Signal: telemetrytypes.SignalMetrics,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: QueryTypeBuilder,
|
||||||
|
Spec: QueryBuilderQuery[LogAggregation]{
|
||||||
|
Name: "B",
|
||||||
|
Disabled: false,
|
||||||
|
Signal: telemetrytypes.SignalLogs,
|
||||||
|
Aggregations: []LogAggregation{
|
||||||
|
{
|
||||||
|
Expression: "count()",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all queries enabled should pass",
|
||||||
|
request: QueryRangeRequest{
|
||||||
|
Start: 1640995200000,
|
||||||
|
End: 1640998800000,
|
||||||
|
RequestType: RequestTypeTimeSeries,
|
||||||
|
CompositeQuery: CompositeQuery{
|
||||||
|
Queries: []QueryEnvelope{
|
||||||
|
{
|
||||||
|
Type: QueryTypeBuilder,
|
||||||
|
Spec: QueryBuilderQuery[LogAggregation]{
|
||||||
|
Name: "A",
|
||||||
|
Disabled: false,
|
||||||
|
Signal: telemetrytypes.SignalLogs,
|
||||||
|
Aggregations: []LogAggregation{
|
||||||
|
{
|
||||||
|
Expression: "count()",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: QueryTypeBuilder,
|
||||||
|
Spec: QueryBuilderQuery[LogAggregation]{
|
||||||
|
Name: "B",
|
||||||
|
Disabled: false,
|
||||||
|
Signal: telemetrytypes.SignalLogs,
|
||||||
|
Aggregations: []LogAggregation{
|
||||||
|
{
|
||||||
|
Expression: "sum(duration)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all formula queries disabled should return error",
|
||||||
|
request: QueryRangeRequest{
|
||||||
|
Start: 1640995200000,
|
||||||
|
End: 1640998800000,
|
||||||
|
RequestType: RequestTypeTimeSeries,
|
||||||
|
CompositeQuery: CompositeQuery{
|
||||||
|
Queries: []QueryEnvelope{
|
||||||
|
{
|
||||||
|
Type: QueryTypeFormula,
|
||||||
|
Spec: QueryBuilderFormula{
|
||||||
|
Name: "F1",
|
||||||
|
Expression: "A + B",
|
||||||
|
Disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: QueryTypeFormula,
|
||||||
|
Spec: QueryBuilderFormula{
|
||||||
|
Name: "F2",
|
||||||
|
Expression: "A * 2",
|
||||||
|
Disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "all queries are disabled - at least one query must be enabled",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all PromQL queries disabled should return error",
|
||||||
|
request: QueryRangeRequest{
|
||||||
|
Start: 1640995200000,
|
||||||
|
End: 1640998800000,
|
||||||
|
RequestType: RequestTypeTimeSeries,
|
||||||
|
CompositeQuery: CompositeQuery{
|
||||||
|
Queries: []QueryEnvelope{
|
||||||
|
{
|
||||||
|
Type: QueryTypePromQL,
|
||||||
|
Spec: PromQuery{
|
||||||
|
Name: "P1",
|
||||||
|
Query: "up",
|
||||||
|
Disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: QueryTypePromQL,
|
||||||
|
Spec: PromQuery{
|
||||||
|
Name: "P2",
|
||||||
|
Query: "rate(http_requests_total[5m])",
|
||||||
|
Disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "all queries are disabled - at least one query must be enabled",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed query types with all disabled should return error",
|
||||||
|
request: QueryRangeRequest{
|
||||||
|
Start: 1640995200000,
|
||||||
|
End: 1640998800000,
|
||||||
|
RequestType: RequestTypeTimeSeries,
|
||||||
|
CompositeQuery: CompositeQuery{
|
||||||
|
Queries: []QueryEnvelope{
|
||||||
|
{
|
||||||
|
Type: QueryTypeBuilder,
|
||||||
|
Spec: QueryBuilderQuery[MetricAggregation]{
|
||||||
|
Name: "A",
|
||||||
|
Disabled: true,
|
||||||
|
Signal: telemetrytypes.SignalMetrics,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: QueryTypeFormula,
|
||||||
|
Spec: QueryBuilderFormula{
|
||||||
|
Name: "F1",
|
||||||
|
Expression: "A + 1",
|
||||||
|
Disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: QueryTypePromQL,
|
||||||
|
Spec: PromQuery{
|
||||||
|
Name: "P1",
|
||||||
|
Query: "up",
|
||||||
|
Disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "all queries are disabled - at least one query must be enabled",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single disabled query should return error",
|
||||||
|
request: QueryRangeRequest{
|
||||||
|
Start: 1640995200000,
|
||||||
|
End: 1640998800000,
|
||||||
|
RequestType: RequestTypeTimeSeries,
|
||||||
|
CompositeQuery: CompositeQuery{
|
||||||
|
Queries: []QueryEnvelope{
|
||||||
|
{
|
||||||
|
Type: QueryTypeBuilder,
|
||||||
|
Spec: QueryBuilderQuery[LogAggregation]{
|
||||||
|
Name: "A",
|
||||||
|
Disabled: true,
|
||||||
|
Signal: telemetrytypes.SignalLogs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "all queries are disabled - at least one query must be enabled",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single enabled query should pass",
|
||||||
|
request: QueryRangeRequest{
|
||||||
|
Start: 1640995200000,
|
||||||
|
End: 1640998800000,
|
||||||
|
RequestType: RequestTypeTimeSeries,
|
||||||
|
CompositeQuery: CompositeQuery{
|
||||||
|
Queries: []QueryEnvelope{
|
||||||
|
{
|
||||||
|
Type: QueryTypeBuilder,
|
||||||
|
Spec: QueryBuilderQuery[LogAggregation]{
|
||||||
|
Name: "A",
|
||||||
|
Disabled: false,
|
||||||
|
Signal: telemetrytypes.SignalLogs,
|
||||||
|
Aggregations: []LogAggregation{
|
||||||
|
{
|
||||||
|
Expression: "count()",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all ClickHouse queries disabled should return error",
|
||||||
|
request: QueryRangeRequest{
|
||||||
|
Start: 1640995200000,
|
||||||
|
End: 1640998800000,
|
||||||
|
RequestType: RequestTypeTimeSeries,
|
||||||
|
CompositeQuery: CompositeQuery{
|
||||||
|
Queries: []QueryEnvelope{
|
||||||
|
{
|
||||||
|
Type: QueryTypeClickHouseSQL,
|
||||||
|
Spec: ClickHouseQuery{
|
||||||
|
Name: "CH1",
|
||||||
|
Query: "SELECT count() FROM logs",
|
||||||
|
Disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "all queries are disabled - at least one query must be enabled",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all trace operator queries disabled should return error",
|
||||||
|
request: QueryRangeRequest{
|
||||||
|
Start: 1640995200000,
|
||||||
|
End: 1640998800000,
|
||||||
|
RequestType: RequestTypeTimeSeries,
|
||||||
|
CompositeQuery: CompositeQuery{
|
||||||
|
Queries: []QueryEnvelope{
|
||||||
|
{
|
||||||
|
Type: QueryTypeTraceOperator,
|
||||||
|
Spec: QueryBuilderTraceOperator{
|
||||||
|
Name: "TO1",
|
||||||
|
Expression: "count()",
|
||||||
|
Disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "all queries are disabled - at least one query must be enabled",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := tt.request.Validate()
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("QueryRangeRequest.Validate() expected error but got none")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tt.errMsg != "" && !contains(err.Error(), tt.errMsg) {
|
||||||
|
t.Errorf("QueryRangeRequest.Validate() error = %v, want to contain %v", err.Error(), tt.errMsg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("QueryRangeRequest.Validate() unexpected error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user