From 5e349d8294588fd6f8c0928bd7c16b08ed7a740d Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 2 Nov 2023 22:52:50 +0530 Subject: [PATCH 01/28] chore: dashboard locking ee query-service (#3856) * chore: dashboard locking ee query-service * chore: remove print statements * chore: remove unused imports * chore: no one is allowed to edit/delete the locked dashboard --------- Co-authored-by: Srikanth Chekuri --- ee/query-service/app/api/api.go | 3 ++ ee/query-service/app/api/dashboard.go | 51 +++++++++++++++++++++++ pkg/query-service/app/dashboards/model.go | 43 +++++++++++++++++-- pkg/query-service/app/parser/metrics.go | 1 - pkg/query-service/dao/sqlite/apdex.go | 2 - 5 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 ee/query-service/app/api/dashboard.go diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go index a17eb7b79a..ddb617188f 100644 --- a/ee/query-service/app/api/api.go +++ b/ee/query-service/app/api/api.go @@ -160,6 +160,9 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet) router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.portalSession)).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) + router.HandleFunc("/api/v2/licenses", am.ViewAccess(ah.listLicensesV2)). Methods(http.MethodGet) diff --git a/ee/query-service/app/api/dashboard.go b/ee/query-service/app/api/dashboard.go new file mode 100644 index 0000000000..abd1aed3f4 --- /dev/null +++ b/ee/query-service/app/api/dashboard.go @@ -0,0 +1,51 @@ +package api + +import ( + "github.com/gorilla/mux" + "go.signoz.io/signoz/pkg/query-service/app/dashboards" + "go.signoz.io/signoz/pkg/query-service/auth" + "go.signoz.io/signoz/pkg/query-service/common" + "go.signoz.io/signoz/pkg/query-service/model" + "net/http" +) + +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"] + dashboard, err := dashboards.GetDashboard(r.Context(), uuid) + if err != nil { + RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error()) + return + } + + user := common.GetUserFromContext(r.Context()) + if !auth.IsAdmin(user) || (dashboard.CreateBy != nil && *dashboard.CreateBy != user.Email) { + RespondError(w, &model.ApiError{Typ: model.ErrorForbidden, Err: err}, "You are not authorized to lock/unlock this dashboard") + return + } + + // Lock/Unlock the dashboard + err = dashboards.LockUnlockDashboard(r.Context(), uuid, lock) + if err != nil { + RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error()) + return + } + + ah.Respond(w, "Dashboard updated successfully") +} diff --git a/pkg/query-service/app/dashboards/model.go b/pkg/query-service/app/dashboards/model.go index fa51864d4c..698b697279 100644 --- a/pkg/query-service/app/dashboards/model.go +++ b/pkg/query-service/app/dashboards/model.go @@ -128,6 +128,12 @@ func InitDB(dataSourceName string) (*sqlx.DB, error) { return nil, fmt.Errorf("error in adding column updated_by to dashboards table: %s", err.Error()) } + locked := `ALTER TABLE dashboards ADD COLUMN locked INTEGER DEFAULT 0;` + _, err = db.Exec(locked) + if err != nil && !strings.Contains(err.Error(), "duplicate column name") { + return nil, fmt.Errorf("error in adding column locked to dashboards table: %s", err.Error()) + } + return db, nil } @@ -141,6 +147,7 @@ type Dashboard struct { UpdateBy *string `json:"updated_by" db:"updated_by"` Title string `json:"-" db:"-"` Data Data `json:"data" db:"data"` + Locked *int `json:"isLocked" db:"locked"` } type Data map[string]interface{} @@ -239,6 +246,12 @@ func DeleteDashboard(ctx context.Context, uuid string, fm interfaces.FeatureLook return dErr } + if user := common.GetUserFromContext(ctx); user != nil { + if dashboard.Locked != nil && *dashboard.Locked == 1 { + return model.BadRequest(fmt.Errorf("dashboard is locked, please unlock the dashboard to be able to delete it")) + } + } + query := `DELETE FROM dashboards WHERE uuid=?` result, err := db.Exec(query, uuid) @@ -289,6 +302,14 @@ func UpdateDashboard(ctx context.Context, uuid string, data map[string]interface return nil, apiErr } + var userEmail string + if user := common.GetUserFromContext(ctx); user != nil { + userEmail = user.Email + if dashboard.Locked != nil && *dashboard.Locked == 1 { + return nil, model.BadRequest(fmt.Errorf("dashboard is locked, please unlock the dashboard to be able to edit it")) + } + } + // check if the count of trace and logs QB panel has changed, if yes, then check feature flag count existingCount, existingTotal := countTraceAndLogsPanel(dashboard.Data) newCount, newTotal := countTraceAndLogsPanel(data) @@ -306,10 +327,6 @@ func UpdateDashboard(ctx context.Context, uuid string, data map[string]interface } dashboard.UpdatedAt = time.Now() - var userEmail string - if user := common.GetUserFromContext(ctx); user != nil { - userEmail = user.Email - } dashboard.UpdateBy = &userEmail dashboard.Data = data @@ -327,6 +344,24 @@ func UpdateDashboard(ctx context.Context, uuid string, data map[string]interface return dashboard, nil } +func LockUnlockDashboard(ctx context.Context, uuid string, lock bool) *model.ApiError { + var query string + if lock { + query = `UPDATE dashboards SET locked=1 WHERE uuid=?;` + } else { + query = `UPDATE dashboards SET locked=0 WHERE uuid=?;` + } + + _, err := db.Exec(query, uuid) + + if err != nil { + zap.S().Errorf("Error in updating dashboard: ", uuid, err) + return &model.ApiError{Typ: model.ErrorExec, Err: err} + } + + return nil +} + func updateFeatureUsage(fm interfaces.FeatureLookup, usage int64) *model.ApiError { feature, err := fm.GetFeatureFlag(model.QueryBuilderPanels) if err != nil { diff --git a/pkg/query-service/app/parser/metrics.go b/pkg/query-service/app/parser/metrics.go index b13ff6d534..3391be78c7 100644 --- a/pkg/query-service/app/parser/metrics.go +++ b/pkg/query-service/app/parser/metrics.go @@ -91,7 +91,6 @@ func ParseMetricAutocompleteTagParams(r *http.Request) (*model.MetricAutocomplet } tagsStr := r.URL.Query().Get("tags") - // fmt.Println(tagsStr) // parsing tags var tags map[string]string diff --git a/pkg/query-service/dao/sqlite/apdex.go b/pkg/query-service/dao/sqlite/apdex.go index 65c1eae350..63280d8fb6 100644 --- a/pkg/query-service/dao/sqlite/apdex.go +++ b/pkg/query-service/dao/sqlite/apdex.go @@ -2,7 +2,6 @@ package sqlite import ( "context" - "fmt" "github.com/jmoiron/sqlx" "go.signoz.io/signoz/pkg/query-service/model" @@ -51,7 +50,6 @@ func (mds *ModelDaoSqlite) GetApdexSettings(ctx context.Context, services []stri func (mds *ModelDaoSqlite) SetApdexSettings(ctx context.Context, apdexSettings *model.ApdexSettings) *model.ApiError { - fmt.Println("apdexSettings:", apdexSettings) _, err := mds.db.NamedExec(` INSERT OR REPLACE INTO apdex_settings ( service_name, From 83716705128ebd0682d1f8e05ae16cf577929b20 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 3 Nov 2023 15:45:45 +0530 Subject: [PATCH 02/28] chore: dashboard locking ee query-service (#3890) --- ee/query-service/app/api/dashboard.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/query-service/app/api/dashboard.go b/ee/query-service/app/api/dashboard.go index abd1aed3f4..83c82a1477 100644 --- a/ee/query-service/app/api/dashboard.go +++ b/ee/query-service/app/api/dashboard.go @@ -35,7 +35,7 @@ func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request } user := common.GetUserFromContext(r.Context()) - if !auth.IsAdmin(user) || (dashboard.CreateBy != nil && *dashboard.CreateBy != user.Email) { + if !auth.IsAdmin(user) && (dashboard.CreateBy != nil && *dashboard.CreateBy != user.Email) { RespondError(w, &model.ApiError{Typ: model.ErrorForbidden, Err: err}, "You are not authorized to lock/unlock this dashboard") return } From 0906886e9ac3813462cc03044131bd8ceb499a24 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Fri, 3 Nov 2023 17:27:09 +0530 Subject: [PATCH 03/28] feat: dashboard lock feature (#3880) * feat: dashboard lock feature * feat: update API method and minor ui updates * feat: update API and author logic * feat: update permissions for author role * feat: use strings and remove console logs --- frontend/public/locales/en/dashboard.json | 4 +- frontend/src/api/dashboard/lockDashboard.ts | 11 +++ frontend/src/api/dashboard/unlockDashboard.ts | 11 +++ .../GridCardLayout/GridCardLayout.tsx | 82 +++++++++++++------ .../src/container/GridCardLayout/config.ts | 7 +- .../src/container/GridCardLayout/styles.ts | 44 +++++----- .../TableComponents/DeleteButton.tsx | 45 ++++++++-- .../ListOfDashboard/TableComponents/Name.tsx | 11 ++- .../src/container/ListOfDashboard/index.tsx | 4 +- .../NewDashboard/ComponentsSlider/styles.ts | 4 +- .../DashboardName}/index.tsx | 9 +- .../SettingsDrawer.tsx | 2 +- .../ShareModal.tsx | 0 .../index.tsx | 49 +++++++++-- .../styles.ts | 0 .../util.ts | 0 frontend/src/container/NewDashboard/index.tsx | 2 +- .../src/providers/Dashboard/Dashboard.tsx | 43 +++++++++- frontend/src/providers/Dashboard/types.ts | 2 + frontend/src/types/api/dashboard/getAll.ts | 1 + frontend/src/types/roles.ts | 4 +- frontend/src/utils/permission/index.ts | 14 ++-- 22 files changed, 265 insertions(+), 84 deletions(-) create mode 100644 frontend/src/api/dashboard/lockDashboard.ts create mode 100644 frontend/src/api/dashboard/unlockDashboard.ts rename frontend/src/container/NewDashboard/{DescriptionOfDashboard/NameOfTheDashboard => DashboardDescription/DashboardName}/index.tsx (72%) rename frontend/src/container/NewDashboard/{DescriptionOfDashboard => DashboardDescription}/SettingsDrawer.tsx (90%) rename frontend/src/container/NewDashboard/{DescriptionOfDashboard => DashboardDescription}/ShareModal.tsx (100%) rename frontend/src/container/NewDashboard/{DescriptionOfDashboard => DashboardDescription}/index.tsx (55%) rename frontend/src/container/NewDashboard/{DescriptionOfDashboard => DashboardDescription}/styles.ts (100%) rename frontend/src/container/NewDashboard/{DescriptionOfDashboard => DashboardDescription}/util.ts (100%) diff --git a/frontend/public/locales/en/dashboard.json b/frontend/public/locales/en/dashboard.json index b69113483d..7c4b894e76 100644 --- a/frontend/public/locales/en/dashboard.json +++ b/frontend/public/locales/en/dashboard.json @@ -20,5 +20,7 @@ "variable_updated_successfully": "Variable updated successfully", "error_while_updating_variable": "Error while updating variable", "dashboard_has_been_updated": "Dashboard has been updated", - "do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?" + "do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?", +"locked_dashboard_delete_tooltip_admin_author": "Dashboard is locked. Please unlock the dashboard to enable delete.", +"locked_dashboard_delete_tooltip_editor": "Dashboard is locked. Please contact admin to delete the dashboard." } diff --git a/frontend/src/api/dashboard/lockDashboard.ts b/frontend/src/api/dashboard/lockDashboard.ts new file mode 100644 index 0000000000..3393de8fa3 --- /dev/null +++ b/frontend/src/api/dashboard/lockDashboard.ts @@ -0,0 +1,11 @@ +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 new file mode 100644 index 0000000000..fd4ffbe41a --- /dev/null +++ b/frontend/src/api/dashboard/unlockDashboard.ts @@ -0,0 +1,11 @@ +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/container/GridCardLayout/GridCardLayout.tsx b/frontend/src/container/GridCardLayout/GridCardLayout.tsx index 57b1f4222d..a293d1395e 100644 --- a/frontend/src/container/GridCardLayout/GridCardLayout.tsx +++ b/frontend/src/container/GridCardLayout/GridCardLayout.tsx @@ -11,8 +11,10 @@ import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; import AppReducer from 'types/reducer/app'; +import { ROLES, USER_ROLES } from 'types/roles'; +import { ComponentTypes } from 'utils/permission'; -import { headerMenuList } from './config'; +import { EditMenuAction, ViewMenuAction } from './config'; import GridCard from './GridCard'; import { Button, @@ -32,10 +34,11 @@ function GraphLayout({ layouts, setLayouts, setSelectedDashboard, + isDashboardLocked, } = useDashboard(); const { t } = useTranslation(['dashboard']); - const { featureResponse, role } = useSelector( + const { featureResponse, role, user } = useSelector( (state) => state.app, ); @@ -45,9 +48,20 @@ function GraphLayout({ const { notifications } = useNotifications(); + let permissions: ComponentTypes[] = ['save_layout', 'add_panel']; + + if (isDashboardLocked) { + permissions = ['edit_locked_dashboard', 'add_panel_locked_dashboard']; + } + + const userRole: ROLES | null = + selectedDashboard?.created_by === user?.email + ? (USER_ROLES.AUTHOR as ROLES) + : role; + const [saveLayoutPermission, addPanelPermission] = useComponentPermission( - ['save_layout', 'add_panel'], - role, + permissions, + userRole, ); const onSaveHandler = (): void => { @@ -83,35 +97,41 @@ function GraphLayout({ }); }; + const widgetActions = !isDashboardLocked + ? [...ViewMenuAction, ...EditMenuAction] + : [...ViewMenuAction]; + return ( <> - - {saveLayoutPermission && ( - - )} + {!isDashboardLocked && ( + + {saveLayoutPermission && ( + + )} - {addPanelPermission && ( - - )} - + {addPanelPermission && ( + + )} + + )} e.id === id); return ( - - + + diff --git a/frontend/src/container/GridCardLayout/config.ts b/frontend/src/container/GridCardLayout/config.ts index 3fa9e8e569..4304c0f231 100644 --- a/frontend/src/container/GridCardLayout/config.ts +++ b/frontend/src/container/GridCardLayout/config.ts @@ -1,13 +1,16 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants'; -export const headerMenuList = [ - MenuItemKeys.View, +export const ViewMenuAction = [MenuItemKeys.View]; + +export const EditMenuAction = [ MenuItemKeys.Clone, MenuItemKeys.Delete, MenuItemKeys.Edit, ]; +export const headerMenuList = [...ViewMenuAction]; + export const EMPTY_WIDGET_LAYOUT = { i: PANEL_TYPES.EMPTY_WIDGET, w: 6, diff --git a/frontend/src/container/GridCardLayout/styles.ts b/frontend/src/container/GridCardLayout/styles.ts index a08ffdc361..9416df6674 100644 --- a/frontend/src/container/GridCardLayout/styles.ts +++ b/frontend/src/container/GridCardLayout/styles.ts @@ -34,29 +34,31 @@ interface Props { export const CardContainer = styled.div` overflow: auto; - :hover { - .react-resizable-handle { - position: absolute; - width: 20px; - height: 20px; - bottom: 0; - right: 0; - background-position: bottom right; - padding: 0 3px 3px 0; - background-repeat: no-repeat; - background-origin: content-box; - box-sizing: border-box; - cursor: se-resize; + &.enable-resize { + :hover { + .react-resizable-handle { + position: absolute; + width: 20px; + height: 20px; + bottom: 0; + right: 0; + background-position: bottom right; + padding: 0 3px 3px 0; + background-repeat: no-repeat; + background-origin: content-box; + box-sizing: border-box; + cursor: se-resize; - ${({ isDarkMode }): StyledCSS => { - const uri = `data:image/svg+xml,%3Csvg viewBox='0 0 6 6' style='background-color:%23ffffff00' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' xml:space='preserve' x='0px' y='0px' width='6px' height='6px'%0A%3E%3Cg opacity='0.302'%3E%3Cpath d='M 6 6 L 0 6 L 0 4.2 L 4 4.2 L 4.2 4.2 L 4.2 0 L 6 0 L 6 6 L 6 6 Z' fill='${ - isDarkMode ? 'white' : 'grey' - }'/%3E%3C/g%3E%3C/svg%3E`; + ${({ isDarkMode }): StyledCSS => { + const uri = `data:image/svg+xml,%3Csvg viewBox='0 0 6 6' style='background-color:%23ffffff00' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' xml:space='preserve' x='0px' y='0px' width='6px' height='6px'%0A%3E%3Cg opacity='0.302'%3E%3Cpath d='M 6 6 L 0 6 L 0 4.2 L 4 4.2 L 4.2 4.2 L 4.2 0 L 6 0 L 6 6 L 6 6 Z' fill='${ + isDarkMode ? 'white' : 'grey' + }'/%3E%3C/g%3E%3C/svg%3E`; - return css` - background-image: ${(): string => `url("${uri}")`}; - `; - }} + return css` + background-image: ${(): string => `url("${uri}")`}; + `; + }} + } } } `; diff --git a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx index c68b0c2617..b6d51445f8 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx @@ -1,18 +1,27 @@ -import { ExclamationCircleOutlined } from '@ant-design/icons'; -import { Modal } from 'antd'; +import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons'; +import { Modal, Tooltip } from 'antd'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard'; import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { useQueryClient } from 'react-query'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import AppReducer from 'types/reducer/app'; +import { USER_ROLES } from 'types/roles'; import { Data } from '../index'; import { TableLinkText } from './styles'; -function DeleteButton({ id }: Data): JSX.Element { +function DeleteButton({ id, createdBy, isLocked }: Data): JSX.Element { const [modal, contextHolder] = Modal.useModal(); + const { role, user } = useSelector((state) => state.app); + const isAuthor = user?.email === createdBy; const queryClient = useQueryClient(); + const { t } = useTranslation(['dashboard']); + const deleteDashboardMutation = useDeleteDashboard(id); const openConfirmationDialog = useCallback((): void => { @@ -32,11 +41,33 @@ function DeleteButton({ id }: Data): JSX.Element { }); }, [modal, deleteDashboardMutation, queryClient]); + const getDeleteTooltipContent = (): string => { + if (isLocked) { + if (role === USER_ROLES.ADMIN || isAuthor) { + return t('dashboard:locked_dashboard_delete_tooltip_admin_author'); + } + + return t('dashboard:locked_dashboard_delete_tooltip_editor'); + } + + return ''; + }; + return ( <> - - Delete - + + { + if (!isLocked) { + openConfirmationDialog(); + } + }} + disabled={isLocked} + > + Delete + + {contextHolder} @@ -55,6 +86,7 @@ function Wrapper(props: Data): JSX.Element { tags, createdBy, lastUpdatedBy, + isLocked, } = props; return ( @@ -69,6 +101,7 @@ function Wrapper(props: Data): JSX.Element { tags, createdBy, lastUpdatedBy, + isLocked, }} /> ); diff --git a/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx b/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx index af53580926..6a85e7e136 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx @@ -1,3 +1,4 @@ +import { LockFilled } from '@ant-design/icons'; import ROUTES from 'constants/routes'; import history from 'lib/history'; import { generatePath } from 'react-router-dom'; @@ -6,9 +7,9 @@ import { Data } from '..'; import { TableLinkText } from './styles'; function Name(name: Data['name'], data: Data): JSX.Element { - const onClickHandler = (): void => { - const { id: DashboardId } = data; + const { id: DashboardId, isLocked } = data; + const onClickHandler = (): void => { history.push( generatePath(ROUTES.DASHBOARD, { dashboardId: DashboardId, @@ -16,7 +17,11 @@ function Name(name: Data['name'], data: Data): JSX.Element { ); }; - return {name}; + return ( + + {isLocked && } {name} + + ); } export default Name; diff --git a/frontend/src/container/ListOfDashboard/index.tsx b/frontend/src/container/ListOfDashboard/index.tsx index cf3fa02a80..a310c5be63 100644 --- a/frontend/src/container/ListOfDashboard/index.tsx +++ b/frontend/src/container/ListOfDashboard/index.tsx @@ -130,7 +130,7 @@ function ListOfAllDashboard(): JSX.Element { dataIndex: 'description', }, { - title: 'Tags (can be multiple)', + title: 'Tags', dataIndex: 'tags', width: 50, render: (value): JSX.Element => , @@ -159,6 +159,7 @@ function ListOfAllDashboard(): JSX.Element { tags: e.data.tags || [], key: e.uuid, createdBy: e.created_by, + isLocked: !!e.isLocked || false, lastUpdatedBy: e.updated_by, refetchDashboardList, })) || []; @@ -342,6 +343,7 @@ export interface Data { createdAt: string; lastUpdatedTime: string; lastUpdatedBy: string; + isLocked: boolean; id: string; } diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/styles.ts b/frontend/src/container/NewDashboard/ComponentsSlider/styles.ts index d7d3c6a7f2..29abafa3d5 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/styles.ts +++ b/frontend/src/container/NewDashboard/ComponentsSlider/styles.ts @@ -3,11 +3,13 @@ import styled from 'styled-components'; export const Container = styled.div` display: flex; - gap: 0.6rem; + justify-content: right; + gap: 8px; `; export const Card = styled(CardComponent)` min-height: 10vh; + min-width: 120px; overflow-y: auto; cursor: pointer; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/NameOfTheDashboard/index.tsx b/frontend/src/container/NewDashboard/DashboardDescription/DashboardName/index.tsx similarity index 72% rename from frontend/src/container/NewDashboard/DescriptionOfDashboard/NameOfTheDashboard/index.tsx rename to frontend/src/container/NewDashboard/DashboardDescription/DashboardName/index.tsx index 32f78d7ec2..53069c78aa 100644 --- a/frontend/src/container/NewDashboard/DescriptionOfDashboard/NameOfTheDashboard/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardDescription/DashboardName/index.tsx @@ -1,10 +1,7 @@ import Input from 'components/Input'; import { ChangeEvent, Dispatch, SetStateAction, useCallback } from 'react'; -function NameOfTheDashboard({ - setName, - name, -}: NameOfTheDashboardProps): JSX.Element { +function DashboardName({ setName, name }: DashboardNameProps): JSX.Element { const onChangeHandler = useCallback( (e: ChangeEvent) => { setName(e.target.value); @@ -22,9 +19,9 @@ function NameOfTheDashboard({ ); } -interface NameOfTheDashboardProps { +interface DashboardNameProps { name: string; setName: Dispatch>; } -export default NameOfTheDashboard; +export default DashboardName; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx b/frontend/src/container/NewDashboard/DashboardDescription/SettingsDrawer.tsx similarity index 90% rename from frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx rename to frontend/src/container/NewDashboard/DashboardDescription/SettingsDrawer.tsx index fc51efd69a..fb512e999a 100644 --- a/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx +++ b/frontend/src/container/NewDashboard/DashboardDescription/SettingsDrawer.tsx @@ -18,7 +18,7 @@ function SettingsDrawer(): JSX.Element { return ( <> - (false); const { t } = useTranslation('common'); - const { role } = useSelector((state) => state.app); + const { user, role } = useSelector((state) => state.app); const [editDashboard] = useComponentPermission(['edit_dashboard'], role); + let isAuthor = false; + + if (selectedDashboard && user && user.email) { + isAuthor = selectedDashboard?.created_by === user?.email; + } + const onToggleHandler = (): void => { isIsJSONModalVisible((state) => !state); }; + const handleLockDashboardToggle = (): void => { + handleDashboardLockToggle(!isDashboardLocked); + }; + return ( + {isDashboardLocked && ( + +   + + )} {title} {description} @@ -55,7 +75,7 @@ function DescriptionOfDashboard(): JSX.Element { )} - {editDashboard && } + {!isDashboardLocked && editDashboard && } + {(isAuthor || role === USER_ROLES.ADMIN) && ( + + + + )} @@ -71,4 +106,4 @@ function DescriptionOfDashboard(): JSX.Element { ); } -export default DescriptionOfDashboard; +export default DashboardDescription; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/styles.ts b/frontend/src/container/NewDashboard/DashboardDescription/styles.ts similarity index 100% rename from frontend/src/container/NewDashboard/DescriptionOfDashboard/styles.ts rename to frontend/src/container/NewDashboard/DashboardDescription/styles.ts diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/util.ts b/frontend/src/container/NewDashboard/DashboardDescription/util.ts similarity index 100% rename from frontend/src/container/NewDashboard/DescriptionOfDashboard/util.ts rename to frontend/src/container/NewDashboard/DashboardDescription/util.ts diff --git a/frontend/src/container/NewDashboard/index.tsx b/frontend/src/container/NewDashboard/index.tsx index 5eff095c6f..ca10b6f89b 100644 --- a/frontend/src/container/NewDashboard/index.tsx +++ b/frontend/src/container/NewDashboard/index.tsx @@ -1,4 +1,4 @@ -import Description from './DescriptionOfDashboard'; +import Description from './DashboardDescription'; import GridGraphs from './GridGraphs'; function NewDashboard(): JSX.Element { diff --git a/frontend/src/providers/Dashboard/Dashboard.tsx b/frontend/src/providers/Dashboard/Dashboard.tsx index 1074465d0b..48190fd3c4 100644 --- a/frontend/src/providers/Dashboard/Dashboard.tsx +++ b/frontend/src/providers/Dashboard/Dashboard.tsx @@ -1,9 +1,12 @@ import { Modal } from 'antd'; import get from 'api/dashboard/get'; +import lockDashboardApi from 'api/dashboard/lockDashboard'; +import unlockDashboardApi from 'api/dashboard/unlockDashboard'; 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 useAxiosError from 'hooks/useAxiosError'; import useTabVisibility from 'hooks/useTabFocus'; import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout'; import { @@ -17,7 +20,7 @@ import { } from 'react'; import { Layout } from 'react-grid-layout'; import { useTranslation } from 'react-i18next'; -import { useQuery, UseQueryResult } from 'react-query'; +import { useMutation, useQuery, UseQueryResult } from 'react-query'; import { useDispatch, useSelector } from 'react-redux'; import { useRouteMatch } from 'react-router-dom'; import { Dispatch } from 'redux'; @@ -32,7 +35,9 @@ import { IDashboardContext } from './types'; const DashboardContext = createContext({ isDashboardSliderOpen: false, + isDashboardLocked: false, handleToggleDashboardSlider: () => {}, + handleDashboardLockToggle: () => {}, dashboardResponse: {} as UseQueryResult, selectedDashboard: {} as Dashboard, dashboardId: '', @@ -50,6 +55,9 @@ export function DashboardProvider({ children, }: PropsWithChildren): JSX.Element { const [isDashboardSliderOpen, setIsDashboardSlider] = useState(false); + + const [isDashboardLocked, setIsDashboardLocked] = useState(false); + const isDashboardPage = useRouteMatch({ path: ROUTES.DASHBOARD, exact: true, @@ -99,6 +107,8 @@ export function DashboardProvider({ onSuccess: (data) => { const updatedDate = dayjs(data.updated_at); + setIsDashboardLocked(data?.isLocked || false); + // on first render if (updatedTimeRef.current === null) { setSelectedDashboard(data); @@ -179,10 +189,39 @@ export function DashboardProvider({ setIsDashboardSlider(value); }; + const handleError = useAxiosError(); + + const { mutate: lockDashboard } = useMutation(lockDashboardApi, { + onSuccess: () => { + setIsDashboardSlider(false); + setIsDashboardLocked(true); + }, + onError: handleError, + }); + + const { mutate: unlockDashboard } = useMutation(unlockDashboardApi, { + onSuccess: () => { + setIsDashboardLocked(false); + }, + onError: handleError, + }); + + const handleDashboardLockToggle = async (value: boolean): Promise => { + if (selectedDashboard) { + if (value) { + lockDashboard(selectedDashboard); + } else { + unlockDashboard(selectedDashboard); + } + } + }; + const value: IDashboardContext = useMemo( () => ({ isDashboardSliderOpen, + isDashboardLocked, handleToggleDashboardSlider, + handleDashboardLockToggle, dashboardResponse, selectedDashboard, dashboardId, @@ -191,8 +230,10 @@ export function DashboardProvider({ setSelectedDashboard, updatedTimeRef, }), + // eslint-disable-next-line react-hooks/exhaustive-deps [ isDashboardSliderOpen, + isDashboardLocked, dashboardResponse, selectedDashboard, dashboardId, diff --git a/frontend/src/providers/Dashboard/types.ts b/frontend/src/providers/Dashboard/types.ts index 9fc15c5f77..11e2da2212 100644 --- a/frontend/src/providers/Dashboard/types.ts +++ b/frontend/src/providers/Dashboard/types.ts @@ -5,7 +5,9 @@ import { Dashboard } from 'types/api/dashboard/getAll'; export interface IDashboardContext { isDashboardSliderOpen: boolean; + isDashboardLocked: boolean; handleToggleDashboardSlider: (value: boolean) => void; + handleDashboardLockToggle: (value: boolean) => void; dashboardResponse: UseQueryResult; selectedDashboard: Dashboard | undefined; dashboardId: string; diff --git a/frontend/src/types/api/dashboard/getAll.ts b/frontend/src/types/api/dashboard/getAll.ts index 0c872e02cc..fafdae6b3c 100644 --- a/frontend/src/types/api/dashboard/getAll.ts +++ b/frontend/src/types/api/dashboard/getAll.ts @@ -45,6 +45,7 @@ export interface Dashboard { created_by: string; updated_by: string; data: DashboardData; + isLocked?: boolean; } export interface DashboardData { diff --git a/frontend/src/types/roles.ts b/frontend/src/types/roles.ts index 0ac7deaf24..18b4b52028 100644 --- a/frontend/src/types/roles.ts +++ b/frontend/src/types/roles.ts @@ -1,11 +1,13 @@ export type ADMIN = 'ADMIN'; export type VIEWER = 'VIEWER'; export type EDITOR = 'EDITOR'; +export type AUTHOR = 'AUTHOR'; -export type ROLES = ADMIN | VIEWER | EDITOR; +export type ROLES = ADMIN | VIEWER | EDITOR | AUTHOR; export const USER_ROLES = { ADMIN: 'ADMIN', VIEWER: 'VIEWER', EDITOR: 'EDITOR', + AUTHOR: 'AUTHOR', }; diff --git a/frontend/src/utils/permission/index.ts b/frontend/src/utils/permission/index.ts index 057eb2adc4..ee1a7a09e9 100644 --- a/frontend/src/utils/permission/index.ts +++ b/frontend/src/utils/permission/index.ts @@ -18,7 +18,9 @@ export type ComponentTypes = | 'new_alert_action' | 'edit_widget' | 'add_panel' - | 'page_pipelines'; + | 'page_pipelines' + | 'edit_locked_dashboard' + | 'add_panel_locked_dashboard'; export const componentPermission: Record = { current_org_settings: ['ADMIN'], @@ -30,14 +32,16 @@ export const componentPermission: Record = { add_new_channel: ['ADMIN'], set_retention_period: ['ADMIN'], action: ['ADMIN', 'EDITOR'], - save_layout: ['ADMIN', 'EDITOR'], - edit_dashboard: ['ADMIN', 'EDITOR'], - delete_widget: ['ADMIN', 'EDITOR'], + save_layout: ['ADMIN', 'EDITOR', 'AUTHOR'], + edit_dashboard: ['ADMIN', 'EDITOR', 'AUTHOR'], + delete_widget: ['ADMIN', 'EDITOR', 'AUTHOR'], new_dashboard: ['ADMIN', 'EDITOR'], new_alert_action: ['ADMIN'], edit_widget: ['ADMIN', 'EDITOR'], - add_panel: ['ADMIN', 'EDITOR'], + add_panel: ['ADMIN', 'EDITOR', 'AUTHOR'], page_pipelines: ['ADMIN', 'EDITOR'], + edit_locked_dashboard: ['ADMIN', 'AUTHOR'], + add_panel_locked_dashboard: ['ADMIN', 'AUTHOR'], }; export const routePermission: Record = { From 050b866173975728da4c813ddd202cb8a2ec2c81 Mon Sep 17 00:00:00 2001 From: Raj Kamal Singh <1133322+rkssisodiya@users.noreply.github.com> Date: Fri, 3 Nov 2023 17:41:09 +0530 Subject: [PATCH 04/28] chore: return warning logs too from collector simulator (#3888) * chore: return warning logs too from collector simulator * chore: also return collector logs in preview API response to help with debugging --------- Co-authored-by: Srikanth Chekuri --- .../app/logparsingpipeline/controller.go | 8 +++++--- .../logparsingpipeline/pipelineBuilder_test.go | 4 ++-- .../app/logparsingpipeline/preview.go | 2 +- .../app/logparsingpipeline/preview_test.go | 16 ++++++++-------- .../collectorsimulator/collectorsimulator.go | 8 ++++---- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/pkg/query-service/app/logparsingpipeline/controller.go b/pkg/query-service/app/logparsingpipeline/controller.go index c4b1b0e3ff..7880ac27b7 100644 --- a/pkg/query-service/app/logparsingpipeline/controller.go +++ b/pkg/query-service/app/logparsingpipeline/controller.go @@ -133,14 +133,15 @@ type PipelinesPreviewRequest struct { } type PipelinesPreviewResponse struct { - OutputLogs []model.SignozLog `json:"logs"` + OutputLogs []model.SignozLog `json:"logs"` + CollectorLogs []string `json:"collectorLogs"` } func (ic *LogParsingPipelineController) PreviewLogsPipelines( ctx context.Context, request *PipelinesPreviewRequest, ) (*PipelinesPreviewResponse, *model.ApiError) { - result, _, err := SimulatePipelinesProcessing( + result, collectorLogs, err := SimulatePipelinesProcessing( ctx, request.Pipelines, request.Logs, ) @@ -149,7 +150,8 @@ func (ic *LogParsingPipelineController) PreviewLogsPipelines( } return &PipelinesPreviewResponse{ - OutputLogs: result, + OutputLogs: result, + CollectorLogs: collectorLogs, }, nil } diff --git a/pkg/query-service/app/logparsingpipeline/pipelineBuilder_test.go b/pkg/query-service/app/logparsingpipeline/pipelineBuilder_test.go index 34afddccce..47a3e70925 100644 --- a/pkg/query-service/app/logparsingpipeline/pipelineBuilder_test.go +++ b/pkg/query-service/app/logparsingpipeline/pipelineBuilder_test.go @@ -351,13 +351,13 @@ func TestNoCollectorErrorsFromProcessorsForMismatchedLogs(t *testing.T) { for _, testCase := range testCases { testPipelines := []Pipeline{makeTestPipeline([]PipelineOperator{testCase.Operator})} - result, collectorErrorLogs, err := SimulatePipelinesProcessing( + result, collectorWarnAndErrorLogs, err := SimulatePipelinesProcessing( context.Background(), testPipelines, []model.SignozLog{testCase.NonMatchingLog}, ) require.Nil(err) - require.Equal(0, len(collectorErrorLogs), strings.Join(collectorErrorLogs, "\n")) + require.Equal(0, len(collectorWarnAndErrorLogs), strings.Join(collectorWarnAndErrorLogs, "\n")) require.Equal(1, len(result)) } } diff --git a/pkg/query-service/app/logparsingpipeline/preview.go b/pkg/query-service/app/logparsingpipeline/preview.go index c1863649e6..4f725cafa5 100644 --- a/pkg/query-service/app/logparsingpipeline/preview.go +++ b/pkg/query-service/app/logparsingpipeline/preview.go @@ -22,7 +22,7 @@ func SimulatePipelinesProcessing( pipelines []Pipeline, logs []model.SignozLog, ) ( - output []model.SignozLog, collectorErrorLogs []string, apiErr *model.ApiError, + output []model.SignozLog, collectorWarnAndErrorLogs []string, apiErr *model.ApiError, ) { if len(pipelines) < 1 { diff --git a/pkg/query-service/app/logparsingpipeline/preview_test.go b/pkg/query-service/app/logparsingpipeline/preview_test.go index 9911fd26c2..a7fa51732b 100644 --- a/pkg/query-service/app/logparsingpipeline/preview_test.go +++ b/pkg/query-service/app/logparsingpipeline/preview_test.go @@ -104,7 +104,7 @@ func TestPipelinePreview(t *testing.T) { }, ) - result, collectorErrorLogs, err := SimulatePipelinesProcessing( + result, collectorWarnAndErrorLogs, err := SimulatePipelinesProcessing( context.Background(), testPipelines, []model.SignozLog{ @@ -114,7 +114,7 @@ func TestPipelinePreview(t *testing.T) { ) require.Nil(err) - require.Equal(0, len(collectorErrorLogs)) + require.Equal(0, len(collectorWarnAndErrorLogs)) require.Equal(2, len(result)) // matching log should have been modified as expected. @@ -190,7 +190,7 @@ func TestGrokParsingPreview(t *testing.T) { "method": "GET", }, ) - result, collectorErrorLogs, err := SimulatePipelinesProcessing( + result, collectorWarnAndErrorLogs, err := SimulatePipelinesProcessing( context.Background(), testPipelines, []model.SignozLog{ @@ -199,7 +199,7 @@ func TestGrokParsingPreview(t *testing.T) { ) require.Nil(err) - require.Equal(0, len(collectorErrorLogs)) + require.Equal(0, len(collectorWarnAndErrorLogs)) require.Equal(1, len(result)) processed := result[0] @@ -280,7 +280,7 @@ func TestTraceParsingPreview(t *testing.T) { TraceFlags: 0, } - result, collectorErrorLogs, err := SimulatePipelinesProcessing( + result, collectorWarnAndErrorLogs, err := SimulatePipelinesProcessing( context.Background(), testPipelines, []model.SignozLog{ @@ -289,7 +289,7 @@ func TestTraceParsingPreview(t *testing.T) { ) require.Nil(err) require.Equal(1, len(result)) - require.Equal(0, len(collectorErrorLogs)) + require.Equal(0, len(collectorWarnAndErrorLogs)) processed := result[0] require.Equal(testTraceId, processed.TraceID) @@ -301,7 +301,7 @@ func TestTraceParsingPreview(t *testing.T) { // trace parser should work even if parse_from value is empty testPipelines[0].Config[0].SpanId.ParseFrom = "" - result, collectorErrorLogs, err = SimulatePipelinesProcessing( + result, collectorWarnAndErrorLogs, err = SimulatePipelinesProcessing( context.Background(), testPipelines, []model.SignozLog{ @@ -310,7 +310,7 @@ func TestTraceParsingPreview(t *testing.T) { ) require.Nil(err) require.Equal(1, len(result)) - require.Equal(0, len(collectorErrorLogs)) + require.Equal(0, len(collectorWarnAndErrorLogs)) require.Equal("", result[0].SpanID) } diff --git a/pkg/query-service/collectorsimulator/collectorsimulator.go b/pkg/query-service/collectorsimulator/collectorsimulator.go index 96a2d9fdf7..4a8236b483 100644 --- a/pkg/query-service/collectorsimulator/collectorsimulator.go +++ b/pkg/query-service/collectorsimulator/collectorsimulator.go @@ -181,14 +181,14 @@ func (l *CollectorSimulator) Shutdown(ctx context.Context) ( simulationErrs = append(simulationErrs, reportedErr.Error()) } - collectorErrorLogs, err := os.ReadFile(l.collectorLogsOutputFilePath) + collectorWarnAndErrorLogs, err := os.ReadFile(l.collectorLogsOutputFilePath) if err != nil { return nil, model.InternalError(fmt.Errorf( "could not read collector logs from tmp file: %w", err, )) } - if len(collectorErrorLogs) > 0 { - errorLines := strings.Split(string(collectorErrorLogs), "\n") + if len(collectorWarnAndErrorLogs) > 0 { + errorLines := strings.Split(string(collectorWarnAndErrorLogs), "\n") simulationErrs = append(simulationErrs, errorLines...) } @@ -219,7 +219,7 @@ func generateSimulationConfig( metrics: level: none logs: - level: error + level: warn output_paths: ["%s"] `, receiverId, exporterId, collectorLogsOutputPath) From 3e65543b5f546a92d2e4cddacc1e821685c1d8bb Mon Sep 17 00:00:00 2001 From: Raj Kamal Singh <1133322+rkssisodiya@users.noreply.github.com> Date: Fri, 3 Nov 2023 18:42:03 +0530 Subject: [PATCH 05/28] Fix: resource filters should work in logs pipelines (#3889) * chore: add test validating resource based filters work in logs pipelines * fix: get resource filters working in logs pipelines --- .../pipelineBuilder_test.go | 59 +++++++++++++++++++ .../queryBuilderToExpr/queryBuilderToExpr.go | 6 +- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/pkg/query-service/app/logparsingpipeline/pipelineBuilder_test.go b/pkg/query-service/app/logparsingpipeline/pipelineBuilder_test.go index 47a3e70925..e696d24cc3 100644 --- a/pkg/query-service/app/logparsingpipeline/pipelineBuilder_test.go +++ b/pkg/query-service/app/logparsingpipeline/pipelineBuilder_test.go @@ -361,3 +361,62 @@ func TestNoCollectorErrorsFromProcessorsForMismatchedLogs(t *testing.T) { require.Equal(1, len(result)) } } + +func TestResourceFiltersWork(t *testing.T) { + require := require.New(t) + + testPipeline := Pipeline{ + OrderId: 1, + Name: "pipeline1", + Alias: "pipeline1", + Enabled: true, + Filter: &v3.FilterSet{ + Operator: "AND", + Items: []v3.FilterItem{ + { + Key: v3.AttributeKey{ + Key: "service", + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeResource, + }, + Operator: "=", + Value: "nginx", + }, + }, + }, + Config: []PipelineOperator{ + { + ID: "add", + Type: "add", + Enabled: true, + Name: "add", + Field: "attributes.test", + Value: "test-value", + }, + }, + } + + testLog := model.SignozLog{ + Timestamp: uint64(time.Now().UnixNano()), + Body: "test log", + Attributes_string: map[string]string{}, + Resources_string: map[string]string{ + "service": "nginx", + }, + SeverityText: entry.Info.String(), + SeverityNumber: uint8(entry.Info), + SpanID: "", + TraceID: "", + } + + result, collectorWarnAndErrorLogs, err := SimulatePipelinesProcessing( + context.Background(), + []Pipeline{testPipeline}, + []model.SignozLog{testLog}, + ) + require.Nil(err) + require.Equal(0, len(collectorWarnAndErrorLogs), strings.Join(collectorWarnAndErrorLogs, "\n")) + require.Equal(1, len(result)) + + require.Equal(result[0].Attributes_string["test"], "test-value") +} diff --git a/pkg/query-service/queryBuilderToExpr/queryBuilderToExpr.go b/pkg/query-service/queryBuilderToExpr/queryBuilderToExpr.go index f71470ff6e..ed68feb071 100644 --- a/pkg/query-service/queryBuilderToExpr/queryBuilderToExpr.go +++ b/pkg/query-service/queryBuilderToExpr/queryBuilderToExpr.go @@ -30,9 +30,9 @@ var logOperatorsToExpr = map[v3.FilterOperator]string{ func getName(v v3.AttributeKey) string { if v.Type == v3.AttributeKeyTypeTag { - return "attributes." + v.Key + return "attributes?." + v.Key } else if v.Type == v3.AttributeKeyTypeResource { - return "resources." + v.Key + return "resource?." + v.Key } return v.Key } @@ -41,7 +41,7 @@ func getTypeName(v v3.AttributeKeyType) string { if v == v3.AttributeKeyTypeTag { return "attributes" } else if v == v3.AttributeKeyTypeResource { - return "resources" + return "resource" } return "" } From dda01678e8a71f657910d630289b4b76d1efac35 Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Mon, 6 Nov 2023 12:16:15 +0530 Subject: [PATCH 06/28] fix: page break in Services overview tab (#3749) * fix: null check * fix: metrics check is updated --- .../MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx | 2 +- .../Tabs/Overview/ApDex/ApDexMetricsApplication.tsx | 2 +- frontend/src/types/api/metrics/getApDex.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx index ade8a1bec3..ac4ae1e03b 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx @@ -77,7 +77,7 @@ function ApDexMetrics({ const isQueryEnabled = topLevelOperationsRoute.length > 0 && - metricsBuckets && + !!metricsBuckets && metricsBuckets?.length > 0 && delta !== undefined; diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetricsApplication.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetricsApplication.tsx index 0e1486b852..62d4e80a15 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetricsApplication.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetricsApplication.tsx @@ -24,7 +24,7 @@ function ApDexMetricsApplication({ Date: Thu, 9 Nov 2023 16:00:02 +0530 Subject: [PATCH 07/28] refactor: global time range for promql query (#3935) --- .../src/container/FormAlertRules/index.tsx | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/frontend/src/container/FormAlertRules/index.tsx b/frontend/src/container/FormAlertRules/index.tsx index 023d6fc294..7b80b2150c 100644 --- a/frontend/src/container/FormAlertRules/index.tsx +++ b/frontend/src/container/FormAlertRules/index.tsx @@ -369,21 +369,7 @@ function FormAlertRules({ /> ); - const renderPromChartPreview = (): JSX.Element => ( - - } - name="Chart Preview" - query={stagedQuery} - alertDef={alertDef} - /> - ); - - const renderChQueryChartPreview = (): JSX.Element => ( + const renderPromAndChQueryChartPreview = (): JSX.Element => ( {currentQuery.queryType === EQueryType.QUERY_BUILDER && renderQBChartPreview()} - {currentQuery.queryType === EQueryType.PROM && renderPromChartPreview()} + {currentQuery.queryType === EQueryType.PROM && + renderPromAndChQueryChartPreview()} {currentQuery.queryType === EQueryType.CLICKHOUSE && - renderChQueryChartPreview()} + renderPromAndChQueryChartPreview()} From d165f727acebddb519d1c80e8ccae74642c843c9 Mon Sep 17 00:00:00 2001 From: Nityananda Gohain Date: Thu, 9 Nov 2023 18:35:52 +0530 Subject: [PATCH 08/28] fix: trace_parser removed (#3937) --- .../otel-collector-config.yaml | 31 +------------------ .../otel-collector-config.yaml | 31 +------------------ 2 files changed, 2 insertions(+), 60 deletions(-) diff --git a/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml b/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml index bc4fa3d3a8..29409919a7 100644 --- a/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml +++ b/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml @@ -61,35 +61,6 @@ receivers: job_name: otel-collector processors: - logstransform/internal: - operators: - - type: regex_parser - id: traceid - # https://regex101.com/r/MMfNjk/1 - regex: '(?i)(trace(-|_||)id("|=| |-|:)*)(?P[A-Fa-f0-9]+)' - parse_from: body - parse_to: attributes.temp_trace - if: 'body matches "(?i)(trace(-|_||)id(\"|=| |-|:)*)(?P[A-Fa-f0-9]+)"' - output: spanid - - type: regex_parser - id: spanid - # https://regex101.com/r/uXSwLc/1 - regex: '(?i)(span(-|_||)id("|=| |-|:)*)(?P[A-Fa-f0-9]+)' - parse_from: body - parse_to: attributes.temp_trace - if: 'body matches "(?i)(span(-|_||)id(\"|=| |-|:)*)(?P[A-Fa-f0-9]+)"' - output: trace_parser - - type: trace_parser - id: trace_parser - trace_id: - parse_from: attributes.temp_trace.trace_id - span_id: - parse_from: attributes.temp_trace.span_id - output: remove_temp - - type: remove - id: remove_temp - field: attributes.temp_trace - if: '"temp_trace" in attributes' batch: send_batch_size: 10000 send_batch_max_size: 11000 @@ -188,5 +159,5 @@ service: exporters: [prometheus] logs: receivers: [otlp, tcplog/docker] - processors: [logstransform/internal, batch] + processors: [batch] exporters: [clickhouselogsexporter] diff --git a/deploy/docker/clickhouse-setup/otel-collector-config.yaml b/deploy/docker/clickhouse-setup/otel-collector-config.yaml index fe6da5f126..204dcd9511 100644 --- a/deploy/docker/clickhouse-setup/otel-collector-config.yaml +++ b/deploy/docker/clickhouse-setup/otel-collector-config.yaml @@ -62,35 +62,6 @@ receivers: processors: - logstransform/internal: - operators: - - type: regex_parser - id: traceid - # https://regex101.com/r/MMfNjk/1 - regex: '(?i)(trace(-|_||)id("|=| |-|:)*)(?P[A-Fa-f0-9]+)' - parse_from: body - parse_to: attributes.temp_trace - if: 'body matches "(?i)(trace(-|_||)id(\"|=| |-|:)*)(?P[A-Fa-f0-9]+)"' - output: spanid - - type: regex_parser - id: spanid - # https://regex101.com/r/uXSwLc/1 - regex: '(?i)(span(-|_||)id("|=| |-|:)*)(?P[A-Fa-f0-9]+)' - parse_from: body - parse_to: attributes.temp_trace - if: 'body matches "(?i)(span(-|_||)id(\"|=| |-|:)*)(?P[A-Fa-f0-9]+)"' - output: trace_parser - - type: trace_parser - id: trace_parser - trace_id: - parse_from: attributes.temp_trace.trace_id - span_id: - parse_from: attributes.temp_trace.span_id - output: remove_temp - - type: remove - id: remove_temp - field: attributes.temp_trace - if: '"temp_trace" in attributes' batch: send_batch_size: 10000 send_batch_max_size: 11000 @@ -193,5 +164,5 @@ service: exporters: [prometheus] logs: receivers: [otlp, tcplog/docker] - processors: [logstransform/internal, batch] + processors: [batch] exporters: [clickhouselogsexporter] \ No newline at end of file From f939d41acdda82f9087c2aca6dd85df999fae339 Mon Sep 17 00:00:00 2001 From: Joe Milton <64725924+joemiltonm@users.noreply.github.com> Date: Thu, 9 Nov 2023 20:16:05 +0530 Subject: [PATCH 09/28] fix(tags): tag modification in triggered alerts page (#3873) Co-authored-by: Yunus M --- frontend/src/container/TriggeredAlerts/NoFilterTable.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/frontend/src/container/TriggeredAlerts/NoFilterTable.tsx b/frontend/src/container/TriggeredAlerts/NoFilterTable.tsx index 84219daf12..f5aca8d4d8 100644 --- a/frontend/src/container/TriggeredAlerts/NoFilterTable.tsx +++ b/frontend/src/container/TriggeredAlerts/NoFilterTable.tsx @@ -1,7 +1,8 @@ /* eslint-disable react/display-name */ -import { Tag, Typography } from 'antd'; +import { Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import { ResizeTable } from 'components/ResizeTable'; +import LabelColumn from 'components/TableRenderer/LabelColumn'; import AlertStatus from 'container/TriggeredAlerts/TableComponents/AlertStatus'; import convertDateToAmAndPm from 'lib/convertDateToAmAndPm'; import getFormattedDate from 'lib/getFormatedDate'; @@ -54,11 +55,7 @@ function NoFilterTable({ } return ( - <> - {withOutSeverityKeys.map((e) => ( - {`${e} : ${labels[e]}`} - ))} - + ); }, }, From 88aabb20600a32a762b8bbf74786646e3e9eabb5 Mon Sep 17 00:00:00 2001 From: Vikrant Gupta <54737045+Vikrant2520@users.noreply.github.com> Date: Thu, 9 Nov 2023 20:35:07 +0530 Subject: [PATCH 10/28] feat: added services page playwright tests (#3928) * feat: added services page playwright tests * feat: added empty page test --- frontend/tests/fixtures/api/services/200.json | 68 +++++++++++++++ .../tests/service/servicesLanding.spec.ts | 82 +++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 frontend/tests/fixtures/api/services/200.json create mode 100644 frontend/tests/service/servicesLanding.spec.ts diff --git a/frontend/tests/fixtures/api/services/200.json b/frontend/tests/fixtures/api/services/200.json new file mode 100644 index 0000000000..be0f923075 --- /dev/null +++ b/frontend/tests/fixtures/api/services/200.json @@ -0,0 +1,68 @@ +[ + { + "serviceName": "redis", + "p99": 35396180, + "avgDuration": 15149389.806977473, + "numCalls": 22329, + "callRate": 12.615254237288136, + "numErrors": 4135, + "errorRate": 18.51851851851852, + "num4XX": 0, + "fourXXRate": 0 + }, + { + "serviceName": "frontend", + "p99": 1173509510.0000002, + "avgDuration": 747007254.5344619, + "numCalls": 1654, + "callRate": 0.9344632768361582, + "numErrors": 0, + "errorRate": 0, + "num4XX": 0, + "fourXXRate": 0 + }, + { + "serviceName": "mysql", + "p99": 776834620, + "avgDuration": 349280732.76904476, + "numCalls": 1654, + "callRate": 0.9344632768361582, + "numErrors": 0, + "errorRate": 0, + "num4XX": 0, + "fourXXRate": 0 + }, + { + "serviceName": "customer", + "p99": 776995390, + "avgDuration": 349451783.5550181, + "numCalls": 1654, + "callRate": 0.9344632768361582, + "numErrors": 0, + "errorRate": 0, + "num4XX": 0, + "fourXXRate": 0 + }, + { + "serviceName": "route", + "p99": 79617600.00000001, + "avgDuration": 50698870.85852479, + "numCalls": 16540, + "callRate": 9.344632768361581, + "numErrors": 0, + "errorRate": 0, + "num4XX": 0, + "fourXXRate": 0 + }, + { + "serviceName": "driver", + "p99": 241056990, + "avgDuration": 204975300.48367593, + "numCalls": 1654, + "callRate": 0.9344632768361582, + "numErrors": 0, + "errorRate": 0, + "num4XX": 0, + "fourXXRate": 0 + } +] diff --git a/frontend/tests/service/servicesLanding.spec.ts b/frontend/tests/service/servicesLanding.spec.ts new file mode 100644 index 0000000000..88896a7a78 --- /dev/null +++ b/frontend/tests/service/servicesLanding.spec.ts @@ -0,0 +1,82 @@ +import { expect, Page, test } from '@playwright/test'; +import ROUTES from 'constants/routes'; + +import servicesSuccessResponse from '../fixtures/api/services/200.json'; +import { loginApi } from '../fixtures/common'; + +let page: Page; + +test.describe('Service Page', () => { + test.beforeEach(async ({ baseURL, browser }) => { + const context = await browser.newContext({ storageState: 'tests/auth.json' }); + const newPage = await context.newPage(); + + await loginApi(newPage); + + await newPage.goto(`${baseURL}${ROUTES.APPLICATION}`); + + page = newPage; + }); + + test('Services Empty Page', async ({ baseURL }) => { + // visit services page + await page.goto(`${baseURL}${ROUTES.APPLICATION}`); + + await page.route(`**/services`, (route) => + route.fulfill({ + status: 200, + json: [], + }), + ); + + // expect noData to be present + await expect(page.getByText('No data')).toBeVisible(); + }); + + test('Services Table Rendered with correct data', async ({ baseURL }) => { + // visit services page + await page.goto(`${baseURL}${ROUTES.APPLICATION}`); + + // assert the URL of the services page + await expect(page).toHaveURL(`${baseURL}${ROUTES.APPLICATION}`); + + await page.route(`**/services`, (route) => + route.fulfill({ + status: 200, + json: servicesSuccessResponse, + }), + ); + + // assert the presence of services breadcrumbs + const breadcrumbServicesText = await page + .locator('.ant-breadcrumb-link a[href="/services"]') + .nth(1) + .textContent(); + await expect(breadcrumbServicesText).toEqual('Services'); + + // expect the services headers to be loaded correctly + const p99Latency = await page + .locator( + `th[aria-label*="this column's title is P99 latency (in ms)"] .ant-table-column-title`, + ) + .textContent(); + + await expect(p99Latency).toEqual('P99 latency (in ms)'); + const errorRate = await page + .locator( + `th[aria-label*="this column's title is Error Rate (% of total)"] .ant-table-column-title`, + ) + .textContent(); + + await expect(errorRate).toEqual('Error Rate (% of total)'); + const operationsPerSecond = await page + .locator( + `th[aria-label="this column's title is Operations Per Second,this column is sortable"] .ant-table-column-title`, + ) + .textContent(); + + await expect(operationsPerSecond).toEqual('Operations Per Second'); + // expect services to be listed in the table + await page.locator('a[href="/services/redis"]').isVisible(); + }); +}); From 30b0d42604aeac39f6a725552500866b4f9a092f Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Fri, 10 Nov 2023 11:16:42 +0530 Subject: [PATCH 11/28] fix: try Signoz is visible on success (#3906) --- frontend/src/container/Header/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/container/Header/index.tsx b/frontend/src/container/Header/index.tsx index a930143a9c..c7eb709452 100644 --- a/frontend/src/container/Header/index.tsx +++ b/frontend/src/container/Header/index.tsx @@ -104,7 +104,7 @@ function HeaderContainer(): JSX.Element { ); }; - const { data: licenseData, isFetching } = useLicense(); + const { data: licenseData, isFetching, status: licenseStatus } = useLicense(); const isLicenseActive = licenseData?.payload?.licenses?.find((e) => e.isCurrent)?.status === @@ -169,7 +169,7 @@ function HeaderContainer(): JSX.Element { - {!isLicenseActive && ( + {licenseStatus === 'success' && !isLicenseActive && ( From 6b2f857a12e7b796c28655842f4b9b48a7967f94 Mon Sep 17 00:00:00 2001 From: CheetoDa <31571545+Calm-Rock@users.noreply.github.com> Date: Fri, 10 Nov 2023 13:09:25 +0530 Subject: [PATCH 12/28] docs: added ror onboarding docs (#3927) * docs: added ror onboarding docs * feat: add ror docs * feat: update ror details in connection status --------- Co-authored-by: Yunus A M Co-authored-by: Vishal Sharma --- .../container/OnboardingContainer/APM/APM.tsx | 7 + .../APM/Javascript/md-docs/express.md | 24 +-- .../APM/Javascript/md-docs/javascript.md | 24 +-- .../APM/Javascript/md-docs/nestjs.md | 173 +++++++++-------- .../APM/RubyOnRails/ROR.styles.scss | 0 .../APM/RubyOnRails/ROR.tsx | 68 +++++++ .../APM/RubyOnRails/RubyOnRails.md | 178 ++++++++++++++++++ .../ConnectionStatus/ConnectionStatus.tsx | 12 +- .../LogsManagement/Docker/docker.md | 27 ++- 9 files changed, 395 insertions(+), 118 deletions(-) create mode 100644 frontend/src/container/OnboardingContainer/APM/RubyOnRails/ROR.styles.scss create mode 100644 frontend/src/container/OnboardingContainer/APM/RubyOnRails/ROR.tsx create mode 100644 frontend/src/container/OnboardingContainer/APM/RubyOnRails/RubyOnRails.md diff --git a/frontend/src/container/OnboardingContainer/APM/APM.tsx b/frontend/src/container/OnboardingContainer/APM/APM.tsx index 95080a92d1..ef81752345 100644 --- a/frontend/src/container/OnboardingContainer/APM/APM.tsx +++ b/frontend/src/container/OnboardingContainer/APM/APM.tsx @@ -12,6 +12,7 @@ import GoLang from './GoLang/GoLang'; import Java from './Java/Java'; import Javascript from './Javascript/Javascript'; import Python from './Python/Python'; +import RoR from './RubyOnRails/ROR'; interface IngestionInfoProps { SIGNOZ_INGESTION_KEY?: string; @@ -39,6 +40,10 @@ const supportedLanguages = [ name: 'go', imgURL: `Logos/java.png`, }, + { + name: 'rails', + imgURL: `Logos/rails.png`, + }, ]; export default function APM({ @@ -91,6 +96,8 @@ export default function APM({ return ; case 'go': return ; + case 'rails': + return ; default: return <> ; } diff --git a/frontend/src/container/OnboardingContainer/APM/Javascript/md-docs/express.md b/frontend/src/container/OnboardingContainer/APM/Javascript/md-docs/express.md index 28d2999047..ca724ee2c5 100644 --- a/frontend/src/container/OnboardingContainer/APM/Javascript/md-docs/express.md +++ b/frontend/src/container/OnboardingContainer/APM/Javascript/md-docs/express.md @@ -20,10 +20,10 @@ From VMs, there are two ways to send data to SigNoz Cloud. Step 1. Install OpenTelemetry packages ```bash -npm install --save @opentelemetry/api@^1.4.1 -npm install --save @opentelemetry/sdk-node@^0.39.1 -npm install --save @opentelemetry/auto-instrumentations-node@^0.37.0 -npm install --save @opentelemetry/exporter-trace-otlp-http@^0.39.1 +npm install --save @opentelemetry/api@^1.6.0 +npm install --save @opentelemetry/sdk-node@^0.45.0 +npm install --save @opentelemetry/auto-instrumentations-node@^0.39.4 +npm install --save @opentelemetry/exporter-trace-otlp-http@^0.45.0 ``` Step 2. Create tracing.js file @@ -87,10 +87,10 @@ You can find instructions to install OTel Collector binary [here](https://signoz Step 1. Install OpenTelemetry packages ```js -npm install --save @opentelemetry/api@^1.4.1 -npm install --save @opentelemetry/sdk-node@^0.39.1 -npm install --save @opentelemetry/auto-instrumentations-node@^0.37.0 -npm install --save @opentelemetry/exporter-trace-otlp-http@^0.39.1 +npm install --save @opentelemetry/api@^1.6.0 +npm install --save @opentelemetry/sdk-node@^0.45.0 +npm install --save @opentelemetry/auto-instrumentations-node@^0.39.4 +npm install --save @opentelemetry/exporter-trace-otlp-http@^0.45.0 ``` Step 2. Create tracing.js file @@ -148,10 +148,10 @@ Once you have set up OTel Collector agent, you can proceed with OpenTelemetry Ja Step 1. Install OpenTelemetry packages ```bash -npm install --save @opentelemetry/api@^1.4.1 -npm install --save @opentelemetry/sdk-node@^0.39.1 -npm install --save @opentelemetry/auto-instrumentations-node@^0.37.0 -npm install --save @opentelemetry/exporter-trace-otlp-http@^0.39.1 +npm install --save @opentelemetry/api@^1.6.0 +npm install --save @opentelemetry/sdk-node@^0.45.0 +npm install --save @opentelemetry/auto-instrumentations-node@^0.39.4 +npm install --save @opentelemetry/exporter-trace-otlp-http@^0.45.0 ``` Step 2. Create tracing.js file diff --git a/frontend/src/container/OnboardingContainer/APM/Javascript/md-docs/javascript.md b/frontend/src/container/OnboardingContainer/APM/Javascript/md-docs/javascript.md index 7bb0198754..a98ef2fc42 100644 --- a/frontend/src/container/OnboardingContainer/APM/Javascript/md-docs/javascript.md +++ b/frontend/src/container/OnboardingContainer/APM/Javascript/md-docs/javascript.md @@ -18,10 +18,10 @@ From VMs, there are two ways to send data to SigNoz Cloud. Step 1. Install OpenTelemetry packages ```bash -npm install --save @opentelemetry/api@^1.4.1 -npm install --save @opentelemetry/sdk-node@^0.39.1 -npm install --save @opentelemetry/auto-instrumentations-node@^0.37.0 -npm install --save @opentelemetry/exporter-trace-otlp-http@^0.39.1 +npm install --save @opentelemetry/api@^1.6.0 +npm install --save @opentelemetry/sdk-node@^0.45.0 +npm install --save @opentelemetry/auto-instrumentations-node@^0.39.4 +npm install --save @opentelemetry/exporter-trace-otlp-http@^0.45.0 ``` Step 2. Create tracing.js file @@ -86,10 +86,10 @@ You can find instructions to install OTel Collector binary [here](https://signoz Step 1. Install OpenTelemetry packages ```js -npm install --save @opentelemetry/api@^1.4.1 -npm install --save @opentelemetry/sdk-node@^0.39.1 -npm install --save @opentelemetry/auto-instrumentations-node@^0.37.0 -npm install --save @opentelemetry/exporter-trace-otlp-http@^0.39.1 +npm install --save @opentelemetry/api@^1.6.0 +npm install --save @opentelemetry/sdk-node@^0.45.0 +npm install --save @opentelemetry/auto-instrumentations-node@^0.39.4 +npm install --save @opentelemetry/exporter-trace-otlp-http@^0.45.0 ``` Step 2. Create tracing.js file @@ -149,10 +149,10 @@ Once you have set up OTel Collector agent, you can proceed with OpenTelemetry Ja Step 1. Install OpenTelemetry packages ```js -npm install --save @opentelemetry/api@^1.4.1 -npm install --save @opentelemetry/sdk-node@^0.39.1 -npm install --save @opentelemetry/auto-instrumentations-node@^0.37.0 -npm install --save @opentelemetry/exporter-trace-otlp-http@^0.39.1 +npm install --save @opentelemetry/api@^1.6.0 +npm install --save @opentelemetry/sdk-node@^0.45.0 +npm install --save @opentelemetry/auto-instrumentations-node@^0.39.4 +npm install --save @opentelemetry/exporter-trace-otlp-http@^0.45.0 ``` Step 2. Create tracing.js file diff --git a/frontend/src/container/OnboardingContainer/APM/Javascript/md-docs/nestjs.md b/frontend/src/container/OnboardingContainer/APM/Javascript/md-docs/nestjs.md index 3d23c81c8e..b947dc3b07 100644 --- a/frontend/src/container/OnboardingContainer/APM/Javascript/md-docs/nestjs.md +++ b/frontend/src/container/OnboardingContainer/APM/Javascript/md-docs/nestjs.md @@ -20,10 +20,10 @@ From VMs, there are two ways to send data to SigNoz Cloud. Step 1. Install OpenTelemetry packages ```bash -npm install --save @opentelemetry/api@^1.4.1 -npm install --save @opentelemetry/sdk-node@^0.39.1 -npm install --save @opentelemetry/auto-instrumentations-node@^0.37.0 -npm install --save @opentelemetry/exporter-trace-otlp-http@^0.39.1 +npm install --save @opentelemetry/api@^1.6.0 +npm install --save @opentelemetry/sdk-node@^0.45.0 +npm install --save @opentelemetry/auto-instrumentations-node@^0.39.4 +npm install --save @opentelemetry/exporter-trace-otlp-http@^0.45.0 ``` Step 2. Create `tracer.ts` file @@ -31,40 +31,43 @@ Step 2. Create `tracer.ts` file This file will have your SigNoz cloud endpoint and service name configued as values of `url` and `SERVICE_NAME` respectively. ```js -'use strict' -const process = require('process'); -const opentelemetry = require('@opentelemetry/sdk-node'); -const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); -const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http'); -const {Resource} = require('@opentelemetry/resources'); -const {SemanticResourceAttributes} = require('@opentelemetry/semantic-conventions'); +'use strict'; +import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; +import { Resource } from '@opentelemetry/resources'; +import * as opentelemetry from '@opentelemetry/sdk-node'; +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; + +// Configure the SDK to export telemetry data to the console +// Enable all auto-instrumentations from the meta package const exporterOptions = { - url: 'https://ingest.{{REGION}}.signoz.cloud:443/v1/traces' - } + url: 'https://ingest.{{REGION}}.signoz.cloud:443/v1/traces', +}; const traceExporter = new OTLPTraceExporter(exporterOptions); const sdk = new opentelemetry.NodeSDK({ traceExporter, instrumentations: [getNodeAutoInstrumentations()], resource: new Resource({ - [SemanticResourceAttributes.SERVICE_NAME]: '{{MYAPP}}' - }) - }); - - // initialize the SDK and register with the OpenTelemetry API - // this enables the API to record telemetry - sdk.start() - - // gracefully shut down the SDK on process exit - process.on('SIGTERM', () => { - sdk.shutdown() + [SemanticResourceAttributes.SERVICE_NAME]: '{{MYAPP}}', + }), +}); + +// initialize the SDK and register with the OpenTelemetry API +// this enables the API to record telemetry +sdk.start(); + +// gracefully shut down the SDK on process exit +process.on('SIGTERM', () => { + sdk + .shutdown() .then(() => console.log('Tracing terminated')) .catch((error) => console.log('Error terminating tracing', error)) .finally(() => process.exit(0)); - }); - - module.exports = sdk +}); + +export default sdk; ``` Step 3. Import the tracer module where your app starts `(Ex —> main.ts)` @@ -112,10 +115,10 @@ You can find instructions to install OTel Collector binary [here](https://signoz Step 1. Install OpenTelemetry packages ```js -npm install --save @opentelemetry/api@^1.4.1 -npm install --save @opentelemetry/sdk-node@^0.39.1 -npm install --save @opentelemetry/auto-instrumentations-node@^0.37.0 -npm install --save @opentelemetry/exporter-trace-otlp-http@^0.39.1 +npm install --save @opentelemetry/api@^1.6.0 +npm install --save @opentelemetry/sdk-node@^0.45.0 +npm install --save @opentelemetry/auto-instrumentations-node@^0.39.4 +npm install --save @opentelemetry/exporter-trace-otlp-http@^0.45.0 ``` Step 2. Create `tracer.ts` file @@ -123,41 +126,43 @@ Step 2. Create `tracer.ts` file This file will have your service name configued as value for `SERVICE_NAME`. ```js -'use strict' -const process = require('process'); -//OpenTelemetry -const opentelemetry = require('@opentelemetry/sdk-node'); -const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); -const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http'); -const {Resource} = require('@opentelemetry/resources'); -const {SemanticResourceAttributes} = require('@opentelemetry/semantic-conventions'); +'use strict'; +import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; +import { Resource } from '@opentelemetry/resources'; +import * as opentelemetry from '@opentelemetry/sdk-node'; +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; + +// Configure the SDK to export telemetry data to the console +// Enable all auto-instrumentations from the meta package const exporterOptions = { - url: 'http://localhost:4318/v1/traces' - } + url: 'http://localhost:4318/v1/traces', +}; const traceExporter = new OTLPTraceExporter(exporterOptions); const sdk = new opentelemetry.NodeSDK({ traceExporter, instrumentations: [getNodeAutoInstrumentations()], resource: new Resource({ - [SemanticResourceAttributes.SERVICE_NAME]: '{{MYAPP}}' - }) - }); - - // initialize the SDK and register with the OpenTelemetry API - // this enables the API to record telemetry - sdk.start() - - // gracefully shut down the SDK on process exit - process.on('SIGTERM', () => { - sdk.shutdown() + [SemanticResourceAttributes.SERVICE_NAME]: '{{MYAPP}}', + }), +}); + +// initialize the SDK and register with the OpenTelemetry API +// this enables the API to record telemetry +sdk.start(); + +// gracefully shut down the SDK on process exit +process.on('SIGTERM', () => { + sdk + .shutdown() .then(() => console.log('Tracing terminated')) .catch((error) => console.log('Error terminating tracing', error)) .finally(() => process.exit(0)); - }); - - module.exports = sdk +}); + +export default sdk; ``` Step 3. Import the tracer module where your app starts @@ -200,10 +205,10 @@ Once you have set up OTel Collector agent, you can proceed with OpenTelemetry Ja Step 1. Install OpenTelemetry packages ```bash -npm install --save @opentelemetry/api@^1.4.1 -npm install --save @opentelemetry/sdk-node@^0.39.1 -npm install --save @opentelemetry/auto-instrumentations-node@^0.37.0 -npm install --save @opentelemetry/exporter-trace-otlp-http@^0.39.1 +npm install --save @opentelemetry/api@^1.6.0 +npm install --save @opentelemetry/sdk-node@^0.45.0 +npm install --save @opentelemetry/auto-instrumentations-node@^0.39.4 +npm install --save @opentelemetry/exporter-trace-otlp-http@^0.45.0 ``` Step 2. Create `tracer.ts` file @@ -211,41 +216,43 @@ Step 2. Create `tracer.ts` file This file will have your service name configued as value for `SERVICE_NAME`. ```js -'use strict' -const process = require('process'); -//OpenTelemetry -const opentelemetry = require('@opentelemetry/sdk-node'); -const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); -const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http'); -const {Resource} = require('@opentelemetry/resources'); -const {SemanticResourceAttributes} = require('@opentelemetry/semantic-conventions'); +'use strict'; +import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; +import { Resource } from '@opentelemetry/resources'; +import * as opentelemetry from '@opentelemetry/sdk-node'; +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; + +// Configure the SDK to export telemetry data to the console +// Enable all auto-instrumentations from the meta package const exporterOptions = { - url: 'http://localhost:4318/v1/traces' - } + url: 'http://localhost:4318/v1/traces', +}; const traceExporter = new OTLPTraceExporter(exporterOptions); const sdk = new opentelemetry.NodeSDK({ traceExporter, instrumentations: [getNodeAutoInstrumentations()], resource: new Resource({ - [SemanticResourceAttributes.SERVICE_NAME]: '{{MYAPP}}' - }) - }); - - // initialize the SDK and register with the OpenTelemetry API - // this enables the API to record telemetry - sdk.start() - - // gracefully shut down the SDK on process exit - process.on('SIGTERM', () => { - sdk.shutdown() + [SemanticResourceAttributes.SERVICE_NAME]: '{{MYAPP}}', + }), +}); + +// initialize the SDK and register with the OpenTelemetry API +// this enables the API to record telemetry +sdk.start(); + +// gracefully shut down the SDK on process exit +process.on('SIGTERM', () => { + sdk + .shutdown() .then(() => console.log('Tracing terminated')) .catch((error) => console.log('Error terminating tracing', error)) .finally(() => process.exit(0)); - }); - - module.exports = sdk +}); + +export default sdk; ``` Step 3. Import the tracer module where your app starts diff --git a/frontend/src/container/OnboardingContainer/APM/RubyOnRails/ROR.styles.scss b/frontend/src/container/OnboardingContainer/APM/RubyOnRails/ROR.styles.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/container/OnboardingContainer/APM/RubyOnRails/ROR.tsx b/frontend/src/container/OnboardingContainer/APM/RubyOnRails/ROR.tsx new file mode 100644 index 0000000000..c19dbf34db --- /dev/null +++ b/frontend/src/container/OnboardingContainer/APM/RubyOnRails/ROR.tsx @@ -0,0 +1,68 @@ +import './ROR.styles.scss'; + +import { Form, Input } from 'antd'; +import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer'; +import Header from 'container/OnboardingContainer/common/Header/Header'; + +import { LangProps } from '../APM'; +import ConnectionStatus from '../common/ConnectionStatus/ConnectionStatus'; +import RORDocs from './RubyOnRails.md'; + +export default function RoR({ + ingestionInfo, + activeStep, +}: LangProps): JSX.Element { + const [form] = Form.useForm(); + const serviceName = Form.useWatch('Service Name', form); + + const variables = { + MYAPP: serviceName || '', + SIGNOZ_INGESTION_KEY: + ingestionInfo.SIGNOZ_INGESTION_KEY || '', + REGION: ingestionInfo.REGION || 'region', + }; + + return ( + <> + {activeStep === 2 && ( +
+
+ +
+
+
Service Name
+ +
+ + + +
+
+
+ +
+ +
+
+ )} + {activeStep === 3 && ( + + )} + + ); +} diff --git a/frontend/src/container/OnboardingContainer/APM/RubyOnRails/RubyOnRails.md b/frontend/src/container/OnboardingContainer/APM/RubyOnRails/RubyOnRails.md new file mode 100644 index 0000000000..3f8eac1455 --- /dev/null +++ b/frontend/src/container/OnboardingContainer/APM/RubyOnRails/RubyOnRails.md @@ -0,0 +1,178 @@ +## Send Traces to SigNoz Cloud + +### Application on VMs + +From VMs, there are two ways to send data to SigNoz Cloud. + +- Send traces directly to SigNoz Cloud (quick start) +- Send traces via OTel Collector binary (recommended) + +#### **Send traces directly to SigNoz Cloud** + +**Step 1. Install dependencies** + +Install dependencies related to OpenTelemetry SDK and exporter using gem. + +```go +gem install opentelemetry-sdk +gem install opentelemetry-exporter-otlp +gem install opentelemetry-instrumentation-all +``` + +Include the required packages into your gemfile. + +```go +gem 'opentelemetry-sdk' +gem 'opentelemetry-exporter-otlp' +gem 'opentelemetry-instrumentation-all' +``` + +Run the bundle install command: + +```go +bundle install +``` + +**Step 2. Initialize the OpenTelemetry SDK** + +Initialize the otel sdk by adding below lines to `config/environment.rb` of your Ruby on Rails application. + +```jsx +require 'opentelemetry/sdk' +require_relative 'application' + +OpenTelemetry::SDK.configure do |c| + c.use_all +end + +Rails.application.initialize! +``` + +**Step 3. Running your Ruby application** + +Run the application using the below: + +```jsx +OTEL_EXPORTER=otlp \ +OTEL_SERVICE_NAME={{MYAPP}} \ +OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.{{REGION}}.signoz.cloud:443 \ +OTEL_EXPORTER_OTLP_HEADERS=signoz-access-token={{SIGNOZ_INGESTION_KEY}} \ +rails server +``` + +--- +#### **Send traces via OTel Collector binary** + +OTel Collector binary helps to collect logs, hostmetrics, resource and infra attributes. It is recommended to install Otel Collector binary to collect and send traces to SigNoz cloud. You can correlate signals and have rich contextual data through this way. + +You can find instructions to install OTel Collector binary [here](https://signoz.io/docs/tutorial/opentelemetry-binary-usage-in-virtual-machine/) in your VM. Once you are done setting up your OTel Collector binary, you can follow the below steps for instrumenting your Ruby on Rails application. + +**Step 1. Install dependencies** + +Install dependencies related to OpenTelemetry SDK and exporter using gem. + +```go +gem install opentelemetry-sdk +gem install opentelemetry-exporter-otlp +gem install opentelemetry-instrumentation-all +``` + +Include the required packages into your gemfile. + +```go +gem 'opentelemetry-sdk' +gem 'opentelemetry-exporter-otlp' +gem 'opentelemetry-instrumentation-all' +``` + +Run the bundle install command: + +```go +bundle install +``` + +**Step 2. Initialize the OpenTelemetry SDK** + +Initialize the otel sdk by adding below lines to `config/environment.rb` of your Ruby on Rails application. + +```jsx +require 'opentelemetry/sdk' +require_relative 'application' + +OpenTelemetry::SDK.configure do |c| + c.use_all +end + +Rails.application.initialize! +``` + +**Step 3. Running your Ruby application** + +Run the application using the below: + +```jsx +OTEL_EXPORTER=otlp \ +OTEL_SERVICE_NAME={{MYAPP}} \ +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \ +rails server +``` + +In case you have OtelCollector Agent in different VM, replace localhost:4318 with `:4318`. + +--- + +### Applications Deployed on Kubernetes + +For Ruby on Rails application deployed on Kubernetes, you need to install OTel Collector agent in your k8s infra to collect and send traces to SigNoz Cloud. You can find the instructions to install OTel Collector agent [here](https://signoz.io/docs/tutorial/kubernetes-infra-metrics/). + + + +**Step 1. Install dependencies** + +Install dependencies related to OpenTelemetry SDK and exporter using gem. + +```go +gem install opentelemetry-sdk +gem install opentelemetry-exporter-otlp +gem install opentelemetry-instrumentation-all +``` + +Include the required packages into your gemfile. + +```go +gem 'opentelemetry-sdk' +gem 'opentelemetry-exporter-otlp' +gem 'opentelemetry-instrumentation-all' +``` + +Run the bundle install command: + +```go +bundle install +``` + +**Step 2. Initialize the OpenTelemetry SDK** + +Initialize the otel sdk by adding below lines to `config/environment.rb` of your Ruby on Rails application. + +```jsx +require 'opentelemetry/sdk' +require_relative 'application' + +OpenTelemetry::SDK.configure do |c| + c.use_all +end + +Rails.application.initialize! +``` + +**Step 3. Running your Ruby application** + +Run the application using the below: + +```jsx +OTEL_EXPORTER=otlp \ +OTEL_SERVICE_NAME={{MYAPP}} \ +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \ +rails server +``` diff --git a/frontend/src/container/OnboardingContainer/APM/common/ConnectionStatus/ConnectionStatus.tsx b/frontend/src/container/OnboardingContainer/APM/common/ConnectionStatus/ConnectionStatus.tsx index de918dced1..02b139dfb4 100644 --- a/frontend/src/container/OnboardingContainer/APM/common/ConnectionStatus/ConnectionStatus.tsx +++ b/frontend/src/container/OnboardingContainer/APM/common/ConnectionStatus/ConnectionStatus.tsx @@ -24,7 +24,7 @@ interface ConnectionStatusProps { framework: string; } -const pollingInterval = 15000; +const pollingInterval = 10000; export default function ConnectionStatus({ serviceName, @@ -103,6 +103,16 @@ export default function ConnectionStatus({ imgClassName="supported-language-img" /> ); + case 'rails': + return ( +
+ ); default: return <> ; diff --git a/frontend/src/container/OnboardingContainer/LogsManagement/Docker/docker.md b/frontend/src/container/OnboardingContainer/LogsManagement/Docker/docker.md index 1c1025963a..d9be2ca45c 100644 --- a/frontend/src/container/OnboardingContainer/LogsManagement/Docker/docker.md +++ b/frontend/src/container/OnboardingContainer/LogsManagement/Docker/docker.md @@ -1,23 +1,30 @@ ## Collect Docker Container Logs in SigNoz Cloud -- Clone this [repository](https://github.com/SigNoz/docker-container-logs) +**Step 1. Clone this repository** -- Update `otel-collector-config.yaml` and set the values of `` and `{region}`. +Clone the GitHub repository as a first step to collect logs - Depending on the choice of your region for SigNoz cloud, the ingest endpoint will vary accordingly. +```bash +git clone https://github.com/SigNoz/docker-container-logs.git +``` - US - ingest.us.signoz.cloud:443 +**Step 2. Update your `.env` file** - IN - ingest.in.signoz.cloud:443 +In the repository that you cloned above, update `.env` file by putting the values of `` and `{region}`. - EU - ingest.eu.signoz.cloud:443 +Depending on the choice of your region for SigNoz cloud, the ingest endpoint will vary accordingly. +US - ingest.us.signoz.cloud:443 - - Start the containers +IN - ingest.in.signoz.cloud:443 + +EU - ingest.eu.signoz.cloud:443 + +**Step 3. Start the containers** - ```bash + ```bash docker compose up -d - ``` + ``` - - If there are no errors your logs will be exported and will be visible on the SigNoz UI. +If there are no errors your logs will be exported and will be visible on the SigNoz UI. From ddc3cc4911fad7515b4dd3fc73a2d806eb5f74e5 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Fri, 10 Nov 2023 17:43:19 +0530 Subject: [PATCH 13/28] chore: dashboards to alerts creation support in query-service (#3924) --- pkg/query-service/rules/alerting.go | 1 + pkg/query-service/rules/manager.go | 12 +++ pkg/query-service/rules/promRule.go | 19 ++++- pkg/query-service/rules/thresholdRule.go | 94 +++++++++++++----------- 4 files changed, 81 insertions(+), 45 deletions(-) diff --git a/pkg/query-service/rules/alerting.go b/pkg/query-service/rules/alerting.go index 60cb4c9384..ad82470e83 100644 --- a/pkg/query-service/rules/alerting.go +++ b/pkg/query-service/rules/alerting.go @@ -144,6 +144,7 @@ type RuleCondition struct { Target *float64 `yaml:"target,omitempty" json:"target,omitempty"` MatchType `json:"matchType,omitempty"` TargetUnit string `json:"targetUnit,omitempty"` + SelectedQuery string `json:"selectedQueryName,omitempty"` } func (rc *RuleCondition) IsValid() bool { diff --git a/pkg/query-service/rules/manager.go b/pkg/query-service/rules/manager.go index 30c643b031..a91ff88101 100644 --- a/pkg/query-service/rules/manager.go +++ b/pkg/query-service/rules/manager.go @@ -714,6 +714,18 @@ func (m *Manager) GetRule(ctx context.Context, id string) (*GettableRule, error) return nil, err } r.Id = fmt.Sprintf("%d", s.Id) + // fetch state of rule from memory + if rm, ok := m.rules[r.Id]; !ok { + r.State = StateDisabled.String() + r.Disabled = true + } else { + r.State = rm.State().String() + } + r.CreatedAt = s.CreatedAt + r.CreatedBy = s.CreatedBy + r.UpdatedAt = s.UpdatedAt + r.UpdatedBy = s.UpdatedBy + return r, nil } diff --git a/pkg/query-service/rules/promRule.go b/pkg/query-service/rules/promRule.go index 25209b21c8..6d0cafa930 100644 --- a/pkg/query-service/rules/promRule.go +++ b/pkg/query-service/rules/promRule.go @@ -297,11 +297,28 @@ func (r *PromRule) SendAlerts(ctx context.Context, ts time.Time, resendDelay tim notifyFunc(ctx, "", alerts...) } +func (r *PromRule) GetSelectedQuery() string { + if r.ruleCondition != nil { + // If the user has explicitly set the selected query, we return that. + if r.ruleCondition.SelectedQuery != "" { + return r.ruleCondition.SelectedQuery + } + // Historically, we used to have only one query in the alerts for promql. + // So, if there is only one query, we return that. + // This is to maintain backward compatibility. + // For new rules, we will have to explicitly set the selected query. + return "A" + } + // This should never happen. + return "" +} + func (r *PromRule) getPqlQuery() (string, error) { if r.ruleCondition.CompositeQuery.QueryType == v3.QueryTypePromQL { if len(r.ruleCondition.CompositeQuery.PromQueries) > 0 { - if promQuery, ok := r.ruleCondition.CompositeQuery.PromQueries["A"]; ok { + selectedQuery := r.GetSelectedQuery() + if promQuery, ok := r.ruleCondition.CompositeQuery.PromQueries[selectedQuery]; ok { query := promQuery.Query if query == "" { return query, fmt.Errorf("a promquery needs to be set for this rule to function") diff --git a/pkg/query-service/rules/thresholdRule.go b/pkg/query-service/rules/thresholdRule.go index c4d6fdbf12..62b8b97635 100644 --- a/pkg/query-service/rules/thresholdRule.go +++ b/pkg/query-service/rules/thresholdRule.go @@ -135,13 +135,6 @@ func (r *ThresholdRule) PreferredChannels() []string { return r.preferredChannels } -func (r *ThresholdRule) target() *float64 { - if r.ruleCondition == nil { - return nil - } - return r.ruleCondition.Target -} - func (r *ThresholdRule) targetVal() float64 { if r.ruleCondition == nil || r.ruleCondition.Target == nil { return 0 @@ -217,26 +210,6 @@ func (r *ThresholdRule) Annotations() labels.BaseLabels { return r.annotations } -func (r *ThresholdRule) sample(alert *Alert, ts time.Time) Sample { - lb := labels.NewBuilder(r.labels) - alertLabels := alert.Labels.(labels.Labels) - for _, l := range alertLabels { - lb.Set(l.Name, l.Value) - } - - lb.Set(labels.MetricNameLabel, alertMetricName) - lb.Set(labels.AlertNameLabel, r.name) - lb.Set(labels.AlertRuleIdLabel, r.ID()) - lb.Set(labels.AlertStateLabel, alert.State.String()) - - s := Sample{ - Metric: lb.Labels(), - Point: Point{T: timestamp.FromTime(ts), V: 1}, - } - - return s -} - // GetEvaluationDuration returns the time in seconds it took to evaluate the alerting rule. func (r *ThresholdRule) GetEvaluationDuration() time.Duration { r.mtx.Lock() @@ -682,6 +655,54 @@ func (r *ThresholdRule) prepareClickhouseQueries(ts time.Time) (map[string]strin return queries, nil } +func (r *ThresholdRule) GetSelectedQuery() string { + + // The acutal query string is not relevant here + // we just need to know the selected query + + var queries map[string]string + var err error + + if r.ruleCondition.QueryType() == v3.QueryTypeBuilder { + queries, err = r.prepareBuilderQueries(time.Now()) + if err != nil { + zap.S().Errorf("ruleid:", r.ID(), "\t msg: failed to prepare metric queries", zap.Error(err)) + return "" + } + } else if r.ruleCondition.QueryType() == v3.QueryTypeClickHouseSQL { + queries, err = r.prepareClickhouseQueries(time.Now()) + if err != nil { + zap.S().Errorf("ruleid:", r.ID(), "\t msg: failed to prepare clickhouse queries", zap.Error(err)) + return "" + } + } + + if r.ruleCondition != nil { + if r.ruleCondition.SelectedQuery != "" { + return r.ruleCondition.SelectedQuery + } + + // The following logic exists for backward compatibility + // If there is no selected query, then + // - check if F1 is present, if yes, return F1 + // - else return the query with max ascii value + // this logic is not really correct. we should be considering + // whether the query is enabled or not. but this is a temporary + // fix to support backward compatibility + if _, ok := queries["F1"]; ok { + return "F1" + } + keys := make([]string, 0, len(queries)) + for k := range queries { + keys = append(keys, k) + } + sort.Strings(keys) + return keys[len(keys)-1] + } + // This should never happen + return "" +} + // query looks if alert condition is being // satisfied and returns the signals func (r *ThresholdRule) buildAndRunQuery(ctx context.Context, ts time.Time, ch clickhouse.Conn) (Vector, error) { @@ -691,7 +712,7 @@ func (r *ThresholdRule) buildAndRunQuery(ctx context.Context, ts time.Time, ch c } // var to hold target query to be executed - queries := make(map[string]string) + var queries map[string]string var err error // fetch the target query based on query type @@ -723,22 +744,7 @@ func (r *ThresholdRule) buildAndRunQuery(ctx context.Context, ts time.Time, ch c zap.S().Debugf("ruleid:", r.ID(), "\t runQueries:", queries) - // find target query label - if query, ok := queries["F1"]; ok { - // found a formula query, run with it - return r.runChQuery(ctx, ch, query) - } - - // no formula in rule condition, now look for - // query label with max ascii val - keys := make([]string, 0, len(queries)) - for k := range queries { - keys = append(keys, k) - } - sort.Strings(keys) - - queryLabel := keys[len(keys)-1] - + queryLabel := r.GetSelectedQuery() zap.S().Debugf("ruleId: ", r.ID(), "\t result query label:", queryLabel) if queryString, ok := queries[queryLabel]; ok { From 758013d7cd533df25c477bf5b6a5a9f377bcbe37 Mon Sep 17 00:00:00 2001 From: Lars Lehtonen Date: Fri, 10 Nov 2023 04:21:17 -0800 Subject: [PATCH 14/28] pkg/query-service/app: fix dropped error (#3842) Co-authored-by: Srikanth Chekuri --- pkg/query-service/app/server.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index f7e6e43d2c..c3968e81be 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -152,6 +152,9 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { } fluxInterval, err := time.ParseDuration(serverOptions.FluxInterval) + if err != nil { + return nil, err + } // ingestion pipelines manager logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(localDB, "sqlite") if err != nil { From 5a9f626da5370078c64f8af5cdad6b55f969f1b7 Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Mon, 13 Nov 2023 13:54:31 +0530 Subject: [PATCH 15/28] feat(FE): dashboard alerts is added (#3908) * feat: create menu items is added in the service application widgets * chore: filter query is updated * fix: build is fixed * feat: selected query is updated * chore: create alerts is updated * feat: dashboard alerts is updated * chore: spacing is updated * feat: dashboard to alerts is updated * fix: build is fixed * feat: alert query options is updated * chore: menu list is updated for tabel panel --------- Co-authored-by: Rajat Dabade Co-authored-by: Srikanth Chekuri --- frontend/public/locales/en-GB/alerts.json | 5 +- frontend/public/locales/en-GB/rules.json | 168 +++++++++--------- frontend/public/locales/en/alerts.json | 5 +- frontend/public/locales/en/rules.json | 168 +++++++++--------- .../FormAlertRules/PromqlSection.tsx | 16 +- .../container/FormAlertRules/RuleOptions.tsx | 49 ++++- .../src/container/FormAlertRules/index.tsx | 37 +++- .../src/container/FormAlertRules/styles.ts | 4 +- .../src/container/FormAlertRules/utils.ts | 19 ++ .../GridCardLayout/GridCard/index.tsx | 7 +- .../GridCardLayout/WidgetHeader/index.tsx | 3 +- .../src/container/GridCardLayout/config.ts | 1 + .../MetricsApplication/Tabs/DBCall.tsx | 4 +- .../MetricsApplication/Tabs/External.tsx | 6 +- .../ApDex/ApDexMetricsApplication.tsx | 4 +- .../container/MetricsApplication/constant.ts | 5 + .../RightContainer/YAxisUnitSelector.tsx | 6 +- .../NewWidget/RightContainer/index.tsx | 54 ++++-- frontend/src/container/NewWidget/index.tsx | 24 ++- frontend/src/types/api/alerts/def.ts | 9 +- 20 files changed, 377 insertions(+), 217 deletions(-) diff --git a/frontend/public/locales/en-GB/alerts.json b/frontend/public/locales/en-GB/alerts.json index 816b71e563..98e9780e6a 100644 --- a/frontend/public/locales/en-GB/alerts.json +++ b/frontend/public/locales/en-GB/alerts.json @@ -34,7 +34,7 @@ "button_returntorules": "Return to rules", "button_cancelchanges": "Cancel", "button_discard": "Discard", - "text_condition1": "Send a notification when the metric is", + "text_condition1": "Send a notification when", "text_condition2": "the threshold", "text_condition3": "during the last", "option_5min": "5 mins", @@ -109,5 +109,6 @@ "traces_based_alert_desc": "Send a notification when a condition occurs in the traces data.", "exceptions_based_alert": "Exceptions-based Alert", "exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.", - "field_unit": "Threshold unit" + "field_unit": "Threshold unit", + "selected_query_placeholder": "Select query" } diff --git a/frontend/public/locales/en-GB/rules.json b/frontend/public/locales/en-GB/rules.json index e67bd35273..c913dc08e0 100644 --- a/frontend/public/locales/en-GB/rules.json +++ b/frontend/public/locales/en-GB/rules.json @@ -1,85 +1,85 @@ { - "preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.", - "preview_chart_threshold_label": "Threshold", - "placeholder_label_key_pair": "Click here to enter a label (key value pairs)", - "button_yes": "Yes", - "button_no": "No", - "remove_label_confirm": "This action will remove all the labels. Do you want to proceed?", - "remove_label_success": "Labels cleared", - "alert_form_step1": "Step 1 - Define the metric", - "alert_form_step2": "Step 2 - Define Alert Conditions", - "alert_form_step3": "Step 3 - Alert Configuration", - "metric_query_max_limit": "Can not create query. You can create maximum of 5 queries", - "confirm_save_title": "Save Changes", - "confirm_save_content_part1": "Your alert built with", - "confirm_save_content_part2": "query will be saved. Press OK to confirm.", - "unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin", - "rule_created": "Rule created successfully", - "rule_edited": "Rule edited successfully", - "expression_missing": "expression is missing in {{where}}", - "metricname_missing": "metric name is missing in {{where}}", - "condition_required": "at least one metric condition is required", - "alertname_required": "alert name is required", - "promql_required": "promql expression is required when query format is set to PromQL", - "button_savechanges": "Save Rule", - "button_createrule": "Create Rule", - "button_returntorules": "Return to rules", - "button_cancelchanges": "Cancel", - "button_discard": "Discard", - "text_condition1": "Send a notification when the metric is", - "text_condition2": "the threshold", - "text_condition3": "during the last", - "option_5min": "5 mins", - "option_10min": "10 mins", - "option_15min": "15 mins", - "option_60min": "60 mins", - "option_4hours": "4 hours", - "option_24hours": "24 hours", - "field_threshold": "Alert Threshold", - "option_allthetimes": "all the times", - "option_atleastonce": "at least once", - "option_onaverage": "on average", - "option_intotal": "in total", - "option_above": "above", - "option_below": "below", - "option_equal": "is equal to", - "option_notequal": "not equal to", - "button_query": "Query", - "button_formula": "Formula", - "tab_qb": "Query Builder", - "tab_promql": "PromQL", - "title_confirm": "Confirm", - "button_ok": "Yes", - "button_cancel": "No", - "field_promql_expr": "PromQL Expression", - "field_alert_name": "Alert Name", - "field_alert_desc": "Alert Description", - "field_labels": "Labels", - "field_severity": "Severity", - "option_critical": "Critical", - "option_error": "Error", - "option_warning": "Warning", - "option_info": "Info", - "user_guide_headline": "Steps to create an Alert", - "user_guide_qb_step1": "Step 1 - Define the metric", - "user_guide_qb_step1a": "Choose a metric which you want to create an alert on", - "user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed", - "user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric", - "user_guide_qb_step1d": "Create a formula based on Queries if needed", - "user_guide_qb_step2": "Step 2 - Define Alert Conditions", - "user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value", - "user_guide_qb_step2b": "Enter the Alert threshold", - "user_guide_qb_step3": "Step 3 -Alert Configuration", - "user_guide_qb_step3a": "Set alert severity, name and descriptions", - "user_guide_qb_step3b": "Add tags to the alert in the Label field if needed", - "user_guide_pql_step1": "Step 1 - Define the metric", - "user_guide_pql_step1a": "Write a PromQL query for the metric", - "user_guide_pql_step1b": "Format the legends based on labels you want to highlight", - "user_guide_pql_step2": "Step 2 - Define Alert Conditions", - "user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value", - "user_guide_pql_step2b": "Enter the Alert threshold", - "user_guide_pql_step3": "Step 3 -Alert Configuration", - "user_guide_pql_step3a": "Set alert severity, name and descriptions", - "user_guide_pql_step3b": "Add tags to the alert in the Label field if needed", - "user_tooltip_more_help": "More details on how to create alerts" -} \ No newline at end of file + "preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.", + "preview_chart_threshold_label": "Threshold", + "placeholder_label_key_pair": "Click here to enter a label (key value pairs)", + "button_yes": "Yes", + "button_no": "No", + "remove_label_confirm": "This action will remove all the labels. Do you want to proceed?", + "remove_label_success": "Labels cleared", + "alert_form_step1": "Step 1 - Define the metric", + "alert_form_step2": "Step 2 - Define Alert Conditions", + "alert_form_step3": "Step 3 - Alert Configuration", + "metric_query_max_limit": "Can not create query. You can create maximum of 5 queries", + "confirm_save_title": "Save Changes", + "confirm_save_content_part1": "Your alert built with", + "confirm_save_content_part2": "query will be saved. Press OK to confirm.", + "unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin", + "rule_created": "Rule created successfully", + "rule_edited": "Rule edited successfully", + "expression_missing": "expression is missing in {{where}}", + "metricname_missing": "metric name is missing in {{where}}", + "condition_required": "at least one metric condition is required", + "alertname_required": "alert name is required", + "promql_required": "promql expression is required when query format is set to PromQL", + "button_savechanges": "Save Rule", + "button_createrule": "Create Rule", + "button_returntorules": "Return to rules", + "button_cancelchanges": "Cancel", + "button_discard": "Discard", + "text_condition1": "Send a notification when", + "text_condition2": "the threshold", + "text_condition3": "during the last", + "option_5min": "5 mins", + "option_10min": "10 mins", + "option_15min": "15 mins", + "option_60min": "60 mins", + "option_4hours": "4 hours", + "option_24hours": "24 hours", + "field_threshold": "Alert Threshold", + "option_allthetimes": "all the times", + "option_atleastonce": "at least once", + "option_onaverage": "on average", + "option_intotal": "in total", + "option_above": "above", + "option_below": "below", + "option_equal": "is equal to", + "option_notequal": "not equal to", + "button_query": "Query", + "button_formula": "Formula", + "tab_qb": "Query Builder", + "tab_promql": "PromQL", + "title_confirm": "Confirm", + "button_ok": "Yes", + "button_cancel": "No", + "field_promql_expr": "PromQL Expression", + "field_alert_name": "Alert Name", + "field_alert_desc": "Alert Description", + "field_labels": "Labels", + "field_severity": "Severity", + "option_critical": "Critical", + "option_error": "Error", + "option_warning": "Warning", + "option_info": "Info", + "user_guide_headline": "Steps to create an Alert", + "user_guide_qb_step1": "Step 1 - Define the metric", + "user_guide_qb_step1a": "Choose a metric which you want to create an alert on", + "user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed", + "user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric", + "user_guide_qb_step1d": "Create a formula based on Queries if needed", + "user_guide_qb_step2": "Step 2 - Define Alert Conditions", + "user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value", + "user_guide_qb_step2b": "Enter the Alert threshold", + "user_guide_qb_step3": "Step 3 -Alert Configuration", + "user_guide_qb_step3a": "Set alert severity, name and descriptions", + "user_guide_qb_step3b": "Add tags to the alert in the Label field if needed", + "user_guide_pql_step1": "Step 1 - Define the metric", + "user_guide_pql_step1a": "Write a PromQL query for the metric", + "user_guide_pql_step1b": "Format the legends based on labels you want to highlight", + "user_guide_pql_step2": "Step 2 - Define Alert Conditions", + "user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value", + "user_guide_pql_step2b": "Enter the Alert threshold", + "user_guide_pql_step3": "Step 3 -Alert Configuration", + "user_guide_pql_step3a": "Set alert severity, name and descriptions", + "user_guide_pql_step3b": "Add tags to the alert in the Label field if needed", + "user_tooltip_more_help": "More details on how to create alerts" +} diff --git a/frontend/public/locales/en/alerts.json b/frontend/public/locales/en/alerts.json index 816b71e563..98e9780e6a 100644 --- a/frontend/public/locales/en/alerts.json +++ b/frontend/public/locales/en/alerts.json @@ -34,7 +34,7 @@ "button_returntorules": "Return to rules", "button_cancelchanges": "Cancel", "button_discard": "Discard", - "text_condition1": "Send a notification when the metric is", + "text_condition1": "Send a notification when", "text_condition2": "the threshold", "text_condition3": "during the last", "option_5min": "5 mins", @@ -109,5 +109,6 @@ "traces_based_alert_desc": "Send a notification when a condition occurs in the traces data.", "exceptions_based_alert": "Exceptions-based Alert", "exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.", - "field_unit": "Threshold unit" + "field_unit": "Threshold unit", + "selected_query_placeholder": "Select query" } diff --git a/frontend/public/locales/en/rules.json b/frontend/public/locales/en/rules.json index e67bd35273..c913dc08e0 100644 --- a/frontend/public/locales/en/rules.json +++ b/frontend/public/locales/en/rules.json @@ -1,85 +1,85 @@ { - "preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.", - "preview_chart_threshold_label": "Threshold", - "placeholder_label_key_pair": "Click here to enter a label (key value pairs)", - "button_yes": "Yes", - "button_no": "No", - "remove_label_confirm": "This action will remove all the labels. Do you want to proceed?", - "remove_label_success": "Labels cleared", - "alert_form_step1": "Step 1 - Define the metric", - "alert_form_step2": "Step 2 - Define Alert Conditions", - "alert_form_step3": "Step 3 - Alert Configuration", - "metric_query_max_limit": "Can not create query. You can create maximum of 5 queries", - "confirm_save_title": "Save Changes", - "confirm_save_content_part1": "Your alert built with", - "confirm_save_content_part2": "query will be saved. Press OK to confirm.", - "unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin", - "rule_created": "Rule created successfully", - "rule_edited": "Rule edited successfully", - "expression_missing": "expression is missing in {{where}}", - "metricname_missing": "metric name is missing in {{where}}", - "condition_required": "at least one metric condition is required", - "alertname_required": "alert name is required", - "promql_required": "promql expression is required when query format is set to PromQL", - "button_savechanges": "Save Rule", - "button_createrule": "Create Rule", - "button_returntorules": "Return to rules", - "button_cancelchanges": "Cancel", - "button_discard": "Discard", - "text_condition1": "Send a notification when the metric is", - "text_condition2": "the threshold", - "text_condition3": "during the last", - "option_5min": "5 mins", - "option_10min": "10 mins", - "option_15min": "15 mins", - "option_60min": "60 mins", - "option_4hours": "4 hours", - "option_24hours": "24 hours", - "field_threshold": "Alert Threshold", - "option_allthetimes": "all the times", - "option_atleastonce": "at least once", - "option_onaverage": "on average", - "option_intotal": "in total", - "option_above": "above", - "option_below": "below", - "option_equal": "is equal to", - "option_notequal": "not equal to", - "button_query": "Query", - "button_formula": "Formula", - "tab_qb": "Query Builder", - "tab_promql": "PromQL", - "title_confirm": "Confirm", - "button_ok": "Yes", - "button_cancel": "No", - "field_promql_expr": "PromQL Expression", - "field_alert_name": "Alert Name", - "field_alert_desc": "Alert Description", - "field_labels": "Labels", - "field_severity": "Severity", - "option_critical": "Critical", - "option_error": "Error", - "option_warning": "Warning", - "option_info": "Info", - "user_guide_headline": "Steps to create an Alert", - "user_guide_qb_step1": "Step 1 - Define the metric", - "user_guide_qb_step1a": "Choose a metric which you want to create an alert on", - "user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed", - "user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric", - "user_guide_qb_step1d": "Create a formula based on Queries if needed", - "user_guide_qb_step2": "Step 2 - Define Alert Conditions", - "user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value", - "user_guide_qb_step2b": "Enter the Alert threshold", - "user_guide_qb_step3": "Step 3 -Alert Configuration", - "user_guide_qb_step3a": "Set alert severity, name and descriptions", - "user_guide_qb_step3b": "Add tags to the alert in the Label field if needed", - "user_guide_pql_step1": "Step 1 - Define the metric", - "user_guide_pql_step1a": "Write a PromQL query for the metric", - "user_guide_pql_step1b": "Format the legends based on labels you want to highlight", - "user_guide_pql_step2": "Step 2 - Define Alert Conditions", - "user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value", - "user_guide_pql_step2b": "Enter the Alert threshold", - "user_guide_pql_step3": "Step 3 -Alert Configuration", - "user_guide_pql_step3a": "Set alert severity, name and descriptions", - "user_guide_pql_step3b": "Add tags to the alert in the Label field if needed", - "user_tooltip_more_help": "More details on how to create alerts" -} \ No newline at end of file + "preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.", + "preview_chart_threshold_label": "Threshold", + "placeholder_label_key_pair": "Click here to enter a label (key value pairs)", + "button_yes": "Yes", + "button_no": "No", + "remove_label_confirm": "This action will remove all the labels. Do you want to proceed?", + "remove_label_success": "Labels cleared", + "alert_form_step1": "Step 1 - Define the metric", + "alert_form_step2": "Step 2 - Define Alert Conditions", + "alert_form_step3": "Step 3 - Alert Configuration", + "metric_query_max_limit": "Can not create query. You can create maximum of 5 queries", + "confirm_save_title": "Save Changes", + "confirm_save_content_part1": "Your alert built with", + "confirm_save_content_part2": "query will be saved. Press OK to confirm.", + "unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin", + "rule_created": "Rule created successfully", + "rule_edited": "Rule edited successfully", + "expression_missing": "expression is missing in {{where}}", + "metricname_missing": "metric name is missing in {{where}}", + "condition_required": "at least one metric condition is required", + "alertname_required": "alert name is required", + "promql_required": "promql expression is required when query format is set to PromQL", + "button_savechanges": "Save Rule", + "button_createrule": "Create Rule", + "button_returntorules": "Return to rules", + "button_cancelchanges": "Cancel", + "button_discard": "Discard", + "text_condition1": "Send a notification when", + "text_condition2": "the threshold", + "text_condition3": "during the last", + "option_5min": "5 mins", + "option_10min": "10 mins", + "option_15min": "15 mins", + "option_60min": "60 mins", + "option_4hours": "4 hours", + "option_24hours": "24 hours", + "field_threshold": "Alert Threshold", + "option_allthetimes": "all the times", + "option_atleastonce": "at least once", + "option_onaverage": "on average", + "option_intotal": "in total", + "option_above": "above", + "option_below": "below", + "option_equal": "is equal to", + "option_notequal": "not equal to", + "button_query": "Query", + "button_formula": "Formula", + "tab_qb": "Query Builder", + "tab_promql": "PromQL", + "title_confirm": "Confirm", + "button_ok": "Yes", + "button_cancel": "No", + "field_promql_expr": "PromQL Expression", + "field_alert_name": "Alert Name", + "field_alert_desc": "Alert Description", + "field_labels": "Labels", + "field_severity": "Severity", + "option_critical": "Critical", + "option_error": "Error", + "option_warning": "Warning", + "option_info": "Info", + "user_guide_headline": "Steps to create an Alert", + "user_guide_qb_step1": "Step 1 - Define the metric", + "user_guide_qb_step1a": "Choose a metric which you want to create an alert on", + "user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed", + "user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric", + "user_guide_qb_step1d": "Create a formula based on Queries if needed", + "user_guide_qb_step2": "Step 2 - Define Alert Conditions", + "user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value", + "user_guide_qb_step2b": "Enter the Alert threshold", + "user_guide_qb_step3": "Step 3 -Alert Configuration", + "user_guide_qb_step3a": "Set alert severity, name and descriptions", + "user_guide_qb_step3b": "Add tags to the alert in the Label field if needed", + "user_guide_pql_step1": "Step 1 - Define the metric", + "user_guide_pql_step1a": "Write a PromQL query for the metric", + "user_guide_pql_step1b": "Format the legends based on labels you want to highlight", + "user_guide_pql_step2": "Step 2 - Define Alert Conditions", + "user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value", + "user_guide_pql_step2b": "Enter the Alert threshold", + "user_guide_pql_step3": "Step 3 -Alert Configuration", + "user_guide_pql_step3a": "Set alert severity, name and descriptions", + "user_guide_pql_step3b": "Add tags to the alert in the Label field if needed", + "user_tooltip_more_help": "More details on how to create alerts" +} diff --git a/frontend/src/container/FormAlertRules/PromqlSection.tsx b/frontend/src/container/FormAlertRules/PromqlSection.tsx index 7e23c2e1dc..91aab81400 100644 --- a/frontend/src/container/FormAlertRules/PromqlSection.tsx +++ b/frontend/src/container/FormAlertRules/PromqlSection.tsx @@ -5,12 +5,16 @@ function PromqlSection(): JSX.Element { const { currentQuery } = useQueryBuilder(); return ( - + <> + {currentQuery.promql.map((query, index) => ( + + ))} + ); } diff --git a/frontend/src/container/FormAlertRules/RuleOptions.tsx b/frontend/src/container/FormAlertRules/RuleOptions.tsx index 1456268bd2..0fa5e404e7 100644 --- a/frontend/src/container/FormAlertRules/RuleOptions.tsx +++ b/frontend/src/container/FormAlertRules/RuleOptions.tsx @@ -7,6 +7,7 @@ import { Space, Typography, } from 'antd'; +import { DefaultOptionType } from 'antd/es/select'; import { getCategoryByOptionId, getCategorySelectOptionByName, @@ -28,6 +29,7 @@ function RuleOptions({ alertDef, setAlertDef, queryCategory, + queryOptions, }: RuleOptionsProps): JSX.Element { // init namespace for translations const { t } = useTranslation('alerts'); @@ -44,6 +46,18 @@ function RuleOptions({ }); }; + const onChangeSelectedQueryName = (value: string | unknown): void => { + if (typeof value !== 'string') return; + + setAlertDef({ + ...alertDef, + condition: { + ...alertDef.condition, + selectedQueryName: value, + }, + }); + }; + const renderCompareOps = (): JSX.Element => ( ( - {t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '} - {renderThresholdMatchOpts()} {t('text_condition3')} {renderEvalWindows()} + {t('text_condition1')} + + is + {renderCompareOps()} {t('text_condition2')} {renderThresholdMatchOpts()}{' '} + {t('text_condition3')} {renderEvalWindows()} ); + const renderPromRuleOptions = (): JSX.Element => ( - {t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '} - {renderPromMatchOpts()} + {t('text_condition1')} + + is + {renderCompareOps()} {t('text_condition2')} {renderPromMatchOpts()} ); @@ -172,7 +208,7 @@ function RuleOptions({ ? renderPromRuleOptions() : renderThresholdRuleOpts()} - + - + +
+ + + + + + +
+ + +
-
- - - - - - -
); } diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/CustomCheckBox.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/CustomCheckBox.tsx index eda971c1e4..922510eaea 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/CustomCheckBox.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/CustomCheckBox.tsx @@ -10,13 +10,11 @@ function CustomCheckBox({ graphVisibilityState = [], checkBoxOnChangeHandler, }: CheckBoxProps): JSX.Element { - const { datasets } = data; - const onChangeHandler = (e: CheckboxChangeEvent): void => { checkBoxOnChangeHandler(e, index); }; - const color = datasets[index]?.borderColor?.toString() || grey[0]; + const color = data[index]?.stroke?.toString() || grey[0]; const isChecked = graphVisibilityState[index] || false; diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/GetLabel.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/GetLabel.tsx index 4ce97d8af2..1cafd49bf5 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/GetLabel.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/TableRender/GetLabel.tsx @@ -6,7 +6,7 @@ import Label from './Label'; export const getLabel = ( labelClickedHandler: (labelIndex: number) => void, ): ColumnType => ({ - render: (label, record): JSX.Element => ( + render: (label: string, record): JSX.Element => (