Merge branch 'main' into formula-eval

This commit is contained in:
srikanthccv 2025-06-03 12:07:05 +05:30
commit 10560dc49f
93 changed files with 2242 additions and 1162 deletions

View File

@ -96,13 +96,7 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
// note: add ee override methods first // note: add ee override methods first
// routes available only in ee version // routes available only in ee version
router.HandleFunc("/api/v1/featureFlags", am.OpenAccess(ah.getFeatureFlags)).Methods(http.MethodGet) router.HandleFunc("/api/v1/featureFlags", am.OpenAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/loginPrecheck", am.OpenAccess(ah.Signoz.Handlers.User.LoginPrecheck)).Methods(http.MethodGet)
// invite
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(ah.Signoz.Handlers.User.GetInvite)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/invite/accept", am.OpenAccess(ah.Signoz.Handlers.User.AcceptInvite)).Methods(http.MethodPost)
// paid plans specific routes // paid plans specific routes
router.HandleFunc("/api/v1/complete/saml", am.OpenAccess(ah.receiveSAML)).Methods(http.MethodPost) router.HandleFunc("/api/v1/complete/saml", am.OpenAccess(ah.receiveSAML)).Methods(http.MethodPost)
@ -114,9 +108,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet) router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingAPI.Portal)).Methods(http.MethodPost) router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingAPI.Portal)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/dashboards/{uuid}/lock", am.EditAccess(ah.lockDashboard)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/dashboards/{uuid}/unlock", am.EditAccess(ah.unlockDashboard)).Methods(http.MethodPut)
// v3 // v3
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Activate)).Methods(http.MethodPost) router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Activate)).Methods(http.MethodPost)
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Refresh)).Methods(http.MethodPut) router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Refresh)).Methods(http.MethodPut)

View File

@ -1,62 +0,0 @@
package api
import (
"net/http"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/gorilla/mux"
)
func (ah *APIHandler) lockDashboard(w http.ResponseWriter, r *http.Request) {
ah.lockUnlockDashboard(w, r, true)
}
func (ah *APIHandler) unlockDashboard(w http.ResponseWriter, r *http.Request) {
ah.lockUnlockDashboard(w, r, false)
}
func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request, lock bool) {
// Locking can only be done by the owner of the dashboard
// or an admin
// - Fetch the dashboard
// - Check if the user is the owner or an admin
// - If yes, lock/unlock the dashboard
// - If no, return 403
// Get the dashboard UUID from the request
uuid := mux.Vars(r)["uuid"]
if strings.HasPrefix(uuid, "integration") {
render.Error(w, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "dashboards created by integrations cannot be modified"))
return
}
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, errors.Newf(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated"))
return
}
dashboard, err := ah.Signoz.Modules.Dashboard.Get(r.Context(), claims.OrgID, uuid)
if err != nil {
render.Error(w, err)
return
}
if err := claims.IsAdmin(); err != nil && (dashboard.CreatedBy != claims.Email) {
render.Error(w, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "You are not authorized to lock/unlock this dashboard"))
return
}
// Lock/Unlock the dashboard
err = ah.Signoz.Modules.Dashboard.LockUnlock(r.Context(), claims.OrgID, uuid, lock)
if err != nil {
render.Error(w, err)
return
}
ah.Respond(w, "Dashboard updated successfully")
}

View File

@ -1,27 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/create';
const createDashboard = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const url = props.uploadedGrafana ? '/dashboards/grafana' : '/dashboards';
try {
const response = await axios.post(url, {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default createDashboard;

View File

@ -1,9 +0,0 @@
import axios from 'api';
import { PayloadProps, Props } from 'types/api/dashboard/delete';
const deleteDashboard = (props: Props): Promise<PayloadProps> =>
axios
.delete<PayloadProps>(`/dashboards/${props.uuid}`)
.then((response) => response.data);
export default deleteDashboard;

View File

@ -1,11 +0,0 @@
import axios from 'api';
import { ApiResponse } from 'types/api';
import { Props } from 'types/api/dashboard/get';
import { Dashboard } from 'types/api/dashboard/getAll';
const getDashboard = (props: Props): Promise<Dashboard> =>
axios
.get<ApiResponse<Dashboard>>(`/dashboards/${props.uuid}`)
.then((res) => res.data.data);
export default getDashboard;

View File

@ -1,8 +0,0 @@
import axios from 'api';
import { ApiResponse } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
export const getAllDashboardList = (): Promise<Dashboard[]> =>
axios
.get<ApiResponse<Dashboard[]>>('/dashboards')
.then((res) => res.data.data);

View File

@ -1,11 +0,0 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
interface LockDashboardProps {
uuid: string;
}
const lockDashboard = (props: LockDashboardProps): Promise<AxiosResponse> =>
axios.put(`/dashboards/${props.uuid}/lock`);
export default lockDashboard;

View File

@ -1,11 +0,0 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
interface UnlockDashboardProps {
uuid: string;
}
const unlockDashboard = (props: UnlockDashboardProps): Promise<AxiosResponse> =>
axios.put(`/dashboards/${props.uuid}/unlock`);
export default unlockDashboard;

View File

@ -1,20 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/update';
const updateDashboard = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const response = await axios.put(`/dashboards/${props.uuid}`, {
...props.data,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateDashboard;

View File

@ -0,0 +1,23 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/create';
import { Dashboard } from 'types/api/dashboard/getAll';
const create = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
try {
const response = await axios.post<PayloadProps>('/dashboards', {
...props,
});
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default create;

View File

@ -0,0 +1,19 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Dashboard, PayloadProps } from 'types/api/dashboard/getAll';
const getAll = async (): Promise<SuccessResponseV2<Dashboard[]>> => {
try {
const response = await axios.get<PayloadProps>('/dashboards');
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getAll;

View File

@ -0,0 +1,21 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/delete';
const deleteDashboard = async (
props: Props,
): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.delete<PayloadProps>(`/dashboards/${props.id}`);
return {
httpStatusCode: response.status,
data: null,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default deleteDashboard;

View File

@ -0,0 +1,20 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/get';
import { Dashboard } from 'types/api/dashboard/getAll';
const get = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
try {
const response = await axios.get<PayloadProps>(`/dashboards/${props.id}`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default get;

View File

@ -0,0 +1,23 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/lockUnlock';
const lock = async (props: Props): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.put<PayloadProps>(
`/dashboards/${props.id}/lock`,
{ lock: props.lock },
);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default lock;

View File

@ -0,0 +1,23 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
import { PayloadProps, Props } from 'types/api/dashboard/update';
const update = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
try {
const response = await axios.put<PayloadProps>(`/dashboards/${props.id}`, {
...props.data,
});
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default update;

View File

@ -1,11 +1,12 @@
import { Button, Typography } from 'antd'; import { Button, Typography } from 'antd';
import createDashboard from 'api/dashboard/create'; import createDashboard from 'api/v1/dashboards/create';
import { ENTITY_VERSION_V4 } from 'constants/app'; import { ENTITY_VERSION_V4 } from 'constants/app';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard'; import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import useAxiosError from 'hooks/useAxiosError'; import { useErrorModal } from 'providers/ErrorModalProvider';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query'; import { useMutation } from 'react-query';
import APIError from 'types/api/error';
import { ExportPanelProps } from '.'; import { ExportPanelProps } from '.';
import { import {
@ -33,26 +34,28 @@ function ExportPanelContainer({
refetch, refetch,
} = useGetAllDashboard(); } = useGetAllDashboard();
const handleError = useAxiosError(); const { showErrorModal } = useErrorModal();
const { const {
mutate: createNewDashboard, mutate: createNewDashboard,
isLoading: createDashboardLoading, isLoading: createDashboardLoading,
} = useMutation(createDashboard, { } = useMutation(createDashboard, {
onSuccess: (data) => { onSuccess: (data) => {
if (data.payload) { if (data.data) {
onExport(data?.payload, true); onExport(data?.data, true);
} }
refetch(); refetch();
}, },
onError: handleError, onError: (error) => {
showErrorModal(error as APIError);
},
}); });
const options = useMemo(() => getSelectOptions(data || []), [data]); const options = useMemo(() => getSelectOptions(data?.data || []), [data]);
const handleExportClick = useCallback((): void => { const handleExportClick = useCallback((): void => {
const currentSelectedDashboard = data?.find( const currentSelectedDashboard = data?.data?.find(
({ uuid }) => uuid === selectedDashboardId, ({ id }) => id === selectedDashboardId,
); );
onExport(currentSelectedDashboard || null, false); onExport(currentSelectedDashboard || null, false);
@ -66,14 +69,18 @@ function ExportPanelContainer({
); );
const handleNewDashboard = useCallback(async () => { const handleNewDashboard = useCallback(async () => {
createNewDashboard({ try {
title: t('new_dashboard_title', { await createNewDashboard({
ns: 'dashboard', title: t('new_dashboard_title', {
}), ns: 'dashboard',
uploadedGrafana: false, }),
version: ENTITY_VERSION_V4, uploadedGrafana: false,
}); version: ENTITY_VERSION_V4,
}, [t, createNewDashboard]); });
} catch (error) {
showErrorModal(error as APIError);
}
}, [createNewDashboard, t, showErrorModal]);
const isDashboardLoading = isAllDashboardsLoading || createDashboardLoading; const isDashboardLoading = isAllDashboardsLoading || createDashboardLoading;

View File

@ -1,12 +1,10 @@
import { SelectProps } from 'antd'; import { SelectProps } from 'antd';
import { PayloadProps as AllDashboardsData } from 'types/api/dashboard/getAll'; import { Dashboard } from 'types/api/dashboard/getAll';
export const getSelectOptions = ( export const getSelectOptions = (data: Dashboard[]): SelectProps['options'] =>
data: AllDashboardsData, data.map(({ id, data }) => ({
): SelectProps['options'] =>
data.map(({ uuid, data }) => ({
label: data.title, label: data.title,
value: uuid, value: id,
})); }));
export const filterOptions: SelectProps['filterOption'] = ( export const filterOptions: SelectProps['filterOption'] = (

View File

@ -38,7 +38,7 @@ export default function DashboardEmptyState(): JSX.Element {
setSelectedRowWidgetId(null); setSelectedRowWidgetId(null);
handleToggleDashboardSlider(true); handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', { logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.uuid, dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title, dashboardName: selectedDashboard?.data.title,
numberOfPanels: selectedDashboard?.data.widgets?.length, numberOfPanels: selectedDashboard?.data.widgets?.length,
}); });

View File

@ -2,6 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { AppProvider } from 'providers/App/App'; import { AppProvider } from 'providers/App/App';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider'; import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import store from 'store'; import store from 'store';
@ -189,24 +190,26 @@ describe('WidgetGraphComponent', () => {
it('should show correct menu items when hovering over more options while loading', async () => { it('should show correct menu items when hovering over more options while loading', async () => {
const { getByTestId, findByRole, getByText, container } = render( const { getByTestId, findByRole, getByText, container } = render(
<MockQueryClientProvider> <MockQueryClientProvider>
<Provider store={store}> <ErrorModalProvider>
<AppProvider> <Provider store={store}>
<WidgetGraphComponent <AppProvider>
widget={mockProps.widget} <WidgetGraphComponent
queryResponse={mockProps.queryResponse} widget={mockProps.widget}
errorMessage={mockProps.errorMessage} queryResponse={mockProps.queryResponse}
version={mockProps.version} errorMessage={mockProps.errorMessage}
headerMenuList={mockProps.headerMenuList} version={mockProps.version}
isWarning={mockProps.isWarning} headerMenuList={mockProps.headerMenuList}
isFetchingResponse={mockProps.isFetchingResponse} isWarning={mockProps.isWarning}
setRequestData={mockProps.setRequestData} isFetchingResponse={mockProps.isFetchingResponse}
onClickHandler={mockProps.onClickHandler} setRequestData={mockProps.setRequestData}
onDragSelect={mockProps.onDragSelect} onClickHandler={mockProps.onClickHandler}
openTracesButton={mockProps.openTracesButton} onDragSelect={mockProps.onDragSelect}
onOpenTraceBtnClick={mockProps.onOpenTraceBtnClick} openTracesButton={mockProps.openTracesButton}
/> onOpenTraceBtnClick={mockProps.onOpenTraceBtnClick}
</AppProvider> />
</Provider> </AppProvider>
</Provider>
</ErrorModalProvider>
</MockQueryClientProvider>, </MockQueryClientProvider>,
); );

View File

@ -4,7 +4,6 @@ import { Skeleton, Tooltip, Typography } from 'antd';
import cx from 'classnames'; import cx from 'classnames';
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer'; import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
import { ToggleGraphProps } from 'components/Graph/types'; import { ToggleGraphProps } from 'components/Graph/types';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import { placeWidgetAtBottom } from 'container/NewWidget/utils'; import { placeWidgetAtBottom } from 'container/NewWidget/utils';
@ -31,7 +30,7 @@ import {
useState, useState,
} from 'react'; } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { Dashboard } from 'types/api/dashboard/getAll'; import { Props } from 'types/api/dashboard/update';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
@ -119,29 +118,23 @@ function WidgetGraphComponent({
const updatedLayout = const updatedLayout =
selectedDashboard.data.layout?.filter((e) => e.i !== widget.id) || []; selectedDashboard.data.layout?.filter((e) => e.i !== widget.id) || [];
const updatedSelectedDashboard: Dashboard = { const updatedSelectedDashboard: Props = {
...selectedDashboard,
data: { data: {
...selectedDashboard.data, ...selectedDashboard.data,
widgets: updatedWidgets, widgets: updatedWidgets,
layout: updatedLayout, layout: updatedLayout,
}, },
uuid: selectedDashboard.uuid, id: selectedDashboard.id,
}; };
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, { updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
onSuccess: (updatedDashboard) => { onSuccess: (updatedDashboard) => {
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []); if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.payload) { if (setSelectedDashboard && updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.payload); setSelectedDashboard(updatedDashboard.data);
} }
setDeleteModal(false); setDeleteModal(false);
}, },
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
}); });
}; };
@ -166,7 +159,8 @@ function WidgetGraphComponent({
updateDashboardMutation.mutateAsync( updateDashboardMutation.mutateAsync(
{ {
...selectedDashboard, id: selectedDashboard.id,
data: { data: {
...selectedDashboard.data, ...selectedDashboard.data,
layout, layout,
@ -183,9 +177,9 @@ function WidgetGraphComponent({
}, },
{ {
onSuccess: (updatedDashboard) => { onSuccess: (updatedDashboard) => {
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []); if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.payload) { if (setSelectedDashboard && updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.payload); setSelectedDashboard(updatedDashboard.data);
} }
notifications.success({ notifications.success({
message: 'Panel cloned successfully, redirecting to new copy.', message: 'Panel cloned successfully, redirecting to new copy.',

View File

@ -6,7 +6,6 @@ import { Button, Form, Input, Modal, Typography } from 'antd';
import { useForm } from 'antd/es/form/Form'; import { useForm } from 'antd/es/form/Form';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import cx from 'classnames'; import cx from 'classnames';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import { themeColors } from 'constants/theme'; import { themeColors } from 'constants/theme';
@ -14,7 +13,6 @@ import { DEFAULT_ROW_NAME } from 'container/NewDashboard/DashboardDescription/ut
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import useComponentPermission from 'hooks/useComponentPermission'; import useComponentPermission from 'hooks/useComponentPermission';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import { defaultTo, isUndefined } from 'lodash-es'; import { defaultTo, isUndefined } from 'lodash-es';
@ -36,7 +34,8 @@ import { ItemCallback, Layout } from 'react-grid-layout';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions'; import { UpdateTimeInterval } from 'store/actions';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; import { Widgets } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update';
import { ROLES, USER_ROLES } from 'types/roles'; import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission'; import { ComponentTypes } from 'utils/permission';
@ -107,7 +106,6 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
const updateDashboardMutation = useUpdateDashboard(); const updateDashboardMutation = useUpdateDashboard();
const { notifications } = useNotifications();
const urlQuery = useUrlQuery(); const urlQuery = useUrlQuery();
let permissions: ComponentTypes[] = ['save_layout', 'add_panel']; let permissions: ComponentTypes[] = ['save_layout', 'add_panel'];
@ -158,20 +156,20 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
useEffect(() => { useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data)) { if (!logEventCalledRef.current && !isUndefined(data)) {
logEvent('Dashboard Detail: Opened', { logEvent('Dashboard Detail: Opened', {
dashboardId: data.uuid, dashboardId: selectedDashboard?.id,
dashboardName: data.title, dashboardName: data.title,
numberOfPanels: data.widgets?.length, numberOfPanels: data.widgets?.length,
numberOfVariables: Object.keys(data?.variables || {}).length || 0, numberOfVariables: Object.keys(data?.variables || {}).length || 0,
}); });
logEventCalledRef.current = true; logEventCalledRef.current = true;
} }
}, [data]); }, [data, selectedDashboard?.id]);
const onSaveHandler = (): void => { const onSaveHandler = (): void => {
if (!selectedDashboard) return; if (!selectedDashboard) return;
const updatedDashboard: Dashboard = { const updatedDashboard: Props = {
...selectedDashboard, id: selectedDashboard.id,
data: { data: {
...selectedDashboard.data, ...selectedDashboard.data,
panelMap: { ...currentPanelMap }, panelMap: { ...currentPanelMap },
@ -186,24 +184,18 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
return widget; return widget;
}), }),
}, },
uuid: selectedDashboard.uuid,
}; };
updateDashboardMutation.mutate(updatedDashboard, { updateDashboardMutation.mutate(updatedDashboard, {
onSuccess: (updatedDashboard) => { onSuccess: (updatedDashboard) => {
setSelectedRowWidgetId(null); setSelectedRowWidgetId(null);
if (updatedDashboard.payload) { if (updatedDashboard.data) {
if (updatedDashboard.payload.data.layout) if (updatedDashboard.data.data.layout)
setLayouts(sortLayout(updatedDashboard.payload.data.layout)); setLayouts(sortLayout(updatedDashboard.data.data.layout));
setSelectedDashboard(updatedDashboard.payload); setSelectedDashboard(updatedDashboard.data);
setPanelMap(updatedDashboard.payload?.data?.panelMap || {}); setPanelMap(updatedDashboard.data?.data?.panelMap || {});
} }
}, },
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
}); });
}; };
@ -286,33 +278,25 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
updatedWidgets?.push(currentWidget); updatedWidgets?.push(currentWidget);
const updatedSelectedDashboard: Dashboard = { const updatedSelectedDashboard: Props = {
...selectedDashboard, id: selectedDashboard.id,
data: { data: {
...selectedDashboard.data, ...selectedDashboard.data,
widgets: updatedWidgets, widgets: updatedWidgets,
}, },
uuid: selectedDashboard.uuid,
}; };
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, { updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
onSuccess: (updatedDashboard) => { onSuccess: (updatedDashboard) => {
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []); if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.payload) { if (setSelectedDashboard && updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.payload); setSelectedDashboard(updatedDashboard.data);
} }
if (setPanelMap) if (setPanelMap) setPanelMap(updatedDashboard.data?.data?.panelMap || {});
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
form.setFieldValue('title', ''); form.setFieldValue('title', '');
setIsSettingsModalOpen(false); setIsSettingsModalOpen(false);
setCurrentSelectRowId(null); setCurrentSelectRowId(null);
}, },
// eslint-disable-next-line sonarjs/no-identical-functions
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
}); });
}; };
@ -447,34 +431,26 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
const updatedPanelMap = { ...currentPanelMap }; const updatedPanelMap = { ...currentPanelMap };
delete updatedPanelMap[currentSelectRowId]; delete updatedPanelMap[currentSelectRowId];
const updatedSelectedDashboard: Dashboard = { const updatedSelectedDashboard: Props = {
...selectedDashboard, id: selectedDashboard.id,
data: { data: {
...selectedDashboard.data, ...selectedDashboard.data,
widgets: updatedWidgets, widgets: updatedWidgets,
layout: updatedLayout, layout: updatedLayout,
panelMap: updatedPanelMap, panelMap: updatedPanelMap,
}, },
uuid: selectedDashboard.uuid,
}; };
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, { updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
onSuccess: (updatedDashboard) => { onSuccess: (updatedDashboard) => {
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []); if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.payload) { if (setSelectedDashboard && updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.payload); setSelectedDashboard(updatedDashboard.data);
} }
if (setPanelMap) if (setPanelMap) setPanelMap(updatedDashboard.data?.data?.panelMap || {});
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
setIsDeleteModalOpen(false); setIsDeleteModalOpen(false);
setCurrentSelectRowId(null); setCurrentSelectRowId(null);
}, },
// eslint-disable-next-line sonarjs/no-identical-functions
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
}); });
}; };
const isDashboardEmpty = useMemo( const isDashboardEmpty = useMemo(

View File

@ -33,7 +33,7 @@ export default function Dashboards({
useEffect(() => { useEffect(() => {
if (!dashboardsList) return; if (!dashboardsList) return;
const sortedDashboards = dashboardsList.sort((a, b) => { const sortedDashboards = dashboardsList.data.sort((a, b) => {
const aUpdateAt = new Date(a.updatedAt).getTime(); const aUpdateAt = new Date(a.updatedAt).getTime();
const bUpdateAt = new Date(b.updatedAt).getTime(); const bUpdateAt = new Date(b.updatedAt).getTime();
return bUpdateAt - aUpdateAt; return bUpdateAt - aUpdateAt;
@ -103,7 +103,7 @@ export default function Dashboards({
<div className="home-dashboards-list-container home-data-item-container"> <div className="home-dashboards-list-container home-data-item-container">
<div className="dashboards-list"> <div className="dashboards-list">
{sortedDashboards.slice(0, 5).map((dashboard) => { {sortedDashboards.slice(0, 5).map((dashboard) => {
const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${dashboard.uuid}`; const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${dashboard.id}`;
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => { const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
event.stopPropagation(); event.stopPropagation();
@ -134,7 +134,7 @@ export default function Dashboards({
<div className="dashboard-item-name-container home-data-item-name-container"> <div className="dashboard-item-name-container home-data-item-name-container">
<img <img
src={ src={
dashboard.id % 2 === 0 Math.random() % 2 === 0
? '/Icons/eight-ball.svg' ? '/Icons/eight-ball.svg'
: '/Icons/circus-tent.svg' : '/Icons/circus-tent.svg'
} }

View File

@ -22,7 +22,7 @@ import {
} from 'antd'; } from 'antd';
import { TableProps } from 'antd/lib'; import { TableProps } from 'antd/lib';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import createDashboard from 'api/dashboard/create'; import createDashboard from 'api/v1/dashboards/create';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import cx from 'classnames'; import cx from 'classnames';
import { ENTITY_VERSION_V4 } from 'constants/app'; import { ENTITY_VERSION_V4 } from 'constants/app';
@ -63,6 +63,7 @@ import {
import { handleContactSupport } from 'pages/Integrations/utils'; import { handleContactSupport } from 'pages/Integrations/utils';
import { useAppContext } from 'providers/App/App'; import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { useTimezone } from 'providers/Timezone'; import { useTimezone } from 'providers/Timezone';
import { import {
ChangeEvent, ChangeEvent,
@ -83,6 +84,7 @@ import {
WidgetRow, WidgetRow,
Widgets, Widgets,
} from 'types/api/dashboard/getAll'; } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal'; import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
import ImportJSON from './ImportJSON'; import ImportJSON from './ImportJSON';
@ -226,7 +228,7 @@ function DashboardsList(): JSX.Element {
useEffect(() => { useEffect(() => {
const filteredDashboards = filterDashboard( const filteredDashboards = filterDashboard(
searchString, searchString,
dashboardListResponse || [], dashboardListResponse?.data || [],
); );
if (sortOrder.columnKey === 'updatedAt') { if (sortOrder.columnKey === 'updatedAt') {
sortDashboardsByUpdatedAt(filteredDashboards || []); sortDashboardsByUpdatedAt(filteredDashboards || []);
@ -256,17 +258,19 @@ function DashboardsList(): JSX.Element {
errorMessage: '', errorMessage: '',
}); });
const { showErrorModal } = useErrorModal();
const data: Data[] = const data: Data[] =
dashboards?.map((e) => ({ dashboards?.map((e) => ({
createdAt: e.createdAt, createdAt: e.createdAt,
description: e.data.description || '', description: e.data.description || '',
id: e.uuid, id: e.id,
lastUpdatedTime: e.updatedAt, lastUpdatedTime: e.updatedAt,
name: e.data.title, name: e.data.title,
tags: e.data.tags || [], tags: e.data.tags || [],
key: e.uuid, key: e.id,
createdBy: e.createdBy, createdBy: e.createdBy,
isLocked: !!e.isLocked || false, isLocked: !!e.locked || false,
lastUpdatedBy: e.updatedBy, lastUpdatedBy: e.updatedBy,
image: e.data.image || Base64Icons[0], image: e.data.image || Base64Icons[0],
variables: e.data.variables, variables: e.data.variables,
@ -292,28 +296,20 @@ function DashboardsList(): JSX.Element {
version: ENTITY_VERSION_V4, version: ENTITY_VERSION_V4,
}); });
if (response.statusCode === 200) { safeNavigate(
safeNavigate( generatePath(ROUTES.DASHBOARD, {
generatePath(ROUTES.DASHBOARD, { dashboardId: response.data.id,
dashboardId: response.payload.uuid, }),
}), );
);
} else {
setNewDashboardState({
...newDashboardState,
loading: false,
error: true,
errorMessage: response.error || 'Something went wrong',
});
}
} catch (error) { } catch (error) {
showErrorModal(error as APIError);
setNewDashboardState({ setNewDashboardState({
...newDashboardState, ...newDashboardState,
error: true, error: true,
errorMessage: (error as AxiosError).toString() || 'Something went Wrong', errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
}); });
} }
}, [newDashboardState, safeNavigate, t]); }, [newDashboardState, safeNavigate, showErrorModal, t]);
const onModalHandler = (uploadedGrafana: boolean): void => { const onModalHandler = (uploadedGrafana: boolean): void => {
logEvent('Dashboard List: Import JSON clicked', {}); logEvent('Dashboard List: Import JSON clicked', {});
@ -327,7 +323,7 @@ function DashboardsList(): JSX.Element {
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || ''; const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
const filteredDashboards = filterDashboard( const filteredDashboards = filterDashboard(
searchText, searchText,
dashboardListResponse || [], dashboardListResponse?.data || [],
); );
setDashboards(filteredDashboards); setDashboards(filteredDashboards);
setIsFilteringDashboards(false); setIsFilteringDashboards(false);
@ -677,7 +673,7 @@ function DashboardsList(): JSX.Element {
!isUndefined(dashboardListResponse) !isUndefined(dashboardListResponse)
) { ) {
logEvent('Dashboard List: Page visited', { logEvent('Dashboard List: Page visited', {
number: dashboardListResponse?.length, number: dashboardListResponse?.data?.length,
}); });
logEventCalledRef.current = true; logEventCalledRef.current = true;
} }

View File

@ -14,19 +14,21 @@ import {
UploadProps, UploadProps,
} from 'antd'; } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import createDashboard from 'api/dashboard/create'; import createDashboard from 'api/v1/dashboards/create';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout'; import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
import { ExternalLink, Github, MonitorDot, MoveRight, X } from 'lucide-react'; import { ExternalLink, Github, MonitorDot, MoveRight, X } from 'lucide-react';
import { useErrorModal } from 'providers/ErrorModalProvider';
// #TODO: Lucide will be removing brand icons like GitHub in the future. In that case, we can use Simple Icons. https://simpleicons.org/ // #TODO: Lucide will be removing brand icons like GitHub in the future. In that case, we can use Simple Icons. https://simpleicons.org/
// See more: https://github.com/lucide-icons/lucide/issues/94 // See more: https://github.com/lucide-icons/lucide/issues/94
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom'; import { generatePath } from 'react-router-dom';
import { DashboardData } from 'types/api/dashboard/getAll'; import { DashboardData } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
function ImportJSON({ function ImportJSON({
isImportJSONModalVisible, isImportJSONModalVisible,
@ -74,6 +76,8 @@ function ImportJSON({
} }
}; };
const { showErrorModal } = useErrorModal();
const onClickLoadJsonHandler = async (): Promise<void> => { const onClickLoadJsonHandler = async (): Promise<void> => {
try { try {
setDashboardCreating(true); setDashboardCreating(true);
@ -81,11 +85,6 @@ function ImportJSON({
const dashboardData = JSON.parse(editorValue) as DashboardData; const dashboardData = JSON.parse(editorValue) as DashboardData;
// Remove uuid from the dashboard data, in all cases - empty, duplicate or any valid not duplicate uuid
if (dashboardData.uuid !== undefined) {
delete dashboardData.uuid;
}
if (dashboardData?.layout) { if (dashboardData?.layout) {
dashboardData.layout = getUpdatedLayout(dashboardData.layout); dashboardData.layout = getUpdatedLayout(dashboardData.layout);
} else { } else {
@ -97,28 +96,19 @@ function ImportJSON({
uploadedGrafana, uploadedGrafana,
}); });
if (response.statusCode === 200) { safeNavigate(
safeNavigate( generatePath(ROUTES.DASHBOARD, {
generatePath(ROUTES.DASHBOARD, { dashboardId: response.data.id,
dashboardId: response.payload.uuid, }),
}), );
); logEvent('Dashboard List: New dashboard imported successfully', {
logEvent('Dashboard List: New dashboard imported successfully', { dashboardId: response.data?.id,
dashboardId: response.payload?.uuid, dashboardName: response.data?.data?.title,
dashboardName: response.payload?.data?.title, });
});
} else {
setIsCreateDashboardError(true);
notifications.error({
message:
response.error ||
t('something_went_wrong', {
ns: 'common',
}),
});
}
setDashboardCreating(false); setDashboardCreating(false);
} catch (error) { } catch (error) {
showErrorModal(error as APIError);
setDashboardCreating(false); setDashboardCreating(false);
setIsCreateDashboardError(true); setIsCreateDashboardError(true);
notifications.error({ notifications.error({

View File

@ -6,8 +6,7 @@ import { executeSearchQueries } from '../utils';
describe('executeSearchQueries', () => { describe('executeSearchQueries', () => {
const firstDashboard: Dashboard = { const firstDashboard: Dashboard = {
id: 11111, id: uuid(),
uuid: uuid(),
createdAt: '', createdAt: '',
updatedAt: '', updatedAt: '',
createdBy: '', createdBy: '',
@ -18,8 +17,7 @@ describe('executeSearchQueries', () => {
}, },
}; };
const secondDashboard: Dashboard = { const secondDashboard: Dashboard = {
id: 22222, id: uuid(),
uuid: uuid(),
createdAt: '', createdAt: '',
updatedAt: '', updatedAt: '',
createdBy: '', createdBy: '',
@ -30,8 +28,7 @@ describe('executeSearchQueries', () => {
}, },
}; };
const thirdDashboard: Dashboard = { const thirdDashboard: Dashboard = {
id: 333333, id: uuid(),
uuid: uuid(),
createdAt: '', createdAt: '',
updatedAt: '', updatedAt: '',
createdBy: '', createdBy: '',

View File

@ -59,7 +59,7 @@ export function DeleteButton({
onClick: (e) => { onClick: (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
deleteDashboardMutation.mutateAsync(undefined, { deleteDashboardMutation.mutate(undefined, {
onSuccess: () => { onSuccess: () => {
notifications.success({ notifications.success({
message: t('dashboard:delete_dashboard_success', { message: t('dashboard:delete_dashboard_success', {

View File

@ -14,7 +14,7 @@ export const generateSearchData = (
dashboards.forEach((dashboard) => { dashboards.forEach((dashboard) => {
dashboardSearchData.push({ dashboardSearchData.push({
id: dashboard.uuid, id: dashboard.id,
title: dashboard.data.title, title: dashboard.data.title,
description: dashboard.data.description, description: dashboard.data.description,
tags: dashboard.data.tags || [], tags: dashboard.data.tags || [],

View File

@ -421,7 +421,7 @@ function LogsExplorerViews({
const dashboardEditView = generateExportToDashboardLink({ const dashboardEditView = generateExportToDashboardLink({
query, query,
panelType: panelTypeParam, panelType: panelTypeParam,
dashboardId: dashboard.uuid, dashboardId: dashboard.id,
widgetId, widgetId,
}); });

View File

@ -76,7 +76,7 @@ function Explorer(): JSX.Element {
const dashboardEditView = generateExportToDashboardLink({ const dashboardEditView = generateExportToDashboardLink({
query: queryToExport || exportDefaultQuery, query: queryToExport || exportDefaultQuery,
panelType: PANEL_TYPES.TIME_SERIES, panelType: PANEL_TYPES.TIME_SERIES,
dashboardId: dashboard?.uuid || '', dashboardId: dashboard.id,
widgetId, widgetId,
}); });

View File

@ -12,7 +12,6 @@ import {
Typography, Typography,
} from 'antd'; } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
@ -44,11 +43,8 @@ import { FullScreenHandle } from 'react-full-screen';
import { Layout } from 'react-grid-layout'; import { Layout } from 'react-grid-layout';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCopyToClipboard } from 'react-use'; import { useCopyToClipboard } from 'react-use';
import { import { DashboardData, IDashboardVariable } from 'types/api/dashboard/getAll';
Dashboard, import { Props } from 'types/api/dashboard/update';
DashboardData,
IDashboardVariable,
} from 'types/api/dashboard/getAll';
import { ROLES, USER_ROLES } from 'types/roles'; import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission'; import { ComponentTypes } from 'utils/permission';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
@ -65,10 +61,9 @@ interface DashboardDescriptionProps {
export function sanitizeDashboardData( export function sanitizeDashboardData(
selectedData: DashboardData, selectedData: DashboardData,
): Omit<DashboardData, 'uuid'> { ): DashboardData {
if (!selectedData?.variables) { if (!selectedData?.variables) {
const { uuid, ...rest } = selectedData; return selectedData;
return rest;
} }
const updatedVariables = Object.entries(selectedData.variables).reduce( const updatedVariables = Object.entries(selectedData.variables).reduce(
@ -80,9 +75,8 @@ export function sanitizeDashboardData(
{} as Record<string, IDashboardVariable>, {} as Record<string, IDashboardVariable>,
); );
const { uuid, ...restData } = selectedData;
return { return {
...restData, ...selectedData,
variables: updatedVariables, variables: updatedVariables,
}; };
} }
@ -108,7 +102,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const selectedData = selectedDashboard const selectedData = selectedDashboard
? { ? {
...selectedDashboard.data, ...selectedDashboard.data,
uuid: selectedDashboard.uuid, uuid: selectedDashboard.id,
} }
: ({} as DashboardData); : ({} as DashboardData);
@ -162,7 +156,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
setSelectedRowWidgetId(null); setSelectedRowWidgetId(null);
handleToggleDashboardSlider(true); handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', { logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.uuid, dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title, dashboardName: selectedDashboard?.data.title,
numberOfPanels: selectedDashboard?.data.widgets?.length, numberOfPanels: selectedDashboard?.data.widgets?.length,
}); });
@ -178,8 +172,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
if (!selectedDashboard) { if (!selectedDashboard) {
return; return;
} }
const updatedDashboard = { const updatedDashboard: Props = {
...selectedDashboard, id: selectedDashboard.id,
data: { data: {
...selectedDashboard.data, ...selectedDashboard.data,
title: updatedTitle, title: updatedTitle,
@ -191,13 +186,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
message: 'Dashboard renamed successfully', message: 'Dashboard renamed successfully',
}); });
setIsRenameDashboardOpen(false); setIsRenameDashboardOpen(false);
if (updatedDashboard.payload) if (updatedDashboard.data) setSelectedDashboard(updatedDashboard.data);
setSelectedDashboard(updatedDashboard.payload);
}, },
onError: () => { onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
setIsRenameDashboardOpen(true); setIsRenameDashboardOpen(true);
}, },
}); });
@ -251,8 +242,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
} }
} }
const updatedDashboard: Dashboard = { const updatedDashboard: Props = {
...selectedDashboard, id: selectedDashboard.id,
data: { data: {
...selectedDashboard.data, ...selectedDashboard.data,
layout: [ layout: [
@ -279,28 +271,21 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
}, },
], ],
}, },
uuid: selectedDashboard.uuid,
}; };
updateDashboardMutation.mutate(updatedDashboard, { updateDashboardMutation.mutate(updatedDashboard, {
// eslint-disable-next-line sonarjs/no-identical-functions // eslint-disable-next-line sonarjs/no-identical-functions
onSuccess: (updatedDashboard) => { onSuccess: (updatedDashboard) => {
if (updatedDashboard.payload) { if (updatedDashboard.data) {
if (updatedDashboard.payload.data.layout) if (updatedDashboard.data.data.layout)
setLayouts(sortLayout(updatedDashboard.payload.data.layout)); setLayouts(sortLayout(updatedDashboard.data.data.layout));
setSelectedDashboard(updatedDashboard.payload); setSelectedDashboard(updatedDashboard.data);
setPanelMap(updatedDashboard.payload?.data?.panelMap || {}); setPanelMap(updatedDashboard.data?.data?.panelMap || {});
} }
setIsPanelNameModalOpen(false); setIsPanelNameModalOpen(false);
setSectionName(DEFAULT_ROW_NAME); setSectionName(DEFAULT_ROW_NAME);
}, },
// eslint-disable-next-line sonarjs/no-identical-functions
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
}); });
} }
@ -445,7 +430,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
<DeleteButton <DeleteButton
createdBy={selectedDashboard?.createdBy || ''} createdBy={selectedDashboard?.createdBy || ''}
name={selectedDashboard?.data.title || ''} name={selectedDashboard?.data.title || ''}
id={String(selectedDashboard?.uuid) || ''} id={String(selectedDashboard?.id) || ''}
isLocked={isDashboardLocked} isLocked={isDashboardLocked}
routeToListPage routeToListPage
/> />

View File

@ -1,10 +1,8 @@
import './GeneralSettings.styles.scss'; import './GeneralSettings.styles.scss';
import { Col, Input, Select, Space, Typography } from 'antd'; import { Col, Input, Select, Space, Typography } from 'antd';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import AddTags from 'container/NewDashboard/DashboardSettings/General/AddTags'; import AddTags from 'container/NewDashboard/DashboardSettings/General/AddTags';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { Check, X } from 'lucide-react'; import { Check, X } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useDashboard } from 'providers/Dashboard/Dashboard';
@ -38,14 +36,12 @@ function GeneralDashboardSettings(): JSX.Element {
const { t } = useTranslation('common'); const { t } = useTranslation('common');
const { notifications } = useNotifications();
const onSaveHandler = (): void => { const onSaveHandler = (): void => {
if (!selectedDashboard) return; if (!selectedDashboard) return;
updateDashboardMutation.mutateAsync( updateDashboardMutation.mutate(
{ {
...selectedDashboard, id: selectedDashboard.id,
data: { data: {
...selectedDashboard.data, ...selectedDashboard.data,
description: updatedDescription, description: updatedDescription,
@ -56,15 +52,11 @@ function GeneralDashboardSettings(): JSX.Element {
}, },
{ {
onSuccess: (updatedDashboard) => { onSuccess: (updatedDashboard) => {
if (updatedDashboard.payload) { if (updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.payload); setSelectedDashboard(updatedDashboard.data);
} }
}, },
onError: () => { onError: () => {},
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
}, },
); );
}; };

View File

@ -171,7 +171,8 @@ function VariablesSetting({
updateMutation.mutateAsync( updateMutation.mutateAsync(
{ {
...selectedDashboard, id: selectedDashboard.id,
data: { data: {
...selectedDashboard.data, ...selectedDashboard.data,
variables: updatedVariablesData, variables: updatedVariablesData,
@ -179,18 +180,13 @@ function VariablesSetting({
}, },
{ {
onSuccess: (updatedDashboard) => { onSuccess: (updatedDashboard) => {
if (updatedDashboard.payload) { if (updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.payload); setSelectedDashboard(updatedDashboard.data);
notifications.success({ notifications.success({
message: t('variable_updated_successfully'), message: t('variable_updated_successfully'),
}); });
} }
}, },
onError: () => {
notifications.error({
message: t('error_while_updating_variable'),
});
},
}, },
); );
}; };

View File

@ -127,7 +127,7 @@ function QuerySection({
panelType: selectedWidget.panelTypes, panelType: selectedWidget.panelTypes,
queryType: currentQuery.queryType, queryType: currentQuery.queryType,
widgetId: selectedWidget.id, widgetId: selectedWidget.id,
dashboardId: selectedDashboard?.uuid, dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title, dashboardName: selectedDashboard?.data.title,
isNewPanel, isNewPanel,
}); });

View File

@ -17,7 +17,6 @@ import { DEFAULT_BUCKET_COUNT } from 'container/PanelWrapper/constants';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys'; import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useAxiosError from 'hooks/useAxiosError';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
@ -41,10 +40,10 @@ import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api'; import { SuccessResponse } from 'types/api';
import { import {
ColumnUnit, ColumnUnit,
Dashboard,
LegendPosition, LegendPosition,
Widgets, Widgets,
} from 'types/api/dashboard/getAll'; } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update';
import { IField } from 'types/api/logs/fields'; import { IField } from 'types/api/logs/fields';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
@ -141,7 +140,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
if (!logEventCalledRef.current) { if (!logEventCalledRef.current) {
logEvent('Panel Edit: Page visited', { logEvent('Panel Edit: Page visited', {
panelType: selectedWidget?.panelTypes, panelType: selectedWidget?.panelTypes,
dashboardId: selectedDashboard?.uuid, dashboardId: selectedDashboard?.id,
widgetId: selectedWidget?.id, widgetId: selectedWidget?.id,
dashboardName: selectedDashboard?.data.title, dashboardName: selectedDashboard?.data.title,
isNewPanel: !!isWidgetNotPresent, isNewPanel: !!isWidgetNotPresent,
@ -345,8 +344,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
return { selectedWidget, preWidgets, afterWidgets }; return { selectedWidget, preWidgets, afterWidgets };
}, [selectedDashboard, query]); }, [selectedDashboard, query]);
const handleError = useAxiosError();
// this loading state is to take care of mismatch in the responses for table and other panels // this loading state is to take care of mismatch in the responses for table and other panels
// hence while changing the query contains the older value and the processing logic fails // hence while changing the query contains the older value and the processing logic fails
const [isLoadingPanelData, setIsLoadingPanelData] = useState<boolean>(false); const [isLoadingPanelData, setIsLoadingPanelData] = useState<boolean>(false);
@ -470,9 +467,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
updatedLayout = newLayoutItem; updatedLayout = newLayoutItem;
} }
const dashboard: Dashboard = { const dashboard: Props = {
...selectedDashboard, id: selectedDashboard.id,
uuid: selectedDashboard.uuid,
data: { data: {
...selectedDashboard.data, ...selectedDashboard.data,
widgets: isNewDashboard widgets: isNewDashboard
@ -540,15 +537,14 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
}; };
updateDashboardMutation.mutateAsync(dashboard, { updateDashboardMutation.mutateAsync(dashboard, {
onSuccess: () => { onSuccess: (updatedDashboard) => {
setSelectedRowWidgetId(null); setSelectedRowWidgetId(null);
setSelectedDashboard(dashboard); setSelectedDashboard(updatedDashboard.data);
setToScrollWidgetId(selectedWidget?.id || ''); setToScrollWidgetId(selectedWidget?.id || '');
safeNavigate({ safeNavigate({
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }), pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
}); });
}, },
onError: handleError,
}); });
}, [ }, [
selectedDashboard, selectedDashboard,
@ -562,7 +558,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
currentQuery, currentQuery,
preWidgets, preWidgets,
updateDashboardMutation, updateDashboardMutation,
handleError,
widgets, widgets,
setSelectedDashboard, setSelectedDashboard,
setToScrollWidgetId, setToScrollWidgetId,
@ -601,7 +596,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
logEvent('Panel Edit: Save changes', { logEvent('Panel Edit: Save changes', {
panelType: selectedWidget.panelTypes, panelType: selectedWidget.panelTypes,
dashboardId: selectedDashboard?.uuid, dashboardId: selectedDashboard?.id,
widgetId: selectedWidget.id, widgetId: selectedWidget.id,
dashboardName: selectedDashboard?.data.title, dashboardName: selectedDashboard?.data.title,
queryType: currentQuery.queryType, queryType: currentQuery.queryType,

View File

@ -1,15 +1,23 @@
import deleteDashboard from 'api/dashboard/delete'; import deleteDashboard from 'api/v1/dashboards/id/delete';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { useMutation, UseMutationResult } from 'react-query'; import { useMutation, UseMutationResult } from 'react-query';
import { PayloadProps } from 'types/api/dashboard/delete'; import { SuccessResponseV2 } from 'types/api';
import APIError from 'types/api/error';
export const useDeleteDashboard = ( export const useDeleteDashboard = (
id: string, id: string,
): UseMutationResult<PayloadProps, unknown, void, unknown> => ): UseMutationResult<SuccessResponseV2<null>, APIError, void, unknown> => {
useMutation({ const { showErrorModal } = useErrorModal();
return useMutation<SuccessResponseV2<null>, APIError>({
mutationKey: REACT_QUERY_KEY.DELETE_DASHBOARD, mutationKey: REACT_QUERY_KEY.DELETE_DASHBOARD,
mutationFn: () => mutationFn: () =>
deleteDashboard({ deleteDashboard({
uuid: id, id,
}), }),
onError: (error: APIError) => {
showErrorModal(error);
},
}); });
};

View File

@ -1,10 +1,22 @@
import { getAllDashboardList } from 'api/dashboard/getAll'; import getAll from 'api/v1/dashboards/getAll';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { useQuery, UseQueryResult } from 'react-query'; import { useQuery, UseQueryResult } from 'react-query';
import { SuccessResponseV2 } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll'; import { Dashboard } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
export const useGetAllDashboard = (): UseQueryResult<Dashboard[], unknown> => export const useGetAllDashboard = (): UseQueryResult<
useQuery<Dashboard[]>({ SuccessResponseV2<Dashboard[]>,
queryFn: getAllDashboardList, APIError
> => {
const { showErrorModal } = useErrorModal();
return useQuery<SuccessResponseV2<Dashboard[]>, APIError>({
queryFn: getAll,
onError: (error) => {
showErrorModal(error);
},
queryKey: REACT_QUERY_KEY.GET_ALL_DASHBOARDS, queryKey: REACT_QUERY_KEY.GET_ALL_DASHBOARDS,
}); });
};

View File

@ -1,25 +1,31 @@
import update from 'api/dashboard/update'; import update from 'api/v1/dashboards/id/update';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { useMutation, UseMutationResult } from 'react-query'; import { useMutation, UseMutationResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { SuccessResponseV2 } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll'; import { Dashboard } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update'; import { Props } from 'types/api/dashboard/update';
import APIError from 'types/api/error';
export const useUpdateDashboard = (): UseUpdateDashboard => { export const useUpdateDashboard = (): UseUpdateDashboard => {
const { updatedTimeRef } = useDashboard(); const { updatedTimeRef } = useDashboard();
const { showErrorModal } = useErrorModal();
return useMutation(update, { return useMutation(update, {
onSuccess: (data) => { onSuccess: (data) => {
if (data.payload) { if (data.data) {
updatedTimeRef.current = dayjs(data.payload.updatedAt); updatedTimeRef.current = dayjs(data.data.updatedAt);
} }
}, },
onError: (error) => {
showErrorModal(error);
},
}); });
}; };
type UseUpdateDashboard = UseMutationResult< type UseUpdateDashboard = UseMutationResult<
SuccessResponse<Dashboard> | ErrorResponse, SuccessResponseV2<Dashboard>,
unknown, APIError,
Props, Props,
unknown unknown
>; >;

View File

@ -38,7 +38,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
logEvent('Panel Edit: Create alert', { logEvent('Panel Edit: Create alert', {
panelType: widget.panelTypes, panelType: widget.panelTypes,
dashboardName: selectedDashboard?.data?.title, dashboardName: selectedDashboard?.data?.title,
dashboardId: selectedDashboard?.uuid, dashboardId: selectedDashboard?.id,
widgetId: widget.id, widgetId: widget.id,
queryType: widget.query.queryType, queryType: widget.query.queryType,
}); });
@ -47,7 +47,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
action: MenuItemKeys.CreateAlerts, action: MenuItemKeys.CreateAlerts,
panelType: widget.panelTypes, panelType: widget.panelTypes,
dashboardName: selectedDashboard?.data?.title, dashboardName: selectedDashboard?.data?.title,
dashboardId: selectedDashboard?.uuid, dashboardId: selectedDashboard?.id,
widgetId: widget.id, widgetId: widget.id,
queryType: widget.query.queryType, queryType: widget.query.queryType,
}); });

View File

@ -3,8 +3,7 @@ export const dashboardSuccessResponse = {
status: 'success', status: 'success',
data: [ data: [
{ {
id: 1, id: '1',
uuid: '1',
createdAt: '2022-11-16T13:29:47.064874419Z', createdAt: '2022-11-16T13:29:47.064874419Z',
createdBy: null, createdBy: null,
updatedAt: '2024-05-21T06:41:30.546630961Z', updatedAt: '2024-05-21T06:41:30.546630961Z',
@ -23,8 +22,7 @@ export const dashboardSuccessResponse = {
}, },
}, },
{ {
id: 2, id: '2',
uuid: '2',
createdAt: '2022-11-16T13:20:47.064874419Z', createdAt: '2022-11-16T13:20:47.064874419Z',
createdBy: null, createdBy: null,
updatedAt: '2024-05-21T06:42:30.546630961Z', updatedAt: '2024-05-21T06:42:30.546630961Z',
@ -53,8 +51,7 @@ export const dashboardEmptyState = {
export const getDashboardById = { export const getDashboardById = {
status: 'success', status: 'success',
data: { data: {
id: 1, id: '1',
uuid: '1',
createdAt: '2022-11-16T13:29:47.064874419Z', createdAt: '2022-11-16T13:29:47.064874419Z',
createdBy: 'integration', createdBy: 'integration',
updatedAt: '2024-05-21T06:41:30.546630961Z', updatedAt: '2024-05-21T06:41:30.546630961Z',
@ -78,8 +75,7 @@ export const getDashboardById = {
export const getNonIntegrationDashboardById = { export const getNonIntegrationDashboardById = {
status: 'success', status: 'success',
data: { data: {
id: 1, id: '1',
uuid: '1',
createdAt: '2022-11-16T13:29:47.064874419Z', createdAt: '2022-11-16T13:29:47.064874419Z',
createdBy: 'thor', createdBy: 'thor',
updatedAt: '2024-05-21T06:41:30.546630961Z', updatedAt: '2024-05-21T06:41:30.546630961Z',

View File

@ -234,7 +234,6 @@ describe('dashboard list page', () => {
const firstDashboardData = dashboardSuccessResponse.data[0]; const firstDashboardData = dashboardSuccessResponse.data[0];
expect(dashboardUtils.sanitizeDashboardData).toHaveBeenCalledWith( expect(dashboardUtils.sanitizeDashboardData).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
id: firstDashboardData.uuid,
title: firstDashboardData.data.title, title: firstDashboardData.data.title,
createdAt: firstDashboardData.createdAt, createdAt: firstDashboardData.createdAt,
}), }),

View File

@ -19,9 +19,9 @@ function DashboardPage(): JSX.Element {
: 'Something went wrong'; : 'Something went wrong';
useEffect(() => { useEffect(() => {
const dashboardTitle = dashboardResponse.data?.data.title; const dashboardTitle = dashboardResponse.data?.data.data.title;
document.title = dashboardTitle || document.title; document.title = dashboardTitle || document.title;
}, [dashboardResponse.data?.data.title, isFetching]); }, [dashboardResponse.data?.data.data.title, isFetching]);
if (isError && !isFetching && errorMessage === ErrorType.NotFound) { if (isError && !isFetching && errorMessage === ErrorType.NotFound) {
return <NotFound />; return <NotFound />;

View File

@ -154,7 +154,7 @@ function TracesExplorer(): JSX.Element {
const dashboardEditView = generateExportToDashboardLink({ const dashboardEditView = generateExportToDashboardLink({
query, query,
panelType: panelTypeParam, panelType: panelTypeParam,
dashboardId: dashboard?.uuid || '', dashboardId: dashboard.id,
widgetId, widgetId,
}); });

View File

@ -1,14 +1,12 @@
/* eslint-disable no-nested-ternary */ /* eslint-disable no-nested-ternary */
import { Modal } from 'antd'; import { Modal } from 'antd';
import getDashboard from 'api/dashboard/get'; import getDashboard from 'api/v1/dashboards/id/get';
import lockDashboardApi from 'api/dashboard/lockDashboard'; import locked from 'api/v1/dashboards/id/lock';
import unlockDashboardApi from 'api/dashboard/unlockDashboard';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { getMinMax } from 'container/TopNav/AutoRefresh/config'; import { getMinMax } from 'container/TopNav/AutoRefresh/config';
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage'; import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
import useAxiosError from 'hooks/useAxiosError';
import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useTabVisibility from 'hooks/useTabFocus'; import useTabVisibility from 'hooks/useTabFocus';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
@ -18,6 +16,7 @@ import isEqual from 'lodash-es/isEqual';
import isUndefined from 'lodash-es/isUndefined'; import isUndefined from 'lodash-es/isUndefined';
import omitBy from 'lodash-es/omitBy'; import omitBy from 'lodash-es/omitBy';
import { useAppContext } from 'providers/App/App'; import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { import {
createContext, createContext,
PropsWithChildren, PropsWithChildren,
@ -36,7 +35,9 @@ import { Dispatch } from 'redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import AppActions from 'types/actions'; import AppActions from 'types/actions';
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime'; import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
import { SuccessResponseV2 } from 'types/api';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll'; import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
@ -52,7 +53,10 @@ const DashboardContext = createContext<IDashboardContext>({
isDashboardLocked: false, isDashboardLocked: false,
handleToggleDashboardSlider: () => {}, handleToggleDashboardSlider: () => {},
handleDashboardLockToggle: () => {}, handleDashboardLockToggle: () => {},
dashboardResponse: {} as UseQueryResult<Dashboard, unknown>, dashboardResponse: {} as UseQueryResult<
SuccessResponseV2<Dashboard>,
APIError
>,
selectedDashboard: {} as Dashboard, selectedDashboard: {} as Dashboard,
dashboardId: '', dashboardId: '',
layouts: [], layouts: [],
@ -116,6 +120,8 @@ export function DashboardProvider({
exact: true, exact: true,
}); });
const { showErrorModal } = useErrorModal();
// added extra checks here in case wrong values appear use the default values rather than empty dashboards // added extra checks here in case wrong values appear use the default values rather than empty dashboards
const supportedOrderColumnKeys = ['createdAt', 'updatedAt']; const supportedOrderColumnKeys = ['createdAt', 'updatedAt'];
@ -270,18 +276,24 @@ export function DashboardProvider({
setIsDashboardFetching(true); setIsDashboardFetching(true);
try { try {
return await getDashboard({ return await getDashboard({
uuid: dashboardId, id: dashboardId,
}); });
} catch (error) {
showErrorModal(error as APIError);
return;
} finally { } finally {
setIsDashboardFetching(false); setIsDashboardFetching(false);
} }
}, },
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
onSuccess: (data) => { onError: (error) => {
const updatedDashboardData = transformDashboardVariables(data); showErrorModal(error as APIError);
},
onSuccess: (data: SuccessResponseV2<Dashboard>) => {
const updatedDashboardData = transformDashboardVariables(data?.data);
const updatedDate = dayjs(updatedDashboardData.updatedAt); const updatedDate = dayjs(updatedDashboardData.updatedAt);
setIsDashboardLocked(updatedDashboardData?.isLocked || false); setIsDashboardLocked(updatedDashboardData?.locked || false);
// on first render // on first render
if (updatedTimeRef.current === null) { if (updatedTimeRef.current === null) {
@ -387,29 +399,25 @@ export function DashboardProvider({
setIsDashboardSlider(value); setIsDashboardSlider(value);
}; };
const handleError = useAxiosError(); const { mutate: lockDashboard } = useMutation(locked, {
onSuccess: (_, props) => {
const { mutate: lockDashboard } = useMutation(lockDashboardApi, {
onSuccess: () => {
setIsDashboardSlider(false); setIsDashboardSlider(false);
setIsDashboardLocked(true); setIsDashboardLocked(props.lock);
}, },
onError: handleError, onError: (error) => {
}); showErrorModal(error as APIError);
const { mutate: unlockDashboard } = useMutation(unlockDashboardApi, {
onSuccess: () => {
setIsDashboardLocked(false);
}, },
onError: handleError,
}); });
const handleDashboardLockToggle = async (value: boolean): Promise<void> => { const handleDashboardLockToggle = async (value: boolean): Promise<void> => {
if (selectedDashboard) { if (selectedDashboard) {
if (value) { try {
lockDashboard(selectedDashboard); await lockDashboard({
} else { id: selectedDashboard.id,
unlockDashboard(selectedDashboard); lock: value,
});
} catch (error) {
showErrorModal(error as APIError);
} }
} }
}; };

View File

@ -1,6 +1,7 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { Layout } from 'react-grid-layout'; import { Layout } from 'react-grid-layout';
import { UseQueryResult } from 'react-query'; import { UseQueryResult } from 'react-query';
import { SuccessResponseV2 } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll'; import { Dashboard } from 'types/api/dashboard/getAll';
export interface DashboardSortOrder { export interface DashboardSortOrder {
@ -19,7 +20,7 @@ export interface IDashboardContext {
isDashboardLocked: boolean; isDashboardLocked: boolean;
handleToggleDashboardSlider: (value: boolean) => void; handleToggleDashboardSlider: (value: boolean) => void;
handleDashboardLockToggle: (value: boolean) => void; handleDashboardLockToggle: (value: boolean) => void;
dashboardResponse: UseQueryResult<Dashboard, unknown>; dashboardResponse: UseQueryResult<SuccessResponseV2<Dashboard>, unknown>;
selectedDashboard: Dashboard | undefined; selectedDashboard: Dashboard | undefined;
dashboardId: string; dashboardId: string;
layouts: Layout[]; layouts: Layout[];

View File

@ -1,11 +1,12 @@
import { Dashboard, DashboardData } from './getAll'; import { Dashboard } from './getAll';
export type Props = export type Props = {
| { title: Dashboard['data']['title'];
title: Dashboard['data']['title']; uploadedGrafana: boolean;
uploadedGrafana: boolean; version?: string;
version?: string; };
}
| { DashboardData: DashboardData; uploadedGrafana: boolean };
export type PayloadProps = Dashboard; export interface PayloadProps {
data: Dashboard;
status: string;
}

View File

@ -1,9 +1,10 @@
import { Dashboard } from './getAll'; import { Dashboard } from './getAll';
export type Props = { export type Props = {
uuid: Dashboard['uuid']; id: Dashboard['id'];
}; };
export interface PayloadProps { export interface PayloadProps {
status: 'success'; status: string;
data: null;
} }

View File

@ -1,7 +1,10 @@
import { Dashboard } from './getAll'; import { Dashboard } from './getAll';
export type Props = { export type Props = {
uuid: Dashboard['uuid']; id: Dashboard['id'];
}; };
export type PayloadProps = Dashboard; export interface PayloadProps {
data: Dashboard;
status: string;
}

View File

@ -9,8 +9,6 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { IField } from '../logs/fields'; import { IField } from '../logs/fields';
import { BaseAutocompleteData } from '../queryBuilder/queryAutocompleteResponse'; import { BaseAutocompleteData } from '../queryBuilder/queryAutocompleteResponse';
export type PayloadProps = Dashboard[];
export const VariableQueryTypeArr = ['QUERY', 'TEXTBOX', 'CUSTOM'] as const; export const VariableQueryTypeArr = ['QUERY', 'TEXTBOX', 'CUSTOM'] as const;
export type TVariableQueryType = typeof VariableQueryTypeArr[number]; export type TVariableQueryType = typeof VariableQueryTypeArr[number];
@ -50,14 +48,18 @@ export interface IDashboardVariable {
change?: boolean; change?: boolean;
} }
export interface Dashboard { export interface Dashboard {
id: number; id: string;
uuid: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
createdBy: string; createdBy: string;
updatedBy: string; updatedBy: string;
data: DashboardData; data: DashboardData;
isLocked?: boolean; locked?: boolean;
}
export interface PayloadProps {
data: Dashboard[];
status: string;
} }
export interface DashboardTemplate { export interface DashboardTemplate {
@ -69,7 +71,7 @@ export interface DashboardTemplate {
} }
export interface DashboardData { export interface DashboardData {
uuid?: string; // uuid?: string;
description?: string; description?: string;
tags?: string[]; tags?: string[];
name?: string; name?: string;

View File

@ -0,0 +1,11 @@
import { Dashboard } from './getAll';
export type Props = {
id: Dashboard['id'];
lock: boolean;
};
export interface PayloadProps {
data: null;
status: string;
}

View File

@ -1,8 +1,11 @@
import { Dashboard, DashboardData } from './getAll'; import { Dashboard, DashboardData } from './getAll';
export type Props = { export type Props = {
uuid: Dashboard['uuid']; id: Dashboard['id'];
data: DashboardData; data: DashboardData;
}; };
export type PayloadProps = Dashboard; export interface PayloadProps {
data: Dashboard;
status: string;
}

2
go.mod
View File

@ -26,7 +26,6 @@ require (
github.com/gorilla/handlers v1.5.1 github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
github.com/gosimple/slug v1.10.0
github.com/huandu/go-sqlbuilder v1.35.0 github.com/huandu/go-sqlbuilder v1.35.0
github.com/jackc/pgx/v5 v5.7.2 github.com/jackc/pgx/v5 v5.7.2
github.com/jmoiron/sqlx v1.3.4 github.com/jmoiron/sqlx v1.3.4
@ -138,7 +137,6 @@ require (
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.14.0 // indirect github.com/googleapis/gax-go/v2 v2.14.0 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/gosimple/unidecode v1.0.0 // indirect
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect

4
go.sum
View File

@ -450,10 +450,6 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gosimple/slug v1.10.0 h1:3XbiQua1IpCdrvuntWvGBxVm+K99wCSxJjlxkP49GGQ=
github.com/gosimple/slug v1.10.0/go.mod h1:MICb3w495l9KNdZm+Xn5b6T2Hn831f9DMxiJ1r+bAjw=
github.com/gosimple/unidecode v1.0.0 h1:kPdvM+qy0tnk4/BrnkrbdJ82xe88xn7c9hcaipDz4dQ=
github.com/gosimple/unidecode v1.0.0/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=

View File

@ -4,25 +4,32 @@ import (
"context" "context"
"net/http" "net/http"
"github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
) )
type Module interface { type Module interface {
Create(ctx context.Context, orgID string, email string, data map[string]interface{}) (*types.Dashboard, error) Create(ctx context.Context, orgID valuer.UUID, createdBy string, data dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error)
List(ctx context.Context, orgID string) ([]*types.Dashboard, error) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error)
Delete(ctx context.Context, orgID, uuid string) error List(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error)
Get(ctx context.Context, orgID, uuid string) (*types.Dashboard, error) Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, data dashboardtypes.UpdatableDashboard) (*dashboardtypes.Dashboard, error)
GetByMetricNames(ctx context.Context, orgID string, metricNames []string) (map[string][]map[string]string, error) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, lock bool) error
Update(ctx context.Context, orgID, userEmail, uuid string, data map[string]interface{}) (*types.Dashboard, error) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
LockUnlock(ctx context.Context, orgID, uuid string, lock bool) error GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]map[string]string, error)
} }
type Handler interface { type Handler interface {
Create(http.ResponseWriter, *http.Request)
Update(http.ResponseWriter, *http.Request)
LockUnlock(http.ResponseWriter, *http.Request)
Delete(http.ResponseWriter, *http.Request) Delete(http.ResponseWriter, *http.Request)
} }

View File

@ -2,12 +2,16 @@ package impldashboard
import ( import (
"context" "context"
"encoding/json"
"net/http" "net/http"
"time" "time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render" "github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/dashboard" "github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
@ -19,8 +23,8 @@ func NewHandler(module dashboard.Module) dashboard.Handler {
return &handler{module: module} return &handler{module: module}
} }
func (handler *handler) Delete(rw http.ResponseWriter, req *http.Request) { func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second) ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel() defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx) claims, err := authtypes.ClaimsFromContext(ctx)
@ -29,13 +33,151 @@ func (handler *handler) Delete(rw http.ResponseWriter, req *http.Request) {
return return
} }
uuid := mux.Vars(req)["uuid"] orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
err = handler.module.Delete(ctx, claims.OrgID, uuid) req := dashboardtypes.PostableDashboard{}
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
render.Error(rw, err)
return
}
dashboard, err := handler.module.Create(ctx, orgID, claims.Email, req)
if err != nil {
render.Error(rw, err)
return
}
gettableDashboard, err := dashboardtypes.NewGettableDashboardFromDashboard(dashboard)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, gettableDashboard)
}
func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
id := mux.Vars(r)["id"]
if id == "" {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
return
}
dashboardID, err := valuer.NewUUID(id)
if err != nil {
render.Error(rw, err)
return
}
req := dashboardtypes.UpdatableDashboard{}
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
render.Error(rw, err)
return
}
dashboard, err := handler.module.Update(ctx, orgID, dashboardID, claims.Email, req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, dashboard)
}
func (handler *handler) LockUnlock(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
id := mux.Vars(r)["id"]
if id == "" {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
return
}
dashboardID, err := valuer.NewUUID(id)
if err != nil {
render.Error(rw, err)
return
}
req := new(dashboardtypes.LockUnlockDashboard)
err = json.NewDecoder(r.Body).Decode(req)
if err != nil {
render.Error(rw, err)
return
}
err = handler.module.LockUnlock(ctx, orgID, dashboardID, claims.Email, *req.Locked)
if err != nil { if err != nil {
render.Error(rw, err) render.Error(rw, err)
return return
} }
render.Success(rw, http.StatusOK, nil) render.Success(rw, http.StatusOK, nil)
}
func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
id := mux.Vars(r)["id"]
if id == "" {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is missing in the path"))
return
}
dashboardID, err := valuer.NewUUID(id)
if err != nil {
render.Error(rw, err)
return
}
err = handler.module.Delete(ctx, orgID, dashboardID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
} }

View File

@ -2,164 +2,40 @@ package impldashboard
import ( import (
"context" "context"
"encoding/json"
"strings" "strings"
"time"
"github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/dashboard" "github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/google/uuid" "github.com/SigNoz/signoz/pkg/valuer"
) )
type module struct { type module struct {
sqlstore sqlstore.SQLStore store dashboardtypes.Store
settings factory.ScopedProviderSettings
} }
func NewModule(sqlstore sqlstore.SQLStore) dashboard.Module { func NewModule(sqlstore sqlstore.SQLStore, settings factory.ProviderSettings) dashboard.Module {
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/modules/impldashboard")
return &module{ return &module{
sqlstore: sqlstore, store: NewStore(sqlstore),
settings: scopedProviderSettings,
} }
} }
// CreateDashboard creates a new dashboard func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy string, postableDashboard dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error) {
func (module *module) Create(ctx context.Context, orgID string, email string, data map[string]interface{}) (*types.Dashboard, error) { dashboard, err := dashboardtypes.NewDashboard(orgID, createdBy, postableDashboard)
dash := &types.Dashboard{
Data: data,
}
dash.OrgID = orgID
dash.CreatedAt = time.Now()
dash.CreatedBy = email
dash.UpdatedAt = time.Now()
dash.UpdatedBy = email
dash.UpdateSlug()
dash.UUID = uuid.New().String()
if data["uuid"] != nil {
dash.UUID = data["uuid"].(string)
}
err := module.
sqlstore.
BunDB().
NewInsert().
Model(dash).
Returning("id").
Scan(ctx, &dash.ID)
if err != nil {
return nil, module.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "dashboard with uuid %s already exists", dash.UUID)
}
return dash, nil
}
func (module *module) List(ctx context.Context, orgID string) ([]*types.Dashboard, error) {
dashboards := []*types.Dashboard{}
err := module.
sqlstore.
BunDB().
NewSelect().
Model(&dashboards).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return dashboards, nil storableDashboard, err := dashboardtypes.NewStorableDashboardFromDashboard(dashboard)
}
func (module *module) Delete(ctx context.Context, orgID, uuid string) error {
dashboard, err := module.Get(ctx, orgID, uuid)
if err != nil {
return err
}
if dashboard.Locked != nil && *dashboard.Locked == 1 {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be able to delete it")
}
result, err := module.
sqlstore.
BunDB().
NewDelete().
Model(&types.Dashboard{}).
Where("org_id = ?", orgID).
Where("uuid = ?", uuid).
Exec(ctx)
if err != nil {
return err
}
affectedRows, err := result.RowsAffected()
if err != nil {
return err
}
if affectedRows == 0 {
return errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "no dashboard found with uuid: %s", uuid)
}
return nil
}
func (module *module) Get(ctx context.Context, orgID, uuid string) (*types.Dashboard, error) {
dashboard := types.Dashboard{}
err := module.
sqlstore.
BunDB().
NewSelect().
Model(&dashboard).
Where("org_id = ?", orgID).
Where("uuid = ?", uuid).
Scan(ctx)
if err != nil {
return nil, module.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "dashboard with uuid %s not found", uuid)
}
return &dashboard, nil
}
func (module *module) Update(ctx context.Context, orgID, userEmail, uuid string, data map[string]interface{}) (*types.Dashboard, error) {
mapData, err := json.Marshal(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = module.store.Create(ctx, storableDashboard)
dashboard, err := module.Get(ctx, orgID, uuid)
if err != nil {
return nil, err
}
if dashboard.Locked != nil && *dashboard.Locked == 1 {
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be able to edit it")
}
// if the total count of panels has reduced by more than 1,
// return error
existingIds := getWidgetIds(dashboard.Data)
newIds := getWidgetIds(data)
differenceIds := getIdDifference(existingIds, newIds)
if len(differenceIds) > 1 {
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "deleting more than one panel is not supported")
}
dashboard.UpdatedAt = time.Now()
dashboard.UpdatedBy = userEmail
dashboard.Data = data
_, err = module.sqlstore.
BunDB().
NewUpdate().
Model(dashboard).
Set("updated_at = ?", dashboard.UpdatedAt).
Set("updated_by = ?", userEmail).
Set("data = ?", mapData).
Where("uuid = ?", dashboard.UUID).Exec(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -167,28 +43,73 @@ func (module *module) Update(ctx context.Context, orgID, userEmail, uuid string,
return dashboard, nil return dashboard, nil
} }
func (module *module) LockUnlock(ctx context.Context, orgID, uuid string, lock bool) error { func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
dashboard, err := module.Get(ctx, orgID, uuid) storableDashboard, err := module.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
dashboard, err := dashboardtypes.NewDashboardFromStorableDashboard(storableDashboard)
if err != nil {
return nil, err
}
return dashboard, nil
}
func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
storableDashboards, err := module.store.List(ctx, orgID)
if err != nil {
return nil, err
}
dashboards, err := dashboardtypes.NewDashboardsFromStorableDashboards(storableDashboards)
if err != nil {
return nil, err
}
return dashboards, nil
}
func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updatableDashboard dashboardtypes.UpdatableDashboard) (*dashboardtypes.Dashboard, error) {
dashboard, err := module.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
err = dashboard.Update(updatableDashboard, updatedBy)
if err != nil {
return nil, err
}
storableDashboard, err := dashboardtypes.NewStorableDashboardFromDashboard(dashboard)
if err != nil {
return nil, err
}
err = module.store.Update(ctx, orgID, storableDashboard)
if err != nil {
return nil, err
}
return dashboard, nil
}
func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, lock bool) error {
dashboard, err := module.Get(ctx, orgID, id)
if err != nil { if err != nil {
return err return err
} }
var lockValue int err = dashboard.LockUnlock(ctx, lock, updatedBy)
if lock { if err != nil {
lockValue = 1 return err
} else { }
lockValue = 0 storableDashboard, err := dashboardtypes.NewStorableDashboardFromDashboard(dashboard)
if err != nil {
return err
} }
_, err = module. err = module.store.Update(ctx, orgID, storableDashboard)
sqlstore.
BunDB().
NewUpdate().
Model(dashboard).
Set("locked = ?", lockValue).
Where("org_id = ?", orgID).
Where("uuid = ?", uuid).
Exec(ctx)
if err != nil { if err != nil {
return err return err
} }
@ -196,15 +117,21 @@ func (module *module) LockUnlock(ctx context.Context, orgID, uuid string, lock b
return nil return nil
} }
func (module *module) GetByMetricNames(ctx context.Context, orgID string, metricNames []string) (map[string][]map[string]string, error) { func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
dashboards := []types.Dashboard{} dashboard, err := module.Get(ctx, orgID, id)
err := module. if err != nil {
sqlstore. return err
BunDB(). }
NewSelect().
Model(&dashboards). if dashboard.Locked {
Where("org_id = ?", orgID). return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be delete it")
Scan(ctx) }
return module.store.Delete(ctx, orgID, id)
}
func (module *module) GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]map[string]string, error) {
dashboards, err := module.List(ctx, orgID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -266,7 +193,7 @@ func (module *module) GetByMetricNames(ctx context.Context, orgID string, metric
for _, metricName := range metricNames { for _, metricName := range metricNames {
if strings.TrimSpace(key) == metricName { if strings.TrimSpace(key) == metricName {
result[metricName] = append(result[metricName], map[string]string{ result[metricName] = append(result[metricName], map[string]string{
"dashboard_id": dashboard.UUID, "dashboard_id": dashboard.ID,
"widget_name": widgetTitle, "widget_name": widgetTitle,
"widget_id": widgetID, "widget_id": widgetID,
"dashboard_name": dashTitle, "dashboard_name": dashTitle,
@ -280,52 +207,3 @@ func (module *module) GetByMetricNames(ctx context.Context, orgID string, metric
return result, nil return result, nil
} }
func getWidgetIds(data map[string]interface{}) []string {
widgetIds := []string{}
if data != nil && data["widgets"] != nil {
widgets, ok := data["widgets"]
if ok {
data, ok := widgets.([]interface{})
if ok {
for _, widget := range data {
sData, ok := widget.(map[string]interface{})
if ok && sData["query"] != nil && sData["id"] != nil {
id, ok := sData["id"].(string)
if ok {
widgetIds = append(widgetIds, id)
}
}
}
}
}
}
return widgetIds
}
func getIdDifference(existingIds []string, newIds []string) []string {
// Convert newIds array to a map for faster lookups
newIdsMap := make(map[string]bool)
for _, id := range newIds {
newIdsMap[id] = true
}
// Initialize a map to keep track of elements in the difference array
differenceMap := make(map[string]bool)
// Initialize the difference array
difference := []string{}
// Iterate through existingIds
for _, id := range existingIds {
// If the id is not found in newIds, and it's not already in the difference array
if _, found := newIdsMap[id]; !found && !differenceMap[id] {
difference = append(difference, id)
differenceMap[id] = true // Mark the id as seen in the difference array
}
}
return difference
}

View File

@ -0,0 +1,99 @@
package impldashboard
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type store struct {
sqlstore sqlstore.SQLStore
}
func NewStore(sqlstore sqlstore.SQLStore) dashboardtypes.Store {
return &store{sqlstore: sqlstore}
}
func (store *store) Create(ctx context.Context, storabledashboard *dashboardtypes.StorableDashboard) error {
_, err := store.
sqlstore.
BunDB().
NewInsert().
Model(storabledashboard).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "dashboard with id %s already exists", storabledashboard.ID)
}
return nil
}
func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.StorableDashboard, error) {
storableDashboard := new(dashboardtypes.StorableDashboard)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(storableDashboard).
Where("id = ?", id).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "dashboard with id %s doesn't exist", id)
}
return storableDashboard, nil
}
func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.StorableDashboard, error) {
storableDashboards := make([]*dashboardtypes.StorableDashboard, 0)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(&storableDashboards).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "no dashboards found in orgID %s", orgID)
}
return storableDashboards, nil
}
func (store *store) Update(ctx context.Context, orgID valuer.UUID, storableDashboard *dashboardtypes.StorableDashboard) error {
_, err := store.
sqlstore.
BunDB().
NewUpdate().
Model(storableDashboard).
WherePK().
Where("org_id = ?", orgID).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapNotFoundErrf(err, errors.CodeAlreadyExists, "dashboard with id %s doesn't exist", storableDashboard.ID)
}
return nil
}
func (store *store) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
_, err := store.
sqlstore.
BunDB().
NewDelete().
Model(new(dashboardtypes.StorableDashboard)).
Where("id = ?", id).
Where("org_id = ?", orgID).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "dashboard with id %s doesn't exist", id)
}
return nil
}

View File

@ -68,6 +68,7 @@ func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) {
err = json.NewDecoder(r.Body).Decode(&req) err = json.NewDecoder(r.Body).Decode(&req)
if err != nil { if err != nil {
render.Error(rw, err) render.Error(rw, err)
return
} }
req.ID = orgID req.ID = orgID

View File

@ -4,11 +4,13 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/SigNoz/signoz/pkg/query-service/constants" "github.com/SigNoz/signoz/pkg/query-service/constants"
"math"
"strconv" "strconv"
"strings" "strings"
"github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/telemetrystore" "github.com/SigNoz/signoz/pkg/telemetrystore"
promValue "github.com/prometheus/prometheus/model/value"
"github.com/prometheus/prometheus/prompb" "github.com/prometheus/prometheus/prompb"
"github.com/prometheus/prometheus/storage" "github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/storage/remote" "github.com/prometheus/prometheus/storage/remote"
@ -188,9 +190,10 @@ func (client *client) querySamples(ctx context.Context, start int64, end int64,
var fingerprint, prevFingerprint uint64 var fingerprint, prevFingerprint uint64
var timestampMs int64 var timestampMs int64
var value float64 var value float64
var flags uint32
for rows.Next() { for rows.Next() {
if err := rows.Scan(&metricName, &fingerprint, &timestampMs, &value); err != nil { if err := rows.Scan(&metricName, &fingerprint, &timestampMs, &value, &flags); err != nil {
return nil, err return nil, err
} }
@ -208,6 +211,10 @@ func (client *client) querySamples(ctx context.Context, start int64, end int64,
} }
} }
if flags&1 == 1 {
value = math.Float64frombits(promValue.StaleNaN)
}
// add samples to current time series // add samples to current time series
ts.Samples = append(ts.Samples, prompb.Sample{ ts.Samples = append(ts.Samples, prompb.Sample{
Timestamp: timestampMs, Timestamp: timestampMs,

View File

@ -13,6 +13,8 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/model" "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
) )
@ -32,9 +34,7 @@ type Controller struct {
serviceConfigRepo ServiceConfigDatabase serviceConfigRepo ServiceConfigDatabase
} }
func NewController(sqlStore sqlstore.SQLStore) ( func NewController(sqlStore sqlstore.SQLStore) (*Controller, error) {
*Controller, error,
) {
accountsRepo, err := newCloudProviderAccountsRepository(sqlStore) accountsRepo, err := newCloudProviderAccountsRepository(sqlStore)
if err != nil { if err != nil {
return nil, fmt.Errorf("couldn't create cloud provider accounts repo: %w", err) return nil, fmt.Errorf("couldn't create cloud provider accounts repo: %w", err)
@ -55,9 +55,7 @@ type ConnectedAccountsListResponse struct {
Accounts []types.Account `json:"accounts"` Accounts []types.Account `json:"accounts"`
} }
func (c *Controller) ListConnectedAccounts( func (c *Controller) ListConnectedAccounts(ctx context.Context, orgId string, cloudProvider string) (
ctx context.Context, orgId string, cloudProvider string,
) (
*ConnectedAccountsListResponse, *model.ApiError, *ConnectedAccountsListResponse, *model.ApiError,
) { ) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil { if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
@ -103,9 +101,7 @@ type GenerateConnectionUrlResponse struct {
ConnectionUrl string `json:"connection_url"` ConnectionUrl string `json:"connection_url"`
} }
func (c *Controller) GenerateConnectionUrl( func (c *Controller) GenerateConnectionUrl(ctx context.Context, orgId string, cloudProvider string, req GenerateConnectionUrlRequest) (*GenerateConnectionUrlResponse, *model.ApiError) {
ctx context.Context, orgId string, cloudProvider string, req GenerateConnectionUrlRequest,
) (*GenerateConnectionUrlResponse, *model.ApiError) {
// Account connection with a simple connection URL may not be available for all providers. // Account connection with a simple connection URL may not be available for all providers.
if cloudProvider != "aws" { if cloudProvider != "aws" {
return nil, model.BadRequest(fmt.Errorf("unsupported cloud provider: %s", cloudProvider)) return nil, model.BadRequest(fmt.Errorf("unsupported cloud provider: %s", cloudProvider))
@ -154,9 +150,7 @@ type AccountStatusResponse struct {
Status types.AccountStatus `json:"status"` Status types.AccountStatus `json:"status"`
} }
func (c *Controller) GetAccountStatus( func (c *Controller) GetAccountStatus(ctx context.Context, orgId string, cloudProvider string, accountId string) (
ctx context.Context, orgId string, cloudProvider string, accountId string,
) (
*AccountStatusResponse, *model.ApiError, *AccountStatusResponse, *model.ApiError,
) { ) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil { if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
@ -198,9 +192,7 @@ type IntegrationConfigForAgent struct {
TelemetryCollectionStrategy *CompiledCollectionStrategy `json:"telemetry,omitempty"` TelemetryCollectionStrategy *CompiledCollectionStrategy `json:"telemetry,omitempty"`
} }
func (c *Controller) CheckInAsAgent( func (c *Controller) CheckInAsAgent(ctx context.Context, orgId string, cloudProvider string, req AgentCheckInRequest) (*AgentCheckInResponse, error) {
ctx context.Context, orgId string, cloudProvider string, req AgentCheckInRequest,
) (*AgentCheckInResponse, error) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil { if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr return nil, apiErr
} }
@ -293,13 +285,7 @@ type UpdateAccountConfigRequest struct {
Config types.AccountConfig `json:"config"` Config types.AccountConfig `json:"config"`
} }
func (c *Controller) UpdateAccountConfig( func (c *Controller) UpdateAccountConfig(ctx context.Context, orgId string, cloudProvider string, accountId string, req UpdateAccountConfigRequest) (*types.Account, *model.ApiError) {
ctx context.Context,
orgId string,
cloudProvider string,
accountId string,
req UpdateAccountConfigRequest,
) (*types.Account, *model.ApiError) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil { if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr return nil, apiErr
} }
@ -316,9 +302,7 @@ func (c *Controller) UpdateAccountConfig(
return &account, nil return &account, nil
} }
func (c *Controller) DisconnectAccount( func (c *Controller) DisconnectAccount(ctx context.Context, orgId string, cloudProvider string, accountId string) (*types.CloudIntegration, *model.ApiError) {
ctx context.Context, orgId string, cloudProvider string, accountId string,
) (*types.CloudIntegration, *model.ApiError) {
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil { if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
return nil, apiErr return nil, apiErr
} }
@ -520,10 +504,8 @@ func (c *Controller) UpdateServiceConfig(
// All dashboards that are available based on cloud integrations configuration // All dashboards that are available based on cloud integrations configuration
// across all cloud providers // across all cloud providers
func (c *Controller) AvailableDashboards(ctx context.Context, orgId string) ( func (c *Controller) AvailableDashboards(ctx context.Context, orgId valuer.UUID) ([]*dashboardtypes.Dashboard, *model.ApiError) {
[]*types.Dashboard, *model.ApiError, allDashboards := []*dashboardtypes.Dashboard{}
) {
allDashboards := []*types.Dashboard{}
for _, provider := range []string{"aws"} { for _, provider := range []string{"aws"} {
providerDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, orgId, provider) providerDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, orgId, provider)
@ -539,11 +521,8 @@ func (c *Controller) AvailableDashboards(ctx context.Context, orgId string) (
return allDashboards, nil return allDashboards, nil
} }
func (c *Controller) AvailableDashboardsForCloudProvider( func (c *Controller) AvailableDashboardsForCloudProvider(ctx context.Context, orgID valuer.UUID, cloudProvider string) ([]*dashboardtypes.Dashboard, *model.ApiError) {
ctx context.Context, orgID string, cloudProvider string, accountRecords, apiErr := c.accountsRepo.listConnected(ctx, orgID.StringValue(), cloudProvider)
) ([]*types.Dashboard, *model.ApiError) {
accountRecords, apiErr := c.accountsRepo.listConnected(ctx, orgID, cloudProvider)
if apiErr != nil { if apiErr != nil {
return nil, model.WrapApiError(apiErr, "couldn't list connected cloud accounts") return nil, model.WrapApiError(apiErr, "couldn't list connected cloud accounts")
} }
@ -554,7 +533,7 @@ func (c *Controller) AvailableDashboardsForCloudProvider(
for _, ar := range accountRecords { for _, ar := range accountRecords {
if ar.AccountID != nil { if ar.AccountID != nil {
configsBySvcId, apiErr := c.serviceConfigRepo.getAllForAccount( configsBySvcId, apiErr := c.serviceConfigRepo.getAllForAccount(
ctx, orgID, ar.ID.StringValue(), ctx, orgID.StringValue(), ar.ID.StringValue(),
) )
if apiErr != nil { if apiErr != nil {
return nil, apiErr return nil, apiErr
@ -573,16 +552,15 @@ func (c *Controller) AvailableDashboardsForCloudProvider(
return nil, apiErr return nil, apiErr
} }
svcDashboards := []*types.Dashboard{} svcDashboards := []*dashboardtypes.Dashboard{}
for _, svc := range allServices { for _, svc := range allServices {
serviceDashboardsCreatedAt := servicesWithAvailableMetrics[svc.Id] serviceDashboardsCreatedAt := servicesWithAvailableMetrics[svc.Id]
if serviceDashboardsCreatedAt != nil { if serviceDashboardsCreatedAt != nil {
for _, d := range svc.Assets.Dashboards { for _, d := range svc.Assets.Dashboards {
isLocked := 1
author := fmt.Sprintf("%s-integration", cloudProvider) author := fmt.Sprintf("%s-integration", cloudProvider)
svcDashboards = append(svcDashboards, &types.Dashboard{ svcDashboards = append(svcDashboards, &dashboardtypes.Dashboard{
UUID: c.dashboardUuid(cloudProvider, svc.Id, d.Id), ID: c.dashboardUuid(cloudProvider, svc.Id, d.Id),
Locked: &isLocked, Locked: true,
Data: *d.Definition, Data: *d.Definition,
TimeAuditable: types.TimeAuditable{ TimeAuditable: types.TimeAuditable{
CreatedAt: *serviceDashboardsCreatedAt, CreatedAt: *serviceDashboardsCreatedAt,
@ -592,6 +570,7 @@ func (c *Controller) AvailableDashboardsForCloudProvider(
CreatedBy: author, CreatedBy: author,
UpdatedBy: author, UpdatedBy: author,
}, },
OrgID: orgID,
}) })
} }
servicesWithAvailableMetrics[svc.Id] = nil servicesWithAvailableMetrics[svc.Id] = nil
@ -600,11 +579,7 @@ func (c *Controller) AvailableDashboardsForCloudProvider(
return svcDashboards, nil return svcDashboards, nil
} }
func (c *Controller) GetDashboardById( func (c *Controller) GetDashboardById(ctx context.Context, orgId valuer.UUID, dashboardUuid string) (*dashboardtypes.Dashboard, *model.ApiError) {
ctx context.Context,
orgId string,
dashboardUuid string,
) (*types.Dashboard, *model.ApiError) {
cloudProvider, _, _, apiErr := c.parseDashboardUuid(dashboardUuid) cloudProvider, _, _, apiErr := c.parseDashboardUuid(dashboardUuid)
if apiErr != nil { if apiErr != nil {
return nil, apiErr return nil, apiErr
@ -612,38 +587,28 @@ func (c *Controller) GetDashboardById(
allDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, orgId, cloudProvider) allDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, orgId, cloudProvider)
if apiErr != nil { if apiErr != nil {
return nil, model.WrapApiError( return nil, model.WrapApiError(apiErr, "couldn't list available dashboards")
apiErr, fmt.Sprintf("couldn't list available dashboards"),
)
} }
for _, d := range allDashboards { for _, d := range allDashboards {
if d.UUID == dashboardUuid { if d.ID == dashboardUuid {
return d, nil return d, nil
} }
} }
return nil, model.NotFoundError(fmt.Errorf( return nil, model.NotFoundError(fmt.Errorf("couldn't find dashboard with uuid: %s", dashboardUuid))
"couldn't find dashboard with uuid: %s", dashboardUuid,
))
} }
func (c *Controller) dashboardUuid( func (c *Controller) dashboardUuid(
cloudProvider string, svcId string, dashboardId string, cloudProvider string, svcId string, dashboardId string,
) string { ) string {
return fmt.Sprintf( return fmt.Sprintf("cloud-integration--%s--%s--%s", cloudProvider, svcId, dashboardId)
"cloud-integration--%s--%s--%s", cloudProvider, svcId, dashboardId,
)
} }
func (c *Controller) parseDashboardUuid(dashboardUuid string) ( func (c *Controller) parseDashboardUuid(dashboardUuid string) (cloudProvider string, svcId string, dashboardId string, apiErr *model.ApiError) {
cloudProvider string, svcId string, dashboardId string, apiErr *model.ApiError,
) {
parts := strings.SplitN(dashboardUuid, "--", 4) parts := strings.SplitN(dashboardUuid, "--", 4)
if len(parts) != 4 || parts[0] != "cloud-integration" { if len(parts) != 4 || parts[0] != "cloud-integration" {
return "", "", "", model.BadRequest(fmt.Errorf( return "", "", "", model.BadRequest(fmt.Errorf("invalid cloud integration dashboard id"))
"invalid cloud integration dashboard id",
))
} }
return parts[1], parts[2], parts[3], nil return parts[1], parts[2], parts[3], nil

View File

@ -1,7 +1,7 @@
package services package services
import ( import (
"github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/dashboardtypes"
) )
type Metadata struct { type Metadata struct {
@ -82,10 +82,10 @@ type AWSLogsStrategy struct {
} }
type Dashboard struct { type Dashboard struct {
Id string `json:"id"` Id string `json:"id"`
Url string `json:"url"` Url string `json:"url"`
Title string `json:"title"` Title string `json:"title"`
Description string `json:"description"` Description string `json:"description"`
Image string `json:"image"` Image string `json:"image"`
Definition *types.DashboardData `json:"definition,omitempty"` Definition *dashboardtypes.StorableDashboardData `json:"definition,omitempty"`
} }

View File

@ -7,7 +7,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/SigNoz/signoz/pkg/query-service/constants"
"io" "io"
"math" "math"
"net/http" "net/http"
@ -21,6 +20,8 @@ import (
"text/template" "text/template"
"time" "time"
"github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/alertmanager" "github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apis/fields" "github.com/SigNoz/signoz/pkg/apis/fields"
errorsV2 "github.com/SigNoz/signoz/pkg/errors" errorsV2 "github.com/SigNoz/signoz/pkg/errors"
@ -60,6 +61,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/postprocess" "github.com/SigNoz/signoz/pkg/query-service/postprocess"
"github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/featuretypes" "github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/pipelinetypes" "github.com/SigNoz/signoz/pkg/types/pipelinetypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes" ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
@ -512,11 +514,12 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/downtime_schedules/{id}", am.EditAccess(aH.editDowntimeSchedule)).Methods(http.MethodPut) router.HandleFunc("/api/v1/downtime_schedules/{id}", am.EditAccess(aH.editDowntimeSchedule)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/downtime_schedules/{id}", am.EditAccess(aH.deleteDowntimeSchedule)).Methods(http.MethodDelete) router.HandleFunc("/api/v1/downtime_schedules/{id}", am.EditAccess(aH.deleteDowntimeSchedule)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/dashboards", am.ViewAccess(aH.getDashboards)).Methods(http.MethodGet) router.HandleFunc("/api/v1/dashboards", am.ViewAccess(aH.List)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/dashboards", am.EditAccess(aH.createDashboards)).Methods(http.MethodPost) router.HandleFunc("/api/v1/dashboards", am.EditAccess(aH.Signoz.Handlers.Dashboard.Create)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/dashboards/{uuid}", am.ViewAccess(aH.getDashboard)).Methods(http.MethodGet) router.HandleFunc("/api/v1/dashboards/{id}", am.ViewAccess(aH.Get)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/dashboards/{uuid}", am.EditAccess(aH.updateDashboard)).Methods(http.MethodPut) router.HandleFunc("/api/v1/dashboards/{id}", am.EditAccess(aH.Signoz.Handlers.Dashboard.Update)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/dashboards/{uuid}", am.EditAccess(aH.Signoz.Handlers.Dashboard.Delete)).Methods(http.MethodDelete) router.HandleFunc("/api/v1/dashboards/{id}", am.EditAccess(aH.Signoz.Handlers.Dashboard.Delete)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/dashboards/{id}/lock", am.EditAccess(aH.Signoz.Handlers.Dashboard.LockUnlock)).Methods(http.MethodPut)
router.HandleFunc("/api/v2/variables/query", am.ViewAccess(aH.queryDashboardVarsV2)).Methods(http.MethodPost) router.HandleFunc("/api/v2/variables/query", am.ViewAccess(aH.queryDashboardVarsV2)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/explorer/views", am.ViewAccess(aH.Signoz.Handlers.SavedView.List)).Methods(http.MethodGet) router.HandleFunc("/api/v1/explorer/views", am.ViewAccess(aH.Signoz.Handlers.SavedView.List)).Methods(http.MethodGet)
@ -528,7 +531,6 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/feedback", am.OpenAccess(aH.submitFeedback)).Methods(http.MethodPost) router.HandleFunc("/api/v1/feedback", am.OpenAccess(aH.submitFeedback)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/event", am.ViewAccess(aH.registerEvent)).Methods(http.MethodPost) router.HandleFunc("/api/v1/event", am.ViewAccess(aH.registerEvent)).Methods(http.MethodPost)
// router.HandleFunc("/api/v1/get_percentiles", aH.getApplicationPercentiles).Methods(http.MethodGet)
router.HandleFunc("/api/v1/services", am.ViewAccess(aH.getServices)).Methods(http.MethodPost) router.HandleFunc("/api/v1/services", am.ViewAccess(aH.getServices)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/services/list", am.ViewAccess(aH.getServicesList)).Methods(http.MethodGet) router.HandleFunc("/api/v1/services/list", am.ViewAccess(aH.getServicesList)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/service/top_operations", am.ViewAccess(aH.getTopOperations)).Methods(http.MethodPost) router.HandleFunc("/api/v1/service/top_operations", am.ViewAccess(aH.getTopOperations)).Methods(http.MethodPost)
@ -1084,77 +1086,6 @@ func (aH *APIHandler) listRules(w http.ResponseWriter, r *http.Request) {
aH.Respond(w, rules) aH.Respond(w, rules)
} }
func (aH *APIHandler) getDashboards(w http.ResponseWriter, r *http.Request) {
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
if errv2 != nil {
render.Error(w, errv2)
return
}
allDashboards, errv2 := aH.Signoz.Modules.Dashboard.List(r.Context(), claims.OrgID)
if errv2 != nil {
render.Error(w, errv2)
return
}
ic := aH.IntegrationsController
installedIntegrationDashboards, err := ic.GetDashboardsForInstalledIntegrations(r.Context(), claims.OrgID)
if err != nil {
zap.L().Error("failed to get dashboards for installed integrations", zap.Error(err))
} else {
allDashboards = append(allDashboards, installedIntegrationDashboards...)
}
cloudIntegrationDashboards, err := aH.CloudIntegrationsController.AvailableDashboards(r.Context(), claims.OrgID)
if err != nil {
zap.L().Error("failed to get cloud dashboards", zap.Error(err))
} else {
allDashboards = append(allDashboards, cloudIntegrationDashboards...)
}
tagsFromReq, ok := r.URL.Query()["tags"]
if !ok || len(tagsFromReq) == 0 || tagsFromReq[0] == "" {
aH.Respond(w, allDashboards)
return
}
tags2Dash := make(map[string][]int)
for i := 0; i < len(allDashboards); i++ {
tags, ok := (allDashboards)[i].Data["tags"].([]interface{})
if !ok {
continue
}
tagsArray := make([]string, len(tags))
for i, v := range tags {
tagsArray[i] = v.(string)
}
for _, tag := range tagsArray {
tags2Dash[tag] = append(tags2Dash[tag], i)
}
}
inter := make([]int, len(allDashboards))
for i := range inter {
inter[i] = i
}
for _, tag := range tagsFromReq {
inter = Intersection(inter, tags2Dash[tag])
}
filteredDashboards := []*types.Dashboard{}
for _, val := range inter {
dash := (allDashboards)[val]
filteredDashboards = append(filteredDashboards, dash)
}
aH.Respond(w, filteredDashboards)
}
func prepareQuery(r *http.Request) (string, error) { func prepareQuery(r *http.Request) (string, error) {
var postData *model.DashboardVars var postData *model.DashboardVars
@ -1224,6 +1155,114 @@ func prepareQuery(r *http.Request) (string, error) {
return newQuery, nil return newQuery, nil
} }
func (aH *APIHandler) Get(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
id := mux.Vars(r)["id"]
if id == "" {
render.Error(rw, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, "id is missing in the path"))
return
}
dashboard := new(dashboardtypes.Dashboard)
if aH.CloudIntegrationsController.IsCloudIntegrationDashboardUuid(id) {
cloudintegrationDashboard, apiErr := aH.CloudIntegrationsController.GetDashboardById(ctx, orgID, id)
if apiErr != nil {
render.Error(rw, errorsV2.Wrapf(apiErr, errorsV2.TypeInternal, errorsV2.CodeInternal, "failed to get dashboard"))
return
}
dashboard = cloudintegrationDashboard
} else if aH.IntegrationsController.IsInstalledIntegrationDashboardID(id) {
integrationDashboard, apiErr := aH.IntegrationsController.GetInstalledIntegrationDashboardById(ctx, orgID, id)
if apiErr != nil {
render.Error(rw, errorsV2.Wrapf(apiErr, errorsV2.TypeInternal, errorsV2.CodeInternal, "failed to get dashboard"))
return
}
dashboard = integrationDashboard
} else {
dashboardID, err := valuer.NewUUID(id)
if err != nil {
render.Error(rw, err)
return
}
sqlDashboard, err := aH.Signoz.Modules.Dashboard.Get(ctx, orgID, dashboardID)
if err != nil {
render.Error(rw, err)
return
}
dashboard = sqlDashboard
}
gettableDashboard, err := dashboardtypes.NewGettableDashboardFromDashboard(dashboard)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, gettableDashboard)
}
func (aH *APIHandler) List(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
dashboards := make([]*dashboardtypes.Dashboard, 0)
sqlDashboards, err := aH.Signoz.Modules.Dashboard.List(ctx, orgID)
if err != nil && !errorsV2.Ast(err, errorsV2.TypeNotFound) {
render.Error(rw, err)
return
}
if sqlDashboards != nil {
dashboards = append(dashboards, sqlDashboards...)
}
installedIntegrationDashboards, apiErr := aH.IntegrationsController.GetDashboardsForInstalledIntegrations(ctx, orgID)
if apiErr != nil {
zap.L().Error("failed to get dashboards for installed integrations", zap.Error(apiErr))
} else {
dashboards = append(dashboards, installedIntegrationDashboards...)
}
cloudIntegrationDashboards, apiErr := aH.CloudIntegrationsController.AvailableDashboards(ctx, orgID)
if apiErr != nil {
zap.L().Error("failed to get dashboards for cloud integrations", zap.Error(apiErr))
} else {
dashboards = append(dashboards, cloudIntegrationDashboards...)
}
gettableDashboards, err := dashboardtypes.NewGettableDashboardsFromDashboards(dashboards)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, gettableDashboards)
}
func (aH *APIHandler) queryDashboardVarsV2(w http.ResponseWriter, r *http.Request) { func (aH *APIHandler) queryDashboardVarsV2(w http.ResponseWriter, r *http.Request) {
query, err := prepareQuery(r) query, err := prepareQuery(r)
if err != nil { if err != nil {
@ -1239,121 +1278,6 @@ func (aH *APIHandler) queryDashboardVarsV2(w http.ResponseWriter, r *http.Reques
aH.Respond(w, dashboardVars) aH.Respond(w, dashboardVars)
} }
func (aH *APIHandler) updateDashboard(w http.ResponseWriter, r *http.Request) {
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
if errv2 != nil {
render.Error(w, errv2)
return
}
uuid := mux.Vars(r)["uuid"]
var postData map[string]interface{}
err := json.NewDecoder(r.Body).Decode(&postData)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading request body")
return
}
err = aH.IsDashboardPostDataSane(&postData)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading request body")
return
}
dashboard, apiError := aH.Signoz.Modules.Dashboard.Update(r.Context(), claims.OrgID, claims.Email, uuid, postData)
if apiError != nil {
render.Error(w, apiError)
return
}
aH.Respond(w, dashboard)
}
func (aH *APIHandler) IsDashboardPostDataSane(data *map[string]interface{}) error {
val, ok := (*data)["title"]
if !ok || val == nil {
return fmt.Errorf("title not found in post data")
}
return nil
}
func (aH *APIHandler) getDashboard(w http.ResponseWriter, r *http.Request) {
uuid := mux.Vars(r)["uuid"]
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
if errv2 != nil {
render.Error(w, errv2)
return
}
dashboard, errv2 := aH.Signoz.Modules.Dashboard.Get(r.Context(), claims.OrgID, uuid)
var apiError *model.ApiError
if errv2 != nil {
if !errorsV2.Ast(errv2, errorsV2.TypeNotFound) {
render.Error(w, errv2)
return
}
if aH.CloudIntegrationsController.IsCloudIntegrationDashboardUuid(uuid) {
dashboard, apiError = aH.CloudIntegrationsController.GetDashboardById(
r.Context(), claims.OrgID, uuid,
)
if apiError != nil {
RespondError(w, apiError, nil)
return
}
} else {
dashboard, apiError = aH.IntegrationsController.GetInstalledIntegrationDashboardById(
r.Context(), claims.OrgID, uuid,
)
if apiError != nil {
RespondError(w, apiError, nil)
return
}
}
}
aH.Respond(w, dashboard)
}
func (aH *APIHandler) createDashboards(w http.ResponseWriter, r *http.Request) {
var postData map[string]interface{}
err := json.NewDecoder(r.Body).Decode(&postData)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, "Error reading request body")
return
}
err = aH.IsDashboardPostDataSane(&postData)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, "Error reading request body")
return
}
claims, errv2 := authtypes.ClaimsFromContext(r.Context())
if errv2 != nil {
render.Error(w, errv2)
return
}
dash, errv2 := aH.Signoz.Modules.Dashboard.Create(r.Context(), claims.OrgID, claims.Email, postData)
if errv2 != nil {
render.Error(w, errv2)
return
}
aH.Respond(w, dash)
}
func (aH *APIHandler) testRule(w http.ResponseWriter, r *http.Request) { func (aH *APIHandler) testRule(w http.ResponseWriter, r *http.Request) {
claims, err := authtypes.ClaimsFromContext(r.Context()) claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil { if err != nil {

View File

@ -7741,4 +7741,4 @@
} }
], ],
"uuid": "e74aeb83-ac4b-4313-8a97-216b62c8fc59" "uuid": "e74aeb83-ac4b-4313-8a97-216b62c8fc59"
} }

View File

@ -7,17 +7,16 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/agentConf" "github.com/SigNoz/signoz/pkg/query-service/agentConf"
"github.com/SigNoz/signoz/pkg/query-service/model" "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/pipelinetypes" "github.com/SigNoz/signoz/pkg/types/pipelinetypes"
"github.com/SigNoz/signoz/pkg/valuer"
) )
type Controller struct { type Controller struct {
mgr *Manager mgr *Manager
} }
func NewController(sqlStore sqlstore.SQLStore) ( func NewController(sqlStore sqlstore.SQLStore) (*Controller, error) {
*Controller, error,
) {
mgr, err := NewManager(sqlStore) mgr, err := NewManager(sqlStore)
if err != nil { if err != nil {
return nil, fmt.Errorf("couldn't create integrations manager: %w", err) return nil, fmt.Errorf("couldn't create integrations manager: %w", err)
@ -34,11 +33,7 @@ type IntegrationsListResponse struct {
// Pagination details to come later // Pagination details to come later
} }
func (c *Controller) ListIntegrations( func (c *Controller) ListIntegrations(ctx context.Context, orgId string, params map[string]string) (*IntegrationsListResponse, *model.ApiError) {
ctx context.Context, orgId string, params map[string]string,
) (
*IntegrationsListResponse, *model.ApiError,
) {
var filters *IntegrationsFilter var filters *IntegrationsFilter
if isInstalledFilter, exists := params["is_installed"]; exists { if isInstalledFilter, exists := params["is_installed"]; exists {
isInstalled := !(isInstalledFilter == "false") isInstalled := !(isInstalledFilter == "false")
@ -57,15 +52,11 @@ func (c *Controller) ListIntegrations(
}, nil }, nil
} }
func (c *Controller) GetIntegration( func (c *Controller) GetIntegration(ctx context.Context, orgId string, integrationId string) (*Integration, *model.ApiError) {
ctx context.Context, orgId string, integrationId string,
) (*Integration, *model.ApiError) {
return c.mgr.GetIntegration(ctx, orgId, integrationId) return c.mgr.GetIntegration(ctx, orgId, integrationId)
} }
func (c *Controller) IsIntegrationInstalled( func (c *Controller) IsIntegrationInstalled(ctx context.Context, orgId string, integrationId string) (bool, *model.ApiError) {
ctx context.Context, orgId string, integrationId string,
) (bool, *model.ApiError) {
installation, apiErr := c.mgr.getInstalledIntegration(ctx, orgId, integrationId) installation, apiErr := c.mgr.getInstalledIntegration(ctx, orgId, integrationId)
if apiErr != nil { if apiErr != nil {
return false, apiErr return false, apiErr
@ -74,9 +65,7 @@ func (c *Controller) IsIntegrationInstalled(
return isInstalled, nil return isInstalled, nil
} }
func (c *Controller) GetIntegrationConnectionTests( func (c *Controller) GetIntegrationConnectionTests(ctx context.Context, orgId string, integrationId string) (*IntegrationConnectionTests, *model.ApiError) {
ctx context.Context, orgId string, integrationId string,
) (*IntegrationConnectionTests, *model.ApiError) {
return c.mgr.GetIntegrationConnectionTests(ctx, orgId, integrationId) return c.mgr.GetIntegrationConnectionTests(ctx, orgId, integrationId)
} }
@ -85,9 +74,7 @@ type InstallIntegrationRequest struct {
Config map[string]interface{} `json:"config"` Config map[string]interface{} `json:"config"`
} }
func (c *Controller) Install( func (c *Controller) Install(ctx context.Context, orgId string, req *InstallIntegrationRequest) (*IntegrationsListItem, *model.ApiError) {
ctx context.Context, orgId string, req *InstallIntegrationRequest,
) (*IntegrationsListItem, *model.ApiError) {
res, apiErr := c.mgr.InstallIntegration( res, apiErr := c.mgr.InstallIntegration(
ctx, orgId, req.IntegrationId, req.Config, ctx, orgId, req.IntegrationId, req.Config,
) )
@ -102,9 +89,7 @@ type UninstallIntegrationRequest struct {
IntegrationId string `json:"integration_id"` IntegrationId string `json:"integration_id"`
} }
func (c *Controller) Uninstall( func (c *Controller) Uninstall(ctx context.Context, orgId string, req *UninstallIntegrationRequest) *model.ApiError {
ctx context.Context, orgId string, req *UninstallIntegrationRequest,
) *model.ApiError {
if len(req.IntegrationId) < 1 { if len(req.IntegrationId) < 1 {
return model.BadRequest(fmt.Errorf( return model.BadRequest(fmt.Errorf(
"integration_id is required", "integration_id is required",
@ -121,20 +106,18 @@ func (c *Controller) Uninstall(
return nil return nil
} }
func (c *Controller) GetPipelinesForInstalledIntegrations( func (c *Controller) GetPipelinesForInstalledIntegrations(ctx context.Context, orgId string) ([]pipelinetypes.GettablePipeline, *model.ApiError) {
ctx context.Context, orgId string,
) ([]pipelinetypes.GettablePipeline, *model.ApiError) {
return c.mgr.GetPipelinesForInstalledIntegrations(ctx, orgId) return c.mgr.GetPipelinesForInstalledIntegrations(ctx, orgId)
} }
func (c *Controller) GetDashboardsForInstalledIntegrations( func (c *Controller) GetDashboardsForInstalledIntegrations(ctx context.Context, orgId valuer.UUID) ([]*dashboardtypes.Dashboard, *model.ApiError) {
ctx context.Context, orgId string,
) ([]*types.Dashboard, *model.ApiError) {
return c.mgr.GetDashboardsForInstalledIntegrations(ctx, orgId) return c.mgr.GetDashboardsForInstalledIntegrations(ctx, orgId)
} }
func (c *Controller) GetInstalledIntegrationDashboardById( func (c *Controller) GetInstalledIntegrationDashboardById(ctx context.Context, orgId valuer.UUID, dashboardUuid string) (*dashboardtypes.Dashboard, *model.ApiError) {
ctx context.Context, orgId string, dashboardUuid string,
) (*types.Dashboard, *model.ApiError) {
return c.mgr.GetInstalledIntegrationDashboardById(ctx, orgId, dashboardUuid) return c.mgr.GetInstalledIntegrationDashboardById(ctx, orgId, dashboardUuid)
} }
func (c *Controller) IsInstalledIntegrationDashboardID(dashboardUuid string) bool {
return c.mgr.IsInstalledIntegrationDashboardUuid(dashboardUuid)
}

View File

@ -10,6 +10,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/utils" "github.com/SigNoz/signoz/pkg/query-service/utils"
"github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/pipelinetypes" "github.com/SigNoz/signoz/pkg/types/pipelinetypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes" ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer" "github.com/SigNoz/signoz/pkg/valuer"
@ -31,8 +32,8 @@ type IntegrationSummary struct {
} }
type IntegrationAssets struct { type IntegrationAssets struct {
Logs LogsAssets `json:"logs"` Logs LogsAssets `json:"logs"`
Dashboards []types.DashboardData `json:"dashboards"` Dashboards []dashboardtypes.StorableDashboardData `json:"dashboards"`
Alerts []ruletypes.PostableRule `json:"alerts"` Alerts []ruletypes.PostableRule `json:"alerts"`
} }
@ -304,17 +305,22 @@ func (m *Manager) parseDashboardUuid(dashboardUuid string) (
return parts[1], parts[2], nil return parts[1], parts[2], nil
} }
func (m *Manager) IsInstalledIntegrationDashboardUuid(dashboardUuid string) bool {
_, _, apiErr := m.parseDashboardUuid(dashboardUuid)
return apiErr == nil
}
func (m *Manager) GetInstalledIntegrationDashboardById( func (m *Manager) GetInstalledIntegrationDashboardById(
ctx context.Context, ctx context.Context,
orgId string, orgId valuer.UUID,
dashboardUuid string, dashboardUuid string,
) (*types.Dashboard, *model.ApiError) { ) (*dashboardtypes.Dashboard, *model.ApiError) {
integrationId, dashboardId, apiErr := m.parseDashboardUuid(dashboardUuid) integrationId, dashboardId, apiErr := m.parseDashboardUuid(dashboardUuid)
if apiErr != nil { if apiErr != nil {
return nil, apiErr return nil, apiErr
} }
integration, apiErr := m.GetIntegration(ctx, orgId, integrationId) integration, apiErr := m.GetIntegration(ctx, orgId.StringValue(), integrationId)
if apiErr != nil { if apiErr != nil {
return nil, apiErr return nil, apiErr
} }
@ -328,11 +334,10 @@ func (m *Manager) GetInstalledIntegrationDashboardById(
for _, dd := range integration.IntegrationDetails.Assets.Dashboards { for _, dd := range integration.IntegrationDetails.Assets.Dashboards {
if dId, exists := dd["id"]; exists { if dId, exists := dd["id"]; exists {
if id, ok := dId.(string); ok && id == dashboardId { if id, ok := dId.(string); ok && id == dashboardId {
isLocked := 1
author := "integration" author := "integration"
return &types.Dashboard{ return &dashboardtypes.Dashboard{
UUID: m.dashboardUuid(integrationId, string(dashboardId)), ID: m.dashboardUuid(integrationId, string(dashboardId)),
Locked: &isLocked, Locked: true,
Data: dd, Data: dd,
TimeAuditable: types.TimeAuditable{ TimeAuditable: types.TimeAuditable{
CreatedAt: integration.Installation.InstalledAt, CreatedAt: integration.Installation.InstalledAt,
@ -342,6 +347,7 @@ func (m *Manager) GetInstalledIntegrationDashboardById(
CreatedBy: author, CreatedBy: author,
UpdatedBy: author, UpdatedBy: author,
}, },
OrgID: orgId,
}, nil }, nil
} }
} }
@ -354,24 +360,23 @@ func (m *Manager) GetInstalledIntegrationDashboardById(
func (m *Manager) GetDashboardsForInstalledIntegrations( func (m *Manager) GetDashboardsForInstalledIntegrations(
ctx context.Context, ctx context.Context,
orgId string, orgId valuer.UUID,
) ([]*types.Dashboard, *model.ApiError) { ) ([]*dashboardtypes.Dashboard, *model.ApiError) {
installedIntegrations, apiErr := m.getInstalledIntegrations(ctx, orgId) installedIntegrations, apiErr := m.getInstalledIntegrations(ctx, orgId.StringValue())
if apiErr != nil { if apiErr != nil {
return nil, apiErr return nil, apiErr
} }
result := []*types.Dashboard{} result := []*dashboardtypes.Dashboard{}
for _, ii := range installedIntegrations { for _, ii := range installedIntegrations {
for _, dd := range ii.Assets.Dashboards { for _, dd := range ii.Assets.Dashboards {
if dId, exists := dd["id"]; exists { if dId, exists := dd["id"]; exists {
if dashboardId, ok := dId.(string); ok { if dashboardId, ok := dId.(string); ok {
isLocked := 1
author := "integration" author := "integration"
result = append(result, &types.Dashboard{ result = append(result, &dashboardtypes.Dashboard{
UUID: m.dashboardUuid(ii.IntegrationSummary.Id, dashboardId), ID: m.dashboardUuid(ii.IntegrationSummary.Id, dashboardId),
Locked: &isLocked, Locked: true,
Data: dd, Data: dd,
TimeAuditable: types.TimeAuditable{ TimeAuditable: types.TimeAuditable{
CreatedAt: ii.Installation.InstalledAt, CreatedAt: ii.Installation.InstalledAt,
@ -381,6 +386,7 @@ func (m *Manager) GetDashboardsForInstalledIntegrations(
CreatedBy: author, CreatedBy: author,
UpdatedBy: author, UpdatedBy: author,
}, },
OrgID: orgId,
}) })
} }
} }

View File

@ -12,6 +12,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/utils" "github.com/SigNoz/signoz/pkg/query-service/utils"
"github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/pipelinetypes" "github.com/SigNoz/signoz/pkg/types/pipelinetypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes" ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
) )
@ -121,7 +122,7 @@ func (t *TestAvailableIntegrationsRepo) list(
}, },
}, },
}, },
Dashboards: []types.DashboardData{}, Dashboards: []dashboardtypes.StorableDashboardData{},
Alerts: []ruletypes.PostableRule{}, Alerts: []ruletypes.PostableRule{},
}, },
ConnectionTests: &IntegrationConnectionTests{ ConnectionTests: &IntegrationConnectionTests{
@ -189,7 +190,7 @@ func (t *TestAvailableIntegrationsRepo) list(
}, },
}, },
}, },
Dashboards: []types.DashboardData{}, Dashboards: []dashboardtypes.StorableDashboardData{},
Alerts: []ruletypes.PostableRule{}, Alerts: []ruletypes.PostableRule{},
}, },
ConnectionTests: &IntegrationConnectionTests{ ConnectionTests: &IntegrationConnectionTests{

View File

@ -51,7 +51,7 @@ func TestPrepareTableQuery(t *testing.T) {
}, },
start: 1701794980000, start: 1701794980000,
end: 1701796780000, end: 1701796780000,
expectedQueryContains: "SELECT ts, sum(per_series_value) as value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, avg(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['system_memory_usage'] AND temporality = 'Unspecified' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND JSONExtractString(labels, 'state') != 'idle') as filtered_time_series USING fingerprint WHERE metric_name IN ['system_memory_usage'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY ts ORDER BY ts ASC", expectedQueryContains: "SELECT ts, sum(per_series_value) as value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, avg(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['system_memory_usage'] AND temporality = 'Unspecified' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND JSONExtractString(labels, 'state') != 'idle') as filtered_time_series USING fingerprint WHERE metric_name IN ['system_memory_usage'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 AND bitAnd(flags, 1) = 0 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY ts ORDER BY ts ASC",
}, },
{ {
name: "test time aggregation = rate, space aggregation = sum, temporality = cumulative", name: "test time aggregation = rate, space aggregation = sum, temporality = cumulative",
@ -93,7 +93,7 @@ func TestPrepareTableQuery(t *testing.T) {
}, },
start: 1701794980000, start: 1701794980000,
end: 1701796780000, end: 1701796780000,
expectedQueryContains: "SELECT service_name, ts, sum(per_series_value) as value FROM (SELECT service_name, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(service_name) as service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND like(JSONExtractString(labels, 'service_name'), '%payment_service%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['http_requests'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)) WHERE isNaN(per_series_value) = 0 GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC", expectedQueryContains: "SELECT service_name, ts, sum(per_series_value) as value FROM (SELECT service_name, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(service_name) as service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND like(JSONExtractString(labels, 'service_name'), '%payment_service%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['http_requests'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 AND bitAnd(flags, 1) = 0 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)) WHERE isNaN(per_series_value) = 0 GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC",
}, },
{ {
name: "test time aggregation = avg, space aggregation = avg, temporality = unspecified, testing metrics and attribute name with dot", name: "test time aggregation = avg, space aggregation = avg, temporality = unspecified, testing metrics and attribute name with dot",

View File

@ -120,6 +120,7 @@ func prepareTimeAggregationSubQuery(start, end, step int64, mq *v3.BuilderQuery)
tableName := helpers.WhichSamplesTableToUse(start, end, mq) tableName := helpers.WhichSamplesTableToUse(start, end, mq)
samplesTableFilter = helpers.AddFlagsFilters(samplesTableFilter, tableName)
// Select the aggregate value for interval // Select the aggregate value for interval
queryTmpl := queryTmpl :=
"SELECT fingerprint, %s" + "SELECT fingerprint, %s" +

View File

@ -66,7 +66,7 @@ func TestPrepareTimeAggregationSubQuery(t *testing.T) {
}, },
start: 1701794980000, start: 1701794980000,
end: 1701796780000, end: 1701796780000,
expectedQueryContains: "SELECT fingerprint, any(service_name) as service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, avg(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND JSONExtractString(labels, 'service_name') != 'payment_service' AND JSONExtractString(labels, 'endpoint') IN ['/paycallback','/payme','/paypal']) as filtered_time_series USING fingerprint WHERE metric_name IN ['http_requests'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts", expectedQueryContains: "SELECT fingerprint, any(service_name) as service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, avg(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND JSONExtractString(labels, 'service_name') != 'payment_service' AND JSONExtractString(labels, 'endpoint') IN ['/paycallback','/payme','/paypal']) as filtered_time_series USING fingerprint WHERE metric_name IN ['http_requests'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 AND bitAnd(flags, 1) = 0 GROUP BY fingerprint, ts ORDER BY fingerprint, ts",
}, },
{ {
name: "test time aggregation = rate, temporality = cumulative", name: "test time aggregation = rate, temporality = cumulative",
@ -107,7 +107,7 @@ func TestPrepareTimeAggregationSubQuery(t *testing.T) {
}, },
start: 1701794980000, start: 1701794980000,
end: 1701796780000, end: 1701796780000,
expectedQueryContains: "SELECT service_name, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(service_name) as service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND like(JSONExtractString(labels, 'service_name'), '%payment_service%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['http_requests'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)", expectedQueryContains: "SELECT service_name, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(service_name) as service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND like(JSONExtractString(labels, 'service_name'), '%payment_service%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['http_requests'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 AND bitAnd(flags, 1) = 0 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)",
}, },
} }
@ -168,7 +168,7 @@ func TestPrepareTimeseriesQuery(t *testing.T) {
}, },
start: 1701794980000, start: 1701794980000,
end: 1701796780000, end: 1701796780000,
expectedQueryContains: "SELECT ts, sum(per_series_value) as value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, avg(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['system_memory_usage'] AND temporality = 'Unspecified' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND JSONExtractString(labels, 'state') != 'idle') as filtered_time_series USING fingerprint WHERE metric_name IN ['system_memory_usage'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY ts ORDER BY ts ASC", expectedQueryContains: "SELECT ts, sum(per_series_value) as value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, avg(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['system_memory_usage'] AND temporality = 'Unspecified' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND JSONExtractString(labels, 'state') != 'idle') as filtered_time_series USING fingerprint WHERE metric_name IN ['system_memory_usage'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 AND bitAnd(flags, 1) = 0 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY ts ORDER BY ts ASC",
}, },
{ {
name: "test time aggregation = rate, space aggregation = sum, temporality = cumulative", name: "test time aggregation = rate, space aggregation = sum, temporality = cumulative",
@ -210,7 +210,7 @@ func TestPrepareTimeseriesQuery(t *testing.T) {
}, },
start: 1701794980000, start: 1701794980000,
end: 1701796780000, end: 1701796780000,
expectedQueryContains: "SELECT service_name, ts, sum(per_series_value) as value FROM (SELECT service_name, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(service_name) as service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND like(JSONExtractString(labels, 'service_name'), '%payment_service%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['http_requests'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)) WHERE isNaN(per_series_value) = 0 GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC", expectedQueryContains: "SELECT service_name, ts, sum(per_series_value) as value FROM (SELECT service_name, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(service_name) as service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND like(JSONExtractString(labels, 'service_name'), '%payment_service%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['http_requests'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 AND bitAnd(flags, 1) = 0 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)) WHERE isNaN(per_series_value) = 0 GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC",
}, },
{ {
name: "test time aggregation = avg, space aggregation = avg, temporality = unspecified, testing metrics and attribute name with dot", name: "test time aggregation = avg, space aggregation = avg, temporality = unspecified, testing metrics and attribute name with dot",

View File

@ -53,7 +53,7 @@ func TestPrepareTableQuery(t *testing.T) {
}, },
start: 1701794980000, start: 1701794980000,
end: 1701796780000, end: 1701796780000,
expectedQueryContains: "SELECT ts, sum(per_series_value) as value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, avg(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['system_memory_usage'] AND temporality = 'Unspecified' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND JSONExtractString(labels, 'state') != 'idle') as filtered_time_series USING fingerprint WHERE metric_name IN ['system_memory_usage'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY ts ORDER BY ts ASC", expectedQueryContains: "SELECT ts, sum(per_series_value) as value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, avg(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['system_memory_usage'] AND temporality = 'Unspecified' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND JSONExtractString(labels, 'state') != 'idle') as filtered_time_series USING fingerprint WHERE metric_name IN ['system_memory_usage'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 AND bitAnd(flags, 1) = 0 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY ts ORDER BY ts ASC",
}, },
{ {
name: "test time aggregation = rate, space aggregation = sum, temporality = delta", name: "test time aggregation = rate, space aggregation = sum, temporality = delta",
@ -95,7 +95,7 @@ func TestPrepareTableQuery(t *testing.T) {
}, },
start: 1701794980000, start: 1701794980000,
end: 1701796780000, end: 1701796780000,
expectedQueryContains: "SELECT service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND like(JSONExtractString(labels, 'service_name'), '%payment_service%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['http_requests'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC", expectedQueryContains: "SELECT service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND like(JSONExtractString(labels, 'service_name'), '%payment_service%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['http_requests'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 AND bitAnd(flags, 1) = 0 GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC",
}, },
{ {
name: "test time aggregation = rate, space aggregation = avg, temporality = delta, testing metrics and attribute name with dot", name: "test time aggregation = rate, space aggregation = avg, temporality = delta, testing metrics and attribute name with dot",
@ -143,7 +143,7 @@ func TestPrepareTableQuery(t *testing.T) {
}, },
start: 1701794980000, start: 1701794980000,
end: 1701796780000, end: 1701796780000,
expectedQueryContains: "SELECT ts, avg(per_series_value) as value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['signoz.latency.sum'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND JSONExtractString(labels, 'host.name') = '4f6ec470feea') as filtered_time_series USING fingerprint WHERE metric_name IN ['signoz.latency.sum'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY ts ORDER BY ts ASC", expectedQueryContains: "SELECT ts, avg(per_series_value) as value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['signoz.latency.sum'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND JSONExtractString(labels, 'host.name') = '4f6ec470feea') as filtered_time_series USING fingerprint WHERE metric_name IN ['signoz.latency.sum'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 AND bitAnd(flags, 1) = 0 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY ts ORDER BY ts ASC",
}, },
} }

View File

@ -66,7 +66,7 @@ func TestPrepareTimeAggregationSubQuery(t *testing.T) {
}, },
start: 1701794980000, start: 1701794980000,
end: 1701796780000, end: 1701796780000,
expectedQueryContains: "SELECT fingerprint, any(service_name) as service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, avg(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND JSONExtractString(labels, 'service_name') != 'payment_service' AND JSONExtractString(labels, 'endpoint') IN ['/paycallback','/payme','/paypal']) as filtered_time_series USING fingerprint WHERE metric_name IN ['http_requests'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts", expectedQueryContains: "SELECT fingerprint, any(service_name) as service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, avg(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND JSONExtractString(labels, 'service_name') != 'payment_service' AND JSONExtractString(labels, 'endpoint') IN ['/paycallback','/payme','/paypal']) as filtered_time_series USING fingerprint WHERE metric_name IN ['http_requests'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 AND bitAnd(flags, 1) = 0 GROUP BY fingerprint, ts ORDER BY fingerprint, ts",
}, },
{ {
name: "test time aggregation = rate, temporality = delta", name: "test time aggregation = rate, temporality = delta",
@ -107,7 +107,7 @@ func TestPrepareTimeAggregationSubQuery(t *testing.T) {
}, },
start: 1701794980000, start: 1701794980000,
end: 1701796780000, end: 1701796780000,
expectedQueryContains: "SELECT fingerprint, any(service_name) as service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND like(JSONExtractString(labels, 'service_name'), '%payment_service%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['http_requests'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts", expectedQueryContains: "SELECT fingerprint, any(service_name) as service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND like(JSONExtractString(labels, 'service_name'), '%payment_service%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['http_requests'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 AND bitAnd(flags, 1) = 0 GROUP BY fingerprint, ts ORDER BY fingerprint, ts",
}, },
} }
@ -168,7 +168,7 @@ func TestPrepareTimeseriesQuery(t *testing.T) {
}, },
start: 1701794980000, start: 1701794980000,
end: 1701796780000, end: 1701796780000,
expectedQueryContains: "SELECT ts, sum(per_series_value) as value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, avg(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['system_memory_usage'] AND temporality = 'Unspecified' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND JSONExtractString(labels, 'state') != 'idle') as filtered_time_series USING fingerprint WHERE metric_name IN ['system_memory_usage'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY ts ORDER BY ts ASC", expectedQueryContains: "SELECT ts, sum(per_series_value) as value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, avg(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['system_memory_usage'] AND temporality = 'Unspecified' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND JSONExtractString(labels, 'state') != 'idle') as filtered_time_series USING fingerprint WHERE metric_name IN ['system_memory_usage'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 AND bitAnd(flags, 1) = 0 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY ts ORDER BY ts ASC",
}, },
{ {
name: "test time aggregation = rate, space aggregation = sum, temporality = delta", name: "test time aggregation = rate, space aggregation = sum, temporality = delta",
@ -210,7 +210,7 @@ func TestPrepareTimeseriesQuery(t *testing.T) {
}, },
start: 1701794980000, start: 1701794980000,
end: 1701796780000, end: 1701796780000,
expectedQueryContains: "SELECT service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND like(JSONExtractString(labels, 'service_name'), '%payment_service%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['http_requests'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC", expectedQueryContains: "SELECT service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['http_requests'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000 AND like(JSONExtractString(labels, 'service_name'), '%payment_service%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['http_requests'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 AND bitAnd(flags, 1) = 0 GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC",
}, },
{ {
name: "test time aggregation = rate, space aggregation percentile99, type = ExponentialHistogram", name: "test time aggregation = rate, space aggregation percentile99, type = ExponentialHistogram",
@ -244,7 +244,7 @@ func TestPrepareTimeseriesQuery(t *testing.T) {
}, },
start: 1701794980000, start: 1701794980000,
end: 1701796780000, end: 1701796780000,
expectedQueryContains: "SELECT service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, quantilesDDMerge(0.01, 0.990000)(sketch)[1] as value FROM signoz_metrics.distributed_exp_hist INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['signoz_latency'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000) as filtered_time_series USING fingerprint WHERE metric_name IN ['signoz_latency'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC", expectedQueryContains: "SELECT service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, quantilesDDMerge(0.01, 0.990000)(sketch)[1] as value FROM signoz_metrics.distributed_exp_hist INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['signoz_latency'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1701792000000 AND unix_milli < 1701796780000) as filtered_time_series USING fingerprint WHERE metric_name IN ['signoz_latency'] AND unix_milli >= 1701794980000 AND unix_milli < 1701796780000 AND bitAnd(flags, 1) = 0 GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC",
}, },
{ {
name: "test time aggregation = rate, space aggregation = max, temporality = delta, testing metrics and attribute name with dot", name: "test time aggregation = rate, space aggregation = max, temporality = delta, testing metrics and attribute name with dot",

View File

@ -25,9 +25,10 @@ func prepareTimeAggregationSubQuery(start, end, step int64, mq *v3.BuilderQuery)
} }
samplesTableFilter := fmt.Sprintf("metric_name IN %s AND unix_milli >= %d AND unix_milli < %d", utils.ClickHouseFormattedMetricNames(mq.AggregateAttribute.Key), start, end) samplesTableFilter := fmt.Sprintf("metric_name IN %s AND unix_milli >= %d AND unix_milli < %d", utils.ClickHouseFormattedMetricNames(mq.AggregateAttribute.Key), start, end)
tableName := helpers.WhichSamplesTableToUse(start, end, mq) tableName := helpers.WhichSamplesTableToUse(start, end, mq)
samplesTableFilter = helpers.AddFlagsFilters(samplesTableFilter, tableName)
// Select the aggregate value for interval // Select the aggregate value for interval
queryTmpl := queryTmpl :=
"SELECT fingerprint, %s" + "SELECT fingerprint, %s" +
@ -87,6 +88,7 @@ func prepareQueryOptimized(start, end, step int64, mq *v3.BuilderQuery) (string,
tableName := helpers.WhichSamplesTableToUse(start, end, mq) tableName := helpers.WhichSamplesTableToUse(start, end, mq)
samplesTableFilter = helpers.AddFlagsFilters(samplesTableFilter, tableName)
// Select the aggregate value for interval // Select the aggregate value for interval
queryTmpl := queryTmpl :=
"SELECT %s" + "SELECT %s" +

View File

@ -436,3 +436,10 @@ func PrepareTimeseriesFilterQueryV3(start, end int64, mq *v3.BuilderQuery) (stri
return filterSubQuery, nil return filterSubQuery, nil
} }
func AddFlagsFilters(samplesTableFilter string, tableName string) string {
if tableName == constants.SIGNOZ_SAMPLES_V4_TABLENAME || tableName == constants.SIGNOZ_SAMPLES_V4_LOCAL_TABLENAME || tableName == constants.SIGNOZ_EXP_HISTOGRAM_TABLENAME || tableName == constants.SIGNOZ_EXP_HISTOGRAM_LOCAL_TABLENAME {
samplesTableFilter = fmt.Sprintf("%s AND %s", samplesTableFilter, "bitAnd(flags, 1) = 0")
}
return samplesTableFilter
}

View File

@ -194,7 +194,7 @@ func TestPrepareMetricQueryCumulativeRate(t *testing.T) {
TimeAggregation: v3.TimeAggregationRate, TimeAggregation: v3.TimeAggregationRate,
SpaceAggregation: v3.SpaceAggregationSum, SpaceAggregation: v3.SpaceAggregationSum,
}, },
expectedQueryContains: "SELECT service_name, ts, sum(per_series_value) as value FROM (SELECT service_name, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(service_name) as service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['signoz_calls_total'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['signoz_calls_total'] AND unix_milli >= 1650991920000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)) WHERE isNaN(per_series_value) = 0 GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC", expectedQueryContains: "SELECT service_name, ts, sum(per_series_value) as value FROM (SELECT service_name, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(service_name) as service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['signoz_calls_total'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['signoz_calls_total'] AND unix_milli >= 1650991920000 AND unix_milli < 1651078380000 AND bitAnd(flags, 1) = 0 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)) WHERE isNaN(per_series_value) = 0 GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC",
}, },
{ {
name: "test time aggregation = rate, space aggregation = sum, temporality = cumulative, multiple group by", name: "test time aggregation = rate, space aggregation = sum, temporality = cumulative, multiple group by",
@ -227,7 +227,7 @@ func TestPrepareMetricQueryCumulativeRate(t *testing.T) {
TimeAggregation: v3.TimeAggregationRate, TimeAggregation: v3.TimeAggregationRate,
SpaceAggregation: v3.SpaceAggregationSum, SpaceAggregation: v3.SpaceAggregationSum,
}, },
expectedQueryContains: "SELECT service_name, endpoint, ts, sum(per_series_value) as value FROM (SELECT service_name, endpoint, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(service_name) as service_name, any(endpoint) as endpoint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, JSONExtractString(labels, 'endpoint') as endpoint, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['signoz_calls_total'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name IN ['signoz_calls_total'] AND unix_milli >= 1650991920000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)) WHERE isNaN(per_series_value) = 0 GROUP BY service_name, endpoint, ts ORDER BY service_name ASC, endpoint ASC, ts ASC", expectedQueryContains: "SELECT service_name, endpoint, ts, sum(per_series_value) as value FROM (SELECT service_name, endpoint, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(service_name) as service_name, any(endpoint) as endpoint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, JSONExtractString(labels, 'endpoint') as endpoint, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['signoz_calls_total'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name IN ['signoz_calls_total'] AND unix_milli >= 1650991920000 AND unix_milli < 1651078380000 AND bitAnd(flags, 1) = 0 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)) WHERE isNaN(per_series_value) = 0 GROUP BY service_name, endpoint, ts ORDER BY service_name ASC, endpoint ASC, ts ASC",
}, },
} }
@ -268,7 +268,7 @@ func TestPrepareMetricQueryDeltaRate(t *testing.T) {
TimeAggregation: v3.TimeAggregationRate, TimeAggregation: v3.TimeAggregationRate,
SpaceAggregation: v3.SpaceAggregationSum, SpaceAggregation: v3.SpaceAggregationSum,
}, },
expectedQueryContains: "SELECT toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['signoz_calls_total'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name IN ['signoz_calls_total'] AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY ts ORDER BY ts ASC", expectedQueryContains: "SELECT toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['signoz_calls_total'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name IN ['signoz_calls_total'] AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 AND bitAnd(flags, 1) = 0 GROUP BY ts ORDER BY ts ASC",
}, },
{ {
name: "test time aggregation = rate, space aggregation = sum, temporality = delta, group by service_name", name: "test time aggregation = rate, space aggregation = sum, temporality = delta, group by service_name",
@ -294,7 +294,7 @@ func TestPrepareMetricQueryDeltaRate(t *testing.T) {
TimeAggregation: v3.TimeAggregationRate, TimeAggregation: v3.TimeAggregationRate,
SpaceAggregation: v3.SpaceAggregationSum, SpaceAggregation: v3.SpaceAggregationSum,
}, },
expectedQueryContains: "SELECT service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['signoz_calls_total'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name IN ['signoz_calls_total'] AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC", expectedQueryContains: "SELECT service_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['signoz_calls_total'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name IN ['signoz_calls_total'] AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 AND bitAnd(flags, 1) = 0 GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC",
}, },
} }
@ -349,7 +349,7 @@ func TestPrepreMetricQueryCumulativeQuantile(t *testing.T) {
Disabled: false, Disabled: false,
SpaceAggregation: v3.SpaceAggregationPercentile99, SpaceAggregation: v3.SpaceAggregationPercentile99,
}, },
expectedQueryContains: "SELECT service_name, ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT service_name, le, ts, sum(per_series_value) as value FROM (SELECT service_name, le, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(service_name) as service_name, any(le) as le, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['signoz_latency_bucket'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['signoz_latency_bucket'] AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)) WHERE isNaN(per_series_value) = 0 GROUP BY service_name, le, ts ORDER BY service_name ASC, le ASC, ts ASC) GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC", expectedQueryContains: "SELECT service_name, ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT service_name, le, ts, sum(per_series_value) as value FROM (SELECT service_name, le, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(service_name) as service_name, any(le) as le, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['signoz_latency_bucket'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['signoz_latency_bucket'] AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 AND bitAnd(flags, 1) = 0 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)) WHERE isNaN(per_series_value) = 0 GROUP BY service_name, le, ts ORDER BY service_name ASC, le ASC, ts ASC) GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC",
}, },
{ {
name: "test temporality = cumulative, quantile = 0.99 without group by", name: "test temporality = cumulative, quantile = 0.99 without group by",
@ -379,7 +379,7 @@ func TestPrepreMetricQueryCumulativeQuantile(t *testing.T) {
Disabled: false, Disabled: false,
SpaceAggregation: v3.SpaceAggregationPercentile99, SpaceAggregation: v3.SpaceAggregationPercentile99,
}, },
expectedQueryContains: "SELECT ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT le, ts, sum(per_series_value) as value FROM (SELECT le, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(le) as le, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['signoz_latency_bucket'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['signoz_latency_bucket'] AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)) WHERE isNaN(per_series_value) = 0 GROUP BY le, ts ORDER BY le ASC, ts ASC) GROUP BY ts ORDER BY ts ASC", expectedQueryContains: "SELECT ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT le, ts, sum(per_series_value) as value FROM (SELECT le, ts, If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan, If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan, (per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) / (ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) as per_series_value FROM (SELECT fingerprint, any(le) as le, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['signoz_latency_bucket'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['signoz_latency_bucket'] AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 AND bitAnd(flags, 1) = 0 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WINDOW rate_window as (PARTITION BY fingerprint ORDER BY fingerprint, ts)) WHERE isNaN(per_series_value) = 0 GROUP BY le, ts ORDER BY le ASC, ts ASC) GROUP BY ts ORDER BY ts ASC",
}, },
} }
@ -434,7 +434,7 @@ func TestPrepreMetricQueryDeltaQuantile(t *testing.T) {
Disabled: false, Disabled: false,
SpaceAggregation: v3.SpaceAggregationPercentile99, SpaceAggregation: v3.SpaceAggregationPercentile99,
}, },
expectedQueryContains: "SELECT service_name, ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT service_name, le, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['signoz_latency_bucket'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['signoz_latency_bucket'] AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY service_name, le, ts ORDER BY service_name ASC, le ASC, ts ASC) GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC", expectedQueryContains: "SELECT service_name, ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT service_name, le, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'service_name') as service_name, JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['signoz_latency_bucket'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['signoz_latency_bucket'] AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 AND bitAnd(flags, 1) = 0 GROUP BY service_name, le, ts ORDER BY service_name ASC, le ASC, ts ASC) GROUP BY service_name, ts ORDER BY service_name ASC, ts ASC",
}, },
{ {
name: "test temporality = delta, quantile = 0.99 no group by", name: "test temporality = delta, quantile = 0.99 no group by",
@ -464,7 +464,7 @@ func TestPrepreMetricQueryDeltaQuantile(t *testing.T) {
Disabled: false, Disabled: false,
SpaceAggregation: v3.SpaceAggregationPercentile99, SpaceAggregation: v3.SpaceAggregationPercentile99,
}, },
expectedQueryContains: "SELECT ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT le, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['signoz_latency_bucket'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['signoz_latency_bucket'] AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY le, ts ORDER BY le ASC, ts ASC) GROUP BY ts ORDER BY ts ASC", expectedQueryContains: "SELECT ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT le, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['signoz_latency_bucket'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND like(JSONExtractString(labels, 'service_name'), '%frontend%')) as filtered_time_series USING fingerprint WHERE metric_name IN ['signoz_latency_bucket'] AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 AND bitAnd(flags, 1) = 0 GROUP BY le, ts ORDER BY le ASC, ts ASC) GROUP BY ts ORDER BY ts ASC",
}, },
} }
@ -505,7 +505,7 @@ func TestPrepareMetricQueryGauge(t *testing.T) {
SpaceAggregation: v3.SpaceAggregationSum, SpaceAggregation: v3.SpaceAggregationSum,
Disabled: false, Disabled: false,
}, },
expectedQueryContains: "SELECT ts, sum(per_series_value) as value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, avg(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['system_cpu_usage'] AND temporality = 'Unspecified' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name IN ['system_cpu_usage'] AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY ts ORDER BY ts ASC", expectedQueryContains: "SELECT ts, sum(per_series_value) as value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, avg(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['system_cpu_usage'] AND temporality = 'Unspecified' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name IN ['system_cpu_usage'] AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 AND bitAnd(flags, 1) = 0 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY ts ORDER BY ts ASC",
}, },
{ {
name: "test value filter with string value", name: "test value filter with string value",
@ -562,7 +562,7 @@ func TestPrepareMetricQueryGauge(t *testing.T) {
Expression: "A", Expression: "A",
Disabled: false, Disabled: false,
}, },
expectedQueryContains: "SELECT host_name, ts, sum(per_series_value) as value FROM (SELECT fingerprint, any(host_name) as host_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, avg(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'host_name') as host_name, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['system_cpu_usage'] AND temporality = 'Unspecified' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name IN ['system_cpu_usage'] AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY host_name, ts ORDER BY host_name ASC, ts ASC", expectedQueryContains: "SELECT host_name, ts, sum(per_series_value) as value FROM (SELECT fingerprint, any(host_name) as host_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, avg(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'host_name') as host_name, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['system_cpu_usage'] AND temporality = 'Unspecified' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000) as filtered_time_series USING fingerprint WHERE metric_name IN ['system_cpu_usage'] AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 AND bitAnd(flags, 1) = 0 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY host_name, ts ORDER BY host_name ASC, ts ASC",
}, },
{ {
name: "test gauge query with multiple group by with metric and attribute name containing dot", name: "test gauge query with multiple group by with metric and attribute name containing dot",
@ -631,7 +631,7 @@ func TestPrepareMetricQueryGauge(t *testing.T) {
ReduceTo: v3.ReduceToOperatorAvg, ReduceTo: v3.ReduceToOperatorAvg,
Having: []v3.Having{}, Having: []v3.Having{},
}, },
expectedQueryContains: "SELECT `os.type`, state, `host.name`, ts, max(per_series_value) as value FROM (SELECT fingerprint, any(`os.type`) as `os.type`, any(state) as state, any(`host.name`) as `host.name`, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'os.type') as `os.type`, JSONExtractString(labels, 'state') as state, JSONExtractString(labels, 'host.name') as `host.name`, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['system.memory.usage'] AND temporality = 'Unspecified' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND JSONExtractString(labels, 'host.name') = 'signoz-host') as filtered_time_series USING fingerprint WHERE metric_name IN ['system.memory.usage'] AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY `os.type`, state, `host.name`, ts ORDER BY `os.type` desc, state asc, `host.name` ASC, ts ASC", expectedQueryContains: "SELECT `os.type`, state, `host.name`, ts, max(per_series_value) as value FROM (SELECT fingerprint, any(`os.type`) as `os.type`, any(state) as state, any(`host.name`) as `host.name`, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, max(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'os.type') as `os.type`, JSONExtractString(labels, 'state') as state, JSONExtractString(labels, 'host.name') as `host.name`, fingerprint FROM signoz_metrics.time_series_v4_1day WHERE metric_name IN ['system.memory.usage'] AND temporality = 'Unspecified' AND __normalized = true AND unix_milli >= 1650931200000 AND unix_milli < 1651078380000 AND JSONExtractString(labels, 'host.name') = 'signoz-host') as filtered_time_series USING fingerprint WHERE metric_name IN ['system.memory.usage'] AND unix_milli >= 1650991980000 AND unix_milli < 1651078380000 AND bitAnd(flags, 1) = 0 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY `os.type`, state, `host.name`, ts ORDER BY `os.type` desc, state asc, `host.name` ASC, ts ASC",
}, },
} }
@ -711,7 +711,7 @@ func TestPrepareMetricQueryValueTypePanelWithGroupBY(t *testing.T) {
}, },
}, },
}, },
expectedQueryContains: "SELECT max(value) as aggregated_value, ts FROM (SELECT state, ts, avg(per_series_value) as value FROM (SELECT fingerprint, any(state) as state, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, anyLast(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'state') as state, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['system_memory_usage'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1735891200000 AND unix_milli < 1735894800000 AND JSONExtractString(labels, 'os_type') = 'linux') as filtered_time_series USING fingerprint WHERE metric_name IN ['system_memory_usage'] AND unix_milli >= 1735891800000 AND unix_milli < 1735894800000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY state, ts ORDER BY state desc, ts ASC) GROUP BY ts ORDER BY ts", expectedQueryContains: "SELECT max(value) as aggregated_value, ts FROM (SELECT state, ts, avg(per_series_value) as value FROM (SELECT fingerprint, any(state) as state, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, anyLast(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'state') as state, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['system_memory_usage'] AND temporality = 'Delta' AND __normalized = true AND unix_milli >= 1735891200000 AND unix_milli < 1735894800000 AND JSONExtractString(labels, 'os_type') = 'linux') as filtered_time_series USING fingerprint WHERE metric_name IN ['system_memory_usage'] AND unix_milli >= 1735891800000 AND unix_milli < 1735894800000 AND bitAnd(flags, 1) = 0 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY state, ts ORDER BY state desc, ts ASC) GROUP BY ts ORDER BY ts",
}, },
{ {
name: "test temporality = cumulative, panel = value, series agg = max group by state, host_name", name: "test temporality = cumulative, panel = value, series agg = max group by state, host_name",
@ -777,7 +777,7 @@ func TestPrepareMetricQueryValueTypePanelWithGroupBY(t *testing.T) {
}, },
}, },
}, },
expectedQueryContains: "SELECT max(value) as aggregated_value, ts FROM (SELECT state, host_name, ts, avg(per_series_value) as value FROM (SELECT fingerprint, any(state) as state, any(host_name) as host_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, anyLast(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'state') as state, JSONExtractString(labels, 'host_name') as host_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['system_memory_usage'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1735891200000 AND unix_milli < 1735894800000 AND JSONExtractString(labels, 'os_type') = 'linux') as filtered_time_series USING fingerprint WHERE metric_name IN ['system_memory_usage'] AND unix_milli >= 1735891800000 AND unix_milli < 1735894800000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY state, host_name, ts ORDER BY state desc, host_name ASC, ts ASC) GROUP BY ts ORDER BY ts", expectedQueryContains: "SELECT max(value) as aggregated_value, ts FROM (SELECT state, host_name, ts, avg(per_series_value) as value FROM (SELECT fingerprint, any(state) as state, any(host_name) as host_name, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) as ts, anyLast(value) as per_series_value FROM signoz_metrics.distributed_samples_v4 INNER JOIN (SELECT DISTINCT JSONExtractString(labels, 'state') as state, JSONExtractString(labels, 'host_name') as host_name, fingerprint FROM signoz_metrics.time_series_v4 WHERE metric_name IN ['system_memory_usage'] AND temporality = 'Cumulative' AND __normalized = true AND unix_milli >= 1735891200000 AND unix_milli < 1735894800000 AND JSONExtractString(labels, 'os_type') = 'linux') as filtered_time_series USING fingerprint WHERE metric_name IN ['system_memory_usage'] AND unix_milli >= 1735891800000 AND unix_milli < 1735894800000 AND bitAnd(flags, 1) = 0 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(per_series_value) = 0 GROUP BY state, host_name, ts ORDER BY state desc, host_name ASC, ts ASC) GROUP BY ts ORDER BY ts",
}, },
} }

View File

@ -161,7 +161,12 @@ func (receiver *SummaryService) GetMetricsSummary(ctx context.Context, orgID val
if errv2 != nil { if errv2 != nil {
return &model.ApiError{Typ: model.ErrorInternal, Err: errv2} return &model.ApiError{Typ: model.ErrorInternal, Err: errv2}
} }
data, err := receiver.dashboard.GetByMetricNames(ctx, claims.OrgID, metricNames)
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
return &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
data, err := receiver.dashboard.GetByMetricNames(ctx, orgID, metricNames)
if err != nil { if err != nil {
return err return err
} }
@ -334,7 +339,11 @@ func (receiver *SummaryService) GetRelatedMetrics(ctx context.Context, params *m
if errv2 != nil { if errv2 != nil {
return &model.ApiError{Typ: model.ErrorInternal, Err: errv2} return &model.ApiError{Typ: model.ErrorInternal, Err: errv2}
} }
names, err := receiver.dashboard.GetByMetricNames(ctx, claims.OrgID, metricNames) orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
return &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
names, err := receiver.dashboard.GetByMetricNames(ctx, orgID, metricNames)
if err != nil { if err != nil {
return err return err
} }

View File

@ -86,6 +86,16 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
return nil, err return nil, err
} }
integrationsController, err := integrations.NewController(serverOptions.SigNoz.SQLStore)
if err != nil {
return nil, err
}
cloudIntegrationsController, err := cloudintegrations.NewController(serverOptions.SigNoz.SQLStore)
if err != nil {
return nil, err
}
reader := clickhouseReader.NewReader( reader := clickhouseReader.NewReader(
serverOptions.SigNoz.SQLStore, serverOptions.SigNoz.SQLStore,
serverOptions.SigNoz.TelemetryStore, serverOptions.SigNoz.TelemetryStore,
@ -113,16 +123,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
return nil, err return nil, err
} }
integrationsController, err := integrations.NewController(serverOptions.SigNoz.SQLStore)
if err != nil {
return nil, fmt.Errorf("couldn't create integrations controller: %w", err)
}
cloudIntegrationsController, err := cloudintegrations.NewController(serverOptions.SigNoz.SQLStore)
if err != nil {
return nil, fmt.Errorf("couldn't create cloud provider integrations controller: %w", err)
}
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController( logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
serverOptions.SigNoz.SQLStore, integrationsController.GetPipelinesForInstalledIntegrations, serverOptions.SigNoz.SQLStore, integrationsController.GetPipelinesForInstalledIntegrations,
) )

View File

@ -138,10 +138,12 @@ var GroupByColMap = map[string]struct{}{
const ( const (
SIGNOZ_METRIC_DBNAME = "signoz_metrics" SIGNOZ_METRIC_DBNAME = "signoz_metrics"
SIGNOZ_SAMPLES_V4_LOCAL_TABLENAME = "samples_v4"
SIGNOZ_SAMPLES_V4_TABLENAME = "distributed_samples_v4" SIGNOZ_SAMPLES_V4_TABLENAME = "distributed_samples_v4"
SIGNOZ_SAMPLES_V4_AGG_5M_TABLENAME = "distributed_samples_v4_agg_5m" SIGNOZ_SAMPLES_V4_AGG_5M_TABLENAME = "distributed_samples_v4_agg_5m"
SIGNOZ_SAMPLES_V4_AGG_30M_TABLENAME = "distributed_samples_v4_agg_30m" SIGNOZ_SAMPLES_V4_AGG_30M_TABLENAME = "distributed_samples_v4_agg_30m"
SIGNOZ_EXP_HISTOGRAM_TABLENAME = "distributed_exp_hist" SIGNOZ_EXP_HISTOGRAM_TABLENAME = "distributed_exp_hist"
SIGNOZ_EXP_HISTOGRAM_LOCAL_TABLENAME = "exp_hist"
SIGNOZ_TRACE_DBNAME = "signoz_traces" SIGNOZ_TRACE_DBNAME = "signoz_traces"
SIGNOZ_SPAN_INDEX_TABLENAME = "distributed_signoz_index_v2" SIGNOZ_SPAN_INDEX_TABLENAME = "distributed_signoz_index_v2"
SIGNOZ_SPAN_INDEX_V3 = "distributed_signoz_index_v3" SIGNOZ_SPAN_INDEX_V3 = "distributed_signoz_index_v3"

View File

@ -8,7 +8,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/model" "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -16,7 +16,7 @@ import (
func GetDashboardsInfo(ctx context.Context, sqlstore sqlstore.SQLStore) (*model.DashboardsInfo, error) { func GetDashboardsInfo(ctx context.Context, sqlstore sqlstore.SQLStore) (*model.DashboardsInfo, error) {
dashboardsInfo := model.DashboardsInfo{} dashboardsInfo := model.DashboardsInfo{}
// fetch dashboards from dashboard db // fetch dashboards from dashboard db
dashboards := []types.Dashboard{} dashboards := []dashboardtypes.Dashboard{}
err := sqlstore.BunDB().NewSelect().Model(&dashboards).Scan(ctx) err := sqlstore.BunDB().NewSelect().Model(&dashboards).Scan(ctx)
if err != nil { if err != nil {
zap.L().Error("Error in processing sql query", zap.Error(err)) zap.L().Error("Error in processing sql query", zap.Error(err))

View File

@ -29,6 +29,7 @@ import (
"github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/pipelinetypes" "github.com/SigNoz/signoz/pkg/types/pipelinetypes"
mockhouse "github.com/srikanthccv/ClickHouse-go-mock" mockhouse "github.com/srikanthccv/ClickHouse-go-mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -357,7 +358,7 @@ func TestDashboardsForInstalledIntegrationDashboards(t *testing.T) {
require.GreaterOrEqual(dashboards[0].UpdatedAt.Unix(), tsBeforeInstallation) require.GreaterOrEqual(dashboards[0].UpdatedAt.Unix(), tsBeforeInstallation)
// Should be able to get installed integrations dashboard by id // Should be able to get installed integrations dashboard by id
dd := integrationsTB.GetDashboardByIdFromQS(dashboards[0].UUID) dd := integrationsTB.GetDashboardByIdFromQS(dashboards[0].ID)
require.GreaterOrEqual(dd.CreatedAt.Unix(), tsBeforeInstallation) require.GreaterOrEqual(dd.CreatedAt.Unix(), tsBeforeInstallation)
require.GreaterOrEqual(dd.UpdatedAt.Unix(), tsBeforeInstallation) require.GreaterOrEqual(dd.UpdatedAt.Unix(), tsBeforeInstallation)
require.Equal(*dd, dashboards[0]) require.Equal(*dd, dashboards[0])
@ -472,7 +473,7 @@ func (tb *IntegrationsTestBed) RequestQSToUninstallIntegration(
tb.RequestQS("/api/v1/integrations/uninstall", request) tb.RequestQS("/api/v1/integrations/uninstall", request)
} }
func (tb *IntegrationsTestBed) GetDashboardsFromQS() []types.Dashboard { func (tb *IntegrationsTestBed) GetDashboardsFromQS() []dashboardtypes.Dashboard {
result := tb.RequestQS("/api/v1/dashboards", nil) result := tb.RequestQS("/api/v1/dashboards", nil)
dataJson, err := json.Marshal(result.Data) dataJson, err := json.Marshal(result.Data)
@ -480,7 +481,7 @@ func (tb *IntegrationsTestBed) GetDashboardsFromQS() []types.Dashboard {
tb.t.Fatalf("could not marshal apiResponse.Data: %v", err) tb.t.Fatalf("could not marshal apiResponse.Data: %v", err)
} }
dashboards := []types.Dashboard{} dashboards := []dashboardtypes.Dashboard{}
err = json.Unmarshal(dataJson, &dashboards) err = json.Unmarshal(dataJson, &dashboards)
if err != nil { if err != nil {
tb.t.Fatalf(" could not unmarshal apiResponse.Data json into dashboards") tb.t.Fatalf(" could not unmarshal apiResponse.Data json into dashboards")
@ -489,7 +490,7 @@ func (tb *IntegrationsTestBed) GetDashboardsFromQS() []types.Dashboard {
return dashboards return dashboards
} }
func (tb *IntegrationsTestBed) GetDashboardByIdFromQS(dashboardUuid string) *types.Dashboard { func (tb *IntegrationsTestBed) GetDashboardByIdFromQS(dashboardUuid string) *dashboardtypes.Dashboard {
result := tb.RequestQS(fmt.Sprintf("/api/v1/dashboards/%s", dashboardUuid), nil) result := tb.RequestQS(fmt.Sprintf("/api/v1/dashboards/%s", dashboardUuid), nil)
dataJson, err := json.Marshal(result.Data) dataJson, err := json.Marshal(result.Data)
@ -497,7 +498,7 @@ func (tb *IntegrationsTestBed) GetDashboardByIdFromQS(dashboardUuid string) *typ
tb.t.Fatalf("could not marshal apiResponse.Data: %v", err) tb.t.Fatalf("could not marshal apiResponse.Data: %v", err)
} }
dashboard := types.Dashboard{} dashboard := dashboardtypes.Dashboard{}
err = json.Unmarshal(dataJson, &dashboard) err = json.Unmarshal(dataJson, &dashboard)
if err != nil { if err != nil {
tb.t.Fatalf(" could not unmarshal apiResponse.Data json into dashboards") tb.t.Fatalf(" could not unmarshal apiResponse.Data json into dashboards")

View File

@ -68,6 +68,7 @@ func NewTestSqliteDB(t *testing.T) (sqlStore sqlstore.SQLStore, testDBFilePath s
sqlmigration.NewMigratePATToFactorAPIKey(sqlStore), sqlmigration.NewMigratePATToFactorAPIKey(sqlStore),
sqlmigration.NewUpdateApiMonitoringFiltersFactory(sqlStore), sqlmigration.NewUpdateApiMonitoringFiltersFactory(sqlStore),
sqlmigration.NewAddKeyOrganizationFactory(sqlStore), sqlmigration.NewAddKeyOrganizationFactory(sqlStore),
sqlmigration.NewUpdateDashboardFactory(sqlStore),
), ),
) )
if err != nil { if err != nil {

View File

@ -54,7 +54,7 @@ func NewModules(
Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewDefaultPreferenceMap()), Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewDefaultPreferenceMap()),
SavedView: implsavedview.NewModule(sqlstore), SavedView: implsavedview.NewModule(sqlstore),
Apdex: implapdex.NewModule(sqlstore), Apdex: implapdex.NewModule(sqlstore),
Dashboard: impldashboard.NewModule(sqlstore), Dashboard: impldashboard.NewModule(sqlstore, providerSettings),
User: user, User: user,
QuickFilter: quickfilter, QuickFilter: quickfilter,
TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)), TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)),

View File

@ -89,6 +89,7 @@ func NewSQLMigrationProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedM
sqlmigration.NewUpdateApiMonitoringFiltersFactory(sqlstore), sqlmigration.NewUpdateApiMonitoringFiltersFactory(sqlstore),
sqlmigration.NewAddKeyOrganizationFactory(sqlstore), sqlmigration.NewAddKeyOrganizationFactory(sqlstore),
sqlmigration.NewAddTraceFunnelsFactory(sqlstore), sqlmigration.NewAddTraceFunnelsFactory(sqlstore),
sqlmigration.NewUpdateDashboardFactory(sqlstore),
) )
} }

View File

@ -0,0 +1,141 @@
package sqlmigration
import (
"context"
"database/sql"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type updateDashboard struct {
store sqlstore.SQLStore
}
type existingDashboard36 struct {
bun.BaseModel `bun:"table:dashboards"`
types.TimeAuditable
types.UserAuditable
OrgID string `json:"-" bun:"org_id,notnull"`
ID int `json:"id" bun:"id,pk,autoincrement"`
UUID string `json:"uuid" bun:"uuid,type:text,notnull,unique"`
Data map[string]interface{} `json:"data" bun:"data,type:text,notnull"`
Locked *int `json:"isLocked" bun:"locked,notnull,default:0"`
}
type newDashboard36 struct {
bun.BaseModel `bun:"table:dashboard"`
types.Identifiable
types.TimeAuditable
types.UserAuditable
Data map[string]interface{} `bun:"data,type:text,notnull"`
Locked bool `bun:"locked,notnull,default:false"`
OrgID valuer.UUID `bun:"org_id,type:text,notnull"`
}
func NewUpdateDashboardFactory(store sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("update_dashboards"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return newUpdateDashboard(ctx, ps, c, store)
})
}
func newUpdateDashboard(_ context.Context, _ factory.ProviderSettings, _ Config, store sqlstore.SQLStore) (SQLMigration, error) {
return &updateDashboard{store: store}, nil
}
func (migration *updateDashboard) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *updateDashboard) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
err = migration.store.Dialect().RenameTableAndModifyModel(ctx, tx, new(existingDashboard36), new(newDashboard36), []string{OrgReference}, func(ctx context.Context) error {
existingDashboards := make([]*existingDashboard36, 0)
err = tx.NewSelect().Model(&existingDashboards).Scan(ctx)
if err != nil {
if err != sql.ErrNoRows {
return err
}
}
if err == nil && len(existingDashboards) > 0 {
newDashboards, err := migration.CopyExistingDashboardsToNewDashboards(existingDashboards)
if err != nil {
return err
}
_, err = tx.
NewInsert().
Model(&newDashboards).
Exec(ctx)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func (migration *updateDashboard) Down(context.Context, *bun.DB) error {
return nil
}
func (migration *updateDashboard) CopyExistingDashboardsToNewDashboards(existingDashboards []*existingDashboard36) ([]*newDashboard36, error) {
newDashboards := make([]*newDashboard36, len(existingDashboards))
for idx, existingDashboard := range existingDashboards {
dashboardID, err := valuer.NewUUID(existingDashboard.UUID)
if err != nil {
return nil, err
}
orgID, err := valuer.NewUUID(existingDashboard.OrgID)
if err != nil {
return nil, err
}
locked := false
if existingDashboard.Locked != nil && *existingDashboard.Locked == 1 {
locked = true
}
newDashboards[idx] = &newDashboard36{
Identifiable: types.Identifiable{
ID: dashboardID,
},
TimeAuditable: existingDashboard.TimeAuditable,
UserAuditable: existingDashboard.UserAuditable,
Data: existingDashboard.Data,
Locked: locked,
OrgID: orgID,
}
}
return newDashboards, nil
}

View File

@ -0,0 +1,145 @@
package telemetrymetrics
import (
"context"
"fmt"
"slices"
"github.com/SigNoz/signoz/pkg/errors"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/huandu/go-sqlbuilder"
)
type conditionBuilder struct {
fm qbtypes.FieldMapper
}
func NewConditionBuilder(fm qbtypes.FieldMapper) *conditionBuilder {
return &conditionBuilder{fm: fm}
}
func (c *conditionBuilder) conditionFor(
ctx context.Context,
key *telemetrytypes.TelemetryFieldKey,
operator qbtypes.FilterOperator,
value any,
sb *sqlbuilder.SelectBuilder,
) (string, error) {
tblFieldName, err := c.fm.FieldFor(ctx, key)
if err != nil {
return "", err
}
switch operator {
case qbtypes.FilterOperatorEqual:
return sb.E(tblFieldName, value), nil
case qbtypes.FilterOperatorNotEqual:
return sb.NE(tblFieldName, value), nil
case qbtypes.FilterOperatorGreaterThan:
return sb.G(tblFieldName, value), nil
case qbtypes.FilterOperatorGreaterThanOrEq:
return sb.GE(tblFieldName, value), nil
case qbtypes.FilterOperatorLessThan:
return sb.LT(tblFieldName, value), nil
case qbtypes.FilterOperatorLessThanOrEq:
return sb.LE(tblFieldName, value), nil
// like and not like
case qbtypes.FilterOperatorLike:
return sb.Like(tblFieldName, value), nil
case qbtypes.FilterOperatorNotLike:
return sb.NotLike(tblFieldName, value), nil
case qbtypes.FilterOperatorILike:
return sb.ILike(tblFieldName, value), nil
case qbtypes.FilterOperatorNotILike:
return sb.NotILike(tblFieldName, value), nil
case qbtypes.FilterOperatorContains:
return sb.ILike(tblFieldName, fmt.Sprintf("%%%s%%", value)), nil
case qbtypes.FilterOperatorNotContains:
return sb.NotILike(tblFieldName, fmt.Sprintf("%%%s%%", value)), nil
case qbtypes.FilterOperatorRegexp:
return fmt.Sprintf(`match(%s, %s)`, tblFieldName, sb.Var(value)), nil
case qbtypes.FilterOperatorNotRegexp:
return fmt.Sprintf(`NOT match(%s, %s)`, tblFieldName, sb.Var(value)), nil
// between and not between
case qbtypes.FilterOperatorBetween:
values, ok := value.([]any)
if !ok {
return "", qbtypes.ErrBetweenValues
}
if len(values) != 2 {
return "", qbtypes.ErrBetweenValues
}
return sb.Between(tblFieldName, values[0], values[1]), nil
case qbtypes.FilterOperatorNotBetween:
values, ok := value.([]any)
if !ok {
return "", qbtypes.ErrBetweenValues
}
if len(values) != 2 {
return "", qbtypes.ErrBetweenValues
}
return sb.NotBetween(tblFieldName, values[0], values[1]), nil
// in and not in
case qbtypes.FilterOperatorIn:
values, ok := value.([]any)
if !ok {
return "", qbtypes.ErrInValues
}
// instead of using IN, we use `=` + `OR` to make use of index
conditions := []string{}
for _, value := range values {
conditions = append(conditions, sb.E(tblFieldName, value))
}
return sb.Or(conditions...), nil
case qbtypes.FilterOperatorNotIn:
values, ok := value.([]any)
if !ok {
return "", qbtypes.ErrInValues
}
// instead of using NOT IN, we use `!=` + `AND` to make use of index
conditions := []string{}
for _, value := range values {
conditions = append(conditions, sb.NE(tblFieldName, value))
}
return sb.And(conditions...), nil
// exists and not exists
// in the UI based query builder, `exists` and `not exists` are used for
// key membership checks, so depending on the column type, the condition changes
case qbtypes.FilterOperatorExists, qbtypes.FilterOperatorNotExists:
// if the field is intrinsic, it always exists
if slices.Contains(IntrinsicFields, key.Name) {
return "true", nil
}
if operator == qbtypes.FilterOperatorExists {
return fmt.Sprintf("has(JSONExtractKeys(labels), '%s')", key.Name), nil
}
return fmt.Sprintf("not has(JSONExtractKeys(labels), '%s')", key.Name), nil
}
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported operator: %v", operator)
}
func (c *conditionBuilder) ConditionFor(
ctx context.Context,
key *telemetrytypes.TelemetryFieldKey,
operator qbtypes.FilterOperator,
value any,
sb *sqlbuilder.SelectBuilder,
) (string, error) {
condition, err := c.conditionFor(ctx, key, operator, value, sb)
if err != nil {
return "", err
}
return condition, nil
}

View File

@ -0,0 +1,295 @@
package telemetrymetrics
import (
"context"
"testing"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/huandu/go-sqlbuilder"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConditionFor(t *testing.T) {
ctx := context.Background()
testCases := []struct {
name string
key telemetrytypes.TelemetryFieldKey
operator qbtypes.FilterOperator
value any
expectedSQL string
expectedArgs []any
expectedError error
}{
{
name: "Equal operator - string",
key: telemetrytypes.TelemetryFieldKey{
Name: "metric_name",
FieldContext: telemetrytypes.FieldContextMetric,
},
operator: qbtypes.FilterOperatorEqual,
value: "http.server.duration",
expectedSQL: "metric_name = ?",
expectedArgs: []any{"http.server.duration"},
expectedError: nil,
},
{
name: "Not Equal operator - metric_name",
key: telemetrytypes.TelemetryFieldKey{
Name: "metric_name",
FieldContext: telemetrytypes.FieldContextMetric,
},
operator: qbtypes.FilterOperatorNotEqual,
value: "http.server.duration",
expectedSQL: "metric_name <> ?",
expectedArgs: []any{"http.server.duration"},
expectedError: nil,
},
{
name: "Like operator - metric_name",
key: telemetrytypes.TelemetryFieldKey{
Name: "metric_name",
FieldContext: telemetrytypes.FieldContextMetric,
},
operator: qbtypes.FilterOperatorLike,
value: "%error%",
expectedSQL: "metric_name LIKE ?",
expectedArgs: []any{"%error%"},
expectedError: nil,
},
{
name: "Not Like operator - metric_name",
key: telemetrytypes.TelemetryFieldKey{
Name: "metric_name",
FieldContext: telemetrytypes.FieldContextMetric,
},
operator: qbtypes.FilterOperatorNotLike,
value: "%error%",
expectedSQL: "metric_name NOT LIKE ?",
expectedArgs: []any{"%error%"},
expectedError: nil,
},
{
name: "ILike operator - string label",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorILike,
value: "%admin%",
expectedSQL: "LOWER(JSONExtractString(labels, 'user.id')) LIKE LOWER(?)",
expectedArgs: []any{"%admin%"},
expectedError: nil,
},
{
name: "Not ILike operator - string label",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorNotILike,
value: "%admin%",
expectedSQL: "LOWER(JSONExtractString(labels, 'user.id')) NOT LIKE LOWER(?)",
expectedArgs: []any{"%admin%"},
expectedError: nil,
},
{
name: "Contains operator - string label",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorContains,
value: "admin",
expectedSQL: "LOWER(JSONExtractString(labels, 'user.id')) LIKE LOWER(?)",
expectedArgs: []any{"%admin%"},
expectedError: nil,
},
{
name: "In operator - metric_name",
key: telemetrytypes.TelemetryFieldKey{
Name: "metric_name",
FieldContext: telemetrytypes.FieldContextMetric,
},
operator: qbtypes.FilterOperatorIn,
value: []any{"http.server.duration", "http.server.request.duration", "http.server.response.duration"},
expectedSQL: "(metric_name = ? OR metric_name = ? OR metric_name = ?)",
expectedArgs: []any{"http.server.duration", "http.server.request.duration", "http.server.response.duration"},
expectedError: nil,
},
{
name: "In operator - invalid value",
key: telemetrytypes.TelemetryFieldKey{
Name: "metric_name",
FieldContext: telemetrytypes.FieldContextMetric,
},
operator: qbtypes.FilterOperatorIn,
value: "error",
expectedSQL: "",
expectedError: qbtypes.ErrInValues,
},
{
name: "Not In operator - metric_name",
key: telemetrytypes.TelemetryFieldKey{
Name: "metric_name",
FieldContext: telemetrytypes.FieldContextMetric,
},
operator: qbtypes.FilterOperatorNotIn,
value: []any{"debug", "info", "trace"},
expectedSQL: "(metric_name <> ? AND metric_name <> ? AND metric_name <> ?)",
expectedArgs: []any{"debug", "info", "trace"},
expectedError: nil,
},
{
name: "Exists operator - string field",
key: telemetrytypes.TelemetryFieldKey{
Name: "metric_name",
FieldContext: telemetrytypes.FieldContextMetric,
},
operator: qbtypes.FilterOperatorExists,
value: nil,
expectedSQL: "true",
expectedError: nil,
},
{
name: "Not Exists operator - string field",
key: telemetrytypes.TelemetryFieldKey{
Name: "metric_name",
FieldContext: telemetrytypes.FieldContextMetric,
},
operator: qbtypes.FilterOperatorNotExists,
value: nil,
expectedSQL: "true",
expectedError: nil,
},
{
name: "Exists operator - type",
key: telemetrytypes.TelemetryFieldKey{
Name: "type",
FieldContext: telemetrytypes.FieldContextMetric,
},
operator: qbtypes.FilterOperatorExists,
value: nil,
expectedSQL: "true",
expectedError: nil,
},
{
name: "Exists operator - string label",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorExists,
value: nil,
expectedSQL: "has(JSONExtractKeys(labels), 'user.id')",
expectedError: nil,
},
{
name: "Not Exists operator - string label",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorNotExists,
value: nil,
expectedSQL: "not has(JSONExtractKeys(labels), 'user.id')",
expectedError: nil,
},
{
name: "Non-existent column",
key: telemetrytypes.TelemetryFieldKey{
Name: "nonexistent_field",
FieldContext: telemetrytypes.FieldContextMetric,
},
operator: qbtypes.FilterOperatorEqual,
value: "value",
expectedSQL: "",
expectedError: qbtypes.ErrColumnNotFound,
},
}
fm := NewFieldMapper()
conditionBuilder := NewConditionBuilder(fm)
for _, tc := range testCases {
sb := sqlbuilder.NewSelectBuilder()
t.Run(tc.name, func(t *testing.T) {
cond, err := conditionBuilder.ConditionFor(ctx, &tc.key, tc.operator, tc.value, sb)
sb.Where(cond)
if tc.expectedError != nil {
assert.Equal(t, tc.expectedError, err)
} else {
require.NoError(t, err)
sql, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
assert.Contains(t, sql, tc.expectedSQL)
assert.Equal(t, tc.expectedArgs, args)
}
})
}
}
func TestConditionForMultipleKeys(t *testing.T) {
ctx := context.Background()
testCases := []struct {
name string
keys []telemetrytypes.TelemetryFieldKey
operator qbtypes.FilterOperator
value any
expectedSQL string
expectedArgs []any
expectedError error
}{
{
name: "Equal operator - string",
keys: []telemetrytypes.TelemetryFieldKey{
{
Name: "metric_name",
FieldContext: telemetrytypes.FieldContextMetric,
},
{
Name: "type",
FieldContext: telemetrytypes.FieldContextMetric,
},
},
operator: qbtypes.FilterOperatorEqual,
value: "error message",
expectedSQL: "metric_name = ? AND type = ?",
expectedArgs: []any{"error message", "error message"},
expectedError: nil,
},
}
fm := NewFieldMapper()
conditionBuilder := NewConditionBuilder(fm)
for _, tc := range testCases {
sb := sqlbuilder.NewSelectBuilder()
t.Run(tc.name, func(t *testing.T) {
var err error
for _, key := range tc.keys {
cond, err := conditionBuilder.ConditionFor(ctx, &key, tc.operator, tc.value, sb)
sb.Where(cond)
if err != nil {
t.Fatalf("Error getting condition for key %s: %v", key.Name, err)
}
}
if tc.expectedError != nil {
assert.Equal(t, tc.expectedError, err)
} else {
require.NoError(t, err)
sql, _ := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
assert.Contains(t, sql, tc.expectedSQL)
}
})
}
}

View File

@ -0,0 +1,8 @@
package telemetrymetrics
var IntrinsicFields = []string{
"temporality",
"metric_name",
"type",
"is_monotonic",
}

View File

@ -0,0 +1,90 @@
package telemetrymetrics
import (
"context"
"fmt"
schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
var (
timeSeriesV4Columns = map[string]*schema.Column{
"temporality": {Name: "temporality", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}},
"metric_name": {Name: "metric_name", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}},
"type": {Name: "type", Type: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString}},
"is_monotonic": {Name: "is_monotonic", Type: schema.ColumnTypeBool},
"fingerprint": {Name: "fingerprint", Type: schema.ColumnTypeUInt64},
"unix_milli": {Name: "unix_milli", Type: schema.ColumnTypeInt64},
"labels": {Name: "labels", Type: schema.ColumnTypeString},
"attrs": {Name: "attrs", Type: schema.MapColumnType{
KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString},
ValueType: schema.ColumnTypeString,
}},
"scope_attrs": {Name: "scope_attrs", Type: schema.MapColumnType{
KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString},
ValueType: schema.ColumnTypeString,
}},
"resource_attrs": {Name: "resource_attrs", Type: schema.MapColumnType{
KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString},
ValueType: schema.ColumnTypeString,
}},
}
)
type fieldMapper struct{}
func NewFieldMapper() qbtypes.FieldMapper {
return &fieldMapper{}
}
func (m *fieldMapper) getColumn(_ context.Context, key *telemetrytypes.TelemetryFieldKey) (*schema.Column, error) {
switch key.FieldContext {
case telemetrytypes.FieldContextResource, telemetrytypes.FieldContextScope, telemetrytypes.FieldContextAttribute:
return timeSeriesV4Columns["labels"], nil
case telemetrytypes.FieldContextMetric, telemetrytypes.FieldContextUnspecified:
col, ok := timeSeriesV4Columns[key.Name]
if !ok {
return nil, qbtypes.ErrColumnNotFound
}
return col, nil
}
return nil, qbtypes.ErrColumnNotFound
}
func (m *fieldMapper) FieldFor(ctx context.Context, key *telemetrytypes.TelemetryFieldKey) (string, error) {
column, err := m.getColumn(ctx, key)
if err != nil {
return "", err
}
switch key.FieldContext {
case telemetrytypes.FieldContextResource, telemetrytypes.FieldContextScope, telemetrytypes.FieldContextAttribute:
return fmt.Sprintf("JSONExtractString(%s, '%s')", column.Name, key.Name), nil
case telemetrytypes.FieldContextMetric:
return column.Name, nil
}
return column.Name, nil
}
func (m *fieldMapper) ColumnFor(ctx context.Context, key *telemetrytypes.TelemetryFieldKey) (*schema.Column, error) {
return m.getColumn(ctx, key)
}
func (m *fieldMapper) ColumnExpressionFor(
ctx context.Context,
field *telemetrytypes.TelemetryFieldKey,
keys map[string][]*telemetrytypes.TelemetryFieldKey,
) (string, error) {
colName, err := m.FieldFor(ctx, field)
if err != nil {
return "", err
}
return fmt.Sprintf("%s AS `%s`", colName, field.Name), nil
}

View File

@ -0,0 +1,220 @@
package telemetrymetrics
import (
"context"
"testing"
schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetColumn(t *testing.T) {
ctx := context.Background()
testCases := []struct {
name string
key telemetrytypes.TelemetryFieldKey
expectedCol *schema.Column
expectedError error
}{
{
name: "Resource field",
key: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
},
expectedCol: timeSeriesV4Columns["labels"],
expectedError: nil,
},
{
name: "Attribute field - string type",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
expectedCol: timeSeriesV4Columns["labels"],
expectedError: nil,
},
{
name: "Attribute field - number type",
key: telemetrytypes.TelemetryFieldKey{
Name: "request.size",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeNumber,
},
expectedCol: timeSeriesV4Columns["labels"],
expectedError: nil,
},
{
name: "Attribute field - int64 type",
key: telemetrytypes.TelemetryFieldKey{
Name: "request.duration",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeInt64,
},
expectedCol: timeSeriesV4Columns["labels"],
expectedError: nil,
},
{
name: "Attribute field - float64 type",
key: telemetrytypes.TelemetryFieldKey{
Name: "cpu.utilization",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeFloat64,
},
expectedCol: timeSeriesV4Columns["labels"],
expectedError: nil,
},
{
name: "Attribute field - bool type",
key: telemetrytypes.TelemetryFieldKey{
Name: "request.success",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeBool,
},
expectedCol: timeSeriesV4Columns["labels"],
expectedError: nil,
},
{
name: "Metric field - temporality",
key: telemetrytypes.TelemetryFieldKey{
Name: "temporality",
FieldContext: telemetrytypes.FieldContextMetric,
},
expectedCol: timeSeriesV4Columns["temporality"],
expectedError: nil,
},
{
name: "Metric field - metric_name",
key: telemetrytypes.TelemetryFieldKey{
Name: "metric_name",
FieldContext: telemetrytypes.FieldContextMetric,
},
expectedCol: timeSeriesV4Columns["metric_name"],
expectedError: nil,
},
{
name: "Metric field - nonexistent",
key: telemetrytypes.TelemetryFieldKey{
Name: "nonexistent_field",
FieldContext: telemetrytypes.FieldContextMetric,
},
expectedCol: nil,
expectedError: qbtypes.ErrColumnNotFound,
},
{
name: "did_user_login",
key: telemetrytypes.TelemetryFieldKey{
Name: "did_user_login",
Signal: telemetrytypes.SignalMetrics,
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeBool,
},
expectedCol: timeSeriesV4Columns["labels"],
expectedError: nil,
},
}
fm := NewFieldMapper()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
col, err := fm.ColumnFor(ctx, &tc.key)
if tc.expectedError != nil {
assert.Equal(t, tc.expectedError, err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expectedCol, col)
}
})
}
}
func TestGetFieldKeyName(t *testing.T) {
ctx := context.Background()
testCases := []struct {
name string
key telemetrytypes.TelemetryFieldKey
expectedResult string
expectedError error
}{
{
name: "Simple column type - metric_name",
key: telemetrytypes.TelemetryFieldKey{
Name: "metric_name",
FieldContext: telemetrytypes.FieldContextMetric,
},
expectedResult: "metric_name",
expectedError: nil,
},
{
name: "Map column type - string label",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
expectedResult: "JSONExtractString(labels, 'user.id')",
expectedError: nil,
},
{
name: "Map column type - number label",
key: telemetrytypes.TelemetryFieldKey{
Name: "request.size",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeNumber,
},
expectedResult: "JSONExtractString(labels, 'request.size')",
expectedError: nil,
},
{
name: "Map column type - bool label",
key: telemetrytypes.TelemetryFieldKey{
Name: "request.success",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeBool,
},
expectedResult: "JSONExtractString(labels, 'request.success')",
expectedError: nil,
},
{
name: "Map column type - resource label",
key: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
},
expectedResult: "JSONExtractString(labels, 'service.name')",
expectedError: nil,
},
{
name: "Non-existent column",
key: telemetrytypes.TelemetryFieldKey{
Name: "nonexistent_field",
FieldContext: telemetrytypes.FieldContextMetric,
},
expectedResult: "",
expectedError: qbtypes.ErrColumnNotFound,
},
}
fm := NewFieldMapper()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := fm.FieldFor(ctx, &tc.key)
if tc.expectedError != nil {
assert.Equal(t, tc.expectedError, err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expectedResult, result)
}
})
}
}

View File

@ -1,80 +0,0 @@
package types
import (
"database/sql/driver"
"encoding/base64"
"encoding/json"
"strings"
"github.com/gosimple/slug"
"github.com/uptrace/bun"
)
type Dashboard struct {
bun.BaseModel `bun:"table:dashboards"`
TimeAuditable
UserAuditable
OrgID string `json:"-" bun:"org_id,notnull"`
ID int `json:"id" bun:"id,pk,autoincrement"`
UUID string `json:"uuid" bun:"uuid,type:text,notnull,unique"`
Data DashboardData `json:"data" bun:"data,type:text,notnull"`
Locked *int `json:"isLocked" bun:"locked,notnull,default:0"`
Slug string `json:"-" bun:"-"`
Title string `json:"-" bun:"-"`
}
// UpdateSlug updates the slug
func (d *Dashboard) UpdateSlug() {
var title string
if val, ok := d.Data["title"]; ok {
title = val.(string)
}
d.Slug = SlugifyTitle(title)
}
func SlugifyTitle(title string) string {
s := slug.Make(strings.ToLower(title))
if s == "" {
// If the dashboard name is only characters outside of the
// sluggable characters, the slug creation will return an
// empty string which will mess up URLs. This failsafe picks
// that up and creates the slug as a base64 identifier instead.
s = base64.RawURLEncoding.EncodeToString([]byte(title))
if slug.MaxLength != 0 && len(s) > slug.MaxLength {
s = s[:slug.MaxLength]
}
}
return s
}
type DashboardData map[string]interface{}
func (c DashboardData) Value() (driver.Value, error) {
return json.Marshal(c)
}
func (c *DashboardData) Scan(src interface{}) error {
var data []byte
if b, ok := src.([]byte); ok {
data = b
} else if s, ok := src.(string); ok {
data = []byte(s)
}
return json.Unmarshal(data, c)
}
type TTLSetting struct {
bun.BaseModel `bun:"table:ttl_setting"`
Identifiable
TimeAuditable
TransactionID string `bun:"transaction_id,type:text,notnull"`
TableName string `bun:"table_name,type:text,notnull"`
TTL int `bun:"ttl,notnull,default:0"`
ColdStorageTTL int `bun:"cold_storage_ttl,notnull,default:0"`
Status string `bun:"status,type:text,notnull"`
OrgID string `json:"-" bun:"org_id,notnull"`
}

View File

@ -0,0 +1,262 @@
package dashboardtypes
import (
"context"
"encoding/json"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
type StorableDashboard struct {
bun.BaseModel `bun:"table:dashboard"`
types.Identifiable
types.TimeAuditable
types.UserAuditable
Data StorableDashboardData `bun:"data,type:text,notnull"`
Locked bool `bun:"locked,notnull,default:false"`
OrgID valuer.UUID `bun:"org_id,notnull"`
}
type Dashboard struct {
types.TimeAuditable
types.UserAuditable
ID string `json:"id"`
Data StorableDashboardData `json:"data"`
Locked bool `json:"locked"`
OrgID valuer.UUID `json:"org_id"`
}
type LockUnlockDashboard struct {
Locked *bool `json:"locked"`
}
type (
StorableDashboardData map[string]interface{}
GettableDashboard = Dashboard
UpdatableDashboard = StorableDashboardData
PostableDashboard = StorableDashboardData
ListableDashboard []*GettableDashboard
)
func NewStorableDashboardFromDashboard(dashboard *Dashboard) (*StorableDashboard, error) {
dashboardID, err := valuer.NewUUID(dashboard.ID)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid")
}
return &StorableDashboard{
Identifiable: types.Identifiable{
ID: dashboardID,
},
TimeAuditable: types.TimeAuditable{
CreatedAt: dashboard.CreatedAt,
UpdatedAt: dashboard.UpdatedAt,
},
UserAuditable: types.UserAuditable{
CreatedBy: dashboard.CreatedBy,
UpdatedBy: dashboard.UpdatedBy,
},
OrgID: dashboard.OrgID,
Data: dashboard.Data,
Locked: dashboard.Locked,
}, nil
}
func NewDashboard(orgID valuer.UUID, createdBy string, storableDashboardData StorableDashboardData) (*Dashboard, error) {
currentTime := time.Now()
return &Dashboard{
ID: valuer.GenerateUUID().StringValue(),
TimeAuditable: types.TimeAuditable{
CreatedAt: currentTime,
UpdatedAt: currentTime,
},
UserAuditable: types.UserAuditable{
CreatedBy: createdBy,
UpdatedBy: createdBy,
},
OrgID: orgID,
Data: storableDashboardData,
Locked: false,
}, nil
}
func NewDashboardFromStorableDashboard(storableDashboard *StorableDashboard) (*Dashboard, error) {
return &Dashboard{
ID: storableDashboard.ID.StringValue(),
TimeAuditable: types.TimeAuditable{
CreatedAt: storableDashboard.CreatedAt,
UpdatedAt: storableDashboard.UpdatedAt,
},
UserAuditable: types.UserAuditable{
CreatedBy: storableDashboard.CreatedBy,
UpdatedBy: storableDashboard.UpdatedBy,
},
OrgID: storableDashboard.OrgID,
Data: storableDashboard.Data,
Locked: storableDashboard.Locked,
}, nil
}
func NewDashboardsFromStorableDashboards(storableDashboards []*StorableDashboard) ([]*Dashboard, error) {
dashboards := make([]*Dashboard, len(storableDashboards))
for idx, storableDashboard := range storableDashboards {
dashboard, err := NewDashboardFromStorableDashboard(storableDashboard)
if err != nil {
return nil, err
}
dashboards[idx] = dashboard
}
return dashboards, nil
}
func NewGettableDashboardsFromDashboards(dashboards []*Dashboard) ([]*GettableDashboard, error) {
gettableDashboards := make([]*GettableDashboard, len(dashboards))
for idx, dashboard := range dashboards {
gettableDashboard, err := NewGettableDashboardFromDashboard(dashboard)
if err != nil {
return nil, err
}
gettableDashboards[idx] = gettableDashboard
}
return gettableDashboards, nil
}
func NewGettableDashboardFromDashboard(dashboard *Dashboard) (*GettableDashboard, error) {
return &GettableDashboard{
ID: dashboard.ID,
TimeAuditable: dashboard.TimeAuditable,
UserAuditable: dashboard.UserAuditable,
OrgID: dashboard.OrgID,
Data: dashboard.Data,
Locked: dashboard.Locked,
}, nil
}
func (storableDashboardData *StorableDashboardData) GetWidgetIds() []string {
data := *storableDashboardData
widgetIds := []string{}
if data != nil && data["widgets"] != nil {
widgets, ok := data["widgets"]
if ok {
data, ok := widgets.([]interface{})
if ok {
for _, widget := range data {
sData, ok := widget.(map[string]interface{})
if ok && sData["query"] != nil && sData["id"] != nil {
id, ok := sData["id"].(string)
if ok {
widgetIds = append(widgetIds, id)
}
}
}
}
}
}
return widgetIds
}
func (dashboard *Dashboard) CanUpdate(data StorableDashboardData) error {
if dashboard.Locked {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "cannot update a locked dashboard, please unlock the dashboard to update")
}
existingIDs := dashboard.Data.GetWidgetIds()
newIDs := data.GetWidgetIds()
newIdsMap := make(map[string]bool)
for _, id := range newIDs {
newIdsMap[id] = true
}
differenceMap := make(map[string]bool)
difference := []string{}
for _, id := range existingIDs {
if _, found := newIdsMap[id]; !found && !differenceMap[id] {
difference = append(difference, id)
differenceMap[id] = true
}
}
if len(difference) > 1 {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "deleting more than one panel is not supported")
}
return nil
}
func (dashboard *Dashboard) Update(updatableDashboard UpdatableDashboard, updatedBy string) error {
err := dashboard.CanUpdate(updatableDashboard)
if err != nil {
return err
}
dashboard.UpdatedBy = updatedBy
dashboard.UpdatedAt = time.Now()
dashboard.Data = updatableDashboard
return nil
}
func (dashboard *Dashboard) CanLockUnlock(ctx context.Context, updatedBy string) error {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return err
}
if dashboard.CreatedBy != updatedBy || claims.Role != types.RoleAdmin {
return errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "you are not authorized to lock/unlock this dashboard")
}
return nil
}
func (dashboard *Dashboard) LockUnlock(ctx context.Context, lock bool, updatedBy string) error {
err := dashboard.CanLockUnlock(ctx, updatedBy)
if err != nil {
return err
}
dashboard.Locked = lock
dashboard.UpdatedBy = updatedBy
dashboard.UpdatedAt = time.Now()
return nil
}
func (lockUnlockDashboard *LockUnlockDashboard) UnmarshalJSON(src []byte) error {
var lockUnlock struct {
Locked *bool `json:"lock"`
}
err := json.Unmarshal(src, &lockUnlock)
if err != nil {
return err
}
if lockUnlock.Locked == nil {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "lock is missing in the request payload")
}
lockUnlockDashboard.Locked = lockUnlock.Locked
return nil
}
type Store interface {
Create(context.Context, *StorableDashboard) error
Get(context.Context, valuer.UUID, valuer.UUID) (*StorableDashboard, error)
List(context.Context, valuer.UUID) ([]*StorableDashboard, error)
Update(context.Context, valuer.UUID, *StorableDashboard) error
Delete(context.Context, valuer.UUID, valuer.UUID) error
}

View File

@ -49,6 +49,18 @@ func NewOrganizationKey(orgID valuer.UUID) uint32 {
return hasher.Sum32() return hasher.Sum32()
} }
type TTLSetting struct {
bun.BaseModel `bun:"table:ttl_setting"`
Identifiable
TimeAuditable
TransactionID string `bun:"transaction_id,type:text,notnull"`
TableName string `bun:"table_name,type:text,notnull"`
TTL int `bun:"ttl,notnull,default:0"`
ColdStorageTTL int `bun:"cold_storage_ttl,notnull,default:0"`
Status string `bun:"status,type:text,notnull"`
OrgID string `json:"-" bun:"org_id,notnull"`
}
type OrganizationStore interface { type OrganizationStore interface {
Create(context.Context, *Organization) error Create(context.Context, *Organization) error
Get(context.Context, valuer.UUID) (*Organization, error) Get(context.Context, valuer.UUID) (*Organization, error)

View File

@ -3,7 +3,7 @@ package querybuildertypesv5
import ( import (
"encoding/json" "encoding/json"
"math" "math"
"sort" "slices"
"time" "time"
"github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/errors"
@ -253,8 +253,17 @@ func FunctionReduceTo(result *TimeSeries, reduceTo ReduceTo) *TimeSeries {
reducedValue = math.NaN() reducedValue = math.NaN()
reducedTimestamp = result.Values[len(result.Values)-1].Timestamp reducedTimestamp = result.Values[len(result.Values)-1].Timestamp
} else { } else {
sort.Slice(values, func(i, j int) bool { slices.SortFunc(values, func(i, j struct {
return values[i].Value < values[j].Value Value float64
Timestamp int64
}) int {
if i.Value < j.Value {
return -1
}
if i.Value > j.Value {
return 1
}
return 0
}) })
if len(values)%2 == 0 { if len(values)%2 == 0 {

View File

@ -2,7 +2,7 @@ package querybuildertypesv5
import ( import (
"math" "math"
"sort" "slices"
"strconv" "strconv"
"github.com/SigNoz/signoz/pkg/valuer" "github.com/SigNoz/signoz/pkg/valuer"
@ -319,7 +319,7 @@ func median(values []float64) float64 {
return math.NaN() return math.NaN()
} }
sort.Float64s(values) slices.Sort(values)
medianIndex := len(values) / 2 medianIndex := len(values) / 2
if len(values)%2 == 0 { if len(values)%2 == 0 {
return (values[medianIndex-1] + values[medianIndex]) / 2 return (values[medianIndex-1] + values[medianIndex]) / 2

View File

@ -2,7 +2,7 @@ package querybuildertypesv5
import ( import (
"math" "math"
"sort" "slices"
"strconv" "strconv"
"strings" "strings"
@ -55,8 +55,14 @@ func ApplySeriesLimit(series []*TimeSeries, orderBy []OrderBy, limit int) []*Tim
} }
// Sort the series based on the order criteria // Sort the series based on the order criteria
sort.SliceStable(series, func(i, j int) bool { slices.SortStableFunc(series, func(i, j *TimeSeries) int {
return compareSeries(series[i], series[j], effectiveOrderBy, seriesValues, seriesLabels) if compareSeries(i, j, effectiveOrderBy, seriesValues, seriesLabels) {
return -1
}
if compareSeries(j, i, effectiveOrderBy, seriesValues, seriesLabels) {
return 1
}
return 0
}) })
// Apply limit if specified // Apply limit if specified