Compare commits

..

9 Commits

Author SHA1 Message Date
Ishan Uniyal
3d64869fac feat: old bug stop propogating to parent on settings click 2026-02-25 14:48:40 +05:30
Ishan Uniyal
eff3611288 feat: close drawer on filter apply 2026-02-25 14:48:40 +05:30
Ishan Uniyal
041d47b94a feat: added popover so removed data attr 2026-02-25 14:48:40 +05:30
Ishan Uniyal
e9af1dee12 feat: outside click bug fix 2026-02-25 14:48:40 +05:30
Ashwin Bhatkal
ff028e366b fix: first query variable without 'ALL' selection (#10417)
* fix: first query variable without all

* chore: resolve self comments

* chore: added tests for getOptions logic
2026-02-25 08:36:39 +00:00
Ishan
c579614d56 feat: color fallback and red checks (#10389)
* feat: color fallback and red checks

* feat: testcase added
2026-02-25 11:54:22 +05:30
Ishan
78ba2ba356 feat: text selection block (#10373)
* feat: text selection block

* chore: added test file
2026-02-25 11:38:15 +05:30
Ishan
7fd4762e2a feat: ui bugs body width and table css (#10377)
* feat: ui bugs body width and table css

* feat: defualt open search overview

* feat: added timerRef to cleanup
2026-02-25 11:25:54 +05:30
Nageshbansal
4e4c9ce5af chore: enable metadataexporter in docker (#10409)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-02-25 03:13:27 +05:30
52 changed files with 894 additions and 170 deletions

View File

@@ -82,6 +82,12 @@ exporters:
timeout: 45s
sending_queue:
enabled: false
metadataexporter:
cache:
provider: in_memory
dsn: tcp://clickhouse:9000/signoz_metadata
enabled: true
timeout: 45s
service:
telemetry:
logs:
@@ -93,19 +99,19 @@ service:
traces:
receivers: [otlp]
processors: [signozspanmetrics/delta, batch]
exporters: [clickhousetraces, signozmeter]
exporters: [clickhousetraces, metadataexporter, signozmeter]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [signozclickhousemetrics, signozmeter]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [signozclickhousemetrics, signozmeter]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
logs:
receivers: [otlp]
processors: [batch]
exporters: [clickhouselogsexporter, signozmeter]
exporters: [clickhouselogsexporter, metadataexporter, signozmeter]
metrics/meter:
receivers: [signozmeter]
processors: [batch/meter]

View File

@@ -82,6 +82,12 @@ exporters:
timeout: 45s
sending_queue:
enabled: false
metadataexporter:
cache:
provider: in_memory
dsn: tcp://clickhouse:9000/signoz_metadata
enabled: true
timeout: 45s
service:
telemetry:
logs:
@@ -93,19 +99,19 @@ service:
traces:
receivers: [otlp]
processors: [signozspanmetrics/delta, batch]
exporters: [clickhousetraces, signozmeter]
exporters: [clickhousetraces, metadataexporter, signozmeter]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [signozclickhousemetrics, signozmeter]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [signozclickhousemetrics, signozmeter]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
logs:
receivers: [otlp]
processors: [batch]
exporters: [clickhouselogsexporter, signozmeter]
exporters: [clickhouselogsexporter, metadataexporter, signozmeter]
metrics/meter:
receivers: [signozmeter]
processors: [batch/meter]

View File

@@ -26,7 +26,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/utils/times"
"github.com/SigNoz/signoz/pkg/query-service/utils/timestamp"
"github.com/SigNoz/signoz/pkg/formatter"
"github.com/SigNoz/signoz/pkg/query-service/formatter"
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"

View File

@@ -86,8 +86,13 @@ function LogDetailInner({
const handleClickOutside = (e: MouseEvent): void => {
const target = e.target as HTMLElement;
// Don't close if clicking on explicitly ignored regions
if (target.closest('[data-log-detail-ignore="true"]')) {
// Don't close if clicking on drawer content, overlays, or portal elements
if (
target.closest('[data-log-detail-ignore="true"]') ||
target.closest('.cm-tooltip-autocomplete') ||
target.closest('.drawer-popover') ||
target.closest('.query-status-popover')
) {
return;
}
@@ -400,7 +405,11 @@ function LogDetailInner({
<div className="log-detail-drawer__content" data-log-detail-ignore="true">
<div className="log-detail-drawer__log">
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
<Tooltip title={removeEscapeCharacters(log?.body)} placement="left">
<Tooltip
title={removeEscapeCharacters(log?.body)}
placement="left"
mouseLeaveDelay={0}
>
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
</Tooltip>
@@ -466,6 +475,7 @@ function LogDetailInner({
title="Show Filters"
placement="topLeft"
aria-label="Show Filters"
mouseLeaveDelay={0}
>
<Button
className="action-btn"
@@ -481,6 +491,7 @@ function LogDetailInner({
aria-label={
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
}
mouseLeaveDelay={0}
>
<Button
className="action-btn"

View File

@@ -27,7 +27,11 @@ function AddToQueryHOC({
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div className={cx('addToQueryContainer', fontSize)} onClick={handleQueryAdd}>
<Popover placement="top" content={popOverContent}>
<Popover
overlayClassName="drawer-popover"
placement="top"
content={popOverContent}
>
{children}
</Popover>
</div>

View File

@@ -32,6 +32,7 @@ function CopyClipboardHOC({
<span onClick={onClick} role="presentation" tabIndex={-1}>
<Popover
placement="top"
overlayClassName="drawer-popover"
content={<span style={{ fontSize: '0.9rem' }}>{tooltipText}</span>}
>
{children}

View File

@@ -21,7 +21,7 @@ export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
export const defaultTableStyle: CSSProperties = {
minWidth: '40rem',
maxWidth: '60rem',
maxWidth: '90rem',
};
export const defaultListViewPanelStyle: CSSProperties = {

View File

@@ -1328,7 +1328,10 @@ function QuerySearch({
)}
<div className="query-where-clause-editor-container">
<Tooltip title={getTooltipContent()} placement="left">
<Tooltip
title={<div data-log-detail-ignore="true">{getTooltipContent()}</div>}
placement="left"
>
<a
href="https://signoz.io/docs/userguide/search-syntax/"
target="_blank"

View File

@@ -9,6 +9,7 @@ import {
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import {
enqueueDescendantsOfVariable,
enqueueFetchOfAllVariables,
@@ -31,6 +32,9 @@ function DashboardVariableSelection(): JSX.Element | null {
const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
const { dashboardVariables } = useDashboardVariables();
const dashboardId = useDashboardVariablesSelector(
(state) => state.dashboardId,
);
const sortedVariablesArray = useDashboardVariablesSelector(
(state) => state.sortedVariablesArray,
);
@@ -96,6 +100,28 @@ function DashboardVariableSelection(): JSX.Element | null {
updateUrlVariable(name || id, value);
}
// Synchronously update the external store with the new variable value so that
// child variables see the updated parent value when they refetch, rather than
// waiting for setSelectedDashboard → useEffect → updateDashboardVariablesStore.
const updatedVariables = { ...dashboardVariables };
if (updatedVariables[id]) {
updatedVariables[id] = {
...updatedVariables[id],
selectedValue: value,
allSelected,
haveCustomValuesSelected,
};
}
if (updatedVariables[name]) {
updatedVariables[name] = {
...updatedVariables[name],
selectedValue: value,
allSelected,
haveCustomValuesSelected,
};
}
updateDashboardVariablesStore({ dashboardId, variables: updatedVariables });
setSelectedDashboard((prev) => {
if (prev) {
const oldVariables = { ...prev?.data.variables };
@@ -130,10 +156,12 @@ function DashboardVariableSelection(): JSX.Element | null {
return prev;
});
// Cascade: enqueue query-type descendants for refetching
// Cascade: enqueue query-type descendants for refetching.
// Safe to call synchronously now that the store already has the updated value.
enqueueDescendantsOfVariable(name);
},
[
dashboardId,
dashboardVariables,
updateLocalStorageDashboardVariables,
updateUrlVariable,

View File

@@ -5,7 +5,7 @@ import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQ
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useVariableFetchState } from 'hooks/dashboard/useVariableFetchState';
import sortValues from 'lib/dashboardVariables/sortVariableValues';
import { isArray, isEmpty, isString } from 'lodash-es';
import { isArray, isEmpty } from 'lodash-es';
import { AppState } from 'store/reducers';
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
import { GlobalReducer } from 'types/reducer/globalTime';
@@ -54,7 +54,7 @@ function QueryVariableInput({
onChange,
onDropdownVisibleChange,
handleClear,
applyDefaultIfNeeded,
getDefaultValue,
} = useDashboardVariableSelectHelper({
variableData,
optionsData,
@@ -68,81 +68,93 @@ function QueryVariableInput({
try {
setErrorMessage(null);
// This is just a check given the previously undefined typed name prop. Not significant
// This will be changed when we change the schema
// TODO: @AshwinBhatkal Perses
if (!variableData.name) {
return;
}
// if the response is not an array, premature return
if (
variablesRes?.variableValues &&
Array.isArray(variablesRes?.variableValues)
!variablesRes?.variableValues ||
!Array.isArray(variablesRes?.variableValues)
) {
const newOptionsData = sortValues(
variablesRes?.variableValues,
variableData.sort,
return;
}
const sortedNewOptions = sortValues(
variablesRes.variableValues,
variableData.sort,
);
const sortedOldOptions = sortValues(optionsData, variableData.sort);
// if options are the same as before, no need to update state or check for selected value validity
// ! selectedValue needs to be set in the first pass though, as options are initially empty array and we need to apply default if needed
// Expecatation is that when oldOptions are not empty, then there is always some selectedValue
if (areArraysEqual(sortedNewOptions, sortedOldOptions)) {
return;
}
setOptionsData(sortedNewOptions);
let isSelectedValueMissingInNewOptions = false;
// Check if currently selected value(s) are present in the new options list
if (isArray(variableData.selectedValue)) {
isSelectedValueMissingInNewOptions = variableData.selectedValue.some(
(val) => !sortedNewOptions.includes(val),
);
} else if (
variableData.selectedValue &&
!sortedNewOptions.includes(variableData.selectedValue)
) {
isSelectedValueMissingInNewOptions = true;
}
const oldOptionsData = sortValues(optionsData, variableData.sort) as never;
// If multi-select with ALL option enabled, and ALL is currently selected, we want to maintain that state and select all new options
// This block does not depend on selected value because of ALL and also because we would only come here if options are different from the previous
if (
variableData.multiSelect &&
variableData.showALLOption &&
variableData.allSelected &&
isSelectedValueMissingInNewOptions
) {
onValueUpdate(variableData.name, variableData.id, sortedNewOptions, true);
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
let valueNotInList = false;
// Update tempSelection to maintain ALL state when dropdown is open
if (tempSelection !== undefined) {
setTempSelection(sortedNewOptions.map((option) => option.toString()));
}
return;
}
if (isArray(variableData.selectedValue)) {
variableData.selectedValue.forEach((val) => {
if (!newOptionsData.includes(val)) {
valueNotInList = true;
}
});
} else if (
isString(variableData.selectedValue) &&
!newOptionsData.includes(variableData.selectedValue)
) {
valueNotInList = true;
}
const value = variableData.selectedValue;
let allSelected = false;
if (variableData.name && (valueNotInList || variableData.allSelected)) {
if (
variableData.allSelected &&
variableData.multiSelect &&
variableData.showALLOption
) {
if (
variableData.name &&
variableData.id &&
!isEmpty(variableData.selectedValue)
) {
onValueUpdate(
variableData.name,
variableData.id,
newOptionsData,
true,
);
}
if (variableData.multiSelect) {
const { selectedValue } = variableData;
allSelected =
sortedNewOptions.length > 0 &&
Array.isArray(selectedValue) &&
sortedNewOptions.every((option) => selectedValue.includes(option));
}
// Update tempSelection to maintain ALL state when dropdown is open
if (tempSelection !== undefined) {
setTempSelection(newOptionsData.map((option) => option.toString()));
}
} else {
const value = variableData.selectedValue;
let allSelected = false;
if (variableData.multiSelect) {
const { selectedValue } = variableData;
allSelected =
newOptionsData.length > 0 &&
Array.isArray(selectedValue) &&
newOptionsData.every((option) => selectedValue.includes(option));
}
if (
variableData.name &&
variableData.id &&
!isEmpty(variableData.selectedValue)
) {
onValueUpdate(variableData.name, variableData.id, value, allSelected);
}
}
}
setOptionsData(newOptionsData);
// Apply default if no value is selected (e.g., new variable, first load)
applyDefaultIfNeeded(newOptionsData);
if (
variableData.name &&
variableData.id &&
!isEmpty(variableData.selectedValue)
) {
onValueUpdate(variableData.name, variableData.id, value, allSelected);
} else {
const defaultValue = getDefaultValue(sortedNewOptions);
if (defaultValue !== undefined) {
onValueUpdate(
variableData.name,
variableData.id,
defaultValue,
allSelected,
);
}
}
} catch (e) {
@@ -155,7 +167,7 @@ function QueryVariableInput({
onValueUpdate,
tempSelection,
setTempSelection,
applyDefaultIfNeeded,
getDefaultValue,
],
);

View File

@@ -1,5 +1,6 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { act, render } from '@testing-library/react';
import * as dashboardVariablesStoreModule from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import {
dashboardVariablesStore,
setDashboardVariablesStore,
@@ -10,6 +11,7 @@ import {
IDashboardVariablesStoreState,
} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import {
enqueueDescendantsOfVariable,
enqueueFetchOfAllVariables,
initializeVariableFetchStore,
} from 'providers/Dashboard/store/variableFetchStore';
@@ -17,6 +19,17 @@ import { IDashboardVariable } from 'types/api/dashboard/getAll';
import DashboardVariableSelection from '../DashboardVariableSelection';
// Mutable container to capture the onValueUpdate callback from VariableItem
const mockVariableItemCallbacks: {
onValueUpdate?: (
name: string,
id: string,
value: IDashboardVariable['selectedValue'],
allSelected: boolean,
haveCustomValuesSelected?: boolean,
) => void;
} = {};
// Mock providers/Dashboard/Dashboard
const mockSetSelectedDashboard = jest.fn();
const mockUpdateLocalStorageDashboardVariables = jest.fn();
@@ -56,10 +69,14 @@ jest.mock('react-redux', () => ({
useSelector: jest.fn().mockReturnValue({ minTime: 1000, maxTime: 2000 }),
}));
// Mock VariableItem to avoid rendering complexity
// VariableItem mock captures the onValueUpdate prop for use in onValueUpdate tests
jest.mock('../VariableItem', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="variable-item" />,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
default: (props: any): JSX.Element => {
mockVariableItemCallbacks.onValueUpdate = props.onValueUpdate;
return <div data-testid="variable-item" />;
},
}));
function createVariable(
@@ -200,4 +217,162 @@ describe('DashboardVariableSelection', () => {
expect(initializeVariableFetchStore).not.toHaveBeenCalled();
expect(enqueueFetchOfAllVariables).not.toHaveBeenCalled();
});
describe('onValueUpdate', () => {
let updateStoreSpy: jest.SpyInstance;
beforeEach(() => {
resetStore();
jest.clearAllMocks();
// Real implementation pass-through — we just want to observe calls
updateStoreSpy = jest.spyOn(
dashboardVariablesStoreModule,
'updateDashboardVariablesStore',
);
});
afterEach(() => {
updateStoreSpy.mockRestore();
});
it('updates dashboardVariablesStore synchronously before enqueueDescendantsOfVariable', () => {
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
env: createVariable({ name: 'env', id: 'env-id', order: 0 }),
},
});
render(<DashboardVariableSelection />);
const callOrder: string[] = [];
updateStoreSpy.mockImplementation(() => {
callOrder.push('updateDashboardVariablesStore');
});
(enqueueDescendantsOfVariable as jest.Mock).mockImplementation(() => {
callOrder.push('enqueueDescendantsOfVariable');
});
act(() => {
mockVariableItemCallbacks.onValueUpdate?.(
'env',
'env-id',
'production',
false,
);
});
expect(callOrder).toEqual([
'updateDashboardVariablesStore',
'enqueueDescendantsOfVariable',
]);
});
it('passes updated variable value to dashboardVariablesStore', () => {
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
env: createVariable({
name: 'env',
id: 'env-id',
order: 0,
selectedValue: 'staging',
}),
},
});
render(<DashboardVariableSelection />);
// Clear spy calls that happened during setup/render
updateStoreSpy.mockClear();
act(() => {
mockVariableItemCallbacks.onValueUpdate?.(
'env',
'env-id',
'production',
false,
);
});
expect(updateStoreSpy).toHaveBeenCalledWith(
expect.objectContaining({
dashboardId: 'dash-1',
variables: expect.objectContaining({
env: expect.objectContaining({
selectedValue: 'production',
allSelected: false,
}),
}),
}),
);
});
it('calls enqueueDescendantsOfVariable synchronously without a timer', () => {
jest.useFakeTimers();
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
env: createVariable({ name: 'env', id: 'env-id', order: 0 }),
},
});
render(<DashboardVariableSelection />);
act(() => {
mockVariableItemCallbacks.onValueUpdate?.(
'env',
'env-id',
'production',
false,
);
});
// Must be called immediately — no timer advancement needed
expect(enqueueDescendantsOfVariable).toHaveBeenCalledWith('env');
jest.useRealTimers();
});
it('propagates allSelected and haveCustomValuesSelected to the store', () => {
setDashboardVariablesStore({
dashboardId: 'dash-1',
variables: {
env: createVariable({
name: 'env',
id: 'env-id',
order: 0,
multiSelect: true,
showALLOption: true,
}),
},
});
render(<DashboardVariableSelection />);
updateStoreSpy.mockClear();
act(() => {
mockVariableItemCallbacks.onValueUpdate?.(
'env',
'env-id',
['production', 'staging'],
true,
false,
);
});
expect(updateStoreSpy).toHaveBeenCalledWith(
expect.objectContaining({
variables: expect.objectContaining({
env: expect.objectContaining({
selectedValue: ['production', 'staging'],
allSelected: true,
haveCustomValuesSelected: false,
}),
}),
}),
);
});
});
});

View File

@@ -0,0 +1,275 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { QueryClient, QueryClientProvider } from 'react-query';
import { act, render, waitFor } from '@testing-library/react';
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
import { variableFetchStore } from 'providers/Dashboard/store/variableFetchStore';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import QueryVariableInput from '../QueryVariableInput';
jest.mock('api/dashboard/variables/dashboardVariablesQuery');
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn().mockReturnValue({ minTime: 1000, maxTime: 2000 }),
}));
function createTestQueryClient(): QueryClient {
return new QueryClient({
defaultOptions: {
queries: { retry: false, refetchOnWindowFocus: false },
},
});
}
function Wrapper({
children,
queryClient,
}: {
children: React.ReactNode;
queryClient: QueryClient;
}): JSX.Element {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
function createVariable(
overrides: Partial<IDashboardVariable> = {},
): IDashboardVariable {
return {
id: 'env-id',
name: 'env',
description: '',
type: 'QUERY',
sort: 'DISABLED',
showALLOption: false,
multiSelect: false,
order: 0,
queryValue: 'SELECT env FROM table',
...overrides,
};
}
/** Put the named variable into 'loading' state so useQuery fires on mount */
function setVariableLoading(name: string): void {
variableFetchStore.update((draft) => {
draft.states[name] = 'loading';
draft.cycleIds[name] = (draft.cycleIds[name] || 0) + 1;
});
}
function resetFetchStore(): void {
variableFetchStore.set(() => ({
states: {},
lastUpdated: {},
cycleIds: {},
}));
}
describe('QueryVariableInput - getOptions logic', () => {
const mockOnValueUpdate = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
resetFetchStore();
});
afterEach(() => {
resetFetchStore();
});
it('applies default value (first option) when selectedValue is empty on first load', async () => {
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
statusCode: 200,
payload: { variableValues: ['production', 'staging', 'dev'] },
});
const variable = createVariable({ selectedValue: undefined });
setVariableLoading('env');
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
await waitFor(() => {
expect(mockOnValueUpdate).toHaveBeenCalledWith(
'env',
'env-id',
'production', // first option by default
false,
);
});
});
it('keeps existing selectedValue when it is present in new options', async () => {
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
statusCode: 200,
payload: { variableValues: ['production', 'staging'] },
});
const variable = createVariable({ selectedValue: 'staging' });
setVariableLoading('env');
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
await waitFor(() => {
expect(mockOnValueUpdate).toHaveBeenCalledWith(
'env',
'env-id',
'staging',
false,
);
});
});
it('selects all new options when allSelected=true and value is missing from new options', async () => {
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
statusCode: 200,
payload: { variableValues: ['production', 'staging'] },
});
const variable = createVariable({
selectedValue: ['old-env'],
allSelected: true,
multiSelect: true,
showALLOption: true,
});
setVariableLoading('env');
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
await waitFor(() => {
expect(mockOnValueUpdate).toHaveBeenCalledWith(
'env',
'env-id',
['production', 'staging'],
true,
);
});
});
it('does not call onValueUpdate a second time when options have not changed', async () => {
const mockQueryFn = jest.fn().mockResolvedValue({
statusCode: 200,
payload: { variableValues: ['production', 'staging'] },
});
(dashboardVariablesQuery as jest.Mock).mockImplementation(mockQueryFn);
const variable = createVariable({ selectedValue: 'production' });
setVariableLoading('env');
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
// Wait for first fetch and onValueUpdate call
await waitFor(() => {
expect(mockOnValueUpdate).toHaveBeenCalledTimes(1);
});
mockOnValueUpdate.mockClear();
// Trigger a second fetch cycle with the same API response
act(() => {
variableFetchStore.update((draft) => {
draft.states['env'] = 'revalidating';
draft.cycleIds['env'] = (draft.cycleIds['env'] || 0) + 1;
});
});
// Wait for second query to fire
await waitFor(() => {
expect(mockQueryFn).toHaveBeenCalledTimes(2);
});
// Options are unchanged, so onValueUpdate must not fire again
expect(mockOnValueUpdate).not.toHaveBeenCalled();
});
it('does not call onValueUpdate when API returns a non-array response', async () => {
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
statusCode: 200,
payload: { variableValues: null },
});
const variable = createVariable({ selectedValue: 'production' });
setVariableLoading('env');
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
await waitFor(() => {
expect(dashboardVariablesQuery).toHaveBeenCalled();
});
expect(mockOnValueUpdate).not.toHaveBeenCalled();
});
it('does not fire the query when variableData.name is empty', () => {
(dashboardVariablesQuery as jest.Mock).mockResolvedValue({
statusCode: 200,
payload: { variableValues: ['production'] },
});
// Variable with no name — useVariableFetchState will be called with ''
// and the query key will have an empty name, leaving it disabled
const variable = createVariable({ name: '' });
// Note: we do NOT put it in 'loading' state since name is empty
// (no variableFetchStore entry for '' means isVariableFetching=false)
const queryClient = createTestQueryClient();
render(
<Wrapper queryClient={queryClient}>
<QueryVariableInput
variableData={variable}
existingVariables={{ 'env-id': variable }}
onValueUpdate={mockOnValueUpdate}
/>
</Wrapper>,
);
expect(dashboardVariablesQuery).not.toHaveBeenCalled();
expect(mockOnValueUpdate).not.toHaveBeenCalled();
});
});

View File

@@ -46,6 +46,9 @@ interface UseDashboardVariableSelectHelperReturn {
applyDefaultIfNeeded: (
overrideOptions?: (string | number | boolean)[],
) => void;
getDefaultValue: (
overrideOptions?: (string | number | boolean)[],
) => string | string[] | undefined;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
@@ -248,5 +251,6 @@ export function useDashboardVariableSelectHelper({
defaultValue,
onChange,
applyDefaultIfNeeded,
getDefaultValue,
};
}

View File

@@ -121,9 +121,23 @@ function BodyTitleRenderer({
return (
<TitleWrapper onClick={handleNodeClick}>
{typeof value !== 'object' && (
<Dropdown menu={menu} trigger={['click']}>
<SettingOutlined style={{ marginRight: 8 }} className="hover-reveal" />
</Dropdown>
<span
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
}}
onMouseDown={(e): void => e.preventDefault()}
>
<Dropdown
menu={menu}
trigger={['click']}
dropdownRender={(originNode): React.ReactNode => (
<div data-log-detail-ignore="true">{originNode}</div>
)}
>
<SettingOutlined style={{ marginRight: 8 }} className="hover-reveal" />
</Dropdown>
</span>
)}
{title.toString()}{' '}
{!parentIsArray && typeof value !== 'object' && (

View File

@@ -13,7 +13,7 @@ function FieldRenderer({ field }: FieldRendererProps): JSX.Element {
<span className="field-renderer-container">
{dataType && newField && logType ? (
<>
<Tooltip placement="left" title={newField}>
<Tooltip placement="left" title={newField} mouseLeaveDelay={0}>
<Typography.Text ellipsis className="label">
{newField}{' '}
</Typography.Text>

View File

@@ -1,6 +1,7 @@
import { ReactNode, useState } from 'react';
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import type { InputRef } from 'antd';
import {
Button,
Collapse,
@@ -46,12 +47,23 @@ function Overview({
handleChangeSelectedView,
}: Props): JSX.Element {
const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(false);
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(true);
const [isAttributesExpanded, setIsAttributesExpanded] = useState<boolean>(
true,
);
const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
const focusTimerRef = useRef<ReturnType<typeof setTimeout>>();
const searchInputRef = useCallback((node: InputRef | null) => {
clearTimeout(focusTimerRef.current);
if (node) {
focusTimerRef.current = setTimeout(() => node.focus(), 100);
}
}, []);
useEffect(() => (): void => clearTimeout(focusTimerRef.current), []);
const isDarkMode = useIsDarkMode();
const options: EditorProps['options'] = {
@@ -196,7 +208,7 @@ function Overview({
<>
{isSearchVisible && (
<Input
autoFocus
ref={searchInputRef}
placeholder="Search for a field..."
className="search-input"
value={fieldSearchInput}

View File

@@ -245,7 +245,7 @@ function TableView({
<Typography.Text>{renderedField}</Typography.Text>
{traceId && (
<Tooltip title="Inspect in Trace">
<Tooltip title="Inspect in Trace" mouseLeaveDelay={0}>
<Button
className="periscope-btn"
onClick={(

View File

@@ -0,0 +1,34 @@
import { Color } from '@signozhq/design-tokens';
import { getColorsForSeverityLabels, isRedLike } from '../utils';
describe('getColorsForSeverityLabels', () => {
it('should return slate for blank labels', () => {
expect(getColorsForSeverityLabels('', 0)).toBe(Color.BG_SLATE_300);
expect(getColorsForSeverityLabels(' ', 0)).toBe(Color.BG_SLATE_300);
});
it('should return correct colors for known severity variants', () => {
expect(getColorsForSeverityLabels('INFO', 0)).toBe(Color.BG_ROBIN_600);
expect(getColorsForSeverityLabels('ERROR', 0)).toBe(Color.BG_CHERRY_600);
expect(getColorsForSeverityLabels('WARN', 0)).toBe(Color.BG_AMBER_600);
expect(getColorsForSeverityLabels('DEBUG', 0)).toBe(Color.BG_AQUA_600);
expect(getColorsForSeverityLabels('TRACE', 0)).toBe(Color.BG_FOREST_600);
expect(getColorsForSeverityLabels('FATAL', 0)).toBe(Color.BG_SAKURA_600);
});
it('should return non-red colors for unrecognized labels at any index', () => {
for (let i = 0; i < 30; i++) {
const color = getColorsForSeverityLabels('4', i);
expect(isRedLike(color)).toBe(false);
}
});
it('should return non-red colors for numeric severity text', () => {
const numericLabels = ['1', '2', '4', '9', '13', '17', '21'];
numericLabels.forEach((label) => {
const color = getColorsForSeverityLabels(label, 0);
expect(isRedLike(color)).toBe(false);
});
});
});

View File

@@ -1,7 +1,16 @@
import { Color } from '@signozhq/design-tokens';
import { themeColors } from 'constants/theme';
import { colors } from 'lib/getRandomColor';
// Function to determine if a color is "red-like" based on its RGB values
export function isRedLike(hex: string): boolean {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return r > 180 && r > g * 1.4 && r > b * 1.4;
}
const SAFE_FALLBACK_COLORS = colors.filter((c) => !isRedLike(c));
const SEVERITY_VARIANT_COLORS: Record<string, string> = {
TRACE: Color.BG_FOREST_600,
Trace: Color.BG_FOREST_500,
@@ -67,8 +76,13 @@ export function getColorsForSeverityLabels(
label: string,
index: number,
): string {
// Check if we have a direct mapping for this severity variant
const variantColor = SEVERITY_VARIANT_COLORS[label.trim()];
const trimmed = label.trim();
if (!trimmed) {
return Color.BG_SLATE_300;
}
const variantColor = SEVERITY_VARIANT_COLORS[trimmed];
if (variantColor) {
return variantColor;
}
@@ -103,5 +117,8 @@ export function getColorsForSeverityLabels(
return Color.BG_SAKURA_500;
}
return colors[index % colors.length] || themeColors.red;
return (
SAFE_FALLBACK_COLORS[index % SAFE_FALLBACK_COLORS.length] ||
Color.BG_SLATE_400
);
}

View File

@@ -111,23 +111,19 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
);
const itemContent = useCallback(
(index: number, log: Record<string, unknown>): JSX.Element => {
return (
<div key={log.id as string}>
<TableRow
tableColumns={tableColumns}
index={index}
log={log}
logs={tableViewProps.logs}
hasActions
fontSize={tableViewProps.fontSize}
onShowLogDetails={onSetActiveLog}
isActiveLog={activeLog?.id === log.id}
onClearActiveLog={onCloseActiveLog}
/>
</div>
);
},
(index: number, log: Record<string, unknown>): JSX.Element => (
<TableRow
tableColumns={tableColumns}
index={index}
log={log}
logs={tableViewProps.logs}
hasActions
fontSize={tableViewProps.fontSize}
onShowLogDetails={onSetActiveLog}
isActiveLog={activeLog?.id === log.id}
onClearActiveLog={onCloseActiveLog}
/>
),
[
tableColumns,
onSetActiveLog,

View File

@@ -0,0 +1,94 @@
import { act, renderHook } from '@testing-library/react';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useIsTextSelected } from 'hooks/useIsTextSelected';
import { ILog } from 'types/api/logs/log';
import useLogDetailHandlers from '../useLogDetailHandlers';
jest.mock('hooks/logs/useActiveLog');
jest.mock('hooks/useIsTextSelected');
const mockOnSetActiveLog = jest.fn();
const mockOnClearActiveLog = jest.fn();
const mockOnAddToQuery = jest.fn();
const mockOnGroupByAttribute = jest.fn();
const mockIsTextSelected = jest.fn();
const mockLog: ILog = {
id: 'log-1',
timestamp: '2024-01-01T00:00:00Z',
date: '2024-01-01',
body: 'test log body',
severityText: 'INFO',
severityNumber: 9,
traceFlags: 0,
traceId: '',
spanID: '',
attributesString: {},
attributesInt: {},
attributesFloat: {},
resources_string: {},
scope_string: {},
attributes_string: {},
severity_text: '',
severity_number: 0,
};
beforeEach(() => {
jest.clearAllMocks();
jest.mocked(useIsTextSelected).mockReturnValue(mockIsTextSelected);
jest.mocked(useActiveLog).mockReturnValue({
activeLog: null,
onSetActiveLog: mockOnSetActiveLog,
onClearActiveLog: mockOnClearActiveLog,
onAddToQuery: mockOnAddToQuery,
onGroupByAttribute: mockOnGroupByAttribute,
});
});
it('should not open log detail when text is selected', () => {
mockIsTextSelected.mockReturnValue(true);
const { result } = renderHook(() => useLogDetailHandlers());
act(() => {
result.current.handleSetActiveLog(mockLog);
});
expect(mockOnSetActiveLog).not.toHaveBeenCalled();
});
it('should open log detail when no text is selected', () => {
mockIsTextSelected.mockReturnValue(false);
const { result } = renderHook(() => useLogDetailHandlers());
act(() => {
result.current.handleSetActiveLog(mockLog);
});
expect(mockOnSetActiveLog).toHaveBeenCalledWith(mockLog);
});
it('should toggle off when clicking the same active log', () => {
mockIsTextSelected.mockReturnValue(false);
jest.mocked(useActiveLog).mockReturnValue({
activeLog: mockLog,
onSetActiveLog: mockOnSetActiveLog,
onClearActiveLog: mockOnClearActiveLog,
onAddToQuery: mockOnAddToQuery,
onGroupByAttribute: mockOnGroupByAttribute,
});
const { result } = renderHook(() => useLogDetailHandlers());
act(() => {
result.current.handleSetActiveLog(mockLog);
});
expect(mockOnClearActiveLog).toHaveBeenCalled();
expect(mockOnSetActiveLog).not.toHaveBeenCalled();
});

View File

@@ -1,15 +1,17 @@
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQueryClient } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { OPERATORS, QueryBuilderKeys } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { MetricsType } from 'container/MetricsApplication/constant';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
import { AppState } from 'store/reducers';
@@ -54,6 +56,20 @@ export const useActiveLog = (): UseActiveLog => {
const [activeLog, setActiveLog] = useState<ILog | null>(null);
// Close drawer/clear active log when query in URL changes
const urlQuery = useUrlQuery();
const compositeQuery = urlQuery.get(QueryParams.compositeQuery) ?? '';
const prevQueryRef = useRef<string | null>(null);
useEffect(() => {
if (
prevQueryRef.current !== null &&
prevQueryRef.current !== compositeQuery
) {
setActiveLog(null);
}
prevQueryRef.current = compositeQuery;
}, [compositeQuery]);
const onSetDetailedLogData = useCallback(
(logData: ILog) => {
dispatch({

View File

@@ -2,6 +2,7 @@ import { useCallback, useState } from 'react';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import type { UseActiveLog } from 'hooks/logs/types';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useIsTextSelected } from 'hooks/useIsTextSelected';
import { ILog } from 'types/api/logs/log';
type SelectedTab = typeof VIEW_TYPES[keyof typeof VIEW_TYPES] | undefined;
@@ -28,9 +29,13 @@ function useLogDetailHandlers({
onAddToQuery,
} = useActiveLog();
const [selectedTab, setSelectedTab] = useState<SelectedTab>(defaultTab);
const isTextSelected = useIsTextSelected();
const handleSetActiveLog = useCallback(
(log: ILog, nextTab: SelectedTab = defaultTab): void => {
if (isTextSelected()) {
return;
}
if (activeLog?.id === log.id) {
onClearActiveLog();
setSelectedTab(undefined);
@@ -39,7 +44,7 @@ function useLogDetailHandlers({
onSetActiveLog(log);
setSelectedTab(nextTab ?? defaultTab);
},
[activeLog?.id, defaultTab, onClearActiveLog, onSetActiveLog],
[activeLog?.id, defaultTab, onClearActiveLog, onSetActiveLog, isTextSelected],
);
const handleCloseLogDetail = useCallback((): void => {

View File

@@ -0,0 +1,10 @@
import { useCallback } from 'react';
export function useIsTextSelected(): () => boolean {
return useCallback((): boolean => {
const selection = window.getSelection();
return (
!!selection && !selection.isCollapsed && selection.toString().length > 0
);
}, []);
}

View File

@@ -3,7 +3,7 @@ package formatter
import (
"fmt"
"github.com/SigNoz/signoz/pkg/converter"
"github.com/SigNoz/signoz/pkg/query-service/converter"
"github.com/dustin/go-humanize"
)

View File

@@ -3,7 +3,7 @@ package formatter
import (
"fmt"
"github.com/SigNoz/signoz/pkg/converter"
"github.com/SigNoz/signoz/pkg/query-service/converter"
"github.com/dustin/go-humanize"
)

View File

@@ -78,8 +78,9 @@ func toFixed(value float64, decimals DecimalCount) string {
}
decimalPos := strings.Index(formatted, ".")
precision := 0
if decimalPos != -1 {
precision := len(formatted) - decimalPos - 1
precision = len(formatted) - decimalPos - 1
if precision < *decimals {
return formatted + strings.Repeat("0", *decimals-precision)
}
@@ -88,8 +89,8 @@ func toFixed(value float64, decimals DecimalCount) string {
return formatted
}
func toFixedScaled(value float64, scaleFormat string) string {
return toFixed(value, nil) + scaleFormat
func toFixedScaled(value float64, decimals DecimalCount, scaleFormat string) string {
return toFixed(value, decimals) + scaleFormat
}
func getDecimalsForValue(value float64) int {

View File

@@ -13,35 +13,35 @@ func (*throughputFormatter) Name() string {
return "throughput"
}
func simpleCountUnit(value float64, symbol string) string {
func simpleCountUnit(value float64, decimals *int, symbol string) string {
units := []string{"", "K", "M", "B", "T"}
scaler := scaledUnits(1000, units, 0)
return scaler(value, nil) + " " + symbol
return scaler(value, decimals) + " " + symbol
}
func (f *throughputFormatter) Format(value float64, unit string) string {
switch unit {
case "cps", "{count}/s":
return simpleCountUnit(value, "c/s")
return simpleCountUnit(value, nil, "c/s")
case "ops", "{ops}/s":
return simpleCountUnit(value, "op/s")
return simpleCountUnit(value, nil, "op/s")
case "reqps", "{req}/s":
return simpleCountUnit(value, "req/s")
return simpleCountUnit(value, nil, "req/s")
case "rps", "{read}/s":
return simpleCountUnit(value, "r/s")
return simpleCountUnit(value, nil, "r/s")
case "wps", "{write}/s":
return simpleCountUnit(value, "w/s")
return simpleCountUnit(value, nil, "w/s")
case "iops", "{iops}/s":
return simpleCountUnit(value, "iops")
return simpleCountUnit(value, nil, "iops")
case "cpm", "{count}/min":
return simpleCountUnit(value, "c/m")
return simpleCountUnit(value, nil, "c/m")
case "opm", "{ops}/min":
return simpleCountUnit(value, "op/m")
return simpleCountUnit(value, nil, "op/m")
case "rpm", "{read}/min":
return simpleCountUnit(value, "r/m")
return simpleCountUnit(value, nil, "r/m")
case "wpm", "{write}/min":
return simpleCountUnit(value, "w/m")
return simpleCountUnit(value, nil, "w/m")
}
// When unit is not matched, return the value as it is.
return fmt.Sprintf("%v", value)

View File

@@ -46,17 +46,17 @@ func toNanoSeconds(value float64) string {
if absValue < 1000 {
return toFixed(value, nil) + " ns"
} else if absValue < 1000000 { // 2000 ns is better represented as 2 µs
return toFixedScaled(value/1000, " µs")
return toFixedScaled(value/1000, nil, " µs")
} else if absValue < 1000000000 { // 2000000 ns is better represented as 2 ms
return toFixedScaled(value/1000000, " ms")
return toFixedScaled(value/1000000, nil, " ms")
} else if absValue < 60000000000 {
return toFixedScaled(value/1000000000, " s")
return toFixedScaled(value/1000000000, nil, " s")
} else if absValue < 3600000000000 {
return toFixedScaled(value/60000000000, " min")
return toFixedScaled(value/60000000000, nil, " min")
} else if absValue < 86400000000000 {
return toFixedScaled(value/3600000000000, " hour")
return toFixedScaled(value/3600000000000, nil, " hour")
} else {
return toFixedScaled(value/86400000000000, " day")
return toFixedScaled(value/86400000000000, nil, " day")
}
}
@@ -66,9 +66,9 @@ func toMicroSeconds(value float64) string {
if absValue < 1000 {
return toFixed(value, nil) + " µs"
} else if absValue < 1000000 { // 2000 µs is better represented as 2 ms
return toFixedScaled(value/1000, " ms")
return toFixedScaled(value/1000, nil, " ms")
} else {
return toFixedScaled(value/1000000, " s")
return toFixedScaled(value/1000000, nil, " s")
}
}
@@ -80,16 +80,16 @@ func toMilliSeconds(value float64) string {
if absValue < 1000 {
return toFixed(value, nil) + " ms"
} else if absValue < 60000 {
return toFixedScaled(value/1000, " s")
return toFixedScaled(value/1000, nil, " s")
} else if absValue < 3600000 {
return toFixedScaled(value/60000, " min")
return toFixedScaled(value/60000, nil, " min")
} else if absValue < 86400000 { // 172800000 ms is better represented as 2 day
return toFixedScaled(value/3600000, " hour")
return toFixedScaled(value/3600000, nil, " hour")
} else if absValue < 31536000000 {
return toFixedScaled(value/86400000, " day")
return toFixedScaled(value/86400000, nil, " day")
}
return toFixedScaled(value/31536000000, " year")
return toFixedScaled(value/31536000000, nil, " year")
}
// toSeconds returns a easy to read string representation of the given value in seconds
@@ -97,24 +97,24 @@ func toSeconds(value float64) string {
absValue := math.Abs(value)
if absValue < 0.000001 {
return toFixedScaled(value*1e9, " ns")
return toFixedScaled(value*1e9, nil, " ns")
} else if absValue < 0.001 {
return toFixedScaled(value*1e6, " µs")
return toFixedScaled(value*1e6, nil, " µs")
} else if absValue < 1 {
return toFixedScaled(value*1e3, " ms")
return toFixedScaled(value*1e3, nil, " ms")
} else if absValue < 60 {
return toFixed(value, nil) + " s"
} else if absValue < 3600 {
return toFixedScaled(value/60, " min")
return toFixedScaled(value/60, nil, " min")
} else if absValue < 86400 { // 56000 s is better represented as 15.56 hour
return toFixedScaled(value/3600, " hour")
return toFixedScaled(value/3600, nil, " hour")
} else if absValue < 604800 {
return toFixedScaled(value/86400, " day")
return toFixedScaled(value/86400, nil, " day")
} else if absValue < 31536000 {
return toFixedScaled(value/604800, " week")
return toFixedScaled(value/604800, nil, " week")
}
return toFixedScaled(value/3.15569e7, " year")
return toFixedScaled(value/3.15569e7, nil, " year")
}
// toMinutes returns a easy to read string representation of the given value in minutes
@@ -124,13 +124,13 @@ func toMinutes(value float64) string {
if absValue < 60 {
return toFixed(value, nil) + " min"
} else if absValue < 1440 {
return toFixedScaled(value/60, " hour")
return toFixedScaled(value/60, nil, " hour")
} else if absValue < 10080 {
return toFixedScaled(value/1440, " day")
return toFixedScaled(value/1440, nil, " day")
} else if absValue < 604800 {
return toFixedScaled(value/10080, " week")
return toFixedScaled(value/10080, nil, " week")
} else {
return toFixedScaled(value/5.25948e5, " year")
return toFixedScaled(value/5.25948e5, nil, " year")
}
}
@@ -142,11 +142,11 @@ func toHours(value float64) string {
if absValue < 24 {
return toFixed(value, nil) + " hour"
} else if absValue < 168 {
return toFixedScaled(value/24, " day")
return toFixedScaled(value/24, nil, " day")
} else if absValue < 8760 {
return toFixedScaled(value/168, " week")
return toFixedScaled(value/168, nil, " week")
} else {
return toFixedScaled(value/8760, " year")
return toFixedScaled(value/8760, nil, " year")
}
}
@@ -157,9 +157,9 @@ func toDays(value float64) string {
if absValue < 7 {
return toFixed(value, nil) + " day"
} else if absValue < 365 {
return toFixedScaled(value/7, " week")
return toFixedScaled(value/7, nil, " week")
} else {
return toFixedScaled(value/365, " year")
return toFixedScaled(value/365, nil, " year")
}
}
@@ -170,6 +170,6 @@ func toWeeks(value float64) string {
if absValue < 52 {
return toFixed(value, nil) + " week"
} else {
return toFixedScaled(value/52, " year")
return toFixedScaled(value/52, nil, " year")
}
}

View File

@@ -9,7 +9,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/formatter"
"github.com/SigNoz/signoz/pkg/query-service/formatter"
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
"github.com/SigNoz/signoz/pkg/query-service/model"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"

View File

@@ -33,7 +33,7 @@ import (
logsv3 "github.com/SigNoz/signoz/pkg/query-service/app/logs/v3"
tracesV4 "github.com/SigNoz/signoz/pkg/query-service/app/traces/v4"
"github.com/SigNoz/signoz/pkg/formatter"
"github.com/SigNoz/signoz/pkg/query-service/formatter"
querierV5 "github.com/SigNoz/signoz/pkg/querier"

View File

@@ -7,7 +7,7 @@ import (
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/converter"
"github.com/SigNoz/signoz/pkg/query-service/converter"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
"github.com/SigNoz/signoz/pkg/valuer"