From b2d6d75eefaea54dbc18f6935098da3bbc0367aa Mon Sep 17 00:00:00 2001 From: Yunus M Date: Thu, 30 Nov 2023 13:56:49 +0530 Subject: [PATCH 01/19] feat: dashboard perf improvements (#4010) * feat: dashboard perf improvements * feat: remove console logs * fix: remove console.log * fix: update tests * fix: update tests --------- Co-authored-by: Srikanth Chekuri --- frontend/src/AppRoutes/pageComponents.ts | 3 +- frontend/src/api/dashboard/update.ts | 4 +- .../variables/dashboardVariablesQuery.ts | 30 ++ frontend/src/api/dashboard/variables/query.ts | 24 - .../ListOfDashboard/DashboardsList.tsx | 378 ++++++++++++++++ .../TableComponents/CreatedBy.tsx | 2 +- .../TableComponents/DeleteButton.tsx | 2 +- .../ListOfDashboard/TableComponents/Name.tsx | 2 +- .../ListOfDashboard/TableComponents/Tags.tsx | 2 +- .../src/container/ListOfDashboard/index.tsx | 379 +--------------- .../DashboardDescription/index.tsx | 6 +- .../VariableItem/VariableItem.styles.scss | 8 + .../Variables/VariableItem/VariableItem.tsx | 425 ++++++++++-------- .../DashboardSettings/Variables/index.tsx | 14 +- .../NewDashboard/DashboardSettings/index.tsx | 2 +- .../DashboardVariableSelection.styles.scss | 8 + .../DashboardVariableSelection.tsx | 110 +++++ .../VariableItem.test.tsx | 154 +++---- .../VariableItem.tsx | 271 ++++++----- .../DashboardVariablesSelection/index.tsx | 118 +---- .../DashboardVariablesSelection/styles.ts | 23 +- .../TopNav/DateTimeSelection/config.ts | 6 +- .../DashboardsListPage.tsx} | 4 +- .../src/pages/DashboardsListPage/index.tsx | 3 + .../src/pages/NewDashboard/DashboardPage.tsx | 33 ++ frontend/src/pages/NewDashboard/index.tsx | 34 +- .../types/api/dashboard/variables/query.ts | 2 +- frontend/tsconfig.json | 4 +- frontend/webpack.config.js | 2 +- 29 files changed, 1092 insertions(+), 961 deletions(-) create mode 100644 frontend/src/api/dashboard/variables/dashboardVariablesQuery.ts delete mode 100644 frontend/src/api/dashboard/variables/query.ts create mode 100644 frontend/src/container/ListOfDashboard/DashboardsList.tsx create mode 100644 frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.styles.scss create mode 100644 frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.styles.scss create mode 100644 frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.tsx rename frontend/src/pages/{Dashboard/index.tsx => DashboardsListPage/DashboardsListPage.tsx} (83%) create mode 100644 frontend/src/pages/DashboardsListPage/index.tsx create mode 100644 frontend/src/pages/NewDashboard/DashboardPage.tsx diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index b2892e3d38..638c019506 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -49,7 +49,8 @@ export const Onboarding = Loadable( ); export const DashboardPage = Loadable( - () => import(/* webpackChunkName: "DashboardPage" */ 'pages/Dashboard'), + () => + import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardsListPage'), ); export const NewDashboardPage = Loadable( diff --git a/frontend/src/api/dashboard/update.ts b/frontend/src/api/dashboard/update.ts index 37341524f8..db5350849e 100644 --- a/frontend/src/api/dashboard/update.ts +++ b/frontend/src/api/dashboard/update.ts @@ -4,7 +4,7 @@ import { AxiosError } from 'axios'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { PayloadProps, Props } from 'types/api/dashboard/update'; -const update = async ( +const updateDashboard = async ( props: Props, ): Promise | ErrorResponse> => { try { @@ -23,4 +23,4 @@ const update = async ( } }; -export default update; +export default updateDashboard; diff --git a/frontend/src/api/dashboard/variables/dashboardVariablesQuery.ts b/frontend/src/api/dashboard/variables/dashboardVariablesQuery.ts new file mode 100644 index 0000000000..8605ce75f1 --- /dev/null +++ b/frontend/src/api/dashboard/variables/dashboardVariablesQuery.ts @@ -0,0 +1,30 @@ +import { ApiV2Instance as axios } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { + Props, + VariableResponseProps, +} from 'types/api/dashboard/variables/query'; + +const dashboardVariablesQuery = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.post(`/variables/query`, props); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + const formattedError = ErrorResponseHandler(error as AxiosError); + + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw { message: 'Error fetching data', details: formattedError }; + } +}; + +export default dashboardVariablesQuery; diff --git a/frontend/src/api/dashboard/variables/query.ts b/frontend/src/api/dashboard/variables/query.ts deleted file mode 100644 index 958fdb7e3a..0000000000 --- a/frontend/src/api/dashboard/variables/query.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ApiV2Instance as axios } from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { PayloadProps, Props } from 'types/api/dashboard/variables/query'; - -const query = async ( - props: Props, -): Promise | ErrorResponse> => { - try { - const response = await axios.post(`/variables/query`, props); - - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; - -export default query; diff --git a/frontend/src/container/ListOfDashboard/DashboardsList.tsx b/frontend/src/container/ListOfDashboard/DashboardsList.tsx new file mode 100644 index 0000000000..8bb56490e8 --- /dev/null +++ b/frontend/src/container/ListOfDashboard/DashboardsList.tsx @@ -0,0 +1,378 @@ +import { PlusOutlined } from '@ant-design/icons'; +import { Card, Col, Dropdown, Input, Row, TableColumnProps } from 'antd'; +import { ItemType } from 'antd/es/menu/hooks/useItems'; +import createDashboard from 'api/dashboard/create'; +import { AxiosError } from 'axios'; +import { + DynamicColumnsKey, + TableDataSource, +} from 'components/ResizeTable/contants'; +import DynamicColumnTable from 'components/ResizeTable/DynamicColumnTable'; +import LabelColumn from 'components/TableRenderer/LabelColumn'; +import TextToolTip from 'components/TextToolTip'; +import ROUTES from 'constants/routes'; +import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard'; +import useComponentPermission from 'hooks/useComponentPermission'; +import useDebouncedFn from 'hooks/useDebouncedFunction'; +import history from 'lib/history'; +import { Key, useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import { generatePath } from 'react-router-dom'; +import { AppState } from 'store/reducers'; +import { Dashboard } from 'types/api/dashboard/getAll'; +import AppReducer from 'types/reducer/app'; + +import DateComponent from '../../components/ResizeTable/TableComponent/DateComponent'; +import ImportJSON from './ImportJSON'; +import { ButtonContainer, NewDashboardButton, TableContainer } from './styles'; +import DeleteButton from './TableComponents/DeleteButton'; +import Name from './TableComponents/Name'; + +const { Search } = Input; + +function DashboardsList(): JSX.Element { + const { + data: dashboardListResponse = [], + isLoading: isDashboardListLoading, + refetch: refetchDashboardList, + } = useGetAllDashboard(); + + const { role } = useSelector((state) => state.app); + + const [action, createNewDashboard] = useComponentPermission( + ['action', 'create_new_dashboards'], + role, + ); + + const { t } = useTranslation('dashboard'); + + const [ + isImportJSONModalVisible, + setIsImportJSONModalVisible, + ] = useState(false); + + const [uploadedGrafana, setUploadedGrafana] = useState(false); + const [isFilteringDashboards, setIsFilteringDashboards] = useState(false); + + const [dashboards, setDashboards] = useState(); + + const sortDashboardsByCreatedAt = (dashboards: Dashboard[]): void => { + const sortedDashboards = dashboards.sort( + (a, b) => + new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), + ); + setDashboards(sortedDashboards); + }; + + useEffect(() => { + sortDashboardsByCreatedAt(dashboardListResponse); + }, [dashboardListResponse]); + + const [newDashboardState, setNewDashboardState] = useState({ + loading: false, + error: false, + errorMessage: '', + }); + + const dynamicColumns: TableColumnProps[] = [ + { + title: 'Created At', + dataIndex: 'createdAt', + width: 30, + key: DynamicColumnsKey.CreatedAt, + sorter: (a: Data, b: Data): number => { + console.log({ a }); + const prev = new Date(a.createdAt).getTime(); + const next = new Date(b.createdAt).getTime(); + + return prev - next; + }, + render: DateComponent, + }, + { + title: 'Created By', + dataIndex: 'createdBy', + width: 30, + key: DynamicColumnsKey.CreatedBy, + }, + { + title: 'Last Updated Time', + width: 30, + dataIndex: 'lastUpdatedTime', + key: DynamicColumnsKey.UpdatedAt, + sorter: (a: Data, b: Data): number => { + const prev = new Date(a.lastUpdatedTime).getTime(); + const next = new Date(b.lastUpdatedTime).getTime(); + + return prev - next; + }, + render: DateComponent, + }, + { + title: 'Last Updated By', + dataIndex: 'lastUpdatedBy', + width: 30, + key: DynamicColumnsKey.UpdatedBy, + }, + ]; + + const columns = useMemo(() => { + const tableColumns: TableColumnProps[] = [ + { + title: 'Name', + dataIndex: 'name', + width: 40, + render: Name, + }, + { + title: 'Description', + width: 50, + dataIndex: 'description', + }, + { + title: 'Tags', + dataIndex: 'tags', + width: 50, + render: (value): JSX.Element => , + }, + ]; + + if (action) { + tableColumns.push({ + title: 'Action', + dataIndex: '', + width: 40, + render: DeleteButton, + }); + } + + return tableColumns; + }, [action]); + + const data: Data[] = + dashboards?.map((e) => ({ + createdAt: e.created_at, + description: e.data.description || '', + id: e.uuid, + lastUpdatedTime: e.updated_at, + name: e.data.title, + tags: e.data.tags || [], + key: e.uuid, + createdBy: e.created_by, + isLocked: !!e.isLocked || false, + lastUpdatedBy: e.updated_by, + refetchDashboardList, + })) || []; + + const onNewDashboardHandler = useCallback(async () => { + try { + setNewDashboardState({ + ...newDashboardState, + loading: true, + }); + const response = await createDashboard({ + title: t('new_dashboard_title', { + ns: 'dashboard', + }), + uploadedGrafana: false, + }); + + if (response.statusCode === 200) { + history.push( + generatePath(ROUTES.DASHBOARD, { + dashboardId: response.payload.uuid, + }), + ); + } else { + setNewDashboardState({ + ...newDashboardState, + loading: false, + error: true, + errorMessage: response.error || 'Something went wrong', + }); + } + } catch (error) { + setNewDashboardState({ + ...newDashboardState, + error: true, + errorMessage: (error as AxiosError).toString() || 'Something went Wrong', + }); + } + }, [newDashboardState, t]); + + const getText = useCallback(() => { + if (!newDashboardState.error && !newDashboardState.loading) { + return 'New Dashboard'; + } + + if (newDashboardState.loading) { + return 'Loading'; + } + + return newDashboardState.errorMessage; + }, [ + newDashboardState.error, + newDashboardState.errorMessage, + newDashboardState.loading, + ]); + + const onModalHandler = (uploadedGrafana: boolean): void => { + setIsImportJSONModalVisible((state) => !state); + setUploadedGrafana(uploadedGrafana); + }; + + const getMenuItems = useMemo(() => { + const menuItems: ItemType[] = [ + { + key: t('import_json').toString(), + label: t('import_json'), + onClick: (): void => onModalHandler(false), + }, + { + key: t('import_grafana_json').toString(), + label: t('import_grafana_json'), + onClick: (): void => onModalHandler(true), + disabled: true, + }, + ]; + + if (createNewDashboard) { + menuItems.unshift({ + key: t('create_dashboard').toString(), + label: t('create_dashboard'), + disabled: isDashboardListLoading, + onClick: onNewDashboardHandler, + }); + } + + return menuItems; + }, [createNewDashboard, isDashboardListLoading, onNewDashboardHandler, t]); + + const searchArrayOfObjects = (searchValue: string): any[] => { + // Convert the searchValue to lowercase for case-insensitive search + const searchValueLowerCase = searchValue.toLowerCase(); + + // Use the filter method to find matching objects + return dashboardListResponse.filter((item: any) => { + // Convert each property value to lowercase for case-insensitive search + const itemValues = Object.values(item?.data).map((value: any) => + value.toString().toLowerCase(), + ); + + // Check if any property value contains the searchValue + return itemValues.some((value) => value.includes(searchValueLowerCase)); + }); + }; + + const handleSearch = useDebouncedFn((event: unknown): void => { + setIsFilteringDashboards(true); + const searchText = (event as React.BaseSyntheticEvent)?.target?.value || ''; + const filteredDashboards = searchArrayOfObjects(searchText); + setDashboards(filteredDashboards); + setIsFilteringDashboards(false); + }, 500); + + const GetHeader = useMemo( + () => ( + + + + + + + + + + + + } + type="primary" + data-testid="create-new-dashboard" + loading={newDashboardState.loading} + danger={newDashboardState.error} + > + {getText()} + + + + + ), + [ + isDashboardListLoading, + handleSearch, + isFilteringDashboards, + getMenuItems, + newDashboardState.loading, + newDashboardState.error, + getText, + ], + ); + + return ( + + {GetHeader} + + + onModalHandler(false)} + /> + + + + ); +} + +export interface Data { + key: Key; + name: string; + description: string; + tags: string[]; + createdBy: string; + createdAt: string; + lastUpdatedTime: string; + lastUpdatedBy: string; + isLocked: boolean; + id: string; +} + +export default DashboardsList; diff --git a/frontend/src/container/ListOfDashboard/TableComponents/CreatedBy.tsx b/frontend/src/container/ListOfDashboard/TableComponents/CreatedBy.tsx index d463f80c03..56c5ec8bfb 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/CreatedBy.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/CreatedBy.tsx @@ -2,7 +2,7 @@ import { Typography } from 'antd'; import convertDateToAmAndPm from 'lib/convertDateToAmAndPm'; import getFormattedDate from 'lib/getFormatedDate'; -import { Data } from '..'; +import { Data } from '../DashboardsList'; function Created(createdBy: Data['createdBy']): JSX.Element { const time = new Date(createdBy); diff --git a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx index 33663b129d..810b99a278 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx @@ -11,7 +11,7 @@ import { AppState } from 'store/reducers'; import AppReducer from 'types/reducer/app'; import { USER_ROLES } from 'types/roles'; -import { Data } from '..'; +import { Data } from '../DashboardsList'; import { TableLinkText } from './styles'; interface DeleteButtonProps { diff --git a/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx b/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx index a3f3427b6c..deb64ced11 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx @@ -2,7 +2,7 @@ import { LockFilled } from '@ant-design/icons'; import ROUTES from 'constants/routes'; import history from 'lib/history'; -import { Data } from '..'; +import { Data } from '../DashboardsList'; import { TableLinkText } from './styles'; function Name(name: Data['name'], data: Data): JSX.Element { diff --git a/frontend/src/container/ListOfDashboard/TableComponents/Tags.tsx b/frontend/src/container/ListOfDashboard/TableComponents/Tags.tsx index bc698487d2..761a6bdee6 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/Tags.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/Tags.tsx @@ -1,7 +1,7 @@ /* eslint-disable react/destructuring-assignment */ import { Tag } from 'antd'; -import { Data } from '../index'; +import { Data } from '../DashboardsList'; function Tags(data: Data['tags']): JSX.Element { return ( diff --git a/frontend/src/container/ListOfDashboard/index.tsx b/frontend/src/container/ListOfDashboard/index.tsx index b9d48aef3c..03cc6ba563 100644 --- a/frontend/src/container/ListOfDashboard/index.tsx +++ b/frontend/src/container/ListOfDashboard/index.tsx @@ -1,378 +1,3 @@ -import { PlusOutlined } from '@ant-design/icons'; -import { Card, Col, Dropdown, Input, Row, TableColumnProps } from 'antd'; -import { ItemType } from 'antd/es/menu/hooks/useItems'; -import createDashboard from 'api/dashboard/create'; -import { AxiosError } from 'axios'; -import { - DynamicColumnsKey, - TableDataSource, -} from 'components/ResizeTable/contants'; -import DynamicColumnTable from 'components/ResizeTable/DynamicColumnTable'; -import LabelColumn from 'components/TableRenderer/LabelColumn'; -import TextToolTip from 'components/TextToolTip'; -import ROUTES from 'constants/routes'; -import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard'; -import useComponentPermission from 'hooks/useComponentPermission'; -import useDebouncedFn from 'hooks/useDebouncedFunction'; -import history from 'lib/history'; -import { Key, useCallback, useEffect, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useSelector } from 'react-redux'; -import { generatePath } from 'react-router-dom'; -import { AppState } from 'store/reducers'; -import { Dashboard } from 'types/api/dashboard/getAll'; -import AppReducer from 'types/reducer/app'; +import DashboardsList from './DashboardsList'; -import DateComponent from '../../components/ResizeTable/TableComponent/DateComponent'; -import ImportJSON from './ImportJSON'; -import { ButtonContainer, NewDashboardButton, TableContainer } from './styles'; -import DeleteButton from './TableComponents/DeleteButton'; -import Name from './TableComponents/Name'; - -const { Search } = Input; - -function ListOfAllDashboard(): JSX.Element { - const { - data: dashboardListResponse = [], - isLoading: isDashboardListLoading, - refetch: refetchDashboardList, - } = useGetAllDashboard(); - - const { role } = useSelector((state) => state.app); - - const [action, createNewDashboard] = useComponentPermission( - ['action', 'create_new_dashboards'], - role, - ); - - const { t } = useTranslation('dashboard'); - - const [ - isImportJSONModalVisible, - setIsImportJSONModalVisible, - ] = useState(false); - - const [uploadedGrafana, setUploadedGrafana] = useState(false); - const [isFilteringDashboards, setIsFilteringDashboards] = useState(false); - - const [dashboards, setDashboards] = useState(); - - const sortDashboardsByCreatedAt = (dashboards: Dashboard[]): void => { - const sortedDashboards = dashboards.sort( - (a, b) => - new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), - ); - setDashboards(sortedDashboards); - }; - - useEffect(() => { - sortDashboardsByCreatedAt(dashboardListResponse); - }, [dashboardListResponse]); - - const [newDashboardState, setNewDashboardState] = useState({ - loading: false, - error: false, - errorMessage: '', - }); - - const dynamicColumns: TableColumnProps[] = [ - { - title: 'Created At', - dataIndex: 'createdAt', - width: 30, - key: DynamicColumnsKey.CreatedAt, - sorter: (a: Data, b: Data): number => { - console.log({ a }); - const prev = new Date(a.createdAt).getTime(); - const next = new Date(b.createdAt).getTime(); - - return prev - next; - }, - render: DateComponent, - }, - { - title: 'Created By', - dataIndex: 'createdBy', - width: 30, - key: DynamicColumnsKey.CreatedBy, - }, - { - title: 'Last Updated Time', - width: 30, - dataIndex: 'lastUpdatedTime', - key: DynamicColumnsKey.UpdatedAt, - sorter: (a: Data, b: Data): number => { - const prev = new Date(a.lastUpdatedTime).getTime(); - const next = new Date(b.lastUpdatedTime).getTime(); - - return prev - next; - }, - render: DateComponent, - }, - { - title: 'Last Updated By', - dataIndex: 'lastUpdatedBy', - width: 30, - key: DynamicColumnsKey.UpdatedBy, - }, - ]; - - const columns = useMemo(() => { - const tableColumns: TableColumnProps[] = [ - { - title: 'Name', - dataIndex: 'name', - width: 40, - render: Name, - }, - { - title: 'Description', - width: 50, - dataIndex: 'description', - }, - { - title: 'Tags', - dataIndex: 'tags', - width: 50, - render: (value): JSX.Element => , - }, - ]; - - if (action) { - tableColumns.push({ - title: 'Action', - dataIndex: '', - width: 40, - render: DeleteButton, - }); - } - - return tableColumns; - }, [action]); - - const data: Data[] = - dashboards?.map((e) => ({ - createdAt: e.created_at, - description: e.data.description || '', - id: e.uuid, - lastUpdatedTime: e.updated_at, - name: e.data.title, - tags: e.data.tags || [], - key: e.uuid, - createdBy: e.created_by, - isLocked: !!e.isLocked || false, - lastUpdatedBy: e.updated_by, - refetchDashboardList, - })) || []; - - const onNewDashboardHandler = useCallback(async () => { - try { - setNewDashboardState({ - ...newDashboardState, - loading: true, - }); - const response = await createDashboard({ - title: t('new_dashboard_title', { - ns: 'dashboard', - }), - uploadedGrafana: false, - }); - - if (response.statusCode === 200) { - history.push( - generatePath(ROUTES.DASHBOARD, { - dashboardId: response.payload.uuid, - }), - ); - } else { - setNewDashboardState({ - ...newDashboardState, - loading: false, - error: true, - errorMessage: response.error || 'Something went wrong', - }); - } - } catch (error) { - setNewDashboardState({ - ...newDashboardState, - error: true, - errorMessage: (error as AxiosError).toString() || 'Something went Wrong', - }); - } - }, [newDashboardState, t]); - - const getText = useCallback(() => { - if (!newDashboardState.error && !newDashboardState.loading) { - return 'New Dashboard'; - } - - if (newDashboardState.loading) { - return 'Loading'; - } - - return newDashboardState.errorMessage; - }, [ - newDashboardState.error, - newDashboardState.errorMessage, - newDashboardState.loading, - ]); - - const onModalHandler = (uploadedGrafana: boolean): void => { - setIsImportJSONModalVisible((state) => !state); - setUploadedGrafana(uploadedGrafana); - }; - - const getMenuItems = useMemo(() => { - const menuItems: ItemType[] = [ - { - key: t('import_json').toString(), - label: t('import_json'), - onClick: (): void => onModalHandler(false), - }, - { - key: t('import_grafana_json').toString(), - label: t('import_grafana_json'), - onClick: (): void => onModalHandler(true), - disabled: true, - }, - ]; - - if (createNewDashboard) { - menuItems.unshift({ - key: t('create_dashboard').toString(), - label: t('create_dashboard'), - disabled: isDashboardListLoading, - onClick: onNewDashboardHandler, - }); - } - - return menuItems; - }, [createNewDashboard, isDashboardListLoading, onNewDashboardHandler, t]); - - const searchArrayOfObjects = (searchValue: string): any[] => { - // Convert the searchValue to lowercase for case-insensitive search - const searchValueLowerCase = searchValue.toLowerCase(); - - // Use the filter method to find matching objects - return dashboardListResponse.filter((item: any) => { - // Convert each property value to lowercase for case-insensitive search - const itemValues = Object.values(item?.data).map((value: any) => - value.toString().toLowerCase(), - ); - - // Check if any property value contains the searchValue - return itemValues.some((value) => value.includes(searchValueLowerCase)); - }); - }; - - const handleSearch = useDebouncedFn((event: unknown): void => { - setIsFilteringDashboards(true); - const searchText = (event as React.BaseSyntheticEvent)?.target?.value || ''; - const filteredDashboards = searchArrayOfObjects(searchText); - setDashboards(filteredDashboards); - setIsFilteringDashboards(false); - }, 500); - - const GetHeader = useMemo( - () => ( - - - - - - - - - - - - } - type="primary" - data-testid="create-new-dashboard" - loading={newDashboardState.loading} - danger={newDashboardState.error} - > - {getText()} - - - - - ), - [ - isDashboardListLoading, - handleSearch, - isFilteringDashboards, - getMenuItems, - newDashboardState.loading, - newDashboardState.error, - getText, - ], - ); - - return ( - - {GetHeader} - - - onModalHandler(false)} - /> - - - - ); -} - -export interface Data { - key: Key; - name: string; - description: string; - tags: string[]; - createdBy: string; - createdAt: string; - lastUpdatedTime: string; - lastUpdatedBy: string; - isLocked: boolean; - id: string; -} - -export default ListOfAllDashboard; +export default DashboardsList; diff --git a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx index 12349cb2c3..beef29497e 100644 --- a/frontend/src/container/NewDashboard/DashboardDescription/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardDescription/index.tsx @@ -50,7 +50,7 @@ function DashboardDescription(): JSX.Element { return ( - + )} - + - + {selectedData && ( ([]); - // Internal states - const [previewLoading, setPreviewLoading] = useState(false); // Error messages const [errorName, setErrorName] = useState(false); const [errorPreview, setErrorPreview] = useState(null); @@ -131,232 +124,268 @@ function VariableItem({ }; // Fetches the preview values for the SQL variable query - const handleQueryResult = async (): Promise => { - setPreviewLoading(true); - setErrorPreview(null); - try { - const variableQueryResponse = await query({ - query: variableQueryValue, - variables: variablePropsToPayloadVariables(existingVariables), - }); - setPreviewLoading(false); - if (variableQueryResponse.error) { - let message = variableQueryResponse.error; - if (variableQueryResponse.error.includes('Syntax error:')) { - message = - 'Please make sure query is valid and dependent variables are selected'; - } - setErrorPreview(message); - return; - } - if (variableQueryResponse.payload?.variableValues) - setPreviewValues( - sortValues( - variableQueryResponse.payload?.variableValues || [], - variableSortType, - ) as never, - ); - } catch (e) { - console.error(e); - } + const handleQueryResult = (response: any): void => { + if (response?.payload?.variableValues) + setPreviewValues( + sortValues( + response.payload?.variableValues || [], + variableSortType, + ) as never, + ); }; - return ( - - {/* Add Variable */} - - - Name - -
- { - setVariableName(e.target.value); - setErrorName( - !validateName(e.target.value) && e.target.value !== variableData.name, - ); - }} - /> -
- - {errorName ? 'Variable name already exists' : ''} - -
-
-
- - - Description - - setVariableDescription(e.target.value)} - /> - - - - Type - + const { isFetching: previewLoading, refetch: runQuery } = useQuery( + [REACT_QUERY_KEY.DASHBOARD_BY_ID, variableData.name, variableName], + { + enabled: false, + queryFn: () => + dashboardVariablesQuery({ + query: variableData.queryValue || '', + variables: variablePropsToPayloadVariables(existingVariables), + }), + refetchOnWindowFocus: false, + onSuccess: (response) => { + handleQueryResult(response); + }, + onError: (error: { + details: { + error: string; + }; + }) => { + const { details } = error; - - - - Options - - {queryType === 'QUERY' && ( + if (details.error) { + let message = details.error; + if (details.error.includes('Syntax error:')) { + message = + 'Please make sure query is valid and dependent variables are selected'; + } + setErrorPreview(message); + } + }, + }, + ); + + const handleTestRunQuery = useCallback(() => { + runQuery(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+
+ {/* Add Variable */} - Query + Name - -
- setVariableQueryValue(e)} - height="300px" - /> - + /> +
+ + {errorName ? 'Variable name already exists' : ''} + +
- )} - {queryType === 'CUSTOM' && ( - Values separated by comma + Description + { - setVariableCustomValue(e.target.value); - setPreviewValues( - sortValues( - commaValuesParser(e.target.value), - variableSortType, - ) as never, - ); - }} + onChange={(e): void => setVariableDescription(e.target.value)} /> - )} - {queryType === 'TEXTBOX' && ( - Default Value + Type - { - setVariableTextboxValue(e.target.value); - }} - placeholder="Default value if any" + + - )} - {(queryType === 'QUERY' || queryType === 'CUSTOM') && ( - <> - + + Options + + {queryType === 'QUERY' && ( +
- Preview of Values + Query -
- {errorPreview ? ( - {errorPreview} - ) : ( - map(previewValues, (value, idx) => ( - {value.toString()} - )) - )} + +
+ setVariableQueryValue(e)} + height="240px" + options={{ + fontSize: 13, + wordWrap: 'on', + lineNumbers: 'off', + glyphMargin: false, + folding: false, + lineDecorationsWidth: 0, + lineNumbersMinChars: 0, + minimap: { + enabled: false, + }, + }} + /> +
- +
+ )} + {queryType === 'CUSTOM' && ( - Sort + Values separated by comma - - + onChange={(e): void => { + setVariableCustomValue(e.target.value); + setPreviewValues( + sortValues( + commaValuesParser(e.target.value), + variableSortType, + ) as never, + ); + }} + /> + )} + {queryType === 'TEXTBOX' && ( - Enable multiple values to be checked + Default Value - { - setVariableMultiSelect(e); - if (!e) { - setVariableShowALLOption(false); - } + setVariableTextboxValue(e.target.value); }} + placeholder="Default value if any" + style={{ width: 400 }} /> - {variableMultiSelect && ( + )} + {(queryType === 'QUERY' || queryType === 'CUSTOM') && ( + <> + + + Preview of Values + +
+ {errorPreview ? ( + {errorPreview} + ) : ( + map(previewValues, (value, idx) => ( + {value.toString()} + )) + )} +
+
+ + + Sort + + + + - Include an option for ALL values + Enable multiple values to be checked setVariableShowALLOption(e)} + checked={variableMultiSelect} + onChange={(e): void => { + setVariableMultiSelect(e); + if (!e) { + setVariableShowALLOption(false); + } + }} /> - )} - - )} - - - - - - + {variableMultiSelect && ( + + + Include an option for ALL values + + setVariableShowALLOption(e)} + /> + + )} + + )} +
+ +
+ + + + + +
+
); } diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx index 754e44f1bc..de23e64068 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx @@ -4,6 +4,7 @@ import { Button, Modal, Row, Space, Tag } from 'antd'; import { ResizeTable } from 'components/ResizeTable'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useNotifications } from 'hooks/useNotifications'; +import { PencilIcon, TrashIcon } from 'lucide-react'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -134,7 +135,7 @@ function VariablesSetting(): JSX.Element { key: 'name', }, { - title: 'Definition', + title: 'Description', dataIndex: 'description', width: 100, key: 'description', @@ -147,19 +148,19 @@ function VariablesSetting(): JSX.Element { ), @@ -187,9 +188,10 @@ function VariablesSetting(): JSX.Element { onVariableViewModeEnter('ADD', {} as IDashboardVariable) } > - New Variables + Add Variable + )} diff --git a/frontend/src/container/NewDashboard/DashboardSettings/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/index.tsx index 4cbc531c9d..dafa0f8789 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/index.tsx @@ -13,7 +13,7 @@ function DashboardSettingsContent(): JSX.Element { { label: 'Variables', key: 'variables', children: }, ]; - return ; + return ; } export default DashboardSettingsContent; diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.styles.scss b/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.styles.scss new file mode 100644 index 0000000000..1d91614ff6 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.styles.scss @@ -0,0 +1,8 @@ +.variable-name { + font-size: 0.8rem; + min-width: 100px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: gray; +} diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.tsx new file mode 100644 index 0000000000..647f72dbb0 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.tsx @@ -0,0 +1,110 @@ +import { Row } from 'antd'; +import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; +import { useNotifications } from 'hooks/useNotifications'; +import { map, sortBy } from 'lodash-es'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { memo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll'; +import AppReducer from 'types/reducer/app'; + +import VariableItem from './VariableItem'; + +function DashboardVariableSelection(): JSX.Element | null { + const { selectedDashboard, setSelectedDashboard } = useDashboard(); + + const { data } = selectedDashboard || {}; + + const { variables } = data || {}; + + const [update, setUpdate] = useState(false); + const [lastUpdatedVar, setLastUpdatedVar] = useState(''); + + const { role } = useSelector((state) => state.app); + + const onVarChanged = (name: string): void => { + setLastUpdatedVar(name); + setUpdate(!update); + }; + + const updateMutation = useUpdateDashboard(); + const { notifications } = useNotifications(); + + const updateVariables = ( + name: string, + updatedVariablesData: Dashboard['data']['variables'], + ): void => { + if (!selectedDashboard) { + return; + } + + updateMutation.mutateAsync( + { + ...selectedDashboard, + data: { + ...selectedDashboard.data, + variables: updatedVariablesData, + }, + }, + { + onSuccess: (updatedDashboard) => { + if (updatedDashboard.payload) { + setSelectedDashboard(updatedDashboard.payload); + } + }, + onError: () => { + notifications.error({ + message: `Error updating ${name} variable`, + }); + }, + }, + ); + }; + + const onValueUpdate = ( + name: string, + value: IDashboardVariable['selectedValue'], + allSelected: boolean, + ): void => { + const updatedVariablesData = { ...variables }; + updatedVariablesData[name].selectedValue = value; + updatedVariablesData[name].allSelected = allSelected; + + console.log('onValue Update', name); + + if (role !== 'VIEWER' && selectedDashboard) { + updateVariables(name, updatedVariablesData); + } + onVarChanged(name); + + setUpdate(!update); + }; + + if (!variables) { + return null; + } + + const variablesKeys = sortBy(Object.keys(variables)); + + return ( + + {variablesKeys && + map(variablesKeys, (variableName) => ( + + ))} + + ); +} + +export default memo(DashboardVariableSelection); diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx index 7543821b60..f3a2c0e4d0 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx @@ -1,6 +1,13 @@ import '@testing-library/jest-dom/extend-expect'; -import { fireEvent, render, screen } from '@testing-library/react'; +import { + act, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; +import MockQueryClientProvider from 'providers/test/MockQueryClientProvider'; import React, { useEffect } from 'react'; import { IDashboardVariable } from 'types/api/dashboard/getAll'; @@ -25,7 +32,6 @@ const mockCustomVariableData: IDashboardVariable = { }; const mockOnValueUpdate = jest.fn(); -const mockOnAllSelectedUpdate = jest.fn(); describe('VariableItem', () => { let useEffectSpy: jest.SpyInstance; @@ -41,13 +47,14 @@ describe('VariableItem', () => { test('renders component with default props', () => { render( - , + + + , ); expect(screen.getByText('$testVariable')).toBeInTheDocument(); @@ -55,45 +62,55 @@ describe('VariableItem', () => { test('renders Input when the variable type is TEXTBOX', () => { render( - , + + + , ); expect(screen.getByPlaceholderText('Enter value')).toBeInTheDocument(); }); - test('calls onChange event handler when Input value changes', () => { + test('calls onChange event handler when Input value changes', async () => { render( - , + + + , ); - const inputElement = screen.getByPlaceholderText('Enter value'); - fireEvent.change(inputElement, { target: { value: 'newValue' } }); - expect(mockOnValueUpdate).toHaveBeenCalledTimes(1); - expect(mockOnValueUpdate).toHaveBeenCalledWith('testVariable', 'newValue'); - expect(mockOnAllSelectedUpdate).toHaveBeenCalledTimes(1); - expect(mockOnAllSelectedUpdate).toHaveBeenCalledWith('testVariable', false); + act(() => { + const inputElement = screen.getByPlaceholderText('Enter value'); + fireEvent.change(inputElement, { target: { value: 'newValue' } }); + }); + + await waitFor(() => { + // expect(mockOnValueUpdate).toHaveBeenCalledTimes(1); + expect(mockOnValueUpdate).toHaveBeenCalledWith( + 'testVariable', + 'newValue', + false, + ); + }); }); test('renders a Select element when variable type is CUSTOM', () => { render( - , + + + , ); expect(screen.getByText('$customVariable')).toBeInTheDocument(); @@ -107,13 +124,14 @@ describe('VariableItem', () => { }; render( - , + + + , ); expect(screen.getByTitle('ALL')).toBeInTheDocument(); @@ -121,48 +139,16 @@ describe('VariableItem', () => { test('calls useEffect when the component mounts', () => { render( - , + + + , ); expect(useEffect).toHaveBeenCalled(); }); - - test('calls useEffect only once when the component mounts', () => { - // Render the component - const { rerender } = render( - , - ); - - // Create an updated version of the mock data - const updatedMockCustomVariableData = { - ...mockCustomVariableData, - selectedValue: 'option1', - }; - - // Re-render the component with the updated data - rerender( - , - ); - - // Check if the useEffect is called with the correct arguments - expect(useEffectSpy).toHaveBeenCalledTimes(4); - }); }); diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx index 2a46d57f8e..4e81b0b116 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx @@ -1,27 +1,35 @@ +import './DashboardVariableSelection.styles.scss'; + import { orange } from '@ant-design/colors'; import { WarningOutlined } from '@ant-design/icons'; import { Input, Popover, Select, Typography } from 'antd'; -import query from 'api/dashboard/variables/query'; +import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import useDebounce from 'hooks/useDebounce'; import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser'; import sortValues from 'lib/dashbaordVariables/sortVariableValues'; import map from 'lodash-es/map'; -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { memo, useEffect, useMemo, useState } from 'react'; +import { useQuery } from 'react-query'; import { IDashboardVariable } from 'types/api/dashboard/getAll'; +import { VariableResponseProps } from 'types/api/dashboard/variables/query'; import { variablePropsToPayloadVariables } from '../utils'; -import { SelectItemStyle, VariableContainer, VariableName } from './styles'; +import { SelectItemStyle, VariableContainer, VariableValue } from './styles'; import { areArraysEqual } from './util'; const ALL_SELECT_VALUE = '__ALL__'; +const variableRegexPattern = /\{\{\s*?\.([^\s}]+)\s*?\}\}/g; + interface VariableItemProps { variableData: IDashboardVariable; existingVariables: Record; onValueUpdate: ( name: string, arg1: IDashboardVariable['selectedValue'], + allSelected: boolean, ) => void; - onAllSelectedUpdate: (name: string, arg1: boolean) => void; lastUpdatedVar: string; } @@ -38,48 +46,74 @@ function VariableItem({ variableData, existingVariables, onValueUpdate, - onAllSelectedUpdate, lastUpdatedVar, }: VariableItemProps): JSX.Element { const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>( [], ); - const [isLoading, setIsLoading] = useState(false); + + const [variableValue, setVaribleValue] = useState( + variableData?.selectedValue?.toString() || '', + ); + + const debouncedVariableValue = useDebounce(variableValue, 500); const [errorMessage, setErrorMessage] = useState(null); - /* eslint-disable sonarjs/cognitive-complexity */ - const getOptions = useCallback(async (): Promise => { - if (variableData.type === 'QUERY') { + useEffect(() => { + const { selectedValue } = variableData; + + if (selectedValue) { + setVaribleValue(selectedValue?.toString()); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [variableData]); + + const getDependentVariables = (queryValue: string): string[] => { + const matches = queryValue.match(variableRegexPattern); + + // Extract variable names from the matches array without {{ . }} + return matches + ? matches.map((match) => match.replace(variableRegexPattern, '$1')) + : []; + }; + + const getQueryKey = (variableData: IDashboardVariable): string[] => { + let dependentVariablesStr = ''; + + const dependentVariables = getDependentVariables( + variableData.queryValue || '', + ); + + const variableName = variableData.name || ''; + + dependentVariables?.forEach((element) => { + dependentVariablesStr += `${element}${existingVariables[element]?.selectedValue}`; + }); + + const variableKey = dependentVariablesStr.replace(/\s/g, ''); + + return [REACT_QUERY_KEY.DASHBOARD_BY_ID, variableName, variableKey]; + }; + + // eslint-disable-next-line sonarjs/cognitive-complexity + const getOptions = (variablesRes: VariableResponseProps | null): void => { + if (variablesRes && variableData.type === 'QUERY') { try { setErrorMessage(null); - setIsLoading(true); - - const response = await query({ - query: variableData.queryValue || '', - variables: variablePropsToPayloadVariables(existingVariables), - }); - - setIsLoading(false); - if (response.error) { - let message = response.error; - if (response.error.includes('Syntax error:')) { - message = - 'Please make sure query is valid and dependent variables are selected'; - } - setErrorMessage(message); - return; - } - if (response.payload?.variableValues) { + + if ( + variablesRes?.variableValues && + Array.isArray(variablesRes?.variableValues) + ) { const newOptionsData = sortValues( - response.payload?.variableValues, + variablesRes?.variableValues, variableData.sort, ); - // Since there is a chance of a variable being dependent on other - // variables, we need to check if the optionsData has changed - // If it has changed, we need to update the dependent variable - // So we compare the new optionsData with the old optionsData + const oldOptionsData = sortValues(optionsData, variableData.sort) as never; + if (!areArraysEqual(newOptionsData, oldOptionsData)) { /* eslint-disable no-useless-escape */ const re = new RegExp(`\\{\\{\\s*?\\.${lastUpdatedVar}\\s*?\\}\\}`); // regex for `{{.var}}` @@ -104,10 +138,10 @@ function VariableItem({ [value] = newOptionsData; } if (variableData.name) { - onValueUpdate(variableData.name, value); - onAllSelectedUpdate(variableData.name, allSelected); + onValueUpdate(variableData.name, value, allSelected); } } + setOptionsData(newOptionsData); } } @@ -122,19 +156,37 @@ function VariableItem({ ) as never, ); } - }, [ - variableData, - existingVariables, - onValueUpdate, - onAllSelectedUpdate, - optionsData, - lastUpdatedVar, - ]); - - useEffect(() => { - getOptions(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [variableData, existingVariables]); + }; + + const { isLoading } = useQuery(getQueryKey(variableData), { + enabled: variableData && variableData.type === 'QUERY', + queryFn: () => + dashboardVariablesQuery({ + query: variableData.queryValue || '', + variables: variablePropsToPayloadVariables(existingVariables), + }), + refetchOnWindowFocus: false, + onSuccess: (response) => { + getOptions(response.payload); + }, + onError: (error: { + details: { + error: string; + }; + }) => { + const { details } = error; + + if (details.error) { + let message = details.error; + if (details.error.includes('Syntax error:')) { + message = + 'Please make sure query is valid and dependent variables are selected'; + } + setErrorMessage(message); + } + }, + }); const handleChange = (value: string | string[]): void => { if (variableData.name) @@ -143,11 +195,9 @@ function VariableItem({ (Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) || (Array.isArray(value) && value.length === 0) ) { - onValueUpdate(variableData.name, optionsData); - onAllSelectedUpdate(variableData.name, true); + onValueUpdate(variableData.name, optionsData, true); } else { - onValueUpdate(variableData.name, value); - onAllSelectedUpdate(variableData.name, false); + onValueUpdate(variableData.name, value, false); } }; @@ -165,61 +215,78 @@ function VariableItem({ ? 'multiple' : undefined; const enableSelectAll = variableData.multiSelect && variableData.showALLOption; + + useEffect(() => { + if (debouncedVariableValue !== variableData?.selectedValue?.toString()) { + handleChange(debouncedVariableValue); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [debouncedVariableValue]); + return ( - ${variableData.name} - {variableData.type === 'TEXTBOX' ? ( - { - handleChange(e.target.value || ''); - }} - style={{ - width: - 50 + ((variableData.selectedValue?.toString()?.length || 0) * 7 || 50), - }} - /> - ) : ( - !errorMessage && ( - - {enableSelectAll && ( - - ALL - - )} - {map(optionsData, (option) => ( - - {option.toString()} - - ))} - - ) - )} - {errorMessage && ( - - {errorMessage}}> - - - - )} + value={variableValue} + onChange={(e): void => { + setVaribleValue(e.target.value || ''); + }} + style={{ + width: + 50 + ((variableData.selectedValue?.toString()?.length || 0) * 7 || 50), + }} + /> + ) : ( + !errorMessage && + optionsData && ( + + ) + )} + {errorMessage && ( + + {errorMessage}} + > + + + + )} + ); } diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx index 439ab98a67..5b8e9e48c6 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx @@ -1,117 +1,3 @@ -import { Row } from 'antd'; -import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; -import { useNotifications } from 'hooks/useNotifications'; -import { map, sortBy } from 'lodash-es'; -import { useDashboard } from 'providers/Dashboard/Dashboard'; -import { memo, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { AppState } from 'store/reducers'; -import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll'; -import AppReducer from 'types/reducer/app'; +import DashboardVariableSelection from './DashboardVariableSelection'; -import VariableItem from './VariableItem'; - -function DashboardVariableSelection(): JSX.Element | null { - const { selectedDashboard, setSelectedDashboard } = useDashboard(); - - const { data } = selectedDashboard || {}; - - const { variables } = data || {}; - - const [update, setUpdate] = useState(false); - const [lastUpdatedVar, setLastUpdatedVar] = useState(''); - - const { role } = useSelector((state) => state.app); - - const onVarChanged = (name: string): void => { - setLastUpdatedVar(name); - setUpdate(!update); - }; - - const updateMutation = useUpdateDashboard(); - const { notifications } = useNotifications(); - - const updateVariables = ( - updatedVariablesData: Dashboard['data']['variables'], - ): void => { - if (!selectedDashboard) { - return; - } - - updateMutation.mutateAsync( - { - ...selectedDashboard, - data: { - ...selectedDashboard.data, - variables: updatedVariablesData, - }, - }, - { - onSuccess: (updatedDashboard) => { - if (updatedDashboard.payload) { - setSelectedDashboard(updatedDashboard.payload); - notifications.success({ - message: 'Variable updated successfully', - }); - } - }, - onError: () => { - notifications.error({ - message: 'Error while updating variable', - }); - }, - }, - ); - }; - - const onValueUpdate = ( - name: string, - value: IDashboardVariable['selectedValue'], - ): void => { - const updatedVariablesData = { ...variables }; - updatedVariablesData[name].selectedValue = value; - - if (role !== 'VIEWER' && selectedDashboard) { - updateVariables(updatedVariablesData); - } - - onVarChanged(name); - }; - const onAllSelectedUpdate = ( - name: string, - value: IDashboardVariable['allSelected'], - ): void => { - const updatedVariablesData = { ...variables }; - updatedVariablesData[name].allSelected = value; - - if (role !== 'VIEWER') { - updateVariables(updatedVariablesData); - } - onVarChanged(name); - }; - - if (!variables) { - return null; - } - - return ( - - {map(sortBy(Object.keys(variables)), (variableName) => ( - - ))} - - ); -} - -export default memo(DashboardVariableSelection); +export default DashboardVariableSelection; diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts b/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts index 76ba50c38c..5c5de3e97e 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts @@ -3,19 +3,40 @@ import { Typography } from 'antd'; import styled from 'styled-components'; export const VariableContainer = styled.div` + max-width: 100%; border: 1px solid ${grey[1]}66; border-radius: 2px; padding: 0; padding-left: 0.5rem; + margin-right: 8px; display: flex; align-items: center; margin-bottom: 0.3rem; + gap: 4px; + padding: 4px; `; export const VariableName = styled(Typography)` font-size: 0.8rem; - font-style: italic; color: ${grey[0]}; + + min-width: 100px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; +`; + +export const VariableValue = styled(Typography)` + font-size: 0.8rem; + color: ${grey[0]}; + + flex: 1; + + display: flex; + justify-content: flex-end; + align-items: center; + max-width: 300px; `; export const SelectItemStyle = { diff --git a/frontend/src/container/TopNav/DateTimeSelection/config.ts b/frontend/src/container/TopNav/DateTimeSelection/config.ts index 7b51837cce..cada5a3194 100644 --- a/frontend/src/container/TopNav/DateTimeSelection/config.ts +++ b/frontend/src/container/TopNav/DateTimeSelection/config.ts @@ -68,11 +68,7 @@ export const getOptions = (routes: string): Option[] => { return Options; }; -export const routesToHideBreadCrumbs = [ - ROUTES.SUPPORT, - ROUTES.ALL_DASHBOARD, - ROUTES.DASHBOARD, -]; +export const routesToHideBreadCrumbs = [ROUTES.SUPPORT, ROUTES.ALL_DASHBOARD]; export const routesToSkip = [ ROUTES.SETTINGS, diff --git a/frontend/src/pages/Dashboard/index.tsx b/frontend/src/pages/DashboardsListPage/DashboardsListPage.tsx similarity index 83% rename from frontend/src/pages/Dashboard/index.tsx rename to frontend/src/pages/DashboardsListPage/DashboardsListPage.tsx index 35b801387a..415c081a27 100644 --- a/frontend/src/pages/Dashboard/index.tsx +++ b/frontend/src/pages/DashboardsListPage/DashboardsListPage.tsx @@ -3,7 +3,7 @@ import ReleaseNote from 'components/ReleaseNote'; import ListOfAllDashboard from 'container/ListOfDashboard'; import { useLocation } from 'react-router-dom'; -function Dashboard(): JSX.Element { +function DashboardsListPage(): JSX.Element { const location = useLocation(); return ( @@ -14,4 +14,4 @@ function Dashboard(): JSX.Element { ); } -export default Dashboard; +export default DashboardsListPage; diff --git a/frontend/src/pages/DashboardsListPage/index.tsx b/frontend/src/pages/DashboardsListPage/index.tsx new file mode 100644 index 0000000000..e0007017e4 --- /dev/null +++ b/frontend/src/pages/DashboardsListPage/index.tsx @@ -0,0 +1,3 @@ +import DashboardsListPage from './DashboardsListPage'; + +export default DashboardsListPage; diff --git a/frontend/src/pages/NewDashboard/DashboardPage.tsx b/frontend/src/pages/NewDashboard/DashboardPage.tsx new file mode 100644 index 0000000000..d880413628 --- /dev/null +++ b/frontend/src/pages/NewDashboard/DashboardPage.tsx @@ -0,0 +1,33 @@ +import { Typography } from 'antd'; +import { AxiosError } from 'axios'; +import NotFound from 'components/NotFound'; +import Spinner from 'components/Spinner'; +import NewDashboard from 'container/NewDashboard'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { ErrorType } from 'types/common'; + +function DashboardPage(): JSX.Element { + const { dashboardResponse } = useDashboard(); + + const { isFetching, isError, isLoading } = dashboardResponse; + + const errorMessage = isError + ? (dashboardResponse?.error as AxiosError)?.response?.data.errorType + : 'Something went wrong'; + + if (isError && !isFetching && errorMessage === ErrorType.NotFound) { + return ; + } + + if (isError && errorMessage) { + return {errorMessage}; + } + + if (isLoading) { + return ; + } + + return ; +} + +export default DashboardPage; diff --git a/frontend/src/pages/NewDashboard/index.tsx b/frontend/src/pages/NewDashboard/index.tsx index 105d21cfdf..1b30cb0919 100644 --- a/frontend/src/pages/NewDashboard/index.tsx +++ b/frontend/src/pages/NewDashboard/index.tsx @@ -1,33 +1,3 @@ -import { Typography } from 'antd'; -import { AxiosError } from 'axios'; -import NotFound from 'components/NotFound'; -import Spinner from 'components/Spinner'; -import NewDashboard from 'container/NewDashboard'; -import { useDashboard } from 'providers/Dashboard/Dashboard'; -import { ErrorType } from 'types/common'; +import DashboardPage from './DashboardPage'; -function NewDashboardPage(): JSX.Element { - const { dashboardResponse } = useDashboard(); - - const { isFetching, isError, isLoading } = dashboardResponse; - - const errorMessage = isError - ? (dashboardResponse?.error as AxiosError)?.response?.data.errorType - : 'Something went wrong'; - - if (isError && !isFetching && errorMessage === ErrorType.NotFound) { - return ; - } - - if (isError && errorMessage) { - return {errorMessage}; - } - - if (isLoading) { - return ; - } - - return ; -} - -export default NewDashboardPage; +export default DashboardPage; diff --git a/frontend/src/types/api/dashboard/variables/query.ts b/frontend/src/types/api/dashboard/variables/query.ts index c535ad72be..6f4589a413 100644 --- a/frontend/src/types/api/dashboard/variables/query.ts +++ b/frontend/src/types/api/dashboard/variables/query.ts @@ -10,6 +10,6 @@ export type Props = { variables: PayloadVariables; }; -export type PayloadProps = { +export type VariableResponseProps = { variableValues: string[] | number[]; }; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index bc93c222d9..a81ac69961 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "sourceMap": true, "outDir": "./dist/", "noImplicitAny": true, "module": "esnext", @@ -20,11 +21,12 @@ "baseUrl": "./src", "downlevelIteration": true, "plugins": [{ "name": "typescript-plugin-css-modules" }], - "types": ["node", "jest"] + "types": ["node", "jest"], }, "exclude": ["node_modules"], "include": [ "./src", + "./src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "./babel.config.js", "./jest.config.ts", "./.eslintrc.js", diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index a5862179a4..58635d8994 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -46,7 +46,7 @@ if (process.env.BUNDLE_ANALYSER === 'true') { */ const config = { mode: 'development', - devtool: 'source-map', + devtool: 'eval-source-map', entry: resolve(__dirname, './src/index.tsx'), devServer: { historyApiFallback: true, From 9fb32acf6d42b090e6cdd1b6d37061d2c666e90b Mon Sep 17 00:00:00 2001 From: Prashant Shahi Date: Thu, 30 Nov 2023 18:02:18 +0530 Subject: [PATCH 02/19] =?UTF-8?q?ci(staging-deployment):=20=E2=9C=8F?= =?UTF-8?q?=EF=B8=8F=20fix=20command=20to=20pull=20latest=20schema=20migra?= =?UTF-8?q?tor=20image=20(#4123)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Prashant Shahi --- .github/workflows/staging-deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/staging-deployment.yaml b/.github/workflows/staging-deployment.yaml index b201eb9f64..ed13dc00e1 100644 --- a/.github/workflows/staging-deployment.yaml +++ b/.github/workflows/staging-deployment.yaml @@ -29,7 +29,7 @@ jobs: export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work docker system prune --force docker pull signoz/signoz-otel-collector:main - docker pull signoz/signoz/signoz-schema-migrator:main + docker pull signoz/signoz-schema-migrator:main cd ~/signoz git status git add . From 0a77c7ab85a936296a3927cf9162df33708567e4 Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Thu, 30 Nov 2023 18:41:26 +0530 Subject: [PATCH 03/19] fix: onRun Query offset is made zero (#4083) --- frontend/src/constants/query.ts | 1 + .../container/TracesExplorer/ListView/index.tsx | 5 +++-- .../container/TracesExplorer/TracesView/index.tsx | 5 +++-- frontend/src/hooks/queryPagination/config.ts | 2 -- .../hooks/queryPagination/useQueryPagination.ts | 5 +++-- frontend/src/providers/QueryBuilder.tsx | 14 ++++++++++++++ 6 files changed, 24 insertions(+), 8 deletions(-) diff --git a/frontend/src/constants/query.ts b/frontend/src/constants/query.ts index af7b76b2c4..d3bd2729d1 100644 --- a/frontend/src/constants/query.ts +++ b/frontend/src/constants/query.ts @@ -27,4 +27,5 @@ export enum QueryParams { viewName = 'viewName', viewKey = 'viewKey', expandedWidgetId = 'expandedWidgetId', + pagination = 'pagination', } diff --git a/frontend/src/container/TracesExplorer/ListView/index.tsx b/frontend/src/container/TracesExplorer/ListView/index.tsx index 4ff128b629..c9dd78df2f 100644 --- a/frontend/src/container/TracesExplorer/ListView/index.tsx +++ b/frontend/src/container/TracesExplorer/ListView/index.tsx @@ -1,11 +1,12 @@ import { ResizeTable } from 'components/ResizeTable'; import { LOCALSTORAGE } from 'constants/localStorage'; +import { QueryParams } from 'constants/query'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { useOptionsMenu } from 'container/OptionsMenu'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; -import { Pagination, URL_PAGINATION } from 'hooks/queryPagination'; +import { Pagination } from 'hooks/queryPagination'; import useDragColumns from 'hooks/useDragColumns'; import { getDraggedColumns } from 'hooks/useDragColumns/utils'; import useUrlQueryData from 'hooks/useUrlQueryData'; @@ -44,7 +45,7 @@ function ListView(): JSX.Element { ); const { queryData: paginationQueryData } = useUrlQueryData( - URL_PAGINATION, + QueryParams.pagination, ); const { data, isFetching, isError } = useGetQueryRange( diff --git a/frontend/src/container/TracesExplorer/TracesView/index.tsx b/frontend/src/container/TracesExplorer/TracesView/index.tsx index 3643e3c5e0..21fa41431c 100644 --- a/frontend/src/container/TracesExplorer/TracesView/index.tsx +++ b/frontend/src/container/TracesExplorer/TracesView/index.tsx @@ -1,10 +1,11 @@ import { Typography } from 'antd'; import { ResizeTable } from 'components/ResizeTable'; +import { QueryParams } from 'constants/query'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; -import { Pagination, URL_PAGINATION } from 'hooks/queryPagination'; +import { Pagination } from 'hooks/queryPagination'; import useUrlQueryData from 'hooks/useUrlQueryData'; import { memo, useMemo } from 'react'; import { useSelector } from 'react-redux'; @@ -24,7 +25,7 @@ function TracesView(): JSX.Element { >((state) => state.globalTime); const { queryData: paginationQueryData } = useUrlQueryData( - URL_PAGINATION, + QueryParams.pagination, ); const { data, isLoading } = useGetQueryRange( diff --git a/frontend/src/hooks/queryPagination/config.ts b/frontend/src/hooks/queryPagination/config.ts index 72dc032051..0e45c7df5c 100644 --- a/frontend/src/hooks/queryPagination/config.ts +++ b/frontend/src/hooks/queryPagination/config.ts @@ -1,3 +1 @@ -export const URL_PAGINATION = 'pagination'; - export const DEFAULT_PER_PAGE_OPTIONS: number[] = [25, 50, 100, 200]; diff --git a/frontend/src/hooks/queryPagination/useQueryPagination.ts b/frontend/src/hooks/queryPagination/useQueryPagination.ts index 29cee3ecb8..bf3cc30bb4 100644 --- a/frontend/src/hooks/queryPagination/useQueryPagination.ts +++ b/frontend/src/hooks/queryPagination/useQueryPagination.ts @@ -1,8 +1,9 @@ +import { QueryParams } from 'constants/query'; import { ControlsProps } from 'container/Controls'; import useUrlQueryData from 'hooks/useUrlQueryData'; import { useCallback, useEffect, useMemo } from 'react'; -import { DEFAULT_PER_PAGE_OPTIONS, URL_PAGINATION } from './config'; +import { DEFAULT_PER_PAGE_OPTIONS } from './config'; import { Pagination } from './types'; import { checkIsValidPaginationData, @@ -22,7 +23,7 @@ const useQueryPagination = ( query: paginationQuery, queryData: paginationQueryData, redirectWithQuery: redirectWithCurrentPagination, - } = useUrlQueryData(URL_PAGINATION); + } = useUrlQueryData(QueryParams.pagination); const handleCountItemsPerPageChange = useCallback( (newLimit: Pagination['limit']) => { diff --git a/frontend/src/providers/QueryBuilder.tsx b/frontend/src/providers/QueryBuilder.tsx index 593bc642fd..bc0521fcbd 100644 --- a/frontend/src/providers/QueryBuilder.tsx +++ b/frontend/src/providers/QueryBuilder.tsx @@ -494,6 +494,20 @@ export function QueryBuilderProvider({ unit: query.unit || initialQueryState.unit, }; + const pagination = urlQuery.get(QueryParams.pagination); + + if (pagination) { + const parsedPagination = JSON.parse(pagination); + + urlQuery.set( + QueryParams.pagination, + JSON.stringify({ + limit: parsedPagination.limit, + offset: 0, + }), + ); + } + urlQuery.set( QueryParams.compositeQuery, encodeURIComponent(JSON.stringify(currentGeneratedQuery)), From a48edac13b963fec037926937a0f0efd9e282a4f Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Thu, 30 Nov 2023 18:56:09 +0530 Subject: [PATCH 04/19] fix: the default query issue in log (#4108) --- frontend/src/components/ExplorerCard/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/ExplorerCard/utils.ts b/frontend/src/components/ExplorerCard/utils.ts index 8ecef3e44a..7385fbcc7f 100644 --- a/frontend/src/components/ExplorerCard/utils.ts +++ b/frontend/src/components/ExplorerCard/utils.ts @@ -153,7 +153,7 @@ export const deleteViewHandler = ({ if (viewId === viewKey) { redirectWithQueryBuilderData( updateAllQueriesOperators( - initialQueriesMap.traces, + initialQueriesMap[sourcePage], panelType || PANEL_TYPES.LIST, sourcePage, ), From be519666a3b424bb6e6b8eff7719b26ee6271a65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 11:14:14 +0530 Subject: [PATCH 05/19] chore(deps): bump @adobe/css-tools from 4.3.1 to 4.3.2 in /frontend (#4134) Bumps [@adobe/css-tools](https://github.com/adobe/css-tools) from 4.3.1 to 4.3.2. - [Changelog](https://github.com/adobe/css-tools/blob/main/History.md) - [Commits](https://github.com/adobe/css-tools/commits) --- updated-dependencies: - dependency-name: "@adobe/css-tools" dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index c4fb7cd456..bc6da58703 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -34,9 +34,9 @@ three-render-objects "1" "@adobe/css-tools@^4.0.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.1.tgz#abfccb8ca78075a2b6187345c26243c1a0842f28" - integrity sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg== + version "4.3.2" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.2.tgz#a6abc715fb6884851fca9dad37fc34739a04fd11" + integrity sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw== "@ampproject/remapping@^2.2.0": version "2.2.1" From b616dca52d23947b86357aa2024865b77cb0d6e2 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Fri, 1 Dec 2023 16:42:19 +0530 Subject: [PATCH 06/19] fix: the full view in service layer (#4133) Co-authored-by: Srikanth Chekuri --- .../GridCardLayout/GridCard/FullView/utils.ts | 33 +++++++++++-------- .../GridCard/WidgetGraphComponent.tsx | 19 +++++++++-- .../MetricsApplication.factory.ts | 3 +- .../MetricsApplication/Tabs/DBCall.tsx | 4 ++- .../MetricsApplication/Tabs/External.tsx | 6 +++- .../MetricsApplication/Tabs/Overview.tsx | 4 ++- .../Tabs/Overview/ApDex/ApDexMetrics.tsx | 6 +++- .../Tabs/Overview/ServiceOverview.tsx | 6 +++- .../container/MetricsApplication/constant.ts | 14 ++++++++ .../src/container/MetricsApplication/types.ts | 1 + 10 files changed, 74 insertions(+), 22 deletions(-) diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/utils.ts b/frontend/src/container/GridCardLayout/GridCard/FullView/utils.ts index 400394d26e..bbbca2e834 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/utils.ts +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/utils.ts @@ -25,19 +25,26 @@ export const getDefaultTableDataSet = ( data: uPlot.AlignedData, ): ExtendedChartDataset[] => options.series.map( - (item: uPlot.Series, index: number): ExtendedChartDataset => ({ - ...item, - index, - show: true, - sum: convertToTwoDecimalsOrZero( - (data[index] as number[]).reduce((a, b) => a + b, 0), - ), - avg: convertToTwoDecimalsOrZero( - (data[index] as number[]).reduce((a, b) => a + b, 0) / data[index].length, - ), - max: convertToTwoDecimalsOrZero(Math.max(...(data[index] as number[]))), - min: convertToTwoDecimalsOrZero(Math.min(...(data[index] as number[]))), - }), + (item: uPlot.Series, index: number): ExtendedChartDataset => { + let arr: number[]; + if (data[index]) { + arr = data[index] as number[]; + } else { + arr = []; + } + + return { + ...item, + index, + show: true, + sum: convertToTwoDecimalsOrZero(arr.reduce((a, b) => a + b, 0) || 0), + avg: convertToTwoDecimalsOrZero( + (arr.reduce((a, b) => a + b, 0) || 0) / (arr.length || 1), + ), + max: convertToTwoDecimalsOrZero(Math.max(...arr)), + min: convertToTwoDecimalsOrZero(Math.min(...arr)), + }; + }, ); export const getAbbreviatedLabel = (label: string): string => { diff --git a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx index ee216280e2..2129220427 100644 --- a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx @@ -47,7 +47,7 @@ function WidgetGraphComponent({ const [deleteModal, setDeleteModal] = useState(false); const [hovered, setHovered] = useState(false); const { notifications } = useNotifications(); - const { pathname } = useLocation(); + const { pathname, search } = useLocation(); const params = useUrlQuery(); @@ -183,10 +183,20 @@ function WidgetGraphComponent({ const queryParams = { [QueryParams.expandedWidgetId]: widget.id, }; + const updatedSearch = createQueryParams(queryParams); + const existingSearch = new URLSearchParams(search); + const isExpandedWidgetIdPresent = existingSearch.has( + QueryParams.expandedWidgetId, + ); + if (isExpandedWidgetIdPresent) { + existingSearch.delete(QueryParams.expandedWidgetId); + } + const separator = existingSearch.toString() ? '&' : ''; + const newSearch = `${existingSearch}${separator}${updatedSearch}`; history.push({ pathname, - search: createQueryParams(queryParams), + search: newSearch, }); }; @@ -199,9 +209,12 @@ function WidgetGraphComponent({ }; const onToggleModelHandler = (): void => { + const existingSearchParams = new URLSearchParams(search); + existingSearchParams.delete(QueryParams.expandedWidgetId); + const updatedQueryParams = Object.fromEntries(existingSearchParams.entries()); history.push({ pathname, - search: createQueryParams({}), + search: createQueryParams(updatedQueryParams), }); }; diff --git a/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts b/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts index 31e949111f..9941308838 100644 --- a/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts +++ b/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts @@ -8,9 +8,10 @@ export const getWidgetQueryBuilder = ({ title = '', panelTypes, yAxisUnit = '', + id, }: GetWidgetQueryBuilderProps): Widgets => ({ description: '', - id: v4(), + id: id || v4(), isStacked: false, nullZeroValues: '', opacity: '0', diff --git a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx index 35e77168b4..31ed0769bf 100644 --- a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx @@ -16,7 +16,7 @@ import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; import { v4 as uuid } from 'uuid'; -import { GraphTitle, MENU_ITEMS } from '../constant'; +import { GraphTitle, MENU_ITEMS, SERVICE_CHART_ID } from '../constant'; import { getWidgetQueryBuilder } from '../MetricsApplication.factory'; import { Card, GraphContainer, Row } from '../styles'; import { Button } from './styles'; @@ -66,6 +66,7 @@ function DBCall(): JSX.Element { title: GraphTitle.DATABASE_CALLS_RPS, panelTypes: PANEL_TYPES.TIME_SERIES, yAxisUnit: 'reqps', + id: SERVICE_CHART_ID.dbCallsRPS, }), [servicename, tagFilterItems], ); @@ -85,6 +86,7 @@ function DBCall(): JSX.Element { title: GraphTitle.DATABASE_CALLS_AVG_DURATION, panelTypes: PANEL_TYPES.TIME_SERIES, yAxisUnit: 'ms', + id: SERVICE_CHART_ID.dbCallsAvgDuration, }), [servicename, tagFilterItems], ); diff --git a/frontend/src/container/MetricsApplication/Tabs/External.tsx b/frontend/src/container/MetricsApplication/Tabs/External.tsx index cd5eaf3806..7748f8002a 100644 --- a/frontend/src/container/MetricsApplication/Tabs/External.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/External.tsx @@ -17,7 +17,7 @@ import { useParams } from 'react-router-dom'; import { EQueryType } from 'types/common/dashboard'; import { v4 as uuid } from 'uuid'; -import { GraphTitle, legend, MENU_ITEMS } from '../constant'; +import { GraphTitle, legend, MENU_ITEMS, SERVICE_CHART_ID } from '../constant'; import { getWidgetQueryBuilder } from '../MetricsApplication.factory'; import { Card, GraphContainer, Row } from '../styles'; import { Button } from './styles'; @@ -57,6 +57,7 @@ function External(): JSX.Element { title: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE, panelTypes: PANEL_TYPES.TIME_SERIES, yAxisUnit: '%', + id: SERVICE_CHART_ID.externalCallErrorPercentage, }), [servicename, tagFilterItems], ); @@ -82,6 +83,7 @@ function External(): JSX.Element { title: GraphTitle.EXTERNAL_CALL_DURATION, panelTypes: PANEL_TYPES.TIME_SERIES, yAxisUnit: 'ms', + id: SERVICE_CHART_ID.externalCallDuration, }), [servicename, tagFilterItems], ); @@ -103,6 +105,7 @@ function External(): JSX.Element { title: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS, panelTypes: PANEL_TYPES.TIME_SERIES, yAxisUnit: 'reqps', + id: SERVICE_CHART_ID.externalCallRPSByAddress, }), [servicename, tagFilterItems], ); @@ -124,6 +127,7 @@ function External(): JSX.Element { title: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS, panelTypes: PANEL_TYPES.TIME_SERIES, yAxisUnit: 'ms', + id: SERVICE_CHART_ID.externalCallDurationByAddress, }), [servicename, tagFilterItems], ); diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx index ff6460e3d7..2b79c861ea 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx @@ -26,7 +26,7 @@ import { GlobalReducer } from 'types/reducer/globalTime'; import { Tags } from 'types/reducer/trace'; import { v4 as uuid } from 'uuid'; -import { GraphTitle } from '../constant'; +import { GraphTitle, SERVICE_CHART_ID } from '../constant'; import { getWidgetQueryBuilder } from '../MetricsApplication.factory'; import { errorPercentage, @@ -131,6 +131,7 @@ function Application(): JSX.Element { title: GraphTitle.RATE_PER_OPS, panelTypes: PANEL_TYPES.TIME_SERIES, yAxisUnit: 'ops', + id: SERVICE_CHART_ID.rps, }), [servicename, tagFilterItems, topLevelOperationsRoute], ); @@ -152,6 +153,7 @@ function Application(): JSX.Element { title: GraphTitle.ERROR_PERCENTAGE, panelTypes: PANEL_TYPES.TIME_SERIES, yAxisUnit: '%', + id: SERVICE_CHART_ID.errorPercentage, }), [servicename, tagFilterItems, topLevelOperationsRoute], ); diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx index d9d134a2a6..e3b03ac577 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx @@ -8,7 +8,10 @@ import { import { PANEL_TYPES } from 'constants/queryBuilder'; import Graph from 'container/GridCardLayout/GridCard'; import DisplayThreshold from 'container/GridCardLayout/WidgetHeader/DisplayThreshold'; -import { GraphTitle } from 'container/MetricsApplication/constant'; +import { + GraphTitle, + SERVICE_CHART_ID, +} from 'container/MetricsApplication/constant'; import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory'; import { apDexMetricsQueryBuilderQueries } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries'; import { ReactNode, useMemo } from 'react'; @@ -59,6 +62,7 @@ function ApDexMetrics({ ), panelTypes: PANEL_TYPES.TIME_SERIES, + id: SERVICE_CHART_ID.apdex, }), [ delta, diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx index ca8fe8c2e2..a6e0e756e1 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx @@ -1,7 +1,10 @@ import { FeatureKeys } from 'constants/features'; import { PANEL_TYPES } from 'constants/queryBuilder'; import Graph from 'container/GridCardLayout/GridCard'; -import { GraphTitle } from 'container/MetricsApplication/constant'; +import { + GraphTitle, + SERVICE_CHART_ID, +} from 'container/MetricsApplication/constant'; import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory'; import { latency } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries'; import { Card, GraphContainer } from 'container/MetricsApplication/styles'; @@ -59,6 +62,7 @@ function ServiceOverview({ title: GraphTitle.LATENCY, panelTypes: PANEL_TYPES.TIME_SERIES, yAxisUnit: 'ns', + id: SERVICE_CHART_ID.latency, }), [servicename, isSpanMetricEnable, topLevelOperationsRoute, tagFilterItems], ); diff --git a/frontend/src/container/MetricsApplication/constant.ts b/frontend/src/container/MetricsApplication/constant.ts index a6d923c470..1e2958a628 100644 --- a/frontend/src/container/MetricsApplication/constant.ts +++ b/frontend/src/container/MetricsApplication/constant.ts @@ -79,3 +79,17 @@ export const topOperationMetricsDownloadOptions: DownloadOptions = { isDownloadEnabled: true, fileName: 'top-operation', } as const; + +export const SERVICE_CHART_ID = { + latency: 'SERVICE_OVERVIEW_LATENCY', + error: 'SERVICE_OVERVIEW_ERROR', + rps: 'SERVICE_OVERVIEW_RPS', + apdex: 'SERVICE_OVERVIEW_APDEX', + errorPercentage: 'SERVICE_OVERVIEW_ERROR_PERCENTAGE', + dbCallsRPS: 'SERVICE_DATABASE_CALLS_RPS', + dbCallsAvgDuration: 'SERVICE_DATABASE_CALLS_AVG_DURATION', + externalCallDurationByAddress: 'SERVICE_EXTERNAL_CALLS_DURATION_BY_ADDRESS', + externalCallErrorPercentage: 'SERVICE_EXTERNAL_CALLS_ERROR_PERCENTAGE', + externalCallDuration: 'SERVICE_EXTERNAL_CALLS_DURATION', + externalCallRPSByAddress: 'SERVICE_EXTERNAL_CALLS_RPS_BY_ADDRESS', +}; diff --git a/frontend/src/container/MetricsApplication/types.ts b/frontend/src/container/MetricsApplication/types.ts index f87ce66a2a..642bf0b057 100644 --- a/frontend/src/container/MetricsApplication/types.ts +++ b/frontend/src/container/MetricsApplication/types.ts @@ -9,6 +9,7 @@ export interface GetWidgetQueryBuilderProps { title?: ReactNode; panelTypes: Widgets['panelTypes']; yAxisUnit?: Widgets['yAxisUnit']; + id?: Widgets['id']; } export interface NavigateToTraceProps { From 3e29161fead6e0508c80105de85410deeadc2b74 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Fri, 1 Dec 2023 17:08:24 +0530 Subject: [PATCH 07/19] fix: update logic for handling data for uplot charts (#4131) * fix: update logic for handling data for uplot charts * fix: hide tooltip if no tooltip values present * fix: remove console log --- .../NewWidget/RightContainer/index.tsx | 42 +----- .../src/lib/uPlotLib/plugins/tooltipPlugin.ts | 48 ++++--- .../lib/uPlotLib/utils/getUplotChartData.ts | 132 ++++++++---------- frontend/src/styles.scss | 5 + 4 files changed, 97 insertions(+), 130 deletions(-) diff --git a/frontend/src/container/NewWidget/RightContainer/index.tsx b/frontend/src/container/NewWidget/RightContainer/index.tsx index d8f650f083..023dd584b2 100644 --- a/frontend/src/container/NewWidget/RightContainer/index.tsx +++ b/frontend/src/container/NewWidget/RightContainer/index.tsx @@ -107,48 +107,8 @@ function RightContainer({ } /> - {/* - Stacked Graphs : - { - setStacked((value) => !value); - }} - /> - */} - - {/* Fill Opacity: */} - - {/* onChangeHandler(setOpacity, number.toString())} - step={1} - /> */} - - {/* Null/Zero values: - - - {nullValueButtons.map((button) => ( - - ))} - */} - - Fill span gaps + Fill gaps { const container = document.createElement('div'); container.classList.add('tooltip-container'); + const overlay = document.getElementById('overlay'); + let tooltipCount = 0; let tooltipTitle = ''; const formattedData: Record = {}; @@ -49,28 +51,40 @@ const generateTooltipContent = ( const { metric = {}, queryName = '', legend = '' } = seriesList[index - 1] || {}; + const value = data[index][idx]; const label = getLabelName(metric, queryName || '', legend || ''); - const value = data[index][idx] || 0; - const tooltipValue = getToolTipValue(value, yAxisUnit); - - const dataObj = { - show: item.show || false, - color: colors[(index - 1) % colors.length], - label, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - focus: item?._focus || false, - value, - tooltipValue, - textContent: `${label} : ${tooltipValue}`, - }; - - formattedData[label] = dataObj; + if (value) { + const tooltipValue = getToolTipValue(value, yAxisUnit); + + const dataObj = { + show: item.show || false, + color: colors[(index - 1) % colors.length], + label, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + focus: item?._focus || false, + value, + tooltipValue, + textContent: `${label} : ${tooltipValue}`, + }; + + tooltipCount += 1; + formattedData[label] = dataObj; + } } }); } + // Show tooltip only if atleast only series has a value at the hovered timestamp + if (tooltipCount <= 0) { + if (overlay && overlay.style.display === 'block') { + overlay.style.display = 'none'; + } + + return container; + } + const sortedData: Record< string, UplotTooltipDataProps @@ -116,8 +130,6 @@ const generateTooltipContent = ( }); } - const overlay = document.getElementById('overlay'); - if (overlay && overlay.style.display === 'none') { overlay.style.display = 'block'; } diff --git a/frontend/src/lib/uPlotLib/utils/getUplotChartData.ts b/frontend/src/lib/uPlotLib/utils/getUplotChartData.ts index be4bfb59e5..d1f5a7ffbc 100644 --- a/frontend/src/lib/uPlotLib/utils/getUplotChartData.ts +++ b/frontend/src/lib/uPlotLib/utils/getUplotChartData.ts @@ -1,44 +1,65 @@ import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; +import { QueryData } from 'types/api/widgets/getQuery'; -// eslint-disable-next-line sonarjs/cognitive-complexity -function fillMissingTimestamps( - sortedTimestamps: number[], - subsetArray: any[], - fillSpans: boolean | undefined, -): any[] { - const filledArray = []; +function getXAxisTimestamps(seriesList: QueryData[]): number[] { + const timestamps = new Set(); - let subsetIndex = 0; - // eslint-disable-next-line no-restricted-syntax - for (const timestamp of sortedTimestamps) { - if ( - subsetIndex < subsetArray.length && - timestamp === subsetArray[subsetIndex][0] - ) { - // Timestamp is present in subsetArray - const seriesPointData = subsetArray[subsetIndex]; + seriesList.forEach((series: { values: [number, string][] }) => { + series.values.forEach((value) => { + timestamps.add(value[0]); + }); + }); - if ( - seriesPointData && - Array.isArray(seriesPointData) && - seriesPointData.length > 0 && - seriesPointData[1] !== 'NaN' - ) { - filledArray.push(subsetArray[subsetIndex]); - } else { - const value = fillSpans ? 0 : null; - filledArray.push([seriesPointData[0], value]); - } + const timestampsArr: number[] | unknown[] = Array.from(timestamps) || []; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return timestampsArr.sort((a, b) => a - b); +} - subsetIndex += 1; - } else { - // Timestamp is missing in subsetArray, fill with [timestamp, 0] +function fillMissingXAxisTimestamps( + timestampArr: number[], + data: any[], + fillSpans: boolean, +): any { + // Generate a set of all timestamps in the range + const allTimestampsSet = new Set(timestampArr); + const processedData = JSON.parse(JSON.stringify(data)); + + // Fill missing timestamps with null values + processedData.forEach((entry: { values: (number | null)[][] }) => { + const existingTimestamps = new Set(entry.values.map((value) => value[0])); + + const missingTimestamps = Array.from(allTimestampsSet).filter( + (timestamp) => !existingTimestamps.has(timestamp), + ); + + missingTimestamps.forEach((timestamp) => { const value = fillSpans ? 0 : null; - filledArray.push([timestamp, value]); - } - } - return filledArray; + entry.values.push([timestamp, value]); + }); + + entry.values.forEach((v) => { + if (Number.isNaN(v[1])) { + const replaceValue = fillSpans ? 0 : null; + // eslint-disable-next-line no-param-reassign + v[1] = replaceValue; + } else if (v[1] !== null) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // eslint-disable-next-line no-param-reassign + v[1] = parseFloat(v[1]); + } + }); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + entry.values.sort((a, b) => a[0] - b[0]); + }); + + return processedData.map((entry: { values: [number, string][] }) => + entry.values.map((value) => value[1]), + ); } export const getUPlotChartData = ( @@ -46,43 +67,12 @@ export const getUPlotChartData = ( fillSpans?: boolean, ): any[] => { const seriesList = apiResponse?.data?.result || []; - const uPlotData = []; - - // this helps us identify the series with the max number of values and helps define the x axis - timestamps - const xSeries = seriesList.reduce( - (maxObj, currentObj) => - currentObj.values.length > maxObj.values.length ? currentObj : maxObj, - seriesList[0], + const timestampArr = getXAxisTimestamps(seriesList); + const yAxisValuesArr = fillMissingXAxisTimestamps( + timestampArr, + seriesList, + fillSpans || false, ); - // sort seriesList - for (let index = 0; index < seriesList.length; index += 1) { - seriesList[index]?.values?.sort((a, b) => a[0] - b[0]); - } - - const timestampArr = xSeries?.values?.map((v) => v[0]); - - // timestamp - uPlotData.push(timestampArr); - - // for each series, push the values - seriesList.forEach((series) => { - const updatedSeries = fillMissingTimestamps( - timestampArr, - series?.values || [], - fillSpans, - ); - - const seriesData = - updatedSeries?.map((v) => { - if (v[1] === null) { - return v[1]; - } - return parseFloat(v[1]); - }) || []; - - uPlotData.push(seriesData); - }); - - return uPlotData; + return [timestampArr, ...yAxisValuesArr]; }; diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 8efda895d4..6712b4c59a 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -42,6 +42,11 @@ body { border-radius: 50%; } } + + &.u-off { + text-decoration: line-through; + text-decoration-thickness: 3px; + } } } From aad44a10373e91e1bf21dd937bf59c74d2a66d11 Mon Sep 17 00:00:00 2001 From: Raj Kamal Singh <1133322+raj-k-singh@users.noreply.github.com> Date: Fri, 1 Dec 2023 17:22:22 +0530 Subject: [PATCH 08/19] Feat: QS: logs pipelines severity parsing processor (#4132) * chore: update test helper for making logs * chore: add happy case test for severity parser * feat: get severity parsing processor working and add more happy path tests * chore: add test for validating severity parser doesn't spam collector logs * chore: add if condition to generated severity_parser operators * chore: add postablePipeline validation for severity parser * chore: minor cleanup in tests * chore: allow trace and fatal in severity mappings --- .../app/logparsingpipeline/model.go | 4 + .../app/logparsingpipeline/pipelineBuilder.go | 10 + .../logparsingpipeline/postablePipeline.go | 12 + .../postablePipeline_test.go | 34 +++ .../app/logparsingpipeline/preview_test.go | 56 +++-- .../severity_parser_test.go | 221 ++++++++++++++++++ .../logparsingpipeline/time_parser_test.go | 4 +- 7 files changed, 322 insertions(+), 19 deletions(-) create mode 100644 pkg/query-service/app/logparsingpipeline/severity_parser_test.go diff --git a/pkg/query-service/app/logparsingpipeline/model.go b/pkg/query-service/app/logparsingpipeline/model.go index dc52f157b8..e37bf85546 100644 --- a/pkg/query-service/app/logparsingpipeline/model.go +++ b/pkg/query-service/app/logparsingpipeline/model.go @@ -66,6 +66,10 @@ type PipelineOperator struct { // time_parser fields. Layout string `json:"layout,omitempty" yaml:"layout,omitempty"` LayoutType string `json:"layout_type,omitempty" yaml:"layout_type,omitempty"` + + // severity parser fields + SeverityMapping map[string][]string `json:"mapping,omitempty" yaml:"mapping,omitempty"` + OverwriteSeverityText bool `json:"overwrite_text,omitempty" yaml:"overwrite_text,omitempty"` } type TimestampParser struct { diff --git a/pkg/query-service/app/logparsingpipeline/pipelineBuilder.go b/pkg/query-service/app/logparsingpipeline/pipelineBuilder.go index 14ac63d3cc..56889ef827 100644 --- a/pkg/query-service/app/logparsingpipeline/pipelineBuilder.go +++ b/pkg/query-service/app/logparsingpipeline/pipelineBuilder.go @@ -138,6 +138,16 @@ func getOperators(ops []PipelineOperator) ([]PipelineOperator, error) { } // TODO(Raj): Maybe add support for gotime too eventually + + } else if operator.Type == "severity_parser" { + parseFromParts := strings.Split(operator.ParseFrom, ".") + parseFromPath := strings.Join(parseFromParts, "?.") + + operator.If = fmt.Sprintf( + `%s != nil && ( type(%s) == "string" || ( type(%s) in ["int", "float"] && %s == float(int(%s)) ) )`, + parseFromPath, parseFromPath, parseFromPath, parseFromPath, parseFromPath, + ) + } filteredOp = append(filteredOp, operator) diff --git a/pkg/query-service/app/logparsingpipeline/postablePipeline.go b/pkg/query-service/app/logparsingpipeline/postablePipeline.go index 1018f2f41d..790c8c5915 100644 --- a/pkg/query-service/app/logparsingpipeline/postablePipeline.go +++ b/pkg/query-service/app/logparsingpipeline/postablePipeline.go @@ -198,6 +198,18 @@ func isValidOperator(op PipelineOperator) error { } } + case "severity_parser": + if op.ParseFrom == "" { + return fmt.Errorf("parse from of severity parsing processor %s cannot be empty", op.ID) + } + + validMappingLevels := []string{"trace", "debug", "info", "warn", "error", "fatal"} + for k, _ := range op.SeverityMapping { + if !slices.Contains(validMappingLevels, strings.ToLower(k)) { + return fmt.Errorf("%s is not a valid severity in processor %s", k, op.ID) + } + } + default: return fmt.Errorf(fmt.Sprintf("operator type %s not supported for %s, use one of (grok_parser, regex_parser, copy, move, add, remove, trace_parser, retain)", op.Type, op.ID)) } diff --git a/pkg/query-service/app/logparsingpipeline/postablePipeline_test.go b/pkg/query-service/app/logparsingpipeline/postablePipeline_test.go index 8f8a6d9c48..371799ebab 100644 --- a/pkg/query-service/app/logparsingpipeline/postablePipeline_test.go +++ b/pkg/query-service/app/logparsingpipeline/postablePipeline_test.go @@ -326,6 +326,40 @@ var operatorTest = []struct { Layout: "%U", }, IsValid: false, + }, { + Name: "Severity Parser - valid", + Operator: PipelineOperator{ + ID: "severity", + Type: "severity_parser", + ParseFrom: "attributes.test_severity", + SeverityMapping: map[string][]string{ + "trace": {"test_trace"}, + "fatal": {"test_fatal"}, + }, + OverwriteSeverityText: true, + }, + IsValid: true, + }, { + Name: "Severity Parser - Parse from is required", + Operator: PipelineOperator{ + ID: "severity", + Type: "severity_parser", + SeverityMapping: map[string][]string{}, + OverwriteSeverityText: true, + }, + IsValid: false, + }, { + Name: "Severity Parser - mapping level must be valid", + Operator: PipelineOperator{ + ID: "severity", + Type: "severity_parser", + ParseFrom: "attributes.test", + SeverityMapping: map[string][]string{ + "not-a-level": {"bad-level"}, + }, + OverwriteSeverityText: true, + }, + IsValid: false, }, } diff --git a/pkg/query-service/app/logparsingpipeline/preview_test.go b/pkg/query-service/app/logparsingpipeline/preview_test.go index a7f01866b7..32bfc3b924 100644 --- a/pkg/query-service/app/logparsingpipeline/preview_test.go +++ b/pkg/query-service/app/logparsingpipeline/preview_test.go @@ -3,6 +3,7 @@ package logparsingpipeline import ( "context" "encoding/json" + "fmt" "strconv" "testing" "time" @@ -91,15 +92,15 @@ func TestPipelinePreview(t *testing.T) { }, } - matchingLog := makeTestLogEntry( + matchingLog := makeTestSignozLog( "test log body", - map[string]string{ + map[string]interface{}{ "method": "GET", }, ) - nonMatchingLog := makeTestLogEntry( + nonMatchingLog := makeTestSignozLog( "test log body", - map[string]string{ + map[string]interface{}{ "method": "POST", }, ) @@ -184,9 +185,9 @@ func TestGrokParsingProcessor(t *testing.T) { }, } - testLog := makeTestLogEntry( + testLog := makeTestSignozLog( "2023-10-26T04:38:00.602Z INFO route/server.go:71 HTTP request received", - map[string]string{ + map[string]interface{}{ "method": "GET", }, ) @@ -314,18 +315,39 @@ func TestTraceParsingProcessor(t *testing.T) { require.Equal("", result[0].SpanID) } -func makeTestLogEntry( +func makeTestSignozLog( body string, - attributes map[string]string, + attributes map[string]interface{}, ) model.SignozLog { - return model.SignozLog{ - Timestamp: uint64(time.Now().UnixNano()), - Body: body, - Attributes_string: attributes, - Resources_string: map[string]string{}, - SeverityText: entry.Info.String(), - SeverityNumber: uint8(entry.Info), - SpanID: uuid.New().String(), - TraceID: uuid.New().String(), + + testLog := model.SignozLog{ + Timestamp: uint64(time.Now().UnixNano()), + Body: body, + Attributes_bool: map[string]bool{}, + Attributes_string: map[string]string{}, + Attributes_int64: map[string]int64{}, + Attributes_float64: map[string]float64{}, + Resources_string: map[string]string{}, + SeverityText: entry.Info.String(), + SeverityNumber: uint8(entry.Info), + SpanID: uuid.New().String(), + TraceID: uuid.New().String(), + } + + for k, v := range attributes { + switch v.(type) { + case bool: + testLog.Attributes_bool[k] = v.(bool) + case string: + testLog.Attributes_string[k] = v.(string) + case int: + testLog.Attributes_int64[k] = int64(v.(int)) + case float64: + testLog.Attributes_float64[k] = v.(float64) + default: + panic(fmt.Sprintf("found attribute value of unsupported type %T in test log", v)) + } } + + return testLog } diff --git a/pkg/query-service/app/logparsingpipeline/severity_parser_test.go b/pkg/query-service/app/logparsingpipeline/severity_parser_test.go new file mode 100644 index 0000000000..12a273d1b0 --- /dev/null +++ b/pkg/query-service/app/logparsingpipeline/severity_parser_test.go @@ -0,0 +1,221 @@ +package logparsingpipeline + +import ( + "context" + "encoding/json" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "go.signoz.io/signoz/pkg/query-service/model" + v3 "go.signoz.io/signoz/pkg/query-service/model/v3" +) + +func TestSeverityParsingProcessor(t *testing.T) { + require := require.New(t) + + testPipelines := []Pipeline{ + { + OrderId: 1, + Name: "pipeline1", + Alias: "pipeline1", + Enabled: true, + Filter: &v3.FilterSet{ + Operator: "AND", + Items: []v3.FilterItem{ + { + Key: v3.AttributeKey{ + Key: "method", + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeTag, + }, + Operator: "=", + Value: "GET", + }, + }, + }, + Config: []PipelineOperator{}, + }, + } + + var severityParserOp PipelineOperator + err := json.Unmarshal([]byte(` + { + "orderId": 1, + "enabled": true, + "type": "severity_parser", + "name": "Test severity parser", + "id": "test-severity-parser", + "parse_from": "attributes.test_severity", + "mapping": { + "trace": ["test_trace"], + "debug": ["test_debug", "2xx"], + "info": ["test_info", "3xx"], + "warn": ["test_warn", "4xx"], + "error": ["test_error", "5xx"], + "fatal": ["test_fatal"] + }, + "overwrite_text": true + } + `), &severityParserOp) + require.Nil(err) + testPipelines[0].Config = append(testPipelines[0].Config, severityParserOp) + + testCases := []struct { + severityValues []interface{} + expectedSeverityText string + expectedSeverityNumber uint8 + }{ + { + severityValues: []interface{}{ + "test_trace", "TEST_TRACE", "trace", "Trace", + }, + expectedSeverityText: "TRACE", + expectedSeverityNumber: 1, + }, + { + severityValues: []interface{}{ + "test_debug", "TEST_DEBUG", "debug", "DEBUG", 202.0, + }, + expectedSeverityText: "DEBUG", + expectedSeverityNumber: 5, + }, { + severityValues: []interface{}{ + "test_info", "TEST_INFO", "info", "INFO", 302.0, + }, + expectedSeverityText: "INFO", + expectedSeverityNumber: 9, + }, { + severityValues: []interface{}{ + "test_warn", "TEST_WARN", "warn", "WARN", 404.0, + }, + expectedSeverityText: "WARN", + expectedSeverityNumber: 13, + }, { + severityValues: []interface{}{ + "test_error", "TEST_ERROR", "error", "ERROR", 500.0, + }, + expectedSeverityText: "ERROR", + expectedSeverityNumber: 17, + }, { + severityValues: []interface{}{ + "test_fatal", "TEST_FATAL", "fatal", "FATAL", + }, + expectedSeverityText: "FATAL", + expectedSeverityNumber: 21, + }, + } + + for _, testCase := range testCases { + inputLogs := []model.SignozLog{} + for _, severityAttribValue := range testCase.severityValues { + inputLogs = append(inputLogs, makeTestSignozLog( + "test log", + map[string]interface{}{ + "method": "GET", + "test_severity": severityAttribValue, + }, + )) + } + + result, collectorWarnAndErrorLogs, err := SimulatePipelinesProcessing( + context.Background(), + testPipelines, + inputLogs, + ) + + require.Nil(err) + require.Equal(len(inputLogs), len(result)) + require.Equal(0, len(collectorWarnAndErrorLogs), strings.Join(collectorWarnAndErrorLogs, "\n")) + processed := result[0] + + require.Equal(testCase.expectedSeverityNumber, processed.SeverityNumber) + require.Equal(testCase.expectedSeverityText, processed.SeverityText) + } + +} + +func TestNoCollectorErrorsFromSeverityParserForMismatchedLogs(t *testing.T) { + require := require.New(t) + + testPipelineFilter := &v3.FilterSet{ + Operator: "AND", + Items: []v3.FilterItem{ + { + Key: v3.AttributeKey{ + Key: "method", + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeTag, + }, + Operator: "=", + Value: "GET", + }, + }, + } + makeTestPipeline := func(config []PipelineOperator) Pipeline { + return Pipeline{ + OrderId: 1, + Name: "pipeline1", + Alias: "pipeline1", + Enabled: true, + Filter: testPipelineFilter, + Config: config, + } + } + + type pipelineTestCase struct { + Name string + Operator PipelineOperator + NonMatchingLog model.SignozLog + } + + testCases := []pipelineTestCase{ + { + "severity parser should ignore logs with missing field", + PipelineOperator{ + ID: "severity", + Type: "severity_parser", + Enabled: true, + Name: "severity parser", + ParseFrom: "attributes.test_severity", + SeverityMapping: map[string][]string{ + "debug": {"debug"}, + }, + OverwriteSeverityText: true, + }, + makeTestSignozLog("mismatching log", map[string]interface{}{ + "method": "GET", + }), + }, { + "severity parser should ignore logs with invalid values.", + PipelineOperator{ + ID: "severity", + Type: "severity_parser", + Enabled: true, + Name: "severity parser", + ParseFrom: "attributes.test_severity", + SeverityMapping: map[string][]string{ + "debug": {"debug"}, + }, + OverwriteSeverityText: true, + }, + makeTestSignozLog("mismatching log", map[string]interface{}{ + "method": "GET", + "test_severity": 200.3, + }), + }, + } + + for _, testCase := range testCases { + testPipelines := []Pipeline{makeTestPipeline([]PipelineOperator{testCase.Operator})} + + result, collectorWarnAndErrorLogs, err := SimulatePipelinesProcessing( + context.Background(), + testPipelines, + []model.SignozLog{testCase.NonMatchingLog}, + ) + require.Nil(err) + require.Equal(0, len(collectorWarnAndErrorLogs), strings.Join(collectorWarnAndErrorLogs, "\n")) + require.Equal(1, len(result)) + } +} diff --git a/pkg/query-service/app/logparsingpipeline/time_parser_test.go b/pkg/query-service/app/logparsingpipeline/time_parser_test.go index 15c41bf73e..26a696091f 100644 --- a/pkg/query-service/app/logparsingpipeline/time_parser_test.go +++ b/pkg/query-service/app/logparsingpipeline/time_parser_test.go @@ -108,9 +108,9 @@ func TestTimestampParsingProcessor(t *testing.T) { testPipelines[0].Config = append(testPipelines[0].Config, timestampParserOp) testTimestampStr := "2023-11-27T12:03:28.239907+0530" - testLog := makeTestLogEntry( + testLog := makeTestSignozLog( "test log", - map[string]string{ + map[string]interface{}{ "method": "GET", "test_timestamp": testTimestampStr, }, From 0b991331d7afea3cb7a3b2a0520a38e4347ff9cd Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Fri, 1 Dec 2023 18:16:25 +0530 Subject: [PATCH 09/19] [Fix]: threshold in alerts (#4074) --- .../FormAlertRules/ChartPreview/index.tsx | 26 +++--- .../FormAlertRules/ChartPreview/utils.ts | 15 ++++ .../RightContainer/alertFomatCategories.ts | 9 ++ .../NewWidget/RightContainer/types.ts | 10 +++ frontend/src/lib/getConvertedValue.ts | 11 ++- .../src/lib/uPlotLib/getUplotChartOptions.ts | 7 +- .../src/lib/uPlotLib/utils/getYAxisScale.ts | 83 +++++++++++++++++++ 7 files changed, 147 insertions(+), 14 deletions(-) create mode 100644 frontend/src/lib/uPlotLib/utils/getYAxisScale.ts diff --git a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx index 9c3effe973..400a6f85be 100644 --- a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx +++ b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx @@ -2,6 +2,7 @@ import { InfoCircleOutlined } from '@ant-design/icons'; import Spinner from 'components/Spinner'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import GridPanelSwitch from 'container/GridPanelSwitch'; +import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories'; import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems'; import { Time } from 'container/TopNav/DateTimeSelection/config'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; @@ -19,7 +20,7 @@ import { EQueryType } from 'types/common/dashboard'; import { GlobalReducer } from 'types/reducer/globalTime'; import { ChartContainer, FailedMessageContainer } from './styles'; -import { covertIntoDataFormats } from './utils'; +import { getThresholdLabel } from './utils'; export interface ChartPreviewProps { name: string; @@ -50,12 +51,6 @@ function ChartPreview({ (state) => state.globalTime, ); - const thresholdValue = covertIntoDataFormats({ - value: threshold, - sourceUnit: alertDef?.condition.targetUnit, - targetUnit: query?.unit, - }); - const canQuery = useMemo((): boolean => { if (!query || query == null) { return false; @@ -110,6 +105,9 @@ function ChartPreview({ const isDarkMode = useIsDarkMode(); + const optionName = + getFormatNameByOptionId(alertDef?.condition.targetUnit || '') || ''; + const options = useMemo( () => getUPlotChartOptions({ @@ -124,10 +122,16 @@ function ChartPreview({ keyIndex: 0, moveThreshold: (): void => {}, selectedGraph: PANEL_TYPES.TIME_SERIES, // no impact - thresholdValue, + thresholdValue: threshold, thresholdLabel: `${t( 'preview_chart_threshold_label', - )} (y=${thresholdValue} ${query?.unit || ''})`, + )} (y=${getThresholdLabel( + optionName, + threshold, + alertDef?.condition.targetUnit, + query?.unit, + )})`, + thresholdUnit: alertDef?.condition.targetUnit, }, ], }), @@ -136,8 +140,10 @@ function ChartPreview({ queryResponse?.data?.payload, containerDimensions, isDarkMode, + threshold, t, - thresholdValue, + optionName, + alertDef?.condition.targetUnit, ], ); diff --git a/frontend/src/container/FormAlertRules/ChartPreview/utils.ts b/frontend/src/container/FormAlertRules/ChartPreview/utils.ts index dd9406b275..f17a6e3865 100644 --- a/frontend/src/container/FormAlertRules/ChartPreview/utils.ts +++ b/frontend/src/container/FormAlertRules/ChartPreview/utils.ts @@ -51,6 +51,21 @@ export function covertIntoDataFormats({ return Number.isNaN(result) ? 0 : result; } +export const getThresholdLabel = ( + optionName: string, + value: number, + unit?: string, + yAxisUnit?: string, +): string => { + if ( + unit === MiscellaneousFormats.PercentUnit || + yAxisUnit === MiscellaneousFormats.PercentUnit + ) { + return `${value * 100}%`; + } + return `${value} ${optionName}`; +}; + interface IUnit { value: number; sourceUnit?: string; diff --git a/frontend/src/container/NewWidget/RightContainer/alertFomatCategories.ts b/frontend/src/container/NewWidget/RightContainer/alertFomatCategories.ts index b9ad3feb18..09acc57d0a 100644 --- a/frontend/src/container/NewWidget/RightContainer/alertFomatCategories.ts +++ b/frontend/src/container/NewWidget/RightContainer/alertFomatCategories.ts @@ -6,6 +6,8 @@ import { CategoryNames, DataFormats, DataRateFormats, + HelperCategory, + HelperFormat, MiscellaneousFormats, ThroughputFormats, TimeFormats, @@ -119,3 +121,10 @@ export const getCategoryByOptionId = (id: string): Category | undefined => export const isCategoryName = (name: string): name is CategoryNames => alertsCategory.some((category) => category.name === name); + +const allFormats: HelperFormat[] = alertsCategory.flatMap( + (category: HelperCategory) => category.formats, +); + +export const getFormatNameByOptionId = (id: string): string | undefined => + allFormats.find((format) => format.id === id)?.name; diff --git a/frontend/src/container/NewWidget/RightContainer/types.ts b/frontend/src/container/NewWidget/RightContainer/types.ts index 36d4d106ac..26b39cb3d8 100644 --- a/frontend/src/container/NewWidget/RightContainer/types.ts +++ b/frontend/src/container/NewWidget/RightContainer/types.ts @@ -362,3 +362,13 @@ export type Category = { }; export type DataTypeCategories = Category[]; + +export interface HelperFormat { + name: string; + id: string; +} + +export interface HelperCategory { + name: string; + formats: Format[]; +} diff --git a/frontend/src/lib/getConvertedValue.ts b/frontend/src/lib/getConvertedValue.ts index 8ebc2135ba..229b2d2677 100644 --- a/frontend/src/lib/getConvertedValue.ts +++ b/frontend/src/lib/getConvertedValue.ts @@ -265,10 +265,15 @@ function findUnitObject( export function convertValue( value: number, - currentUnit: string, - targetUnit: string, + currentUnit?: string, + targetUnit?: string, ): number | null { - if (targetUnit === 'none') { + if ( + targetUnit === 'none' || + !currentUnit || + !targetUnit || + currentUnit === targetUnit + ) { return value; } const currentUnitObj = findUnitObject(currentUnit); diff --git a/frontend/src/lib/uPlotLib/getUplotChartOptions.ts b/frontend/src/lib/uPlotLib/getUplotChartOptions.ts index 6cb07299bb..3a5ec4b2c6 100644 --- a/frontend/src/lib/uPlotLib/getUplotChartOptions.ts +++ b/frontend/src/lib/uPlotLib/getUplotChartOptions.ts @@ -15,6 +15,7 @@ import onClickPlugin, { OnClickPluginOpts } from './plugins/onClickPlugin'; import tooltipPlugin from './plugins/tooltipPlugin'; import getAxes from './utils/getAxes'; import getSeries from './utils/getSeriesData'; +import { getYAxisScale } from './utils/getYAxisScale'; interface GetUPlotChartOptions { id?: string; @@ -79,7 +80,11 @@ export const getUPlotChartOptions = ({ auto: true, // Automatically adjust scale range }, y: { - auto: true, + ...getYAxisScale( + thresholds, + apiResponse?.data.newResult.data.result, + yAxisUnit, + ), }, }, plugins: [ diff --git a/frontend/src/lib/uPlotLib/utils/getYAxisScale.ts b/frontend/src/lib/uPlotLib/utils/getYAxisScale.ts new file mode 100644 index 0000000000..d8b672b98e --- /dev/null +++ b/frontend/src/lib/uPlotLib/utils/getYAxisScale.ts @@ -0,0 +1,83 @@ +import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types'; +import { convertValue } from 'lib/getConvertedValue'; +import { QueryDataV3 } from 'types/api/widgets/getQuery'; + +function findMinMaxValues(data: QueryDataV3[]): [number, number] { + let min = 0; + let max = 0; + data?.forEach((entry) => { + entry.series?.forEach((series) => { + series.values.forEach((valueObj) => { + const value = parseFloat(valueObj.value); + if (!value) return; + min = Math.min(min, value); + max = Math.max(max, value); + }); + }); + }); + + return [min, max]; +} + +function findMinMaxThresholdValues( + thresholds: ThresholdProps[], + yAxisUnit?: string, +): [number, number] { + let minThresholdValue = 0; + let maxThresholdValue = 0; + + thresholds.forEach((entry) => { + const { thresholdValue, thresholdUnit } = entry; + if (thresholdValue === undefined) return; + minThresholdValue = Math.min( + minThresholdValue, + convertValue(thresholdValue, thresholdUnit, yAxisUnit) || 0, + ); + maxThresholdValue = Math.max( + maxThresholdValue, + convertValue(thresholdValue, thresholdUnit, yAxisUnit) || 0, + ); + }); + + return [minThresholdValue, maxThresholdValue]; +} + +function getRange( + thresholds: ThresholdProps[], + series: QueryDataV3[], + yAxisUnit?: string, +): [number, number] { + const [minThresholdValue, maxThresholdValue] = findMinMaxThresholdValues( + thresholds, + yAxisUnit, + ); + const [minSeriesValue, maxSeriesValue] = findMinMaxValues(series); + + const min = Math.min(minThresholdValue, minSeriesValue); + const max = Math.max(maxThresholdValue, maxSeriesValue); + + return [min, max]; +} + +function areAllSeriesEmpty(series: QueryDataV3[]): boolean { + return series.every((entry) => { + if (!entry.series) return true; + return entry.series.every((series) => series.values.length === 0); + }); +} + +export const getYAxisScale = ( + thresholds?: ThresholdProps[], + series?: QueryDataV3[], + yAxisUnit?: string, +): { + auto: boolean; + range?: [number, number]; +} => { + if (!thresholds || !series) return { auto: true }; + + if (areAllSeriesEmpty(series)) return { auto: true }; + + const [min, max] = getRange(thresholds, series, yAxisUnit); + return { auto: false, range: [min, max] }; +}; From a20693fa9fd3be9e708c7cd5da23d515869037b4 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Fri, 1 Dec 2023 21:55:21 +0530 Subject: [PATCH 10/19] fix: add onboarding complete event (#4140) --- .../common/ModuleStepsContainer/ModuleStepsContainer.tsx | 6 ++++++ frontend/src/index.html.ejs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/container/OnboardingContainer/common/ModuleStepsContainer/ModuleStepsContainer.tsx b/frontend/src/container/OnboardingContainer/common/ModuleStepsContainer/ModuleStepsContainer.tsx index 136602adf4..df46b938f5 100644 --- a/frontend/src/container/OnboardingContainer/common/ModuleStepsContainer/ModuleStepsContainer.tsx +++ b/frontend/src/container/OnboardingContainer/common/ModuleStepsContainer/ModuleStepsContainer.tsx @@ -11,6 +11,7 @@ import ROUTES from 'constants/routes'; import { stepsMap } from 'container/OnboardingContainer/constants/stepsConfig'; import { DataSourceType } from 'container/OnboardingContainer/Steps/DataSource/DataSource'; import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUtils'; +import useAnalytics from 'hooks/analytics/useAnalytics'; import history from 'lib/history'; import { isEmpty } from 'lodash-es'; import { useState } from 'react'; @@ -71,6 +72,7 @@ export default function ModuleStepsContainer({ const [current, setCurrent] = useState(0); const [metaData, setMetaData] = useState(defaultMetaData); + const { trackEvent } = useAnalytics(); const lastStepIndex = selectedModuleSteps.length - 1; const isValidForm = (): boolean => { @@ -126,6 +128,10 @@ export default function ModuleStepsContainer({ }; const redirectToModules = (): void => { + trackEvent('Onboarding Complete', { + module: selectedModule.id, + }); + if (selectedModule.id === ModulesMap.APM) { history.push(ROUTES.APPLICATION); } else if (selectedModule.id === ModulesMap.LogsManagement) { diff --git a/frontend/src/index.html.ejs b/frontend/src/index.html.ejs index d6f1afb64a..f46fd07f01 100644 --- a/frontend/src/index.html.ejs +++ b/frontend/src/index.html.ejs @@ -115,7 +115,7 @@