diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go index 40a28944da..15042a4d95 100644 --- a/ee/query-service/app/api/api.go +++ b/ee/query-service/app/api/api.go @@ -96,13 +96,7 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) { // note: add ee override methods first // routes available only in ee version - 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 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/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 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) diff --git a/ee/query-service/app/api/dashboard.go b/ee/query-service/app/api/dashboard.go deleted file mode 100644 index a0e30ecf8c..0000000000 --- a/ee/query-service/app/api/dashboard.go +++ /dev/null @@ -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") -} diff --git a/frontend/src/api/dashboard/create.ts b/frontend/src/api/dashboard/create.ts deleted file mode 100644 index bf5458ac40..0000000000 --- a/frontend/src/api/dashboard/create.ts +++ /dev/null @@ -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 | 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; diff --git a/frontend/src/api/dashboard/delete.ts b/frontend/src/api/dashboard/delete.ts deleted file mode 100644 index 8faf711383..0000000000 --- a/frontend/src/api/dashboard/delete.ts +++ /dev/null @@ -1,9 +0,0 @@ -import axios from 'api'; -import { PayloadProps, Props } from 'types/api/dashboard/delete'; - -const deleteDashboard = (props: Props): Promise => - axios - .delete(`/dashboards/${props.uuid}`) - .then((response) => response.data); - -export default deleteDashboard; diff --git a/frontend/src/api/dashboard/get.ts b/frontend/src/api/dashboard/get.ts deleted file mode 100644 index 01e04c6c0f..0000000000 --- a/frontend/src/api/dashboard/get.ts +++ /dev/null @@ -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 => - axios - .get>(`/dashboards/${props.uuid}`) - .then((res) => res.data.data); - -export default getDashboard; diff --git a/frontend/src/api/dashboard/getAll.ts b/frontend/src/api/dashboard/getAll.ts deleted file mode 100644 index aafe44b3ed..0000000000 --- a/frontend/src/api/dashboard/getAll.ts +++ /dev/null @@ -1,8 +0,0 @@ -import axios from 'api'; -import { ApiResponse } from 'types/api'; -import { Dashboard } from 'types/api/dashboard/getAll'; - -export const getAllDashboardList = (): Promise => - axios - .get>('/dashboards') - .then((res) => res.data.data); diff --git a/frontend/src/api/dashboard/lockDashboard.ts b/frontend/src/api/dashboard/lockDashboard.ts deleted file mode 100644 index 3393de8fa3..0000000000 --- a/frontend/src/api/dashboard/lockDashboard.ts +++ /dev/null @@ -1,11 +0,0 @@ -import axios from 'api'; -import { AxiosResponse } from 'axios'; - -interface LockDashboardProps { - uuid: string; -} - -const lockDashboard = (props: LockDashboardProps): Promise => - axios.put(`/dashboards/${props.uuid}/lock`); - -export default lockDashboard; diff --git a/frontend/src/api/dashboard/unlockDashboard.ts b/frontend/src/api/dashboard/unlockDashboard.ts deleted file mode 100644 index fd4ffbe41a..0000000000 --- a/frontend/src/api/dashboard/unlockDashboard.ts +++ /dev/null @@ -1,11 +0,0 @@ -import axios from 'api'; -import { AxiosResponse } from 'axios'; - -interface UnlockDashboardProps { - uuid: string; -} - -const unlockDashboard = (props: UnlockDashboardProps): Promise => - axios.put(`/dashboards/${props.uuid}/unlock`); - -export default unlockDashboard; diff --git a/frontend/src/api/dashboard/update.ts b/frontend/src/api/dashboard/update.ts deleted file mode 100644 index 21216e051f..0000000000 --- a/frontend/src/api/dashboard/update.ts +++ /dev/null @@ -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 | 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; diff --git a/frontend/src/api/v1/dashboards/create.ts b/frontend/src/api/v1/dashboards/create.ts new file mode 100644 index 0000000000..1eb7bdc12e --- /dev/null +++ b/frontend/src/api/v1/dashboards/create.ts @@ -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> => { + try { + const response = await axios.post('/dashboards', { + ...props, + }); + + return { + httpStatusCode: response.status, + data: response.data.data, + }; + } catch (error) { + ErrorResponseHandlerV2(error as AxiosError); + } +}; + +export default create; diff --git a/frontend/src/api/v1/dashboards/getAll.ts b/frontend/src/api/v1/dashboards/getAll.ts new file mode 100644 index 0000000000..7d89ef94f0 --- /dev/null +++ b/frontend/src/api/v1/dashboards/getAll.ts @@ -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> => { + try { + const response = await axios.get('/dashboards'); + return { + httpStatusCode: response.status, + data: response.data.data, + }; + } catch (error) { + ErrorResponseHandlerV2(error as AxiosError); + } +}; + +export default getAll; diff --git a/frontend/src/api/v1/dashboards/id/delete.ts b/frontend/src/api/v1/dashboards/id/delete.ts new file mode 100644 index 0000000000..e6c974df02 --- /dev/null +++ b/frontend/src/api/v1/dashboards/id/delete.ts @@ -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> => { + try { + const response = await axios.delete(`/dashboards/${props.id}`); + return { + httpStatusCode: response.status, + data: null, + }; + } catch (error) { + ErrorResponseHandlerV2(error as AxiosError); + } +}; + +export default deleteDashboard; diff --git a/frontend/src/api/v1/dashboards/id/get.ts b/frontend/src/api/v1/dashboards/id/get.ts new file mode 100644 index 0000000000..dfde573342 --- /dev/null +++ b/frontend/src/api/v1/dashboards/id/get.ts @@ -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> => { + try { + const response = await axios.get(`/dashboards/${props.id}`); + return { + httpStatusCode: response.status, + data: response.data.data, + }; + } catch (error) { + ErrorResponseHandlerV2(error as AxiosError); + } +}; + +export default get; diff --git a/frontend/src/api/v1/dashboards/id/lock.ts b/frontend/src/api/v1/dashboards/id/lock.ts new file mode 100644 index 0000000000..289a4ddc99 --- /dev/null +++ b/frontend/src/api/v1/dashboards/id/lock.ts @@ -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> => { + try { + const response = await axios.put( + `/dashboards/${props.id}/lock`, + { lock: props.lock }, + ); + + return { + httpStatusCode: response.status, + data: response.data.data, + }; + } catch (error) { + ErrorResponseHandlerV2(error as AxiosError); + } +}; + +export default lock; diff --git a/frontend/src/api/v1/dashboards/id/update.ts b/frontend/src/api/v1/dashboards/id/update.ts new file mode 100644 index 0000000000..82e98039bc --- /dev/null +++ b/frontend/src/api/v1/dashboards/id/update.ts @@ -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> => { + try { + const response = await axios.put(`/dashboards/${props.id}`, { + ...props.data, + }); + + return { + httpStatusCode: response.status, + data: response.data.data, + }; + } catch (error) { + ErrorResponseHandlerV2(error as AxiosError); + } +}; + +export default update; diff --git a/frontend/src/container/ExportPanel/ExportPanelContainer.tsx b/frontend/src/container/ExportPanel/ExportPanelContainer.tsx index a0927e3692..6a1c3f6c2e 100644 --- a/frontend/src/container/ExportPanel/ExportPanelContainer.tsx +++ b/frontend/src/container/ExportPanel/ExportPanelContainer.tsx @@ -1,11 +1,12 @@ 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 { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard'; -import useAxiosError from 'hooks/useAxiosError'; +import { useErrorModal } from 'providers/ErrorModalProvider'; import { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation } from 'react-query'; +import APIError from 'types/api/error'; import { ExportPanelProps } from '.'; import { @@ -33,26 +34,28 @@ function ExportPanelContainer({ refetch, } = useGetAllDashboard(); - const handleError = useAxiosError(); + const { showErrorModal } = useErrorModal(); const { mutate: createNewDashboard, isLoading: createDashboardLoading, } = useMutation(createDashboard, { onSuccess: (data) => { - if (data.payload) { - onExport(data?.payload, true); + if (data.data) { + onExport(data?.data, true); } 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 currentSelectedDashboard = data?.find( - ({ uuid }) => uuid === selectedDashboardId, + const currentSelectedDashboard = data?.data?.find( + ({ id }) => id === selectedDashboardId, ); onExport(currentSelectedDashboard || null, false); @@ -66,14 +69,18 @@ function ExportPanelContainer({ ); const handleNewDashboard = useCallback(async () => { - createNewDashboard({ - title: t('new_dashboard_title', { - ns: 'dashboard', - }), - uploadedGrafana: false, - version: ENTITY_VERSION_V4, - }); - }, [t, createNewDashboard]); + try { + await createNewDashboard({ + title: t('new_dashboard_title', { + ns: 'dashboard', + }), + uploadedGrafana: false, + version: ENTITY_VERSION_V4, + }); + } catch (error) { + showErrorModal(error as APIError); + } + }, [createNewDashboard, t, showErrorModal]); const isDashboardLoading = isAllDashboardsLoading || createDashboardLoading; diff --git a/frontend/src/container/ExportPanel/utils.ts b/frontend/src/container/ExportPanel/utils.ts index f2d36a278b..7f3a45d753 100644 --- a/frontend/src/container/ExportPanel/utils.ts +++ b/frontend/src/container/ExportPanel/utils.ts @@ -1,12 +1,10 @@ import { SelectProps } from 'antd'; -import { PayloadProps as AllDashboardsData } from 'types/api/dashboard/getAll'; +import { Dashboard } from 'types/api/dashboard/getAll'; -export const getSelectOptions = ( - data: AllDashboardsData, -): SelectProps['options'] => - data.map(({ uuid, data }) => ({ +export const getSelectOptions = (data: Dashboard[]): SelectProps['options'] => + data.map(({ id, data }) => ({ label: data.title, - value: uuid, + value: id, })); export const filterOptions: SelectProps['filterOption'] = ( diff --git a/frontend/src/container/GridCardLayout/DashboardEmptyState/DashboardEmptyState.tsx b/frontend/src/container/GridCardLayout/DashboardEmptyState/DashboardEmptyState.tsx index 9c5566be0f..76c7dda0ad 100644 --- a/frontend/src/container/GridCardLayout/DashboardEmptyState/DashboardEmptyState.tsx +++ b/frontend/src/container/GridCardLayout/DashboardEmptyState/DashboardEmptyState.tsx @@ -38,7 +38,7 @@ export default function DashboardEmptyState(): JSX.Element { setSelectedRowWidgetId(null); handleToggleDashboardSlider(true); logEvent('Dashboard Detail: Add new panel clicked', { - dashboardId: selectedDashboard?.uuid, + dashboardId: selectedDashboard?.id, dashboardName: selectedDashboard?.data.title, numberOfPanels: selectedDashboard?.data.widgets?.length, }); diff --git a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.test.tsx b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.test.tsx index deb92aa4f5..e812088545 100644 --- a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.test.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.test.tsx @@ -2,6 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { PANEL_TYPES } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; import { AppProvider } from 'providers/App/App'; +import { ErrorModalProvider } from 'providers/ErrorModalProvider'; import MockQueryClientProvider from 'providers/test/MockQueryClientProvider'; import { Provider } from 'react-redux'; import store from 'store'; @@ -189,24 +190,26 @@ describe('WidgetGraphComponent', () => { it('should show correct menu items when hovering over more options while loading', async () => { const { getByTestId, findByRole, getByText, container } = render( - - - - - + + + + + + + , ); diff --git a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx index d39fe0c080..6020705d62 100644 --- a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx @@ -4,7 +4,6 @@ import { Skeleton, Tooltip, Typography } from 'antd'; import cx from 'classnames'; import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer'; import { ToggleGraphProps } from 'components/Graph/types'; -import { SOMETHING_WENT_WRONG } from 'constants/api'; import { QueryParams } from 'constants/query'; import { PANEL_TYPES } from 'constants/queryBuilder'; import { placeWidgetAtBottom } from 'container/NewWidget/utils'; @@ -31,7 +30,7 @@ import { useState, } from 'react'; 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 { v4 } from 'uuid'; @@ -119,29 +118,23 @@ function WidgetGraphComponent({ const updatedLayout = selectedDashboard.data.layout?.filter((e) => e.i !== widget.id) || []; - const updatedSelectedDashboard: Dashboard = { - ...selectedDashboard, + const updatedSelectedDashboard: Props = { data: { ...selectedDashboard.data, widgets: updatedWidgets, layout: updatedLayout, }, - uuid: selectedDashboard.uuid, + id: selectedDashboard.id, }; updateDashboardMutation.mutateAsync(updatedSelectedDashboard, { onSuccess: (updatedDashboard) => { - if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []); - if (setSelectedDashboard && updatedDashboard.payload) { - setSelectedDashboard(updatedDashboard.payload); + if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []); + if (setSelectedDashboard && updatedDashboard.data) { + setSelectedDashboard(updatedDashboard.data); } setDeleteModal(false); }, - onError: () => { - notifications.error({ - message: SOMETHING_WENT_WRONG, - }); - }, }); }; @@ -166,7 +159,8 @@ function WidgetGraphComponent({ updateDashboardMutation.mutateAsync( { - ...selectedDashboard, + id: selectedDashboard.id, + data: { ...selectedDashboard.data, layout, @@ -183,9 +177,9 @@ function WidgetGraphComponent({ }, { onSuccess: (updatedDashboard) => { - if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []); - if (setSelectedDashboard && updatedDashboard.payload) { - setSelectedDashboard(updatedDashboard.payload); + if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []); + if (setSelectedDashboard && updatedDashboard.data) { + setSelectedDashboard(updatedDashboard.data); } notifications.success({ message: 'Panel cloned successfully, redirecting to new copy.', diff --git a/frontend/src/container/GridCardLayout/GridCardLayout.tsx b/frontend/src/container/GridCardLayout/GridCardLayout.tsx index ae6d3a1543..93f3c4e49c 100644 --- a/frontend/src/container/GridCardLayout/GridCardLayout.tsx +++ b/frontend/src/container/GridCardLayout/GridCardLayout.tsx @@ -6,7 +6,6 @@ import { Button, Form, Input, Modal, Typography } from 'antd'; import { useForm } from 'antd/es/form/Form'; import logEvent from 'api/common/logEvent'; import cx from 'classnames'; -import { SOMETHING_WENT_WRONG } from 'constants/api'; import { QueryParams } from 'constants/query'; import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder'; 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 useComponentPermission from 'hooks/useComponentPermission'; import { useIsDarkMode } from 'hooks/useDarkMode'; -import { useNotifications } from 'hooks/useNotifications'; import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; import { defaultTo, isUndefined } from 'lodash-es'; @@ -36,7 +34,8 @@ import { ItemCallback, Layout } from 'react-grid-layout'; import { useDispatch } from 'react-redux'; import { useLocation } from 'react-router-dom'; 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 { ComponentTypes } from 'utils/permission'; @@ -107,7 +106,6 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element { const updateDashboardMutation = useUpdateDashboard(); - const { notifications } = useNotifications(); const urlQuery = useUrlQuery(); let permissions: ComponentTypes[] = ['save_layout', 'add_panel']; @@ -158,20 +156,20 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element { useEffect(() => { if (!logEventCalledRef.current && !isUndefined(data)) { logEvent('Dashboard Detail: Opened', { - dashboardId: data.uuid, + dashboardId: selectedDashboard?.id, dashboardName: data.title, numberOfPanels: data.widgets?.length, numberOfVariables: Object.keys(data?.variables || {}).length || 0, }); logEventCalledRef.current = true; } - }, [data]); + }, [data, selectedDashboard?.id]); const onSaveHandler = (): void => { if (!selectedDashboard) return; - const updatedDashboard: Dashboard = { - ...selectedDashboard, + const updatedDashboard: Props = { + id: selectedDashboard.id, data: { ...selectedDashboard.data, panelMap: { ...currentPanelMap }, @@ -186,24 +184,18 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element { return widget; }), }, - uuid: selectedDashboard.uuid, }; updateDashboardMutation.mutate(updatedDashboard, { onSuccess: (updatedDashboard) => { setSelectedRowWidgetId(null); - if (updatedDashboard.payload) { - if (updatedDashboard.payload.data.layout) - setLayouts(sortLayout(updatedDashboard.payload.data.layout)); - setSelectedDashboard(updatedDashboard.payload); - setPanelMap(updatedDashboard.payload?.data?.panelMap || {}); + if (updatedDashboard.data) { + if (updatedDashboard.data.data.layout) + setLayouts(sortLayout(updatedDashboard.data.data.layout)); + setSelectedDashboard(updatedDashboard.data); + 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); - const updatedSelectedDashboard: Dashboard = { - ...selectedDashboard, + const updatedSelectedDashboard: Props = { + id: selectedDashboard.id, data: { ...selectedDashboard.data, widgets: updatedWidgets, }, - uuid: selectedDashboard.uuid, }; updateDashboardMutation.mutateAsync(updatedSelectedDashboard, { onSuccess: (updatedDashboard) => { - if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []); - if (setSelectedDashboard && updatedDashboard.payload) { - setSelectedDashboard(updatedDashboard.payload); + if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []); + if (setSelectedDashboard && updatedDashboard.data) { + setSelectedDashboard(updatedDashboard.data); } - if (setPanelMap) - setPanelMap(updatedDashboard.payload?.data?.panelMap || {}); + if (setPanelMap) setPanelMap(updatedDashboard.data?.data?.panelMap || {}); form.setFieldValue('title', ''); setIsSettingsModalOpen(false); 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 }; delete updatedPanelMap[currentSelectRowId]; - const updatedSelectedDashboard: Dashboard = { - ...selectedDashboard, + const updatedSelectedDashboard: Props = { + id: selectedDashboard.id, data: { ...selectedDashboard.data, widgets: updatedWidgets, layout: updatedLayout, panelMap: updatedPanelMap, }, - uuid: selectedDashboard.uuid, }; updateDashboardMutation.mutateAsync(updatedSelectedDashboard, { onSuccess: (updatedDashboard) => { - if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []); - if (setSelectedDashboard && updatedDashboard.payload) { - setSelectedDashboard(updatedDashboard.payload); + if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []); + if (setSelectedDashboard && updatedDashboard.data) { + setSelectedDashboard(updatedDashboard.data); } - if (setPanelMap) - setPanelMap(updatedDashboard.payload?.data?.panelMap || {}); + if (setPanelMap) setPanelMap(updatedDashboard.data?.data?.panelMap || {}); setIsDeleteModalOpen(false); setCurrentSelectRowId(null); }, - // eslint-disable-next-line sonarjs/no-identical-functions - onError: () => { - notifications.error({ - message: SOMETHING_WENT_WRONG, - }); - }, }); }; const isDashboardEmpty = useMemo( diff --git a/frontend/src/container/Home/Dashboards/Dashboards.tsx b/frontend/src/container/Home/Dashboards/Dashboards.tsx index 4e10a42df4..b08b6f6e06 100644 --- a/frontend/src/container/Home/Dashboards/Dashboards.tsx +++ b/frontend/src/container/Home/Dashboards/Dashboards.tsx @@ -33,7 +33,7 @@ export default function Dashboards({ useEffect(() => { if (!dashboardsList) return; - const sortedDashboards = dashboardsList.sort((a, b) => { + const sortedDashboards = dashboardsList.data.sort((a, b) => { const aUpdateAt = new Date(a.updatedAt).getTime(); const bUpdateAt = new Date(b.updatedAt).getTime(); return bUpdateAt - aUpdateAt; @@ -103,7 +103,7 @@ export default function Dashboards({
{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): void => { event.stopPropagation(); @@ -134,7 +134,7 @@ export default function Dashboards({
{ const filteredDashboards = filterDashboard( searchString, - dashboardListResponse || [], + dashboardListResponse?.data || [], ); if (sortOrder.columnKey === 'updatedAt') { sortDashboardsByUpdatedAt(filteredDashboards || []); @@ -256,17 +258,19 @@ function DashboardsList(): JSX.Element { errorMessage: '', }); + const { showErrorModal } = useErrorModal(); + const data: Data[] = dashboards?.map((e) => ({ createdAt: e.createdAt, description: e.data.description || '', - id: e.uuid, + id: e.id, lastUpdatedTime: e.updatedAt, name: e.data.title, tags: e.data.tags || [], - key: e.uuid, + key: e.id, createdBy: e.createdBy, - isLocked: !!e.isLocked || false, + isLocked: !!e.locked || false, lastUpdatedBy: e.updatedBy, image: e.data.image || Base64Icons[0], variables: e.data.variables, @@ -292,28 +296,20 @@ function DashboardsList(): JSX.Element { version: ENTITY_VERSION_V4, }); - if (response.statusCode === 200) { - safeNavigate( - generatePath(ROUTES.DASHBOARD, { - dashboardId: response.payload.uuid, - }), - ); - } else { - setNewDashboardState({ - ...newDashboardState, - loading: false, - error: true, - errorMessage: response.error || 'Something went wrong', - }); - } + safeNavigate( + generatePath(ROUTES.DASHBOARD, { + dashboardId: response.data.id, + }), + ); } catch (error) { + showErrorModal(error as APIError); setNewDashboardState({ ...newDashboardState, error: true, errorMessage: (error as AxiosError).toString() || 'Something went Wrong', }); } - }, [newDashboardState, safeNavigate, t]); + }, [newDashboardState, safeNavigate, showErrorModal, t]); const onModalHandler = (uploadedGrafana: boolean): void => { logEvent('Dashboard List: Import JSON clicked', {}); @@ -327,7 +323,7 @@ function DashboardsList(): JSX.Element { const searchText = (event as React.BaseSyntheticEvent)?.target?.value || ''; const filteredDashboards = filterDashboard( searchText, - dashboardListResponse || [], + dashboardListResponse?.data || [], ); setDashboards(filteredDashboards); setIsFilteringDashboards(false); @@ -677,7 +673,7 @@ function DashboardsList(): JSX.Element { !isUndefined(dashboardListResponse) ) { logEvent('Dashboard List: Page visited', { - number: dashboardListResponse?.length, + number: dashboardListResponse?.data?.length, }); logEventCalledRef.current = true; } diff --git a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx index 1ea39e0239..7f5f980666 100644 --- a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx +++ b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx @@ -14,19 +14,21 @@ import { UploadProps, } from 'antd'; 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 { useIsDarkMode } from 'hooks/useDarkMode'; import { useNotifications } from 'hooks/useNotifications'; import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout'; 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/ // See more: https://github.com/lucide-icons/lucide/issues/94 import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { generatePath } from 'react-router-dom'; import { DashboardData } from 'types/api/dashboard/getAll'; +import APIError from 'types/api/error'; function ImportJSON({ isImportJSONModalVisible, @@ -74,6 +76,8 @@ function ImportJSON({ } }; + const { showErrorModal } = useErrorModal(); + const onClickLoadJsonHandler = async (): Promise => { try { setDashboardCreating(true); @@ -81,11 +85,6 @@ function ImportJSON({ 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) { dashboardData.layout = getUpdatedLayout(dashboardData.layout); } else { @@ -97,28 +96,19 @@ function ImportJSON({ uploadedGrafana, }); - if (response.statusCode === 200) { - safeNavigate( - generatePath(ROUTES.DASHBOARD, { - dashboardId: response.payload.uuid, - }), - ); - logEvent('Dashboard List: New dashboard imported successfully', { - dashboardId: response.payload?.uuid, - dashboardName: response.payload?.data?.title, - }); - } else { - setIsCreateDashboardError(true); - notifications.error({ - message: - response.error || - t('something_went_wrong', { - ns: 'common', - }), - }); - } + safeNavigate( + generatePath(ROUTES.DASHBOARD, { + dashboardId: response.data.id, + }), + ); + logEvent('Dashboard List: New dashboard imported successfully', { + dashboardId: response.data?.id, + dashboardName: response.data?.data?.title, + }); + setDashboardCreating(false); } catch (error) { + showErrorModal(error as APIError); setDashboardCreating(false); setIsCreateDashboardError(true); notifications.error({ diff --git a/frontend/src/container/ListOfDashboard/SearchFilter/__tests__/utils.test.ts b/frontend/src/container/ListOfDashboard/SearchFilter/__tests__/utils.test.ts index a77cf8ccb5..e15e6bfa0c 100644 --- a/frontend/src/container/ListOfDashboard/SearchFilter/__tests__/utils.test.ts +++ b/frontend/src/container/ListOfDashboard/SearchFilter/__tests__/utils.test.ts @@ -6,8 +6,7 @@ import { executeSearchQueries } from '../utils'; describe('executeSearchQueries', () => { const firstDashboard: Dashboard = { - id: 11111, - uuid: uuid(), + id: uuid(), createdAt: '', updatedAt: '', createdBy: '', @@ -18,8 +17,7 @@ describe('executeSearchQueries', () => { }, }; const secondDashboard: Dashboard = { - id: 22222, - uuid: uuid(), + id: uuid(), createdAt: '', updatedAt: '', createdBy: '', @@ -30,8 +28,7 @@ describe('executeSearchQueries', () => { }, }; const thirdDashboard: Dashboard = { - id: 333333, - uuid: uuid(), + id: uuid(), createdAt: '', updatedAt: '', createdBy: '', diff --git a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx index 3c300e143d..17ccc9b921 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx @@ -59,7 +59,7 @@ export function DeleteButton({ onClick: (e) => { e.preventDefault(); e.stopPropagation(); - deleteDashboardMutation.mutateAsync(undefined, { + deleteDashboardMutation.mutate(undefined, { onSuccess: () => { notifications.success({ message: t('dashboard:delete_dashboard_success', { diff --git a/frontend/src/container/ListOfDashboard/dashboardSearchAndFilter.ts b/frontend/src/container/ListOfDashboard/dashboardSearchAndFilter.ts index 9e10994c4e..324bf11fc9 100644 --- a/frontend/src/container/ListOfDashboard/dashboardSearchAndFilter.ts +++ b/frontend/src/container/ListOfDashboard/dashboardSearchAndFilter.ts @@ -14,7 +14,7 @@ export const generateSearchData = ( dashboards.forEach((dashboard) => { dashboardSearchData.push({ - id: dashboard.uuid, + id: dashboard.id, title: dashboard.data.title, description: dashboard.data.description, tags: dashboard.data.tags || [], diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index 1050b9f598..22c321bbce 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -421,7 +421,7 @@ function LogsExplorerViews({ const dashboardEditView = generateExportToDashboardLink({ query, panelType: panelTypeParam, - dashboardId: dashboard.uuid, + dashboardId: dashboard.id, widgetId, }); diff --git a/frontend/src/container/MetricsExplorer/Explorer/Explorer.tsx b/frontend/src/container/MetricsExplorer/Explorer/Explorer.tsx index 958ea7cfc4..1f04fcd9c6 100644 --- a/frontend/src/container/MetricsExplorer/Explorer/Explorer.tsx +++ b/frontend/src/container/MetricsExplorer/Explorer/Explorer.tsx @@ -76,7 +76,7 @@ function Explorer(): JSX.Element { const dashboardEditView = generateExportToDashboardLink({ query: queryToExport || exportDefaultQuery, panelType: PANEL_TYPES.TIME_SERIES, - dashboardId: dashboard?.uuid || '', + dashboardId: dashboard.id, widgetId, }); diff --git a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx index cf7263d04e..287cf1219c 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx @@ -12,7 +12,6 @@ import { Typography, } from 'antd'; import logEvent from 'api/common/logEvent'; -import { SOMETHING_WENT_WRONG } from 'constants/api'; import { QueryParams } from 'constants/query'; import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; @@ -44,11 +43,8 @@ import { FullScreenHandle } from 'react-full-screen'; import { Layout } from 'react-grid-layout'; import { useTranslation } from 'react-i18next'; import { useCopyToClipboard } from 'react-use'; -import { - Dashboard, - DashboardData, - IDashboardVariable, -} from 'types/api/dashboard/getAll'; +import { DashboardData, IDashboardVariable } from 'types/api/dashboard/getAll'; +import { Props } from 'types/api/dashboard/update'; import { ROLES, USER_ROLES } from 'types/roles'; import { ComponentTypes } from 'utils/permission'; import { v4 as uuid } from 'uuid'; @@ -65,10 +61,9 @@ interface DashboardDescriptionProps { export function sanitizeDashboardData( selectedData: DashboardData, -): Omit { +): DashboardData { if (!selectedData?.variables) { - const { uuid, ...rest } = selectedData; - return rest; + return selectedData; } const updatedVariables = Object.entries(selectedData.variables).reduce( @@ -80,9 +75,8 @@ export function sanitizeDashboardData( {} as Record, ); - const { uuid, ...restData } = selectedData; return { - ...restData, + ...selectedData, variables: updatedVariables, }; } @@ -108,7 +102,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element { const selectedData = selectedDashboard ? { ...selectedDashboard.data, - uuid: selectedDashboard.uuid, + uuid: selectedDashboard.id, } : ({} as DashboardData); @@ -162,7 +156,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element { setSelectedRowWidgetId(null); handleToggleDashboardSlider(true); logEvent('Dashboard Detail: Add new panel clicked', { - dashboardId: selectedDashboard?.uuid, + dashboardId: selectedDashboard?.id, dashboardName: selectedDashboard?.data.title, numberOfPanels: selectedDashboard?.data.widgets?.length, }); @@ -178,8 +172,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element { if (!selectedDashboard) { return; } - const updatedDashboard = { - ...selectedDashboard, + const updatedDashboard: Props = { + id: selectedDashboard.id, + data: { ...selectedDashboard.data, title: updatedTitle, @@ -191,13 +186,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element { message: 'Dashboard renamed successfully', }); setIsRenameDashboardOpen(false); - if (updatedDashboard.payload) - setSelectedDashboard(updatedDashboard.payload); + if (updatedDashboard.data) setSelectedDashboard(updatedDashboard.data); }, onError: () => { - notifications.error({ - message: SOMETHING_WENT_WRONG, - }); setIsRenameDashboardOpen(true); }, }); @@ -251,8 +242,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element { } } - const updatedDashboard: Dashboard = { - ...selectedDashboard, + const updatedDashboard: Props = { + id: selectedDashboard.id, + data: { ...selectedDashboard.data, layout: [ @@ -279,28 +271,21 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element { }, ], }, - uuid: selectedDashboard.uuid, }; updateDashboardMutation.mutate(updatedDashboard, { // eslint-disable-next-line sonarjs/no-identical-functions onSuccess: (updatedDashboard) => { - if (updatedDashboard.payload) { - if (updatedDashboard.payload.data.layout) - setLayouts(sortLayout(updatedDashboard.payload.data.layout)); - setSelectedDashboard(updatedDashboard.payload); - setPanelMap(updatedDashboard.payload?.data?.panelMap || {}); + if (updatedDashboard.data) { + if (updatedDashboard.data.data.layout) + setLayouts(sortLayout(updatedDashboard.data.data.layout)); + setSelectedDashboard(updatedDashboard.data); + setPanelMap(updatedDashboard.data?.data?.panelMap || {}); } setIsPanelNameModalOpen(false); 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 { diff --git a/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx index 066b60074e..3bb907ff7f 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx @@ -1,10 +1,8 @@ import './GeneralSettings.styles.scss'; import { Col, Input, Select, Space, Typography } from 'antd'; -import { SOMETHING_WENT_WRONG } from 'constants/api'; import AddTags from 'container/NewDashboard/DashboardSettings/General/AddTags'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; -import { useNotifications } from 'hooks/useNotifications'; import { isEqual } from 'lodash-es'; import { Check, X } from 'lucide-react'; import { useDashboard } from 'providers/Dashboard/Dashboard'; @@ -38,14 +36,12 @@ function GeneralDashboardSettings(): JSX.Element { const { t } = useTranslation('common'); - const { notifications } = useNotifications(); - const onSaveHandler = (): void => { if (!selectedDashboard) return; - updateDashboardMutation.mutateAsync( + updateDashboardMutation.mutate( { - ...selectedDashboard, + id: selectedDashboard.id, data: { ...selectedDashboard.data, description: updatedDescription, @@ -56,15 +52,11 @@ function GeneralDashboardSettings(): JSX.Element { }, { onSuccess: (updatedDashboard) => { - if (updatedDashboard.payload) { - setSelectedDashboard(updatedDashboard.payload); + if (updatedDashboard.data) { + setSelectedDashboard(updatedDashboard.data); } }, - onError: () => { - notifications.error({ - message: SOMETHING_WENT_WRONG, - }); - }, + onError: () => {}, }, ); }; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx index 70ffcb28a2..19c79868d3 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx @@ -171,7 +171,8 @@ function VariablesSetting({ updateMutation.mutateAsync( { - ...selectedDashboard, + id: selectedDashboard.id, + data: { ...selectedDashboard.data, variables: updatedVariablesData, @@ -179,18 +180,13 @@ function VariablesSetting({ }, { onSuccess: (updatedDashboard) => { - if (updatedDashboard.payload) { - setSelectedDashboard(updatedDashboard.payload); + if (updatedDashboard.data) { + setSelectedDashboard(updatedDashboard.data); notifications.success({ message: t('variable_updated_successfully'), }); } }, - onError: () => { - notifications.error({ - message: t('error_while_updating_variable'), - }); - }, }, ); }; diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx index ba0840649d..c52a423b90 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx @@ -127,7 +127,7 @@ function QuerySection({ panelType: selectedWidget.panelTypes, queryType: currentQuery.queryType, widgetId: selectedWidget.id, - dashboardId: selectedDashboard?.uuid, + dashboardId: selectedDashboard?.id, dashboardName: selectedDashboard?.data.title, isNewPanel, }); diff --git a/frontend/src/container/NewWidget/index.tsx b/frontend/src/container/NewWidget/index.tsx index 8c384af6e5..9b1e33692d 100644 --- a/frontend/src/container/NewWidget/index.tsx +++ b/frontend/src/container/NewWidget/index.tsx @@ -17,7 +17,6 @@ import { DEFAULT_BUCKET_COUNT } from 'container/PanelWrapper/constants'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; -import useAxiosError from 'hooks/useAxiosError'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; @@ -41,10 +40,10 @@ import { AppState } from 'store/reducers'; import { SuccessResponse } from 'types/api'; import { ColumnUnit, - Dashboard, LegendPosition, Widgets, } from 'types/api/dashboard/getAll'; +import { Props } from 'types/api/dashboard/update'; import { IField } from 'types/api/logs/fields'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { EQueryType } from 'types/common/dashboard'; @@ -141,7 +140,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { if (!logEventCalledRef.current) { logEvent('Panel Edit: Page visited', { panelType: selectedWidget?.panelTypes, - dashboardId: selectedDashboard?.uuid, + dashboardId: selectedDashboard?.id, widgetId: selectedWidget?.id, dashboardName: selectedDashboard?.data.title, isNewPanel: !!isWidgetNotPresent, @@ -345,8 +344,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { return { selectedWidget, preWidgets, afterWidgets }; }, [selectedDashboard, query]); - const handleError = useAxiosError(); - // 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 const [isLoadingPanelData, setIsLoadingPanelData] = useState(false); @@ -470,9 +467,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { updatedLayout = newLayoutItem; } - const dashboard: Dashboard = { - ...selectedDashboard, - uuid: selectedDashboard.uuid, + const dashboard: Props = { + id: selectedDashboard.id, + data: { ...selectedDashboard.data, widgets: isNewDashboard @@ -540,15 +537,14 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { }; updateDashboardMutation.mutateAsync(dashboard, { - onSuccess: () => { + onSuccess: (updatedDashboard) => { setSelectedRowWidgetId(null); - setSelectedDashboard(dashboard); + setSelectedDashboard(updatedDashboard.data); setToScrollWidgetId(selectedWidget?.id || ''); safeNavigate({ pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }), }); }, - onError: handleError, }); }, [ selectedDashboard, @@ -562,7 +558,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { currentQuery, preWidgets, updateDashboardMutation, - handleError, widgets, setSelectedDashboard, setToScrollWidgetId, @@ -601,7 +596,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { logEvent('Panel Edit: Save changes', { panelType: selectedWidget.panelTypes, - dashboardId: selectedDashboard?.uuid, + dashboardId: selectedDashboard?.id, widgetId: selectedWidget.id, dashboardName: selectedDashboard?.data.title, queryType: currentQuery.queryType, diff --git a/frontend/src/hooks/dashboard/useDeleteDashboard.tsx b/frontend/src/hooks/dashboard/useDeleteDashboard.tsx index 35a3c27fce..0ebda0fd39 100644 --- a/frontend/src/hooks/dashboard/useDeleteDashboard.tsx +++ b/frontend/src/hooks/dashboard/useDeleteDashboard.tsx @@ -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 { useErrorModal } from 'providers/ErrorModalProvider'; 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 = ( id: string, -): UseMutationResult => - useMutation({ +): UseMutationResult, APIError, void, unknown> => { + const { showErrorModal } = useErrorModal(); + + return useMutation, APIError>({ mutationKey: REACT_QUERY_KEY.DELETE_DASHBOARD, mutationFn: () => deleteDashboard({ - uuid: id, + id, }), + onError: (error: APIError) => { + showErrorModal(error); + }, }); +}; diff --git a/frontend/src/hooks/dashboard/useGetAllDashboard.tsx b/frontend/src/hooks/dashboard/useGetAllDashboard.tsx index 91fea33517..fedb2b1de0 100644 --- a/frontend/src/hooks/dashboard/useGetAllDashboard.tsx +++ b/frontend/src/hooks/dashboard/useGetAllDashboard.tsx @@ -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 { useErrorModal } from 'providers/ErrorModalProvider'; import { useQuery, UseQueryResult } from 'react-query'; +import { SuccessResponseV2 } from 'types/api'; import { Dashboard } from 'types/api/dashboard/getAll'; +import APIError from 'types/api/error'; -export const useGetAllDashboard = (): UseQueryResult => - useQuery({ - queryFn: getAllDashboardList, +export const useGetAllDashboard = (): UseQueryResult< + SuccessResponseV2, + APIError +> => { + const { showErrorModal } = useErrorModal(); + + return useQuery, APIError>({ + queryFn: getAll, + onError: (error) => { + showErrorModal(error); + }, queryKey: REACT_QUERY_KEY.GET_ALL_DASHBOARDS, }); +}; diff --git a/frontend/src/hooks/dashboard/useUpdateDashboard.tsx b/frontend/src/hooks/dashboard/useUpdateDashboard.tsx index e8dbad4ae6..fd20c90e7e 100644 --- a/frontend/src/hooks/dashboard/useUpdateDashboard.tsx +++ b/frontend/src/hooks/dashboard/useUpdateDashboard.tsx @@ -1,25 +1,31 @@ -import update from 'api/dashboard/update'; +import update from 'api/v1/dashboards/id/update'; import dayjs from 'dayjs'; import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { useErrorModal } from 'providers/ErrorModalProvider'; 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 { Props } from 'types/api/dashboard/update'; +import APIError from 'types/api/error'; export const useUpdateDashboard = (): UseUpdateDashboard => { const { updatedTimeRef } = useDashboard(); + const { showErrorModal } = useErrorModal(); return useMutation(update, { onSuccess: (data) => { - if (data.payload) { - updatedTimeRef.current = dayjs(data.payload.updatedAt); + if (data.data) { + updatedTimeRef.current = dayjs(data.data.updatedAt); } }, + onError: (error) => { + showErrorModal(error); + }, }); }; type UseUpdateDashboard = UseMutationResult< - SuccessResponse | ErrorResponse, - unknown, + SuccessResponseV2, + APIError, Props, unknown >; diff --git a/frontend/src/hooks/queryBuilder/useCreateAlerts.tsx b/frontend/src/hooks/queryBuilder/useCreateAlerts.tsx index c1a87bfc46..d85586ddfa 100644 --- a/frontend/src/hooks/queryBuilder/useCreateAlerts.tsx +++ b/frontend/src/hooks/queryBuilder/useCreateAlerts.tsx @@ -38,7 +38,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => { logEvent('Panel Edit: Create alert', { panelType: widget.panelTypes, dashboardName: selectedDashboard?.data?.title, - dashboardId: selectedDashboard?.uuid, + dashboardId: selectedDashboard?.id, widgetId: widget.id, queryType: widget.query.queryType, }); @@ -47,7 +47,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => { action: MenuItemKeys.CreateAlerts, panelType: widget.panelTypes, dashboardName: selectedDashboard?.data?.title, - dashboardId: selectedDashboard?.uuid, + dashboardId: selectedDashboard?.id, widgetId: widget.id, queryType: widget.query.queryType, }); diff --git a/frontend/src/mocks-server/__mockdata__/dashboards.ts b/frontend/src/mocks-server/__mockdata__/dashboards.ts index 8987d0d8a2..1db096a6a4 100644 --- a/frontend/src/mocks-server/__mockdata__/dashboards.ts +++ b/frontend/src/mocks-server/__mockdata__/dashboards.ts @@ -3,8 +3,7 @@ export const dashboardSuccessResponse = { status: 'success', data: [ { - id: 1, - uuid: '1', + id: '1', createdAt: '2022-11-16T13:29:47.064874419Z', createdBy: null, updatedAt: '2024-05-21T06:41:30.546630961Z', @@ -23,8 +22,7 @@ export const dashboardSuccessResponse = { }, }, { - id: 2, - uuid: '2', + id: '2', createdAt: '2022-11-16T13:20:47.064874419Z', createdBy: null, updatedAt: '2024-05-21T06:42:30.546630961Z', @@ -53,8 +51,7 @@ export const dashboardEmptyState = { export const getDashboardById = { status: 'success', data: { - id: 1, - uuid: '1', + id: '1', createdAt: '2022-11-16T13:29:47.064874419Z', createdBy: 'integration', updatedAt: '2024-05-21T06:41:30.546630961Z', @@ -78,8 +75,7 @@ export const getDashboardById = { export const getNonIntegrationDashboardById = { status: 'success', data: { - id: 1, - uuid: '1', + id: '1', createdAt: '2022-11-16T13:29:47.064874419Z', createdBy: 'thor', updatedAt: '2024-05-21T06:41:30.546630961Z', diff --git a/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx b/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx index e6d6490cfc..0b92d2f0f8 100644 --- a/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx +++ b/frontend/src/pages/DashboardsListPage/__tests__/DashboardListPage.test.tsx @@ -234,7 +234,6 @@ describe('dashboard list page', () => { const firstDashboardData = dashboardSuccessResponse.data[0]; expect(dashboardUtils.sanitizeDashboardData).toHaveBeenCalledWith( expect.objectContaining({ - id: firstDashboardData.uuid, title: firstDashboardData.data.title, createdAt: firstDashboardData.createdAt, }), diff --git a/frontend/src/pages/NewDashboard/DashboardPage.tsx b/frontend/src/pages/NewDashboard/DashboardPage.tsx index befefa88eb..05f8fe168d 100644 --- a/frontend/src/pages/NewDashboard/DashboardPage.tsx +++ b/frontend/src/pages/NewDashboard/DashboardPage.tsx @@ -19,9 +19,9 @@ function DashboardPage(): JSX.Element { : 'Something went wrong'; useEffect(() => { - const dashboardTitle = dashboardResponse.data?.data.title; + const dashboardTitle = dashboardResponse.data?.data.data.title; document.title = dashboardTitle || document.title; - }, [dashboardResponse.data?.data.title, isFetching]); + }, [dashboardResponse.data?.data.data.title, isFetching]); if (isError && !isFetching && errorMessage === ErrorType.NotFound) { return ; diff --git a/frontend/src/pages/TracesExplorer/index.tsx b/frontend/src/pages/TracesExplorer/index.tsx index 2042d1dad8..28324a0baf 100644 --- a/frontend/src/pages/TracesExplorer/index.tsx +++ b/frontend/src/pages/TracesExplorer/index.tsx @@ -154,7 +154,7 @@ function TracesExplorer(): JSX.Element { const dashboardEditView = generateExportToDashboardLink({ query, panelType: panelTypeParam, - dashboardId: dashboard?.uuid || '', + dashboardId: dashboard.id, widgetId, }); diff --git a/frontend/src/providers/Dashboard/Dashboard.tsx b/frontend/src/providers/Dashboard/Dashboard.tsx index 4fa7c3be4b..745eabcc25 100644 --- a/frontend/src/providers/Dashboard/Dashboard.tsx +++ b/frontend/src/providers/Dashboard/Dashboard.tsx @@ -1,14 +1,12 @@ /* eslint-disable no-nested-ternary */ import { Modal } from 'antd'; -import getDashboard from 'api/dashboard/get'; -import lockDashboardApi from 'api/dashboard/lockDashboard'; -import unlockDashboardApi from 'api/dashboard/unlockDashboard'; +import getDashboard from 'api/v1/dashboards/id/get'; +import locked from 'api/v1/dashboards/id/lock'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import ROUTES from 'constants/routes'; import { getMinMax } from 'container/TopNav/AutoRefresh/config'; import dayjs, { Dayjs } from 'dayjs'; import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage'; -import useAxiosError from 'hooks/useAxiosError'; import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useTabVisibility from 'hooks/useTabFocus'; import useUrlQuery from 'hooks/useUrlQuery'; @@ -18,6 +16,7 @@ import isEqual from 'lodash-es/isEqual'; import isUndefined from 'lodash-es/isUndefined'; import omitBy from 'lodash-es/omitBy'; import { useAppContext } from 'providers/App/App'; +import { useErrorModal } from 'providers/ErrorModalProvider'; import { createContext, PropsWithChildren, @@ -36,7 +35,9 @@ import { Dispatch } from 'redux'; import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime'; +import { SuccessResponseV2 } from 'types/api'; import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll'; +import APIError from 'types/api/error'; import { GlobalReducer } from 'types/reducer/globalTime'; import { v4 as generateUUID } from 'uuid'; @@ -52,7 +53,10 @@ const DashboardContext = createContext({ isDashboardLocked: false, handleToggleDashboardSlider: () => {}, handleDashboardLockToggle: () => {}, - dashboardResponse: {} as UseQueryResult, + dashboardResponse: {} as UseQueryResult< + SuccessResponseV2, + APIError + >, selectedDashboard: {} as Dashboard, dashboardId: '', layouts: [], @@ -116,6 +120,8 @@ export function DashboardProvider({ exact: true, }); + const { showErrorModal } = useErrorModal(); + // added extra checks here in case wrong values appear use the default values rather than empty dashboards const supportedOrderColumnKeys = ['createdAt', 'updatedAt']; @@ -270,18 +276,24 @@ export function DashboardProvider({ setIsDashboardFetching(true); try { return await getDashboard({ - uuid: dashboardId, + id: dashboardId, }); + } catch (error) { + showErrorModal(error as APIError); + return; } finally { setIsDashboardFetching(false); } }, refetchOnWindowFocus: false, - onSuccess: (data) => { - const updatedDashboardData = transformDashboardVariables(data); + onError: (error) => { + showErrorModal(error as APIError); + }, + onSuccess: (data: SuccessResponseV2) => { + const updatedDashboardData = transformDashboardVariables(data?.data); const updatedDate = dayjs(updatedDashboardData.updatedAt); - setIsDashboardLocked(updatedDashboardData?.isLocked || false); + setIsDashboardLocked(updatedDashboardData?.locked || false); // on first render if (updatedTimeRef.current === null) { @@ -387,29 +399,25 @@ export function DashboardProvider({ setIsDashboardSlider(value); }; - const handleError = useAxiosError(); - - const { mutate: lockDashboard } = useMutation(lockDashboardApi, { - onSuccess: () => { + const { mutate: lockDashboard } = useMutation(locked, { + onSuccess: (_, props) => { setIsDashboardSlider(false); - setIsDashboardLocked(true); + setIsDashboardLocked(props.lock); }, - onError: handleError, - }); - - const { mutate: unlockDashboard } = useMutation(unlockDashboardApi, { - onSuccess: () => { - setIsDashboardLocked(false); + onError: (error) => { + showErrorModal(error as APIError); }, - onError: handleError, }); const handleDashboardLockToggle = async (value: boolean): Promise => { if (selectedDashboard) { - if (value) { - lockDashboard(selectedDashboard); - } else { - unlockDashboard(selectedDashboard); + try { + await lockDashboard({ + id: selectedDashboard.id, + lock: value, + }); + } catch (error) { + showErrorModal(error as APIError); } } }; diff --git a/frontend/src/providers/Dashboard/types.ts b/frontend/src/providers/Dashboard/types.ts index 13ab53f8e8..e7ee7d9739 100644 --- a/frontend/src/providers/Dashboard/types.ts +++ b/frontend/src/providers/Dashboard/types.ts @@ -1,6 +1,7 @@ import dayjs from 'dayjs'; import { Layout } from 'react-grid-layout'; import { UseQueryResult } from 'react-query'; +import { SuccessResponseV2 } from 'types/api'; import { Dashboard } from 'types/api/dashboard/getAll'; export interface DashboardSortOrder { @@ -19,7 +20,7 @@ export interface IDashboardContext { isDashboardLocked: boolean; handleToggleDashboardSlider: (value: boolean) => void; handleDashboardLockToggle: (value: boolean) => void; - dashboardResponse: UseQueryResult; + dashboardResponse: UseQueryResult, unknown>; selectedDashboard: Dashboard | undefined; dashboardId: string; layouts: Layout[]; diff --git a/frontend/src/types/api/dashboard/create.ts b/frontend/src/types/api/dashboard/create.ts index b553ecd17b..1db418814d 100644 --- a/frontend/src/types/api/dashboard/create.ts +++ b/frontend/src/types/api/dashboard/create.ts @@ -1,11 +1,12 @@ -import { Dashboard, DashboardData } from './getAll'; +import { Dashboard } from './getAll'; -export type Props = - | { - title: Dashboard['data']['title']; - uploadedGrafana: boolean; - version?: string; - } - | { DashboardData: DashboardData; uploadedGrafana: boolean }; +export type Props = { + title: Dashboard['data']['title']; + uploadedGrafana: boolean; + version?: string; +}; -export type PayloadProps = Dashboard; +export interface PayloadProps { + data: Dashboard; + status: string; +} diff --git a/frontend/src/types/api/dashboard/delete.ts b/frontend/src/types/api/dashboard/delete.ts index 9bdc519b47..afad4087a5 100644 --- a/frontend/src/types/api/dashboard/delete.ts +++ b/frontend/src/types/api/dashboard/delete.ts @@ -1,9 +1,10 @@ import { Dashboard } from './getAll'; export type Props = { - uuid: Dashboard['uuid']; + id: Dashboard['id']; }; export interface PayloadProps { - status: 'success'; + status: string; + data: null; } diff --git a/frontend/src/types/api/dashboard/get.ts b/frontend/src/types/api/dashboard/get.ts index 2c440c2f02..37a7381021 100644 --- a/frontend/src/types/api/dashboard/get.ts +++ b/frontend/src/types/api/dashboard/get.ts @@ -1,7 +1,10 @@ import { Dashboard } from './getAll'; export type Props = { - uuid: Dashboard['uuid']; + id: Dashboard['id']; }; -export type PayloadProps = Dashboard; +export interface PayloadProps { + data: Dashboard; + status: string; +} diff --git a/frontend/src/types/api/dashboard/getAll.ts b/frontend/src/types/api/dashboard/getAll.ts index 2e6d883287..2115b94e82 100644 --- a/frontend/src/types/api/dashboard/getAll.ts +++ b/frontend/src/types/api/dashboard/getAll.ts @@ -9,8 +9,6 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { IField } from '../logs/fields'; import { BaseAutocompleteData } from '../queryBuilder/queryAutocompleteResponse'; -export type PayloadProps = Dashboard[]; - export const VariableQueryTypeArr = ['QUERY', 'TEXTBOX', 'CUSTOM'] as const; export type TVariableQueryType = typeof VariableQueryTypeArr[number]; @@ -50,14 +48,18 @@ export interface IDashboardVariable { change?: boolean; } export interface Dashboard { - id: number; - uuid: string; + id: string; createdAt: string; updatedAt: string; createdBy: string; updatedBy: string; data: DashboardData; - isLocked?: boolean; + locked?: boolean; +} + +export interface PayloadProps { + data: Dashboard[]; + status: string; } export interface DashboardTemplate { @@ -69,7 +71,7 @@ export interface DashboardTemplate { } export interface DashboardData { - uuid?: string; + // uuid?: string; description?: string; tags?: string[]; name?: string; diff --git a/frontend/src/types/api/dashboard/lockUnlock.ts b/frontend/src/types/api/dashboard/lockUnlock.ts new file mode 100644 index 0000000000..06374c3aa7 --- /dev/null +++ b/frontend/src/types/api/dashboard/lockUnlock.ts @@ -0,0 +1,11 @@ +import { Dashboard } from './getAll'; + +export type Props = { + id: Dashboard['id']; + lock: boolean; +}; + +export interface PayloadProps { + data: null; + status: string; +} diff --git a/frontend/src/types/api/dashboard/update.ts b/frontend/src/types/api/dashboard/update.ts index 3839a84c88..38a481a876 100644 --- a/frontend/src/types/api/dashboard/update.ts +++ b/frontend/src/types/api/dashboard/update.ts @@ -1,8 +1,11 @@ import { Dashboard, DashboardData } from './getAll'; export type Props = { - uuid: Dashboard['uuid']; + id: Dashboard['id']; data: DashboardData; }; -export type PayloadProps = Dashboard; +export interface PayloadProps { + data: Dashboard; + status: string; +} diff --git a/go.mod b/go.mod index 6af7b55b7e..bbe8dc9ecd 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,6 @@ require ( github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.0 - github.com/gosimple/slug v1.10.0 github.com/huandu/go-sqlbuilder v1.35.0 github.com/jackc/pgx/v5 v5.7.2 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/gax-go/v2 v2.14.0 // 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/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect diff --git a/go.sum b/go.sum index 03f23b73ff..0dfc703d7d 100644 --- a/go.sum +++ b/go.sum @@ -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/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 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/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= diff --git a/pkg/modules/dashboard/dashboard.go b/pkg/modules/dashboard/dashboard.go index 9463ddfea9..d8ee841f80 100644 --- a/pkg/modules/dashboard/dashboard.go +++ b/pkg/modules/dashboard/dashboard.go @@ -4,25 +4,32 @@ import ( "context" "net/http" - "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/types/dashboardtypes" + "github.com/SigNoz/signoz/pkg/valuer" ) 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 { + Create(http.ResponseWriter, *http.Request) + + Update(http.ResponseWriter, *http.Request) + + LockUnlock(http.ResponseWriter, *http.Request) + Delete(http.ResponseWriter, *http.Request) } diff --git a/pkg/modules/dashboard/impldashboard/handler.go b/pkg/modules/dashboard/impldashboard/handler.go index b5ab1ada09..449cc228ea 100644 --- a/pkg/modules/dashboard/impldashboard/handler.go +++ b/pkg/modules/dashboard/impldashboard/handler.go @@ -2,12 +2,16 @@ package impldashboard import ( "context" + "encoding/json" "net/http" "time" + "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/http/render" "github.com/SigNoz/signoz/pkg/modules/dashboard" "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/types/dashboardtypes" + "github.com/SigNoz/signoz/pkg/valuer" "github.com/gorilla/mux" ) @@ -19,8 +23,8 @@ func NewHandler(module dashboard.Module) dashboard.Handler { return &handler{module: module} } -func (handler *handler) Delete(rw http.ResponseWriter, req *http.Request) { - ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second) +func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() claims, err := authtypes.ClaimsFromContext(ctx) @@ -29,13 +33,151 @@ func (handler *handler) Delete(rw http.ResponseWriter, req *http.Request) { 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 { render.Error(rw, err) return } 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) } diff --git a/pkg/modules/dashboard/impldashboard/module.go b/pkg/modules/dashboard/impldashboard/module.go index 174ddcd096..83ed50a6b3 100644 --- a/pkg/modules/dashboard/impldashboard/module.go +++ b/pkg/modules/dashboard/impldashboard/module.go @@ -2,164 +2,40 @@ package impldashboard import ( "context" - "encoding/json" "strings" - "time" "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/modules/dashboard" "github.com/SigNoz/signoz/pkg/sqlstore" - "github.com/SigNoz/signoz/pkg/types" - "github.com/google/uuid" + "github.com/SigNoz/signoz/pkg/types/dashboardtypes" + "github.com/SigNoz/signoz/pkg/valuer" ) 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{ - sqlstore: sqlstore, + store: NewStore(sqlstore), + settings: scopedProviderSettings, } } -// CreateDashboard creates a new dashboard -func (module *module) Create(ctx context.Context, orgID string, email string, data map[string]interface{}) (*types.Dashboard, error) { - 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) +func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy string, postableDashboard dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error) { + dashboard, err := dashboardtypes.NewDashboard(orgID, createdBy, postableDashboard) if err != nil { return nil, err } - return dashboards, nil -} - -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) + storableDashboard, err := dashboardtypes.NewStorableDashboardFromDashboard(dashboard) if err != nil { return nil, err } - - 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) + err = module.store.Create(ctx, storableDashboard) if err != nil { return nil, err } @@ -167,28 +43,73 @@ func (module *module) Update(ctx context.Context, orgID, userEmail, uuid string, return dashboard, nil } -func (module *module) LockUnlock(ctx context.Context, orgID, uuid string, lock bool) error { - dashboard, err := module.Get(ctx, orgID, uuid) +func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) { + 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 { return err } - var lockValue int - if lock { - lockValue = 1 - } else { - lockValue = 0 + err = dashboard.LockUnlock(ctx, lock, updatedBy) + if err != nil { + return err + } + storableDashboard, err := dashboardtypes.NewStorableDashboardFromDashboard(dashboard) + if err != nil { + return err } - _, err = module. - sqlstore. - BunDB(). - NewUpdate(). - Model(dashboard). - Set("locked = ?", lockValue). - Where("org_id = ?", orgID). - Where("uuid = ?", uuid). - Exec(ctx) + err = module.store.Update(ctx, orgID, storableDashboard) if err != nil { return err } @@ -196,15 +117,21 @@ func (module *module) LockUnlock(ctx context.Context, orgID, uuid string, lock b return nil } -func (module *module) GetByMetricNames(ctx context.Context, orgID string, metricNames []string) (map[string][]map[string]string, error) { - dashboards := []types.Dashboard{} - err := module. - sqlstore. - BunDB(). - NewSelect(). - Model(&dashboards). - Where("org_id = ?", orgID). - Scan(ctx) +func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error { + dashboard, err := module.Get(ctx, orgID, id) + if err != nil { + return err + } + + if dashboard.Locked { + return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be delete it") + } + + 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 { return nil, err } @@ -266,7 +193,7 @@ func (module *module) GetByMetricNames(ctx context.Context, orgID string, metric for _, metricName := range metricNames { if strings.TrimSpace(key) == metricName { result[metricName] = append(result[metricName], map[string]string{ - "dashboard_id": dashboard.UUID, + "dashboard_id": dashboard.ID, "widget_name": widgetTitle, "widget_id": widgetID, "dashboard_name": dashTitle, @@ -280,52 +207,3 @@ func (module *module) GetByMetricNames(ctx context.Context, orgID string, metric 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 -} diff --git a/pkg/modules/dashboard/impldashboard/store.go b/pkg/modules/dashboard/impldashboard/store.go new file mode 100644 index 0000000000..1b449d0cc7 --- /dev/null +++ b/pkg/modules/dashboard/impldashboard/store.go @@ -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 +} diff --git a/pkg/modules/organization/implorganization/handler.go b/pkg/modules/organization/implorganization/handler.go index ce15529932..ab3efc7fe9 100644 --- a/pkg/modules/organization/implorganization/handler.go +++ b/pkg/modules/organization/implorganization/handler.go @@ -68,6 +68,7 @@ func (handler *handler) Update(rw http.ResponseWriter, r *http.Request) { err = json.NewDecoder(r.Body).Decode(&req) if err != nil { render.Error(rw, err) + return } req.ID = orgID diff --git a/pkg/query-service/app/cloudintegrations/controller.go b/pkg/query-service/app/cloudintegrations/controller.go index 8ca790d05c..e617d6e7db 100644 --- a/pkg/query-service/app/cloudintegrations/controller.go +++ b/pkg/query-service/app/cloudintegrations/controller.go @@ -13,6 +13,8 @@ import ( "github.com/SigNoz/signoz/pkg/query-service/model" "github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/types/dashboardtypes" + "github.com/SigNoz/signoz/pkg/valuer" "golang.org/x/exp/maps" ) @@ -32,9 +34,7 @@ type Controller struct { serviceConfigRepo ServiceConfigDatabase } -func NewController(sqlStore sqlstore.SQLStore) ( - *Controller, error, -) { +func NewController(sqlStore sqlstore.SQLStore) (*Controller, error) { accountsRepo, err := newCloudProviderAccountsRepository(sqlStore) if err != nil { 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"` } -func (c *Controller) ListConnectedAccounts( - ctx context.Context, orgId string, cloudProvider string, -) ( +func (c *Controller) ListConnectedAccounts(ctx context.Context, orgId string, cloudProvider string) ( *ConnectedAccountsListResponse, *model.ApiError, ) { if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil { @@ -103,9 +101,7 @@ type GenerateConnectionUrlResponse struct { ConnectionUrl string `json:"connection_url"` } -func (c *Controller) GenerateConnectionUrl( - ctx context.Context, orgId string, cloudProvider string, req GenerateConnectionUrlRequest, -) (*GenerateConnectionUrlResponse, *model.ApiError) { +func (c *Controller) GenerateConnectionUrl(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. if cloudProvider != "aws" { return nil, model.BadRequest(fmt.Errorf("unsupported cloud provider: %s", cloudProvider)) @@ -154,9 +150,7 @@ type AccountStatusResponse struct { Status types.AccountStatus `json:"status"` } -func (c *Controller) GetAccountStatus( - ctx context.Context, orgId string, cloudProvider string, accountId string, -) ( +func (c *Controller) GetAccountStatus(ctx context.Context, orgId string, cloudProvider string, accountId string) ( *AccountStatusResponse, *model.ApiError, ) { if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil { @@ -198,9 +192,7 @@ type IntegrationConfigForAgent struct { TelemetryCollectionStrategy *CompiledCollectionStrategy `json:"telemetry,omitempty"` } -func (c *Controller) CheckInAsAgent( - ctx context.Context, orgId string, cloudProvider string, req AgentCheckInRequest, -) (*AgentCheckInResponse, error) { +func (c *Controller) CheckInAsAgent(ctx context.Context, orgId string, cloudProvider string, req AgentCheckInRequest) (*AgentCheckInResponse, error) { if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil { return nil, apiErr } @@ -293,13 +285,7 @@ type UpdateAccountConfigRequest struct { Config types.AccountConfig `json:"config"` } -func (c *Controller) UpdateAccountConfig( - ctx context.Context, - orgId string, - cloudProvider string, - accountId string, - req UpdateAccountConfigRequest, -) (*types.Account, *model.ApiError) { +func (c *Controller) UpdateAccountConfig(ctx context.Context, orgId string, cloudProvider string, accountId string, req UpdateAccountConfigRequest) (*types.Account, *model.ApiError) { if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil { return nil, apiErr } @@ -316,9 +302,7 @@ func (c *Controller) UpdateAccountConfig( return &account, nil } -func (c *Controller) DisconnectAccount( - ctx context.Context, orgId string, cloudProvider string, accountId string, -) (*types.CloudIntegration, *model.ApiError) { +func (c *Controller) DisconnectAccount(ctx context.Context, orgId string, cloudProvider string, accountId string) (*types.CloudIntegration, *model.ApiError) { if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil { return nil, apiErr } @@ -520,10 +504,8 @@ func (c *Controller) UpdateServiceConfig( // All dashboards that are available based on cloud integrations configuration // across all cloud providers -func (c *Controller) AvailableDashboards(ctx context.Context, orgId string) ( - []*types.Dashboard, *model.ApiError, -) { - allDashboards := []*types.Dashboard{} +func (c *Controller) AvailableDashboards(ctx context.Context, orgId valuer.UUID) ([]*dashboardtypes.Dashboard, *model.ApiError) { + allDashboards := []*dashboardtypes.Dashboard{} for _, provider := range []string{"aws"} { providerDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, orgId, provider) @@ -539,11 +521,8 @@ func (c *Controller) AvailableDashboards(ctx context.Context, orgId string) ( return allDashboards, nil } -func (c *Controller) AvailableDashboardsForCloudProvider( - ctx context.Context, orgID string, cloudProvider string, -) ([]*types.Dashboard, *model.ApiError) { - - accountRecords, apiErr := c.accountsRepo.listConnected(ctx, orgID, cloudProvider) +func (c *Controller) AvailableDashboardsForCloudProvider(ctx context.Context, orgID valuer.UUID, cloudProvider string) ([]*dashboardtypes.Dashboard, *model.ApiError) { + accountRecords, apiErr := c.accountsRepo.listConnected(ctx, orgID.StringValue(), cloudProvider) if apiErr != nil { return nil, model.WrapApiError(apiErr, "couldn't list connected cloud accounts") } @@ -554,7 +533,7 @@ func (c *Controller) AvailableDashboardsForCloudProvider( for _, ar := range accountRecords { if ar.AccountID != nil { configsBySvcId, apiErr := c.serviceConfigRepo.getAllForAccount( - ctx, orgID, ar.ID.StringValue(), + ctx, orgID.StringValue(), ar.ID.StringValue(), ) if apiErr != nil { return nil, apiErr @@ -573,16 +552,15 @@ func (c *Controller) AvailableDashboardsForCloudProvider( return nil, apiErr } - svcDashboards := []*types.Dashboard{} + svcDashboards := []*dashboardtypes.Dashboard{} for _, svc := range allServices { serviceDashboardsCreatedAt := servicesWithAvailableMetrics[svc.Id] if serviceDashboardsCreatedAt != nil { for _, d := range svc.Assets.Dashboards { - isLocked := 1 author := fmt.Sprintf("%s-integration", cloudProvider) - svcDashboards = append(svcDashboards, &types.Dashboard{ - UUID: c.dashboardUuid(cloudProvider, svc.Id, d.Id), - Locked: &isLocked, + svcDashboards = append(svcDashboards, &dashboardtypes.Dashboard{ + ID: c.dashboardUuid(cloudProvider, svc.Id, d.Id), + Locked: true, Data: *d.Definition, TimeAuditable: types.TimeAuditable{ CreatedAt: *serviceDashboardsCreatedAt, @@ -592,6 +570,7 @@ func (c *Controller) AvailableDashboardsForCloudProvider( CreatedBy: author, UpdatedBy: author, }, + OrgID: orgID, }) } servicesWithAvailableMetrics[svc.Id] = nil @@ -600,11 +579,7 @@ func (c *Controller) AvailableDashboardsForCloudProvider( return svcDashboards, nil } -func (c *Controller) GetDashboardById( - ctx context.Context, - orgId string, - dashboardUuid string, -) (*types.Dashboard, *model.ApiError) { +func (c *Controller) GetDashboardById(ctx context.Context, orgId valuer.UUID, dashboardUuid string) (*dashboardtypes.Dashboard, *model.ApiError) { cloudProvider, _, _, apiErr := c.parseDashboardUuid(dashboardUuid) if apiErr != nil { return nil, apiErr @@ -612,38 +587,28 @@ func (c *Controller) GetDashboardById( allDashboards, apiErr := c.AvailableDashboardsForCloudProvider(ctx, orgId, cloudProvider) if apiErr != nil { - return nil, model.WrapApiError( - apiErr, fmt.Sprintf("couldn't list available dashboards"), - ) + return nil, model.WrapApiError(apiErr, "couldn't list available dashboards") } for _, d := range allDashboards { - if d.UUID == dashboardUuid { + if d.ID == dashboardUuid { return d, nil } } - return nil, model.NotFoundError(fmt.Errorf( - "couldn't find dashboard with uuid: %s", dashboardUuid, - )) + return nil, model.NotFoundError(fmt.Errorf("couldn't find dashboard with uuid: %s", dashboardUuid)) } func (c *Controller) dashboardUuid( cloudProvider string, svcId string, dashboardId string, ) string { - return fmt.Sprintf( - "cloud-integration--%s--%s--%s", cloudProvider, svcId, dashboardId, - ) + return fmt.Sprintf("cloud-integration--%s--%s--%s", cloudProvider, svcId, dashboardId) } -func (c *Controller) parseDashboardUuid(dashboardUuid string) ( - cloudProvider string, svcId string, dashboardId string, apiErr *model.ApiError, -) { +func (c *Controller) parseDashboardUuid(dashboardUuid string) (cloudProvider string, svcId string, dashboardId string, apiErr *model.ApiError) { parts := strings.SplitN(dashboardUuid, "--", 4) if len(parts) != 4 || parts[0] != "cloud-integration" { - return "", "", "", model.BadRequest(fmt.Errorf( - "invalid cloud integration dashboard id", - )) + return "", "", "", model.BadRequest(fmt.Errorf("invalid cloud integration dashboard id")) } return parts[1], parts[2], parts[3], nil diff --git a/pkg/query-service/app/cloudintegrations/services/models.go b/pkg/query-service/app/cloudintegrations/services/models.go index 138470d5d7..09fe861e4e 100644 --- a/pkg/query-service/app/cloudintegrations/services/models.go +++ b/pkg/query-service/app/cloudintegrations/services/models.go @@ -1,7 +1,7 @@ package services import ( - "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/types/dashboardtypes" ) type Metadata struct { @@ -82,10 +82,10 @@ type AWSLogsStrategy struct { } type Dashboard struct { - Id string `json:"id"` - Url string `json:"url"` - Title string `json:"title"` - Description string `json:"description"` - Image string `json:"image"` - Definition *types.DashboardData `json:"definition,omitempty"` + Id string `json:"id"` + Url string `json:"url"` + Title string `json:"title"` + Description string `json:"description"` + Image string `json:"image"` + Definition *dashboardtypes.StorableDashboardData `json:"definition,omitempty"` } diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 3badb6c882..c3e6c7fb32 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -7,7 +7,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/SigNoz/signoz/pkg/query-service/constants" "io" "math" "net/http" @@ -21,6 +20,8 @@ import ( "text/template" "time" + "github.com/SigNoz/signoz/pkg/query-service/constants" + "github.com/SigNoz/signoz/pkg/alertmanager" "github.com/SigNoz/signoz/pkg/apis/fields" 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/types" "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/pipelinetypes" 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.deleteDowntimeSchedule)).Methods(http.MethodDelete) - router.HandleFunc("/api/v1/dashboards", am.ViewAccess(aH.getDashboards)).Methods(http.MethodGet) - router.HandleFunc("/api/v1/dashboards", am.EditAccess(aH.createDashboards)).Methods(http.MethodPost) - router.HandleFunc("/api/v1/dashboards/{uuid}", am.ViewAccess(aH.getDashboard)).Methods(http.MethodGet) - router.HandleFunc("/api/v1/dashboards/{uuid}", am.EditAccess(aH.updateDashboard)).Methods(http.MethodPut) - router.HandleFunc("/api/v1/dashboards/{uuid}", am.EditAccess(aH.Signoz.Handlers.Dashboard.Delete)).Methods(http.MethodDelete) + router.HandleFunc("/api/v1/dashboards", am.ViewAccess(aH.List)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/dashboards", am.EditAccess(aH.Signoz.Handlers.Dashboard.Create)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/dashboards/{id}", am.ViewAccess(aH.Get)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/dashboards/{id}", am.EditAccess(aH.Signoz.Handlers.Dashboard.Update)).Methods(http.MethodPut) + 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/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/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/list", am.ViewAccess(aH.getServicesList)).Methods(http.MethodGet) 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) } -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) { var postData *model.DashboardVars @@ -1224,6 +1155,114 @@ func prepareQuery(r *http.Request) (string, error) { 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) { query, err := prepareQuery(r) if err != nil { @@ -1239,121 +1278,6 @@ func (aH *APIHandler) queryDashboardVarsV2(w http.ResponseWriter, r *http.Reques 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) { claims, err := authtypes.ClaimsFromContext(r.Context()) if err != nil { diff --git a/pkg/query-service/app/integrations/builtin_integrations/clickhouse/assets/dashboards/overview.json b/pkg/query-service/app/integrations/builtin_integrations/clickhouse/assets/dashboards/overview.json index d938e3b0aa..98b714d021 100644 --- a/pkg/query-service/app/integrations/builtin_integrations/clickhouse/assets/dashboards/overview.json +++ b/pkg/query-service/app/integrations/builtin_integrations/clickhouse/assets/dashboards/overview.json @@ -7741,4 +7741,4 @@ } ], "uuid": "e74aeb83-ac4b-4313-8a97-216b62c8fc59" -} \ No newline at end of file +} diff --git a/pkg/query-service/app/integrations/controller.go b/pkg/query-service/app/integrations/controller.go index 97447cca46..aa44ea8cc4 100644 --- a/pkg/query-service/app/integrations/controller.go +++ b/pkg/query-service/app/integrations/controller.go @@ -7,17 +7,16 @@ import ( "github.com/SigNoz/signoz/pkg/query-service/agentConf" "github.com/SigNoz/signoz/pkg/query-service/model" "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/valuer" ) type Controller struct { mgr *Manager } -func NewController(sqlStore sqlstore.SQLStore) ( - *Controller, error, -) { +func NewController(sqlStore sqlstore.SQLStore) (*Controller, error) { mgr, err := NewManager(sqlStore) if err != nil { return nil, fmt.Errorf("couldn't create integrations manager: %w", err) @@ -34,11 +33,7 @@ type IntegrationsListResponse struct { // Pagination details to come later } -func (c *Controller) ListIntegrations( - ctx context.Context, orgId string, params map[string]string, -) ( - *IntegrationsListResponse, *model.ApiError, -) { +func (c *Controller) ListIntegrations(ctx context.Context, orgId string, params map[string]string) (*IntegrationsListResponse, *model.ApiError) { var filters *IntegrationsFilter if isInstalledFilter, exists := params["is_installed"]; exists { isInstalled := !(isInstalledFilter == "false") @@ -57,15 +52,11 @@ func (c *Controller) ListIntegrations( }, nil } -func (c *Controller) GetIntegration( - ctx context.Context, orgId string, integrationId string, -) (*Integration, *model.ApiError) { +func (c *Controller) GetIntegration(ctx context.Context, orgId string, integrationId string) (*Integration, *model.ApiError) { return c.mgr.GetIntegration(ctx, orgId, integrationId) } -func (c *Controller) IsIntegrationInstalled( - ctx context.Context, orgId string, integrationId string, -) (bool, *model.ApiError) { +func (c *Controller) IsIntegrationInstalled(ctx context.Context, orgId string, integrationId string) (bool, *model.ApiError) { installation, apiErr := c.mgr.getInstalledIntegration(ctx, orgId, integrationId) if apiErr != nil { return false, apiErr @@ -74,9 +65,7 @@ func (c *Controller) IsIntegrationInstalled( return isInstalled, nil } -func (c *Controller) GetIntegrationConnectionTests( - ctx context.Context, orgId string, integrationId string, -) (*IntegrationConnectionTests, *model.ApiError) { +func (c *Controller) GetIntegrationConnectionTests(ctx context.Context, orgId string, integrationId string) (*IntegrationConnectionTests, *model.ApiError) { return c.mgr.GetIntegrationConnectionTests(ctx, orgId, integrationId) } @@ -85,9 +74,7 @@ type InstallIntegrationRequest struct { Config map[string]interface{} `json:"config"` } -func (c *Controller) Install( - ctx context.Context, orgId string, req *InstallIntegrationRequest, -) (*IntegrationsListItem, *model.ApiError) { +func (c *Controller) Install(ctx context.Context, orgId string, req *InstallIntegrationRequest) (*IntegrationsListItem, *model.ApiError) { res, apiErr := c.mgr.InstallIntegration( ctx, orgId, req.IntegrationId, req.Config, ) @@ -102,9 +89,7 @@ type UninstallIntegrationRequest struct { IntegrationId string `json:"integration_id"` } -func (c *Controller) Uninstall( - ctx context.Context, orgId string, req *UninstallIntegrationRequest, -) *model.ApiError { +func (c *Controller) Uninstall(ctx context.Context, orgId string, req *UninstallIntegrationRequest) *model.ApiError { if len(req.IntegrationId) < 1 { return model.BadRequest(fmt.Errorf( "integration_id is required", @@ -121,20 +106,18 @@ func (c *Controller) Uninstall( return nil } -func (c *Controller) GetPipelinesForInstalledIntegrations( - ctx context.Context, orgId string, -) ([]pipelinetypes.GettablePipeline, *model.ApiError) { +func (c *Controller) GetPipelinesForInstalledIntegrations(ctx context.Context, orgId string) ([]pipelinetypes.GettablePipeline, *model.ApiError) { return c.mgr.GetPipelinesForInstalledIntegrations(ctx, orgId) } -func (c *Controller) GetDashboardsForInstalledIntegrations( - ctx context.Context, orgId string, -) ([]*types.Dashboard, *model.ApiError) { +func (c *Controller) GetDashboardsForInstalledIntegrations(ctx context.Context, orgId valuer.UUID) ([]*dashboardtypes.Dashboard, *model.ApiError) { return c.mgr.GetDashboardsForInstalledIntegrations(ctx, orgId) } -func (c *Controller) GetInstalledIntegrationDashboardById( - ctx context.Context, orgId string, dashboardUuid string, -) (*types.Dashboard, *model.ApiError) { +func (c *Controller) GetInstalledIntegrationDashboardById(ctx context.Context, orgId valuer.UUID, dashboardUuid string) (*dashboardtypes.Dashboard, *model.ApiError) { return c.mgr.GetInstalledIntegrationDashboardById(ctx, orgId, dashboardUuid) } + +func (c *Controller) IsInstalledIntegrationDashboardID(dashboardUuid string) bool { + return c.mgr.IsInstalledIntegrationDashboardUuid(dashboardUuid) +} diff --git a/pkg/query-service/app/integrations/manager.go b/pkg/query-service/app/integrations/manager.go index c305a4d769..21d7e3ea2d 100644 --- a/pkg/query-service/app/integrations/manager.go +++ b/pkg/query-service/app/integrations/manager.go @@ -10,6 +10,7 @@ import ( "github.com/SigNoz/signoz/pkg/query-service/utils" "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" ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes" "github.com/SigNoz/signoz/pkg/valuer" @@ -31,8 +32,8 @@ type IntegrationSummary struct { } type IntegrationAssets struct { - Logs LogsAssets `json:"logs"` - Dashboards []types.DashboardData `json:"dashboards"` + Logs LogsAssets `json:"logs"` + Dashboards []dashboardtypes.StorableDashboardData `json:"dashboards"` Alerts []ruletypes.PostableRule `json:"alerts"` } @@ -304,17 +305,22 @@ func (m *Manager) parseDashboardUuid(dashboardUuid string) ( return parts[1], parts[2], nil } +func (m *Manager) IsInstalledIntegrationDashboardUuid(dashboardUuid string) bool { + _, _, apiErr := m.parseDashboardUuid(dashboardUuid) + return apiErr == nil +} + func (m *Manager) GetInstalledIntegrationDashboardById( ctx context.Context, - orgId string, + orgId valuer.UUID, dashboardUuid string, -) (*types.Dashboard, *model.ApiError) { +) (*dashboardtypes.Dashboard, *model.ApiError) { integrationId, dashboardId, apiErr := m.parseDashboardUuid(dashboardUuid) if apiErr != nil { return nil, apiErr } - integration, apiErr := m.GetIntegration(ctx, orgId, integrationId) + integration, apiErr := m.GetIntegration(ctx, orgId.StringValue(), integrationId) if apiErr != nil { return nil, apiErr } @@ -328,11 +334,10 @@ func (m *Manager) GetInstalledIntegrationDashboardById( for _, dd := range integration.IntegrationDetails.Assets.Dashboards { if dId, exists := dd["id"]; exists { if id, ok := dId.(string); ok && id == dashboardId { - isLocked := 1 author := "integration" - return &types.Dashboard{ - UUID: m.dashboardUuid(integrationId, string(dashboardId)), - Locked: &isLocked, + return &dashboardtypes.Dashboard{ + ID: m.dashboardUuid(integrationId, string(dashboardId)), + Locked: true, Data: dd, TimeAuditable: types.TimeAuditable{ CreatedAt: integration.Installation.InstalledAt, @@ -342,6 +347,7 @@ func (m *Manager) GetInstalledIntegrationDashboardById( CreatedBy: author, UpdatedBy: author, }, + OrgID: orgId, }, nil } } @@ -354,24 +360,23 @@ func (m *Manager) GetInstalledIntegrationDashboardById( func (m *Manager) GetDashboardsForInstalledIntegrations( ctx context.Context, - orgId string, -) ([]*types.Dashboard, *model.ApiError) { - installedIntegrations, apiErr := m.getInstalledIntegrations(ctx, orgId) + orgId valuer.UUID, +) ([]*dashboardtypes.Dashboard, *model.ApiError) { + installedIntegrations, apiErr := m.getInstalledIntegrations(ctx, orgId.StringValue()) if apiErr != nil { return nil, apiErr } - result := []*types.Dashboard{} + result := []*dashboardtypes.Dashboard{} for _, ii := range installedIntegrations { for _, dd := range ii.Assets.Dashboards { if dId, exists := dd["id"]; exists { if dashboardId, ok := dId.(string); ok { - isLocked := 1 author := "integration" - result = append(result, &types.Dashboard{ - UUID: m.dashboardUuid(ii.IntegrationSummary.Id, dashboardId), - Locked: &isLocked, + result = append(result, &dashboardtypes.Dashboard{ + ID: m.dashboardUuid(ii.IntegrationSummary.Id, dashboardId), + Locked: true, Data: dd, TimeAuditable: types.TimeAuditable{ CreatedAt: ii.Installation.InstalledAt, @@ -381,6 +386,7 @@ func (m *Manager) GetDashboardsForInstalledIntegrations( CreatedBy: author, UpdatedBy: author, }, + OrgID: orgId, }) } } diff --git a/pkg/query-service/app/integrations/test_utils.go b/pkg/query-service/app/integrations/test_utils.go index 9230a63322..27ba204b96 100644 --- a/pkg/query-service/app/integrations/test_utils.go +++ b/pkg/query-service/app/integrations/test_utils.go @@ -12,6 +12,7 @@ import ( "github.com/SigNoz/signoz/pkg/query-service/utils" "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" 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{}, }, ConnectionTests: &IntegrationConnectionTests{ @@ -189,7 +190,7 @@ func (t *TestAvailableIntegrationsRepo) list( }, }, }, - Dashboards: []types.DashboardData{}, + Dashboards: []dashboardtypes.StorableDashboardData{}, Alerts: []ruletypes.PostableRule{}, }, ConnectionTests: &IntegrationConnectionTests{ diff --git a/pkg/query-service/app/metricsexplorer/summary.go b/pkg/query-service/app/metricsexplorer/summary.go index d31548f9cd..2f51013f72 100644 --- a/pkg/query-service/app/metricsexplorer/summary.go +++ b/pkg/query-service/app/metricsexplorer/summary.go @@ -161,7 +161,12 @@ func (receiver *SummaryService) GetMetricsSummary(ctx context.Context, orgID val if errv2 != nil { 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 { return err } @@ -334,7 +339,11 @@ func (receiver *SummaryService) GetRelatedMetrics(ctx context.Context, params *m if errv2 != nil { 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 { return err } diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index 3074328f5e..46cbf3f567 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -86,6 +86,16 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { 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( serverOptions.SigNoz.SQLStore, serverOptions.SigNoz.TelemetryStore, @@ -113,16 +123,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { 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( serverOptions.SigNoz.SQLStore, integrationsController.GetPipelinesForInstalledIntegrations, ) diff --git a/pkg/query-service/telemetry/dashboard.go b/pkg/query-service/telemetry/dashboard.go index 5a29a03440..c48c95e927 100644 --- a/pkg/query-service/telemetry/dashboard.go +++ b/pkg/query-service/telemetry/dashboard.go @@ -8,7 +8,7 @@ import ( "github.com/SigNoz/signoz/pkg/query-service/model" "github.com/SigNoz/signoz/pkg/sqlstore" - "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/types/dashboardtypes" "go.uber.org/zap" ) @@ -16,7 +16,7 @@ import ( func GetDashboardsInfo(ctx context.Context, sqlstore sqlstore.SQLStore) (*model.DashboardsInfo, error) { dashboardsInfo := model.DashboardsInfo{} // fetch dashboards from dashboard db - dashboards := []types.Dashboard{} + dashboards := []dashboardtypes.Dashboard{} err := sqlstore.BunDB().NewSelect().Model(&dashboards).Scan(ctx) if err != nil { zap.L().Error("Error in processing sql query", zap.Error(err)) diff --git a/pkg/query-service/tests/integration/signoz_integrations_test.go b/pkg/query-service/tests/integration/signoz_integrations_test.go index a4aad441c4..b5e83b7d02 100644 --- a/pkg/query-service/tests/integration/signoz_integrations_test.go +++ b/pkg/query-service/tests/integration/signoz_integrations_test.go @@ -29,6 +29,7 @@ import ( "github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/types/dashboardtypes" "github.com/SigNoz/signoz/pkg/types/pipelinetypes" mockhouse "github.com/srikanthccv/ClickHouse-go-mock" "github.com/stretchr/testify/require" @@ -357,7 +358,7 @@ func TestDashboardsForInstalledIntegrationDashboards(t *testing.T) { require.GreaterOrEqual(dashboards[0].UpdatedAt.Unix(), tsBeforeInstallation) // 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.UpdatedAt.Unix(), tsBeforeInstallation) require.Equal(*dd, dashboards[0]) @@ -472,7 +473,7 @@ func (tb *IntegrationsTestBed) RequestQSToUninstallIntegration( 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) 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) } - dashboards := []types.Dashboard{} + dashboards := []dashboardtypes.Dashboard{} err = json.Unmarshal(dataJson, &dashboards) if err != nil { tb.t.Fatalf(" could not unmarshal apiResponse.Data json into dashboards") @@ -489,7 +490,7 @@ func (tb *IntegrationsTestBed) GetDashboardsFromQS() []types.Dashboard { 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) 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) } - dashboard := types.Dashboard{} + dashboard := dashboardtypes.Dashboard{} err = json.Unmarshal(dataJson, &dashboard) if err != nil { tb.t.Fatalf(" could not unmarshal apiResponse.Data json into dashboards") diff --git a/pkg/query-service/utils/testutils.go b/pkg/query-service/utils/testutils.go index c429f44378..ebad37a9ab 100644 --- a/pkg/query-service/utils/testutils.go +++ b/pkg/query-service/utils/testutils.go @@ -68,6 +68,7 @@ func NewTestSqliteDB(t *testing.T) (sqlStore sqlstore.SQLStore, testDBFilePath s sqlmigration.NewMigratePATToFactorAPIKey(sqlStore), sqlmigration.NewUpdateApiMonitoringFiltersFactory(sqlStore), sqlmigration.NewAddKeyOrganizationFactory(sqlStore), + sqlmigration.NewUpdateDashboardFactory(sqlStore), ), ) if err != nil { diff --git a/pkg/signoz/module.go b/pkg/signoz/module.go index 33d9be5983..bda2fcb702 100644 --- a/pkg/signoz/module.go +++ b/pkg/signoz/module.go @@ -54,7 +54,7 @@ func NewModules( Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewDefaultPreferenceMap()), SavedView: implsavedview.NewModule(sqlstore), Apdex: implapdex.NewModule(sqlstore), - Dashboard: impldashboard.NewModule(sqlstore), + Dashboard: impldashboard.NewModule(sqlstore, providerSettings), User: user, QuickFilter: quickfilter, TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)), diff --git a/pkg/signoz/provider.go b/pkg/signoz/provider.go index 6076890333..4d55471cc4 100644 --- a/pkg/signoz/provider.go +++ b/pkg/signoz/provider.go @@ -89,6 +89,7 @@ func NewSQLMigrationProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedM sqlmigration.NewUpdateApiMonitoringFiltersFactory(sqlstore), sqlmigration.NewAddKeyOrganizationFactory(sqlstore), sqlmigration.NewAddTraceFunnelsFactory(sqlstore), + sqlmigration.NewUpdateDashboardFactory(sqlstore), ) } diff --git a/pkg/sqlmigration/038_update_dashboard.go b/pkg/sqlmigration/038_update_dashboard.go new file mode 100644 index 0000000000..c950d6cbfc --- /dev/null +++ b/pkg/sqlmigration/038_update_dashboard.go @@ -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 +} diff --git a/pkg/types/dashboard.go b/pkg/types/dashboard.go deleted file mode 100644 index a18d2df60e..0000000000 --- a/pkg/types/dashboard.go +++ /dev/null @@ -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"` -} diff --git a/pkg/types/dashboardtypes/dashboard.go b/pkg/types/dashboardtypes/dashboard.go new file mode 100644 index 0000000000..6f967503b6 --- /dev/null +++ b/pkg/types/dashboardtypes/dashboard.go @@ -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 +} diff --git a/pkg/types/organization.go b/pkg/types/organization.go index 2c177b2cbf..49fa8db910 100644 --- a/pkg/types/organization.go +++ b/pkg/types/organization.go @@ -49,6 +49,18 @@ func NewOrganizationKey(orgID valuer.UUID) uint32 { 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 { Create(context.Context, *Organization) error Get(context.Context, valuer.UUID) (*Organization, error)