mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-20 15:20:31 +01:00
Compare commits
1 Commits
feat/warni
...
fix/metada
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7f4a3b900 |
@@ -20,35 +20,6 @@ function CalendarContainer({
|
||||
}): JSX.Element {
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
// this is to override the default behavior of the shadcn calendar component
|
||||
// if a range is already selected, clicking on a date will reset selection and set the new date as the start date
|
||||
const handleSelect = (
|
||||
_selected: DateRange | undefined,
|
||||
clickedDate?: Date,
|
||||
): void => {
|
||||
if (!clickedDate) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No dates selected → start new
|
||||
if (!dateRange?.from) {
|
||||
onSelectDateRange({ from: clickedDate });
|
||||
return;
|
||||
}
|
||||
|
||||
// Only start selected → complete the range
|
||||
if (dateRange.from && !dateRange.to) {
|
||||
if (clickedDate < dateRange.from) {
|
||||
onSelectDateRange({ from: clickedDate, to: dateRange.from });
|
||||
} else {
|
||||
onSelectDateRange({ from: dateRange.from, to: clickedDate });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
onSelectDateRange({ from: clickedDate, to: undefined });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="calendar-container">
|
||||
<div className="calendar-container-header">
|
||||
@@ -73,7 +44,7 @@ function CalendarContainer({
|
||||
disabled={{
|
||||
after: dayjs().toDate(),
|
||||
}}
|
||||
onSelect={handleSelect}
|
||||
onSelect={onSelectDateRange}
|
||||
/>
|
||||
|
||||
<div className="calendar-actions">
|
||||
|
||||
@@ -1,3 +1,47 @@
|
||||
.settings-container-root {
|
||||
.ant-drawer-wrapper-body {
|
||||
border-left: 1px solid var(--bg-slate-500);
|
||||
background: var(--bg-ink-400);
|
||||
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.ant-drawer-header {
|
||||
height: 48px;
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
padding: 14px 14px 14px 11px;
|
||||
|
||||
.ant-drawer-header-title {
|
||||
gap: 16px;
|
||||
|
||||
.ant-drawer-title {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid #161922;
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-inline-end: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
padding: 16px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-description-container {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
@@ -532,6 +576,28 @@
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.settings-container-root {
|
||||
.ant-drawer-wrapper-body {
|
||||
border-left: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.ant-drawer-header {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
.ant-drawer-header-title {
|
||||
.ant-drawer-title {
|
||||
color: var(--bg-ink-400);
|
||||
border-left: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
color: var(--bg-ink-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-description-container {
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import './Description.styles.scss';
|
||||
|
||||
import { Button } from 'antd';
|
||||
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import DashboardSettingsContent from '../DashboardSettings';
|
||||
import { DrawerContainer } from './styles';
|
||||
|
||||
function SettingsDrawer({ drawerTitle }: { drawerTitle: string }): JSX.Element {
|
||||
const [visible, setVisible] = useState<boolean>(false);
|
||||
|
||||
const variableViewModeRef = useRef<() => void>();
|
||||
|
||||
const showDrawer = (): void => {
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
setVisible(false);
|
||||
variableViewModeRef?.current?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="text"
|
||||
className="configure-button"
|
||||
icon={<ConfigureIcon />}
|
||||
data-testid="show-drawer"
|
||||
onClick={showDrawer}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
|
||||
<DrawerContainer
|
||||
title={drawerTitle}
|
||||
placement="right"
|
||||
width="50%"
|
||||
onClose={handleClose}
|
||||
open={visible}
|
||||
rootClassName="settings-container-root"
|
||||
>
|
||||
<OverlayScrollbar>
|
||||
<DashboardSettingsContent variableViewModeRef={variableViewModeRef} />
|
||||
</OverlayScrollbar>
|
||||
</DrawerContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsDrawer;
|
||||
@@ -1,67 +0,0 @@
|
||||
.settings-container-root {
|
||||
.ant-drawer-wrapper-body {
|
||||
border-left: 1px solid var(--bg-slate-500);
|
||||
background: var(--bg-ink-400);
|
||||
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.ant-drawer-header {
|
||||
height: 48px;
|
||||
border-bottom: 1px solid var(--bg-slate-500);
|
||||
padding: 14px 14px 14px 11px;
|
||||
|
||||
.ant-drawer-header-title {
|
||||
gap: 16px;
|
||||
|
||||
.ant-drawer-title {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid var(--bg-slate-500);
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-inline-end: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
padding: 16px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.settings-container-root {
|
||||
.ant-drawer-wrapper-body {
|
||||
border-left: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.ant-drawer-header {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
.ant-drawer-header-title {
|
||||
.ant-drawer-title {
|
||||
color: var(--bg-ink-400);
|
||||
border-left: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
color: var(--bg-ink-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import './SettingsDrawer.styles.scss';
|
||||
|
||||
import { Drawer } from 'antd';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { memo, PropsWithChildren, ReactElement } from 'react';
|
||||
|
||||
type SettingsDrawerProps = PropsWithChildren<{
|
||||
drawerTitle: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}>;
|
||||
|
||||
function SettingsDrawer({
|
||||
children,
|
||||
drawerTitle,
|
||||
isOpen,
|
||||
onClose,
|
||||
}: SettingsDrawerProps): JSX.Element {
|
||||
return (
|
||||
<Drawer
|
||||
title={drawerTitle}
|
||||
placement="right"
|
||||
width="50%"
|
||||
onClose={onClose}
|
||||
open={isOpen}
|
||||
rootClassName="settings-container-root"
|
||||
>
|
||||
{/* Need to type cast because of OverlayScrollbar type definition. We should be good once we remove it. */}
|
||||
<OverlayScrollbar>{children as ReactElement}</OverlayScrollbar>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(SettingsDrawer);
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -41,7 +40,7 @@ import {
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { sortLayout } from 'providers/Dashboard/util';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { FullScreenHandle } from 'react-full-screen';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -53,11 +52,9 @@ import { ComponentTypes } from 'utils/permission';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import DashboardGraphSlider from '../ComponentsSlider';
|
||||
import DashboardSettings from '../DashboardSettings';
|
||||
import { Base64Icons } from '../DashboardSettings/General/utils';
|
||||
import DashboardVariableSelection from '../DashboardVariablesSelection';
|
||||
import SettingsDrawer from './SettingsDrawer';
|
||||
import { VariablesSettingsTab } from './types';
|
||||
import { DEFAULT_ROW_NAME, downloadObjectAsJson } from './utils';
|
||||
|
||||
interface DashboardDescriptionProps {
|
||||
@@ -104,11 +101,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
handleDashboardLockToggle,
|
||||
} = useDashboard();
|
||||
|
||||
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
|
||||
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
|
||||
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
|
||||
|
||||
const isPublicDashboardEnabled = isCloudUser || isEnterpriseSelfHostedUser;
|
||||
@@ -348,18 +340,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
publicDashboardResponse?.data,
|
||||
]);
|
||||
|
||||
const onConfigureClick = useCallback((): void => {
|
||||
setIsSettingsDrawerOpen(true);
|
||||
}, []);
|
||||
|
||||
const onSettingsDrawerClose = useCallback((): void => {
|
||||
setIsSettingsDrawerOpen(false);
|
||||
// good use case for a state library like Jotai
|
||||
if (variablesSettingsTabHandle.current) {
|
||||
variablesSettingsTabHandle.current.resetState();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card className="dashboard-description-container">
|
||||
<div className="dashboard-header">
|
||||
@@ -524,26 +504,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
/>
|
||||
</Popover>
|
||||
{!isDashboardLocked && editDashboard && (
|
||||
<>
|
||||
<Button
|
||||
type="text"
|
||||
className="configure-button"
|
||||
icon={<ConfigureIcon />}
|
||||
data-testid="show-drawer"
|
||||
onClick={onConfigureClick}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
<SettingsDrawer
|
||||
drawerTitle="Dashboard Configuration"
|
||||
isOpen={isSettingsDrawerOpen}
|
||||
onClose={onSettingsDrawerClose}
|
||||
>
|
||||
<DashboardSettings
|
||||
variablesSettingsTabHandle={variablesSettingsTabHandle}
|
||||
/>
|
||||
</SettingsDrawer>
|
||||
</>
|
||||
<SettingsDrawer drawerTitle="Dashboard Configuration" />
|
||||
)}
|
||||
{!isDashboardLocked && addPanelPermission && (
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Button as ButtonComponent, Drawer } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
margin-top: 0.5rem;
|
||||
`;
|
||||
|
||||
export const Button = styled(ButtonComponent)`
|
||||
&&& {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DrawerContainer = styled(Drawer)`
|
||||
.ant-drawer-header {
|
||||
padding: 16px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
padding-top: 0;
|
||||
}
|
||||
`;
|
||||
@@ -1,7 +0,0 @@
|
||||
import { MutableRefObject } from 'react';
|
||||
|
||||
export interface VariablesSettingsTab {
|
||||
resetState: () => void;
|
||||
}
|
||||
|
||||
export type VariablesSettingsTabHandle = MutableRefObject<VariablesSettingsTab | null>;
|
||||
@@ -14,7 +14,6 @@ import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Button, Modal, Row, Space, Table, Typography } from 'antd';
|
||||
import { RowProps } from 'antd/lib';
|
||||
import { VariablesSettingsTabHandle } from 'container/DashboardContainer/DashboardDescription/types';
|
||||
import { convertVariablesToDbFormat } from 'container/DashboardContainer/DashboardVariablesSelection/util';
|
||||
import { useAddDynamicVariableToPanels } from 'hooks/dashboard/useAddDynamicVariableToPanels';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
@@ -78,10 +77,10 @@ function TableRow({ children, ...props }: RowProps): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
function VariablesSettings({
|
||||
variablesSettingsTabHandle,
|
||||
function VariablesSetting({
|
||||
variableViewModeRef,
|
||||
}: {
|
||||
variablesSettingsTabHandle: VariablesSettingsTabHandle;
|
||||
variableViewModeRef: React.MutableRefObject<(() => void) | undefined>;
|
||||
}): JSX.Element {
|
||||
const variableToDelete = useRef<IDashboardVariable | null>(null);
|
||||
const [deleteVariableModal, setDeleteVariableModal] = useState(false);
|
||||
@@ -127,13 +126,11 @@ function VariablesSettings({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!variablesSettingsTabHandle) return;
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
variablesSettingsTabHandle.current = {
|
||||
resetState: onDoneVariableViewMode,
|
||||
};
|
||||
}, [variablesSettingsTabHandle]);
|
||||
if (variableViewModeRef) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
variableViewModeRef.current = onDoneVariableViewMode;
|
||||
}
|
||||
}, [variableViewModeRef]);
|
||||
|
||||
const updateMutation = useUpdateDashboard();
|
||||
|
||||
@@ -513,4 +510,4 @@ function VariablesSettings({
|
||||
);
|
||||
}
|
||||
|
||||
export default VariablesSettings;
|
||||
export default VariablesSetting;
|
||||
@@ -6,15 +6,14 @@ import { Braces, Globe, Table } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import { VariablesSettingsTabHandle } from '../DashboardDescription/types';
|
||||
import DashboardVariableSettings from './DashboardVariableSettings';
|
||||
import GeneralDashboardSettings from './General';
|
||||
import PublicDashboardSetting from './PublicDashboard';
|
||||
import VariablesSetting from './Variables';
|
||||
|
||||
function DashboardSettings({
|
||||
variablesSettingsTabHandle,
|
||||
function DashboardSettingsContent({
|
||||
variableViewModeRef,
|
||||
}: {
|
||||
variablesSettingsTabHandle: VariablesSettingsTabHandle;
|
||||
variableViewModeRef: React.MutableRefObject<(() => void) | undefined>;
|
||||
}): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
|
||||
@@ -64,11 +63,7 @@ function DashboardSettings({
|
||||
</Button>
|
||||
),
|
||||
key: 'variables',
|
||||
children: (
|
||||
<DashboardVariableSettings
|
||||
variablesSettingsTabHandle={variablesSettingsTabHandle}
|
||||
/>
|
||||
),
|
||||
children: <VariablesSetting variableViewModeRef={variableViewModeRef} />,
|
||||
},
|
||||
...(enablePublicDashboard ? [publicDashboardItem] : []),
|
||||
];
|
||||
@@ -76,4 +71,4 @@ function DashboardSettings({
|
||||
return <Tabs items={items} animated className="settings-tabs" />;
|
||||
}
|
||||
|
||||
export default DashboardSettings;
|
||||
export default DashboardSettingsContent;
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from 'types/api/dashboard/getAll';
|
||||
|
||||
import { useAddDynamicVariableToPanels } from '../../../hooks/dashboard/useAddDynamicVariableToPanels';
|
||||
import { WidgetSelector } from '../DashboardSettings/DashboardVariableSettings/VariableItem/WidgetSelector';
|
||||
import { WidgetSelector } from '../DashboardSettings/Variables/VariableItem/WidgetSelector';
|
||||
|
||||
// Mock scrollIntoView since it's not available in JSDOM
|
||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Provider } from 'react-redux';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import VariableItem from '../DashboardSettings/DashboardVariableSettings/VariableItem/VariableItem';
|
||||
import VariableItem from '../DashboardSettings/Variables/VariableItem/VariableItem';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('api/dashboard/variables/dashboardVariablesQuery');
|
||||
|
||||
@@ -4,14 +4,11 @@ import './DashboardEmptyState.styles.scss';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
|
||||
import SettingsDrawer from 'container/DashboardContainer/DashboardDescription/SettingsDrawer';
|
||||
import { VariablesSettingsTab } from 'container/DashboardContainer/DashboardDescription/types';
|
||||
import DashboardSettings from 'container/DashboardContainer/DashboardSettings';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { ComponentTypes } from 'utils/permission';
|
||||
|
||||
@@ -23,11 +20,6 @@ export default function DashboardEmptyState(): JSX.Element {
|
||||
setSelectedRowWidgetId,
|
||||
} = useDashboard();
|
||||
|
||||
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
|
||||
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
|
||||
const { user } = useAppContext();
|
||||
let permissions: ComponentTypes[] = ['add_panel'];
|
||||
|
||||
@@ -52,19 +44,6 @@ export default function DashboardEmptyState(): JSX.Element {
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [handleToggleDashboardSlider]);
|
||||
|
||||
const onConfigureClick = useCallback((): void => {
|
||||
setIsSettingsDrawerOpen(true);
|
||||
}, []);
|
||||
|
||||
const onSettingsDrawerClose = useCallback((): void => {
|
||||
setIsSettingsDrawerOpen(false);
|
||||
|
||||
if (variablesSettingsTabHandle.current) {
|
||||
variablesSettingsTabHandle.current.resetState();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="dashboard-empty-state">
|
||||
<div className="dashboard-content">
|
||||
@@ -98,26 +77,7 @@ export default function DashboardEmptyState(): JSX.Element {
|
||||
Give it a name, add description, tags and variables
|
||||
</Typography.Text>
|
||||
</div>
|
||||
{/* This Empty State needs to be consolidated. The SettingsDrawer should be global to the
|
||||
whole dashboard page instead of confined to this Empty State */}
|
||||
<Button
|
||||
type="text"
|
||||
className="configure-button"
|
||||
icon={<ConfigureIcon />}
|
||||
data-testid="show-drawer"
|
||||
onClick={onConfigureClick}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
<SettingsDrawer
|
||||
drawerTitle="Dashboard Configuration"
|
||||
isOpen={isSettingsDrawerOpen}
|
||||
onClose={onSettingsDrawerClose}
|
||||
>
|
||||
<DashboardSettings
|
||||
variablesSettingsTabHandle={variablesSettingsTabHandle}
|
||||
/>
|
||||
</SettingsDrawer>
|
||||
<SettingsDrawer drawerTitle="Dashboard Configuration" />
|
||||
</div>
|
||||
<div className="actions-1">
|
||||
<div className="actions-add-panel">
|
||||
|
||||
@@ -92,10 +92,6 @@ function TableView({
|
||||
}
|
||||
});
|
||||
}
|
||||
// pin trace_id by default when present
|
||||
if (logData?.trace_id) {
|
||||
pinnedAttributes.trace_id = true;
|
||||
}
|
||||
|
||||
setPinnedAttributes(pinnedAttributes);
|
||||
}, [
|
||||
|
||||
@@ -330,6 +330,14 @@
|
||||
}
|
||||
|
||||
.periscope-calendar-day {
|
||||
background: none !important;
|
||||
|
||||
&.periscope-calendar-today {
|
||||
&.text-accent-foreground {
|
||||
color: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
&:hover {
|
||||
background-color: var(--bg-robin-500) !important;
|
||||
@@ -548,6 +556,12 @@
|
||||
|
||||
.calendar-container {
|
||||
.periscope-calendar-day {
|
||||
&.periscope-calendar-today {
|
||||
&.text-accent-foreground {
|
||||
color: var(--bg-ink-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
&:hover {
|
||||
background-color: var(--bg-robin-500) !important;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Tooltip } from 'antd';
|
||||
import { Clock } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { RefreshTextContainer } from './styles';
|
||||
import { RefreshTextContainer, Typography } from './styles';
|
||||
|
||||
function RefreshText({
|
||||
onLastRefreshHandler,
|
||||
@@ -25,9 +23,7 @@ function RefreshText({
|
||||
|
||||
return (
|
||||
<RefreshTextContainer refreshButtonHidden={refreshButtonHidden}>
|
||||
<Tooltip title={refreshText}>
|
||||
<Clock size={12} />
|
||||
</Tooltip>
|
||||
<Typography>{refreshText}</Typography>
|
||||
</RefreshTextContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { addTagFiltersToDashboard } from 'container/DashboardContainer/DashboardSettings/DashboardVariableSettings/addTagFiltersToDashboard';
|
||||
import { addTagFiltersToDashboard } from 'container/DashboardContainer/DashboardSettings/Variables/addTagFiltersToDashboard';
|
||||
import { useCallback } from 'react';
|
||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -491,18 +491,16 @@ const fillDataFromList = (
|
||||
listItem: ListItem,
|
||||
columns: DynamicColumns,
|
||||
): void => {
|
||||
const listData = listItem.data || {};
|
||||
columns.forEach((column) => {
|
||||
if (isFormula(column.field)) return;
|
||||
|
||||
Object.keys(listData).forEach((label) => {
|
||||
Object.keys(listItem.data).forEach((label) => {
|
||||
if (column.dataIndex === label) {
|
||||
const listValue = listData[label as ListItemKey];
|
||||
if (listValue !== '') {
|
||||
if (isObject(listValue)) {
|
||||
column.data.push(JSON.stringify(listValue));
|
||||
if (listItem.data[label as ListItemKey] !== '') {
|
||||
if (isObject(listItem.data[label as ListItemKey])) {
|
||||
column.data.push(JSON.stringify(listItem.data[label as ListItemKey]));
|
||||
} else {
|
||||
column.data.push(listValue?.toString() ?? '');
|
||||
column.data.push(listItem.data[label as ListItemKey].toString());
|
||||
}
|
||||
} else {
|
||||
column.data.push('N/A');
|
||||
@@ -540,7 +538,7 @@ const fillDataFromTable = (
|
||||
);
|
||||
|
||||
columns.forEach((column) => {
|
||||
const rowData = row.data || {};
|
||||
const rowData = row.data;
|
||||
const columnField = column.id || column.title || column.field;
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(rowData, columnField)) {
|
||||
|
||||
@@ -17,7 +17,6 @@ export interface ILog {
|
||||
attributesFloat: Record<string, never>;
|
||||
severity_text: string;
|
||||
severity_number: number;
|
||||
trace_id?: string;
|
||||
}
|
||||
|
||||
type OmitAttributesResources = Pick<
|
||||
|
||||
@@ -380,23 +380,6 @@ describe('extractQueryPairs', () => {
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('should treat lowercase exists as non-value operator', () => {
|
||||
const input = 'body exists service.name contains "test"';
|
||||
const result = extractQueryPairs(input);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].key).toBe('body');
|
||||
expect(result[0].operator).toBe('exists');
|
||||
expect(result[0].value).toBeUndefined();
|
||||
expect(result[0].valuesPosition).toEqual([]);
|
||||
expect(result[0].isComplete).toBe(false);
|
||||
expect(result[1].key).toBe('service.name');
|
||||
expect(result[1].operator).toBe('contains');
|
||||
expect(result[1].value).toBe('"test"');
|
||||
expect(result[1].valuesPosition).toEqual([]);
|
||||
expect(result[1].isComplete).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createContext', () => {
|
||||
|
||||
@@ -10,12 +10,12 @@ import {
|
||||
isFunctionToken,
|
||||
isKeyToken,
|
||||
isMultiValueOperator,
|
||||
isNonValueOperator,
|
||||
isNonValueOperatorToken,
|
||||
isOperatorToken,
|
||||
isQueryPairComplete,
|
||||
isValueToken,
|
||||
} from './tokenUtils';
|
||||
import { NON_VALUE_OPERATORS } from 'constants/antlrQueryConstants';
|
||||
|
||||
// Function to create a context object
|
||||
export function createContext(
|
||||
@@ -1319,7 +1319,7 @@ export function extractQueryPairs(query: string): IQueryPair[] {
|
||||
currentPair &&
|
||||
currentPair.key &&
|
||||
currentPair.operator &&
|
||||
!isNonValueOperator(currentPair.operator) &&
|
||||
!NON_VALUE_OPERATORS.includes(currentPair.operator) &&
|
||||
!currentPair.value
|
||||
) {
|
||||
currentPair.value = token.text;
|
||||
|
||||
@@ -87,6 +87,16 @@ export function isWrappedUnderQuotes(token: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function isQueryPairComplete(queryPair: Partial<IQueryPair>): boolean {
|
||||
if (!queryPair) return false;
|
||||
// A complete query pair must have a key, an operator, and a value (or EXISTS operator)
|
||||
if (queryPair.operator && NON_VALUE_OPERATORS.includes(queryPair.operator)) {
|
||||
return !!queryPair.key && !!queryPair.operator;
|
||||
}
|
||||
// For other operators, we need a value as well
|
||||
return Boolean(queryPair.key && queryPair.operator && queryPair.value);
|
||||
}
|
||||
|
||||
export function isFunctionOperator(operator: string): boolean {
|
||||
const functionOperators = Object.values(QUERY_BUILDER_FUNCTIONS);
|
||||
|
||||
@@ -124,13 +134,3 @@ export function isNonValueOperator(operator: string): boolean {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isQueryPairComplete(queryPair: Partial<IQueryPair>): boolean {
|
||||
if (!queryPair) return false;
|
||||
// A complete query pair must have a key, an operator, and a value (or EXISTS operator)
|
||||
if (queryPair.operator && isNonValueOperator(queryPair.operator)) {
|
||||
return !!queryPair.key && !!queryPair.operator;
|
||||
}
|
||||
// For other operators, we need a value as well
|
||||
return Boolean(queryPair.key && queryPair.operator && queryPair.value);
|
||||
}
|
||||
|
||||
@@ -490,7 +490,6 @@ func (bc *bucketCache) mergeTimeSeriesValues(ctx context.Context, buckets []*cac
|
||||
key string
|
||||
}
|
||||
seriesMap := make(map[seriesKey]*qbtypes.TimeSeries, estimatedSeries)
|
||||
aliasMap := make(map[int]string)
|
||||
|
||||
for _, bucket := range buckets {
|
||||
var tsData *qbtypes.TimeSeriesData
|
||||
@@ -500,10 +499,6 @@ func (bc *bucketCache) mergeTimeSeriesValues(ctx context.Context, buckets []*cac
|
||||
}
|
||||
|
||||
for _, aggBucket := range tsData.Aggregations {
|
||||
if aggBucket.Alias != "" {
|
||||
aliasMap[aggBucket.Index] = aggBucket.Alias
|
||||
}
|
||||
|
||||
for _, series := range aggBucket.Series {
|
||||
// Create series key from labels
|
||||
key := seriesKey{
|
||||
@@ -576,13 +571,7 @@ func (bc *bucketCache) mergeTimeSeriesValues(ctx context.Context, buckets []*cac
|
||||
}
|
||||
}
|
||||
|
||||
var alias string
|
||||
if aliasMap[index] != "" {
|
||||
alias = aliasMap[index]
|
||||
}
|
||||
|
||||
result.Aggregations = append(result.Aggregations, &qbtypes.AggregationBucket{
|
||||
Alias: alias,
|
||||
Index: index,
|
||||
Series: seriesList,
|
||||
})
|
||||
@@ -747,7 +736,6 @@ func (bc *bucketCache) trimResultToFluxBoundary(result *qbtypes.Result, fluxBoun
|
||||
for _, aggBucket := range tsData.Aggregations {
|
||||
trimmedBucket := &qbtypes.AggregationBucket{
|
||||
Index: aggBucket.Index,
|
||||
Alias: aggBucket.Alias,
|
||||
}
|
||||
|
||||
for _, series := range aggBucket.Series {
|
||||
|
||||
@@ -200,7 +200,7 @@ func (q *builderQuery[T]) Execute(ctx context.Context) (*qbtypes.Result, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result.Warnings = append(result.Warnings, stmt.Warnings...)
|
||||
result.Warnings = stmt.Warnings
|
||||
result.WarningsDocURL = stmt.WarningsDocURL
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -12,23 +12,17 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
const (
|
||||
diagnosticColumnIndexBase = 1000000
|
||||
)
|
||||
|
||||
var (
|
||||
aggRe = regexp.MustCompile(`^__result_(\d+)$`)
|
||||
// legacyReservedColumnTargetAliases identifies result value from a user
|
||||
// written clickhouse query. The column alias indcate which value is
|
||||
// to be considered as final result (or target)
|
||||
legacyReservedColumnTargetAliases = []string{"__result", "__value", "result", "res", "value"}
|
||||
diagnosticColumnAliases = []string{telemetrymetrics.DiagnosticColumnCumulativeHistLeCount, telemetrymetrics.DiagnosticColumnCumulativeHistLeSum}
|
||||
)
|
||||
|
||||
// consume reads every row and shapes it into the payload expected for the
|
||||
@@ -75,7 +69,6 @@ func readAsTimeSeries(rows driver.Rows, queryWindow *qbtypes.TimeRange, step qbt
|
||||
key string // deterministic join of label values
|
||||
}
|
||||
seriesMap := map[sKey]*qbtypes.TimeSeries{}
|
||||
diagnosticSeriesMap := map[string]*qbtypes.TimeSeries{}
|
||||
|
||||
stepMs := uint64(step.Duration.Milliseconds())
|
||||
|
||||
@@ -120,13 +113,12 @@ func readAsTimeSeries(rows driver.Rows, queryWindow *qbtypes.TimeRange, step qbt
|
||||
}
|
||||
|
||||
var (
|
||||
ts int64
|
||||
lblVals = make([]string, 0, lblValsCapacity)
|
||||
lblObjs = make([]*qbtypes.Label, 0, lblValsCapacity)
|
||||
aggValues = map[int]float64{} // all __result_N in this row
|
||||
diagnosticValues = map[string]float64{} // all diagnostic columns in this row
|
||||
fallbackValue float64 // value when NO __result_N columns exist
|
||||
fallbackSeen bool
|
||||
ts int64
|
||||
lblVals = make([]string, 0, lblValsCapacity)
|
||||
lblObjs = make([]*qbtypes.Label, 0, lblValsCapacity)
|
||||
aggValues = map[int]float64{} // all __result_N in this row
|
||||
fallbackValue float64 // value when NO __result_N columns exist
|
||||
fallbackSeen bool
|
||||
)
|
||||
|
||||
for idx, ptr := range slots {
|
||||
@@ -138,9 +130,7 @@ func readAsTimeSeries(rows driver.Rows, queryWindow *qbtypes.TimeRange, step qbt
|
||||
|
||||
case *float64, *float32, *int64, *int32, *uint64, *uint32:
|
||||
val := numericAsFloat(reflect.ValueOf(ptr).Elem().Interface())
|
||||
if slices.Contains(diagnosticColumnAliases, name) {
|
||||
diagnosticValues[name] = val
|
||||
} else if m := aggRe.FindStringSubmatch(name); m != nil {
|
||||
if m := aggRe.FindStringSubmatch(name); m != nil {
|
||||
id, _ := strconv.Atoi(m[1])
|
||||
aggValues[id] = val
|
||||
} else if numericColsCount == 1 { // classic single-value query
|
||||
@@ -162,9 +152,7 @@ func readAsTimeSeries(rows driver.Rows, queryWindow *qbtypes.TimeRange, step qbt
|
||||
tempVal := reflect.ValueOf(ptr)
|
||||
if tempVal.IsValid() && !tempVal.IsNil() && !tempVal.Elem().IsNil() {
|
||||
val := numericAsFloat(tempVal.Elem().Elem().Interface())
|
||||
if slices.Contains(diagnosticColumnAliases, name) {
|
||||
diagnosticValues[name] = val
|
||||
} else if m := aggRe.FindStringSubmatch(name); m != nil {
|
||||
if m := aggRe.FindStringSubmatch(name); m != nil {
|
||||
id, _ := strconv.Atoi(m[1])
|
||||
aggValues[id] = val
|
||||
} else if numericColsCount == 1 { // classic single-value query
|
||||
@@ -207,23 +195,6 @@ func readAsTimeSeries(rows driver.Rows, queryWindow *qbtypes.TimeRange, step qbt
|
||||
}
|
||||
}
|
||||
|
||||
// fetch and store diagnostic values in diagnosticSeriesMap
|
||||
for diagnosticColName, val := range diagnosticValues {
|
||||
if math.IsNaN(val) || math.IsInf(val, 0) {
|
||||
continue
|
||||
}
|
||||
diagSeries, ok := diagnosticSeriesMap[diagnosticColName]
|
||||
if !ok {
|
||||
diagSeries = &qbtypes.TimeSeries{}
|
||||
diagnosticSeriesMap[diagnosticColName] = diagSeries
|
||||
}
|
||||
diagSeries.Values = append(diagSeries.Values, &qbtypes.TimeSeriesValue{
|
||||
Timestamp: ts,
|
||||
Value: val,
|
||||
Partial: isPartialValue(ts),
|
||||
})
|
||||
}
|
||||
|
||||
// Edge-case: no __result_N columns, but a single numeric column present
|
||||
if len(aggValues) == 0 && fallbackSeen {
|
||||
aggValues[0] = fallbackValue
|
||||
@@ -260,20 +231,6 @@ func readAsTimeSeries(rows driver.Rows, queryWindow *qbtypes.TimeRange, step qbt
|
||||
return nil, err
|
||||
}
|
||||
|
||||
diagnosticBuckets := make([]*qbtypes.AggregationBucket, 0)
|
||||
// TODO(nikhilmantri0902, srikanthccv): below HACK - this is a temporary index introduced becausing caching grouping and merging happens on index
|
||||
// Should we improve the caching grouping and merging to not depend on index?
|
||||
diagNosticTemporayIndex := diagnosticColumnIndexBase
|
||||
for diagColName, diagSeries := range diagnosticSeriesMap {
|
||||
diagnosticBucket := &qbtypes.AggregationBucket{
|
||||
Index: diagNosticTemporayIndex,
|
||||
Alias: diagColName,
|
||||
}
|
||||
diagnosticBucket.Series = append(diagnosticBucket.Series, diagSeries)
|
||||
diagnosticBuckets = append(diagnosticBuckets, diagnosticBucket)
|
||||
diagNosticTemporayIndex++
|
||||
}
|
||||
|
||||
maxAgg := -1
|
||||
for k := range seriesMap {
|
||||
if k.agg > maxAgg {
|
||||
@@ -283,8 +240,6 @@ func readAsTimeSeries(rows driver.Rows, queryWindow *qbtypes.TimeRange, step qbt
|
||||
if maxAgg < 0 {
|
||||
return &qbtypes.TimeSeriesData{
|
||||
QueryName: queryName,
|
||||
// return with diagNostic buckets
|
||||
Aggregations: diagnosticBuckets,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -306,9 +261,6 @@ func readAsTimeSeries(rows driver.Rows, queryWindow *qbtypes.TimeRange, step qbt
|
||||
}
|
||||
}
|
||||
|
||||
// add diagNostic buckets
|
||||
nonEmpty = append(nonEmpty, diagnosticBuckets...)
|
||||
|
||||
return &qbtypes.TimeSeriesData{
|
||||
QueryName: queryName,
|
||||
Aggregations: nonEmpty,
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/govaluate"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
@@ -47,7 +46,7 @@ func getQueryName(spec any) string {
|
||||
return getqueryInfo(spec).Name
|
||||
}
|
||||
|
||||
func (q *querier) postProcessResults(ctx context.Context, results map[string]any, req *qbtypes.QueryRangeRequest) (map[string]any, []string, string, error) {
|
||||
func (q *querier) postProcessResults(ctx context.Context, results map[string]any, req *qbtypes.QueryRangeRequest) (map[string]any, error) {
|
||||
// Convert results to typed format for processing
|
||||
typedResults := make(map[string]*qbtypes.Result)
|
||||
for name, result := range results {
|
||||
@@ -97,7 +96,7 @@ func (q *querier) postProcessResults(ctx context.Context, results map[string]any
|
||||
for name, v := range typedResults {
|
||||
retResult[name] = v.Value
|
||||
}
|
||||
return retResult, nil, "", nil
|
||||
return retResult, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,11 +108,11 @@ func (q *querier) postProcessResults(ctx context.Context, results map[string]any
|
||||
firstQueryName := getQueryName(req.CompositeQuery.Queries[0].Spec)
|
||||
if firstQueryName != "" && tableResult["table"] != nil {
|
||||
// Return table under first query name
|
||||
return map[string]any{firstQueryName: tableResult["table"]}, nil, "", nil
|
||||
return map[string]any{firstQueryName: tableResult["table"]}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return tableResult, nil, "", nil
|
||||
return tableResult, nil
|
||||
}
|
||||
|
||||
if req.RequestType == qbtypes.RequestTypeTimeSeries && req.FormatOptions != nil && req.FormatOptions.FillGaps {
|
||||
@@ -156,19 +155,7 @@ func (q *querier) postProcessResults(ctx context.Context, results map[string]any
|
||||
finalResults[name] = result.Value
|
||||
}
|
||||
|
||||
// collect postProcessWarnings from typed results
|
||||
var postProcessWarnings []string
|
||||
var postProcessWarningsDocURL string
|
||||
for _, res := range typedResults {
|
||||
if len(res.Warnings) > 0 {
|
||||
postProcessWarnings = append(postProcessWarnings, res.Warnings...)
|
||||
}
|
||||
if res.WarningsDocURL != "" {
|
||||
postProcessWarningsDocURL = res.WarningsDocURL
|
||||
}
|
||||
}
|
||||
|
||||
return finalResults, postProcessWarnings, postProcessWarningsDocURL, nil
|
||||
return finalResults, nil
|
||||
}
|
||||
|
||||
// postProcessBuilderQuery applies postprocessing to a single builder query result
|
||||
@@ -191,86 +178,6 @@ func postProcessBuilderQuery[T any](
|
||||
return result
|
||||
}
|
||||
|
||||
func removeDiagnosticSeriesAndCheckWarnings(result *qbtypes.Result) {
|
||||
tsData, ok := result.Value.(*qbtypes.TimeSeriesData)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if tsData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var nonDiagnosticBuckets []*qbtypes.AggregationBucket
|
||||
var bucketSum, bucketCount *qbtypes.AggregationBucket
|
||||
|
||||
// First pass: identify diagnostic buckets and separate them from non-diagnostic ones
|
||||
for _, b := range tsData.Aggregations {
|
||||
if !slices.Contains(diagnosticColumnAliases, b.Alias) {
|
||||
nonDiagnosticBuckets = append(nonDiagnosticBuckets, b)
|
||||
continue
|
||||
}
|
||||
// warning columns
|
||||
switch b.Alias {
|
||||
case telemetrymetrics.DiagnosticColumnCumulativeHistLeSum:
|
||||
bucketSum = b
|
||||
case telemetrymetrics.DiagnosticColumnCumulativeHistLeCount:
|
||||
bucketCount = b
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: calculate warnings based on diagnostic buckets (only once, after identifying both)
|
||||
allZero := false
|
||||
if bucketSum != nil {
|
||||
allZero = true
|
||||
if len(bucketSum.Series) == 0 {
|
||||
allZero = false // dont calculate for no values
|
||||
} else {
|
||||
for _, s := range bucketSum.Series {
|
||||
for _, v := range s.Values {
|
||||
if v.Value > 0 {
|
||||
allZero = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allZero {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allSparse := false
|
||||
if bucketCount != nil {
|
||||
allSparse = true
|
||||
if len(bucketCount.Series) == 0 {
|
||||
allSparse = false // dont calculate for no values
|
||||
} else {
|
||||
for _, s := range bucketCount.Series {
|
||||
for _, v := range s.Values {
|
||||
if v.Value >= 2 {
|
||||
allSparse = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allSparse {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if allZero {
|
||||
result.Warnings = append(result.Warnings, "No change observed for this cumulative metric in the selected range.")
|
||||
}
|
||||
if allSparse {
|
||||
result.Warnings = append(result.Warnings, "Only +Inf bucket present; add finite buckets to compute quantiles.")
|
||||
}
|
||||
|
||||
tsData.Aggregations = nonDiagnosticBuckets
|
||||
result.Value = tsData
|
||||
}
|
||||
|
||||
// postProcessMetricQuery applies postprocessing to a metric query result
|
||||
func postProcessMetricQuery(
|
||||
q *querier,
|
||||
@@ -279,8 +186,6 @@ func postProcessMetricQuery(
|
||||
req *qbtypes.QueryRangeRequest,
|
||||
) *qbtypes.Result {
|
||||
|
||||
removeDiagnosticSeriesAndCheckWarnings(result)
|
||||
|
||||
config := query.Aggregations[0]
|
||||
spaceAggOrderBy := fmt.Sprintf("%s(%s)", config.SpaceAggregation.StringValue(), config.MetricName)
|
||||
timeAggOrderBy := fmt.Sprintf("%s(%s)", config.TimeAggregation.StringValue(), config.MetricName)
|
||||
|
||||
@@ -588,19 +588,11 @@ func (q *querier) run(
|
||||
}
|
||||
}
|
||||
|
||||
processedResults, processedResultsWarnings, processedResultsDocsURL, err := q.postProcessResults(ctx, results, req)
|
||||
processedResults, err := q.postProcessResults(ctx, results, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Merge warnings collected during post-processing.
|
||||
if len(processedResultsWarnings) > 0 {
|
||||
warnings = append(warnings, processedResultsWarnings...)
|
||||
}
|
||||
if processedResultsDocsURL != "" {
|
||||
warningsDocURL = processedResultsDocsURL
|
||||
}
|
||||
|
||||
// attach step interval to metadata so client can make informed decisions, ex: width of the bar
|
||||
// or go to related logs/traces from a point in line/bar chart with correct time range
|
||||
stepIntervals := make(map[string]uint64, len(steps))
|
||||
|
||||
@@ -3258,7 +3258,12 @@ func (r *ClickHouseReader) GetMetricAggregateAttributes(ctx context.Context, org
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
for _, name := range metricNames {
|
||||
metadata := metadataMap[name]
|
||||
metadata, exists := metadataMap[name]
|
||||
if !exists {
|
||||
zap.L().Warn("metric metadata not found, skipping", zap.String("metric_name", name))
|
||||
// Skip metrics without metadata to avoid nil pointer dereference
|
||||
continue
|
||||
}
|
||||
|
||||
typ := string(metadata.MetricType)
|
||||
temporality := string(metadata.Temporality)
|
||||
|
||||
@@ -2,11 +2,6 @@ package telemetrymetrics
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
|
||||
const (
|
||||
DiagnosticColumnCumulativeHistLeSum = "__diagnostic_cumulative_hist_le_sum"
|
||||
DiagnosticColumnCumulativeHistLeCount = "__diagnostic_cumulative_hist_le_count"
|
||||
)
|
||||
|
||||
var IntrinsicFields = []string{
|
||||
"__normalized",
|
||||
"temporality",
|
||||
|
||||
@@ -559,8 +559,12 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
|
||||
cteArgs [][]any,
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
|
||||
) (*qbtypes.Statement, error) {
|
||||
var combined string
|
||||
combined := querybuilder.CombineCTEs(cteFragments)
|
||||
|
||||
var args []any
|
||||
for _, a := range cteArgs {
|
||||
args = append(args, a...)
|
||||
}
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
|
||||
@@ -570,59 +574,25 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
|
||||
}
|
||||
|
||||
if quantile != 0 && query.Aggregations[0].Type != metrictypes.ExpHistogramType {
|
||||
// Build diagnostics CTE to pre-aggregate arrays and counts per ts/group.
|
||||
diag := sqlbuilder.NewSelectBuilder()
|
||||
diag.Select("ts")
|
||||
for _, g := range query.GroupBy {
|
||||
diag.SelectMore(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
|
||||
}
|
||||
diag.SelectMore("groupArray(le) AS les")
|
||||
diag.SelectMore("groupArray(value) AS vals")
|
||||
diag.SelectMore("arraySum(groupArray(value)) AS bucket_sum")
|
||||
diag.SelectMore("countDistinct(le) AS bucket_count")
|
||||
diag.From("__spatial_aggregation_cte")
|
||||
for _, g := range query.GroupBy {
|
||||
diag.GroupBy(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
|
||||
}
|
||||
diag.GroupBy("ts")
|
||||
diagQ, diagArgs := diag.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
cteFragments = append(cteFragments, fmt.Sprintf("__diagnostics_cte AS (%s)", diagQ))
|
||||
cteArgs = append(cteArgs, diagArgs)
|
||||
|
||||
combined = querybuilder.CombineCTEs(cteFragments)
|
||||
for _, a := range cteArgs {
|
||||
args = append(args, a...)
|
||||
}
|
||||
|
||||
// Final select from diagnostics cte
|
||||
sb.Select("ts")
|
||||
for _, g := range query.GroupBy {
|
||||
sb.SelectMore(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
|
||||
}
|
||||
// expose quantile and diagnostics as __result_* so they are parsed, then stripped in post-processing
|
||||
|
||||
// TODO(nikhilmantri0902): putting below as __result_0, __result_1, __result_2,
|
||||
// but is this correct and not a hack to extract the warnings?
|
||||
// what if the aliases are used by client, there will be collision in select statements ...
|
||||
|
||||
sb.SelectMore(fmt.Sprintf(
|
||||
"histogramQuantile(arrayMap(x -> toFloat64(x), les), vals, %.3f) AS __result_0",
|
||||
"histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), %.3f) AS value",
|
||||
quantile,
|
||||
))
|
||||
sb.SelectMore(fmt.Sprintf("bucket_sum AS %s", DiagnosticColumnCumulativeHistLeSum))
|
||||
sb.SelectMore(fmt.Sprintf("bucket_count AS %s", DiagnosticColumnCumulativeHistLeCount))
|
||||
|
||||
sb.From("__diagnostics_cte")
|
||||
sb.From("__spatial_aggregation_cte")
|
||||
for _, g := range query.GroupBy {
|
||||
sb.GroupBy(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
|
||||
}
|
||||
sb.GroupBy("ts")
|
||||
if query.Having != nil && query.Having.Expression != "" {
|
||||
rewriter := querybuilder.NewHavingExpressionRewriter()
|
||||
rewrittenExpr := rewriter.RewriteForMetrics(query.Having.Expression, query.Aggregations)
|
||||
sb.Having(rewrittenExpr)
|
||||
}
|
||||
} else {
|
||||
combined = querybuilder.CombineCTEs(cteFragments)
|
||||
for _, a := range cteArgs {
|
||||
args = append(args, a...)
|
||||
}
|
||||
sb.Select("*")
|
||||
sb.From("__spatial_aggregation_cte")
|
||||
if query.Having != nil && query.Having.Expression != "" {
|
||||
@@ -633,8 +603,5 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
|
||||
}
|
||||
|
||||
q, a := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
return &qbtypes.Statement{
|
||||
Query: combined + q,
|
||||
Args: append(args, a...),
|
||||
}, nil
|
||||
return &qbtypes.Statement{Query: combined + q, Args: append(args, a...)}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user