diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index f8eb005883..e4e5171e33 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -34,7 +34,7 @@ jobs: id: short-sha - name: Get branch name id: branch-name - uses: tj-actions/branch-names@v5.1 + uses: tj-actions/branch-names@v7.0.7 - name: Set docker tag environment run: | if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then @@ -78,7 +78,7 @@ jobs: id: short-sha - name: Get branch name id: branch-name - uses: tj-actions/branch-names@v5.1 + uses: tj-actions/branch-names@v7.0.7 - name: Set docker tag environment run: | if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then @@ -127,7 +127,7 @@ jobs: id: short-sha - name: Get branch name id: branch-name - uses: tj-actions/branch-names@v5.1 + uses: tj-actions/branch-names@v7.0.7 - name: Set docker tag environment run: | if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then @@ -176,7 +176,7 @@ jobs: id: short-sha - name: Get branch name id: branch-name - uses: tj-actions/branch-names@v5.1 + uses: tj-actions/branch-names@v7.0.7 - name: Set docker tag environment run: | if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then 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 . diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index 19d2884644..763e2ce4bf 100644 --- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml @@ -146,7 +146,7 @@ services: condition: on-failure query-service: - image: signoz/query-service:0.34.4 + image: signoz/query-service:0.35.0 command: [ "-config=/root/config/prometheus.yml", @@ -186,7 +186,7 @@ services: <<: *db-depend frontend: - image: signoz/frontend:0.34.4 + image: signoz/frontend:0.35.0 deploy: restart_policy: condition: on-failure diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml index 7429fb648a..ddf80d6e14 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.yaml @@ -164,7 +164,7 @@ services: # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` query-service: - image: signoz/query-service:${DOCKER_TAG:-0.34.4} + image: signoz/query-service:${DOCKER_TAG:-0.35.0} container_name: signoz-query-service command: [ @@ -203,7 +203,7 @@ services: <<: *db-depend frontend: - image: signoz/frontend:${DOCKER_TAG:-0.34.4} + image: signoz/frontend:${DOCKER_TAG:-0.35.0} container_name: signoz-frontend restart: on-failure depends_on: diff --git a/ee/query-service/Dockerfile b/ee/query-service/Dockerfile index dad09b3cbd..d7a6786377 100644 --- a/ee/query-service/Dockerfile +++ b/ee/query-service/Dockerfile @@ -1,5 +1,5 @@ # use a minimal alpine image -FROM alpine:3.18.3 +FROM alpine:3.18.5 # Add Maintainer Info LABEL maintainer="signoz" 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/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, ), 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/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/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/GridCardLayout/WidgetHeader/WidgetHeader.styles.scss b/frontend/src/container/GridCardLayout/WidgetHeader/WidgetHeader.styles.scss new file mode 100644 index 0000000000..e03f176570 --- /dev/null +++ b/frontend/src/container/GridCardLayout/WidgetHeader/WidgetHeader.styles.scss @@ -0,0 +1,30 @@ +.widget-header-container { + display: flex; + justify-content: space-between; + align-items: center; + height: 30px; + width: 100%; + padding: 0.5rem; +} + +.widget-header-title { + max-width: 80%; +} + +.widget-header-actions { + display: flex; + align-items: center; +} +.widget-header-more-options { + visibility: hidden; + border: none; + box-shadow: none; +} + +.widget-header-hover { + visibility: visible; +} + +.widget-api-actions { + padding-right: 0.25rem; +} diff --git a/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx index a1a588b85a..0fb2f90ead 100644 --- a/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx +++ b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx @@ -1,21 +1,23 @@ +import './WidgetHeader.styles.scss'; + import { AlertOutlined, CopyOutlined, DeleteOutlined, - DownOutlined, EditFilled, ExclamationCircleOutlined, FullscreenOutlined, + MoreOutlined, WarningOutlined, } from '@ant-design/icons'; -import { Dropdown, MenuProps, Tooltip, Typography } from 'antd'; +import { Button, Dropdown, MenuProps, Tooltip, Typography } from 'antd'; import Spinner from 'components/Spinner'; import { QueryParams } from 'constants/query'; import { PANEL_TYPES } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; import useComponentPermission from 'hooks/useComponentPermission'; import history from 'lib/history'; -import { ReactNode, useCallback, useMemo, useState } from 'react'; +import { ReactNode, useCallback, useMemo } from 'react'; import { UseQueryResult } from 'react-query'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; @@ -23,23 +25,9 @@ import { ErrorResponse, SuccessResponse } from 'types/api'; import { Widgets } from 'types/api/dashboard/getAll'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import AppReducer from 'types/reducer/app'; -import { popupContainer } from 'utils/selectPopupContainer'; -import { - errorTooltipPosition, - overlayStyles, - spinnerStyles, - tooltipStyles, - WARNING_MESSAGE, -} from './config'; +import { errorTooltipPosition, WARNING_MESSAGE } from './config'; import { MENUITEM_KEYS_VS_LABELS, MenuItemKeys } from './contants'; -import { - ArrowContainer, - HeaderContainer, - HeaderContentContainer, - ThesholdContainer, - WidgetHeaderContainer, -} from './styles'; import { MenuItem } from './types'; import { generateMenuList, isTWidgetOptions } from './utils'; @@ -72,9 +60,6 @@ function WidgetHeader({ headerMenuList, isWarning, }: IWidgetHeaderProps): JSX.Element | null { - const [localHover, setLocalHover] = useState(false); - const [isOpen, setIsOpen] = useState(false); - const onEditHandler = useCallback((): void => { const widgetId = widget.id; history.push( @@ -112,7 +97,6 @@ function WidgetHeader({ if (functionToCall) { functionToCall(); - setIsOpen(false); } } }, @@ -169,10 +153,6 @@ function WidgetHeader({ const updatedMenuList = useMemo(() => generateMenuList(actions), [actions]); - const onClickHandler = (): void => { - setIsOpen(!isOpen); - }; - const menu = useMemo( () => ({ items: updatedMenuList, @@ -186,49 +166,49 @@ function WidgetHeader({ } return ( - - + - setLocalHover(true)} - onMouseOut={(): void => setLocalHover(false)} - hover={localHover} - onClick={onClickHandler} - > - - - {title} - - - - - - - + {title} + +
+
{threshold}
+ {queryResponse.isFetching && !queryResponse.isError && ( + + )} + {queryResponse.isError && ( + + + + )} - {threshold} - {queryResponse.isFetching && !queryResponse.isError && ( - - )} - {queryResponse.isError && ( - - - - )} - - {isWarning && ( - - - - )} - + {isWarning && ( + + + + )} + +
+ ); } diff --git a/frontend/src/container/GridCardLayout/WidgetHeader/styles.ts b/frontend/src/container/GridCardLayout/WidgetHeader/styles.ts index 71d01fe6e5..3470cd657b 100644 --- a/frontend/src/container/GridCardLayout/WidgetHeader/styles.ts +++ b/frontend/src/container/GridCardLayout/WidgetHeader/styles.ts @@ -41,8 +41,6 @@ export const WidgetHeaderContainer = styled.div` export const ArrowContainer = styled.span<{ hover: boolean }>` visibility: ${({ hover }): string => (hover ? 'visible' : 'hidden')}; - position: absolute; - right: -1rem; `; export const Typography = styled(TypographyComponent)` diff --git a/frontend/src/container/Header/Header.styles.scss b/frontend/src/container/Header/Header.styles.scss index 82dd9b81ff..08db1f8b99 100644 --- a/frontend/src/container/Header/Header.styles.scss +++ b/frontend/src/container/Header/Header.styles.scss @@ -8,5 +8,18 @@ .upgrade-link { padding: 0px; padding-right: 4px; + display: inline !important; color: white; + text-decoration: underline; + text-decoration-color: white; + text-decoration-thickness: 2px; + text-underline-offset: 2px; + + &:hover { + color: white; + text-decoration: underline; + text-decoration-color: white; + text-decoration-thickness: 2px; + text-underline-offset: 2px; + } } diff --git a/frontend/src/container/Header/index.tsx b/frontend/src/container/Header/index.tsx index ff4c560c67..b008e65537 100644 --- a/frontend/src/container/Header/index.tsx +++ b/frontend/src/container/Header/index.tsx @@ -1,3 +1,6 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/anchor-is-valid */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import './Header.styles.scss'; import { @@ -135,16 +138,17 @@ function HeaderContainer(): JSX.Element { <> {showTrialExpiryBanner && (
- You are in free trial period. Your free trial will end on + You are in free trial period. Your free trial will end on{' '} {getFormattedDate(licenseData?.payload?.trialEnd || Date.now())}. {role === 'ADMIN' ? ( - Please - + to continue using SigNoz features. ) : ( 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/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 { 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, + ); }; + + 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; + + 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 */} - - - Name - -
- { - setVariableName(e.target.value); - setErrorName( - !validateName(e.target.value) && e.target.value !== variableData.name, - ); - }} - /> +
+
+ {/* Add Variable */} + + + Name +
- - {errorName ? 'Variable name already exists' : ''} - -
-
- - - - Description - - - setVariableDescription(e.target.value)} - /> - - - - Type - - - - - - Options - - {queryType === 'QUERY' && ( - - - Query - - -
- setVariableQueryValue(e)} - height="300px" - /> - -
-
- )} - {queryType === 'CUSTOM' && ( - - - Values separated by comma - - { - setVariableCustomValue(e.target.value); - setPreviewValues( - sortValues( - commaValuesParser(e.target.value), - variableSortType, - ) as never, - ); - }} - /> - - )} - {queryType === 'TEXTBOX' && ( - - - Default Value - - { - setVariableTextboxValue(e.target.value); - }} - placeholder="Default value if any" - style={{ width: 400 }} - /> - - )} - {(queryType === 'QUERY' || queryType === 'CUSTOM') && ( - <> - - - Preview of Values - -
- {errorPreview ? ( - {errorPreview} - ) : ( - map(previewValues, (value, idx) => ( - {value.toString()} - )) - )} -
-
- - - Sort - - - - + value={variableName} + onChange={(e): void => { + 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 + + + + + + Options + + {queryType === 'QUERY' && ( +
+ + Query + + +
+ setVariableQueryValue(e)} + height="240px" + options={{ + fontSize: 13, + wordWrap: 'on', + lineNumbers: 'off', + glyphMargin: false, + folding: false, + lineDecorationsWidth: 0, + lineNumbersMinChars: 0, + minimap: { + enabled: false, + }, + }} + /> + +
+
+ )} + {queryType === 'CUSTOM' && ( - Enable multiple values to be checked + Values separated by comma - { - setVariableMultiSelect(e); - if (!e) { - setVariableShowALLOption(false); - } + setVariableCustomValue(e.target.value); + setPreviewValues( + sortValues( + commaValuesParser(e.target.value), + variableSortType, + ) as never, + ); }} /> - {variableMultiSelect && ( + )} + {queryType === 'TEXTBOX' && ( + + + Default Value + + { + setVariableTextboxValue(e.target.value); + }} + placeholder="Default value if any" + style={{ width: 400 }} + /> + + )} + {(queryType === 'QUERY' || queryType === 'CUSTOM') && ( + <> - Include an option for ALL values + Preview of Values + +
+ {errorPreview ? ( + {errorPreview} + ) : ( + map(previewValues, (value, idx) => ( + {value.toString()} + )) + )} +
+
+ + + Sort + + + + + + + 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..408605a89b 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,86 @@ 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]); + + useEffect(() => { + // Fetch options for CUSTOM Type + if (variableData.type === 'CUSTOM') { + getOptions(null); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + 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/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/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 (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/container/PipelinePage/PipelineListsView/AddNewProcessor/FormFields/CSVInput.tsx b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/FormFields/CSVInput.tsx new file mode 100644 index 0000000000..81050c1ba9 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/FormFields/CSVInput.tsx @@ -0,0 +1,25 @@ +import { Input, InputProps } from 'antd'; +import { ChangeEventHandler, useState } from 'react'; + +function CSVInput({ value, onChange, ...otherProps }: InputProps): JSX.Element { + const [inputValue, setInputValue] = useState( + ((value as string[]) || []).join(', '), + ); + + const onChangeHandler = (onChange as unknown) as (v: string[]) => void; + + const onInputChange: ChangeEventHandler = (e) => { + const newValue = e.target.value; + setInputValue(newValue); + + if (onChangeHandler) { + const splitValues = newValue.split(',').map((v) => v.trim()); + onChangeHandler(splitValues); + } + }; + + // eslint-disable-next-line react/jsx-props-no-spreading + return ; +} + +export default CSVInput; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/ProcessorForm.tsx b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/ProcessorForm.tsx new file mode 100644 index 0000000000..1d3b12e2c0 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/ProcessorForm.tsx @@ -0,0 +1,107 @@ +import './styles.scss'; + +import { Form, Input, Select } from 'antd'; +import { ModalFooterTitle } from 'container/PipelinePage/styles'; +import { useTranslation } from 'react-i18next'; + +import { formValidationRules } from '../config'; +import { processorFields, ProcessorFormField } from './config'; +import CSVInput from './FormFields/CSVInput'; +import { FormWrapper, PipelineIndexIcon, StyledSelect } from './styles'; + +function ProcessorFieldInput({ + fieldData, +}: ProcessorFieldInputProps): JSX.Element | null { + const { t } = useTranslation('pipeline'); + + // Watch form values so we can evaluate shouldRender on + // conditional fields when form values are updated. + const form = Form.useFormInstance(); + Form.useWatch(fieldData?.dependencies || [], form); + + if (fieldData.shouldRender && !fieldData.shouldRender(form)) { + return null; + } + + // Do not render display elements for hidden inputs. + if (fieldData?.hidden) { + return ( + + + + ); + } + + let inputField; + if (fieldData?.options) { + inputField = ( + + {fieldData.options.map(({ value, label }) => ( + + {label} + + ))} + + ); + } else if (Array.isArray(fieldData?.initialValue)) { + inputField = ; + } else { + inputField = ; + } + + return ( +
+ {!fieldData?.compact && ( + + {Number(fieldData.id) + 1} + + )} + + {fieldData.fieldName}} + name={fieldData.name} + initialValue={fieldData.initialValue} + rules={fieldData.rules ? fieldData.rules : formValidationRules} + dependencies={fieldData.dependencies || []} + > + {inputField} + + +
+ ); +} + +interface ProcessorFieldInputProps { + fieldData: ProcessorFormField; +} + +function ProcessorForm({ processorType }: ProcessorFormProps): JSX.Element { + return ( +
+ {processorFields[processorType]?.map((fieldData: ProcessorFormField) => ( + + ))} +
+ ); +} + +interface ProcessorFormProps { + processorType: string; +} + +export default ProcessorForm; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/config.ts b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/config.ts index d6337e2b5c..2c9a676898 100644 --- a/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/config.ts +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/config.ts @@ -1,5 +1,7 @@ +import { FormInstance } from 'antd'; import { Rule, RuleRender } from 'antd/es/form'; import { NamePath } from 'antd/es/form/interface'; +import { ProcessorData } from 'types/api/pipeline/def'; type ProcessorType = { key: string; @@ -14,6 +16,8 @@ export const processorTypes: Array = [ { key: 'regex_parser', value: 'regex_parser', label: 'Regex' }, { key: 'json_parser', value: 'json_parser', label: 'Json Parser' }, { key: 'trace_parser', value: 'trace_parser', label: 'Trace Parser' }, + { key: 'time_parser', value: 'time_parser', label: 'Timestamp Parser' }, + { key: 'severity_parser', value: 'severity_parser', label: 'Severity Parser' }, { key: 'add', value: 'add', label: 'Add' }, { key: 'remove', value: 'remove', label: 'Remove' }, // { key: 'retain', value: 'retain', label: 'Retain' }, @Chintan - Commented as per Nitya's suggestion @@ -23,14 +27,31 @@ export const processorTypes: Array = [ export const DEFAULT_PROCESSOR_TYPE = processorTypes[0].value; +export type ProcessorFieldOption = { + label: string; + value: string; +}; + +// TODO(Raj): Refactor Processor Form code after putting e2e UI tests in place. export type ProcessorFormField = { id: number; fieldName: string; placeholder: string; name: string | NamePath; rules?: Array; - initialValue?: string; + hidden?: boolean; + initialValue?: boolean | string | Array; dependencies?: Array; + options?: Array; + shouldRender?: (form: FormInstance) => boolean; + onFormValuesChanged?: ( + changedValues: ProcessorData, + form: FormInstance, + ) => void; + + // Should this field have its own row or should it + // be packed with other compact fields. + compact?: boolean; }; const traceParserFieldValidator: RuleRender = (form) => ({ @@ -206,6 +227,182 @@ export const processorFields: { [key: string]: Array } = { ], }, ], + time_parser: [ + { + id: 1, + fieldName: 'Name of Timestamp Parsing Processor', + placeholder: 'processor_name_placeholder', + name: 'name', + }, + { + id: 2, + fieldName: 'Parse Timestamp Value From', + placeholder: 'processor_parsefrom_placeholder', + name: 'parse_from', + initialValue: 'attributes.timestamp', + }, + { + id: 3, + fieldName: 'Timestamp Format Type', + placeholder: '', + name: 'layout_type', + initialValue: 'strptime', + options: [ + { + label: 'Unix Epoch', + value: 'epoch', + }, + { + label: 'strptime Format', + value: 'strptime', + }, + ], + onFormValuesChanged: ( + changedValues: ProcessorData, + form: FormInstance, + ): void => { + if (changedValues?.layout_type) { + const newLayoutValue = + changedValues.layout_type === 'strptime' ? '%Y-%m-%dT%H:%M:%S.%f%z' : 's'; + + form.setFieldValue('layout', newLayoutValue); + } + }, + }, + { + id: 4, + fieldName: 'Epoch Format', + placeholder: '', + name: 'layout', + dependencies: ['layout_type'], + shouldRender: (form: FormInstance): boolean => { + const layoutType = form.getFieldValue('layout_type'); + return layoutType === 'epoch'; + }, + initialValue: 's', + options: [ + { + label: 'seconds', + value: 's', + }, + { + label: 'milliseconds', + value: 'ms', + }, + { + label: 'microseconds', + value: 'us', + }, + { + label: 'nanoseconds', + value: 'ns', + }, + { + label: 'seconds.milliseconds (eg: 1136214245.123)', + value: 's.ms', + }, + { + label: 'seconds.microseconds (eg: 1136214245.123456)', + value: 's.us', + }, + { + label: 'seconds.nanoseconds (eg: 1136214245.123456789)', + value: 's.ns', + }, + ], + }, + { + id: 4, + fieldName: 'Timestamp Format', + placeholder: 'strptime directives based format. Eg: %Y-%m-%dT%H:%M:%S.%f%z', + name: 'layout', + dependencies: ['layout_type'], + shouldRender: (form: FormInstance): boolean => { + const layoutType = form.getFieldValue('layout_type'); + return layoutType === 'strptime'; + }, + initialValue: '%Y-%m-%dT%H:%M:%S.%f%z', + }, + ], + severity_parser: [ + { + id: 1, + fieldName: 'Name of Severity Parsing Processor', + placeholder: 'processor_name_placeholder', + name: 'name', + }, + { + id: 2, + fieldName: 'Parse Severity Value From', + placeholder: 'processor_parsefrom_placeholder', + name: 'parse_from', + initialValue: 'attributes.logLevel', + }, + { + id: 3, + fieldName: 'Values for level TRACE', + placeholder: 'Specify comma separated values. Eg: trace, 0', + name: ['mapping', 'trace'], + rules: [], + initialValue: ['trace'], + compact: true, + }, + { + id: 4, + fieldName: 'Values for level DEBUG', + placeholder: 'Specify comma separated values. Eg: debug, 2xx', + name: ['mapping', 'debug'], + rules: [], + initialValue: ['debug'], + compact: true, + }, + { + id: 5, + fieldName: 'Values for level INFO', + placeholder: 'Specify comma separated values. Eg: info, 3xx', + name: ['mapping', 'info'], + rules: [], + initialValue: ['info'], + compact: true, + }, + { + id: 6, + fieldName: 'Values for level WARN', + placeholder: 'Specify comma separated values. Eg: warning, 4xx', + name: ['mapping', 'warn'], + rules: [], + initialValue: ['warn'], + compact: true, + }, + { + id: 7, + fieldName: 'Values for level ERROR', + placeholder: 'Specify comma separated values. Eg: error, 5xx', + name: ['mapping', 'error'], + rules: [], + initialValue: ['error'], + compact: true, + }, + { + id: 8, + fieldName: 'Values for level FATAL', + placeholder: 'Specify comma separated values. Eg: fatal, panic', + name: ['mapping', 'fatal'], + rules: [], + initialValue: ['fatal'], + compact: true, + }, + { + id: 9, + fieldName: 'Override Severity Text', + placeholder: + 'Should the parsed severity set both severity and severityText?', + name: ['overwrite_text'], + rules: [], + initialValue: true, + hidden: true, + }, + ], retain: [ { id: 1, diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/index.tsx index 8281655952..661fc4043a 100644 --- a/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/index.tsx +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/index.tsx @@ -11,9 +11,9 @@ import { v4 } from 'uuid'; import { ModalButtonWrapper, ModalTitle } from '../styles'; import { getEditedDataSource, getRecordIndex } from '../utils'; -import { DEFAULT_PROCESSOR_TYPE } from './config'; +import { DEFAULT_PROCESSOR_TYPE, processorFields } from './config'; import TypeSelect from './FormFields/TypeSelect'; -import { renderProcessorForm } from './utils'; +import ProcessorForm from './ProcessorForm'; function AddNewProcessor({ isActionType, @@ -141,6 +141,17 @@ function AddNewProcessor({ const isOpen = useMemo(() => isEdit || isAdd, [isAdd, isEdit]); + const onFormValuesChanged = useCallback( + (changedValues: ProcessorData): void => { + processorFields[processorType].forEach((field) => { + if (field.onFormValuesChanged) { + field.onFormValuesChanged(changedValues, form); + } + }); + }, + [form, processorType], + ); + return ( {modalTitle}} @@ -157,9 +168,10 @@ function AddNewProcessor({ onFinish={onFinish} autoComplete="off" form={form} + onValuesChange={onFormValuesChanged} > - {renderProcessorForm(processorType)} + diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/styles.scss b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/styles.scss new file mode 100644 index 0000000000..1fabdd233f --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/styles.scss @@ -0,0 +1,27 @@ + +.processor-form-container { + position: relative; + width: 100%; + + display: flex; + flex-wrap: wrap +} + +.processor-field-container { + display: flex; + flex-direction: row; + align-items: flex-start; + padding: 0rem; + gap: 1rem; + width: 100%; +} + +.compact-processor-field-container { + display: flex; + flex-direction: row; + align-items: flex-start; + padding: 0rem; + min-width: 40%; + flex-grow: 1; + margin-left: 2.5rem; +} diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/utils.tsx b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/utils.tsx deleted file mode 100644 index 1534b704ea..0000000000 --- a/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/utils.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { processorFields, ProcessorFormField } from './config'; -import NameInput from './FormFields/NameInput'; - -export const renderProcessorForm = ( - processorType: string, -): Array => - processorFields[processorType]?.map((fieldData: ProcessorFormField) => ( - - )); diff --git a/frontend/src/container/PipelinePage/PipelineListsView/Preview/hooks/usePipelinePreview.ts b/frontend/src/container/PipelinePage/PipelineListsView/Preview/hooks/usePipelinePreview.ts index ad875cdcd3..7e739fdf52 100644 --- a/frontend/src/container/PipelinePage/PipelineListsView/Preview/hooks/usePipelinePreview.ts +++ b/frontend/src/container/PipelinePage/PipelineListsView/Preview/hooks/usePipelinePreview.ts @@ -27,7 +27,8 @@ const usePipelinePreview = ({ // ILog allows both number and string while the API needs a number const simulationInput = inputLogs.map((l) => ({ ...l, - timestamp: new Date(l.timestamp).getTime(), + // log timestamps in query service API are unix nanos + timestamp: new Date(l.timestamp).getTime() * 10 ** 6, })); const response = useQuery({ @@ -42,9 +43,15 @@ const usePipelinePreview = ({ const { isFetching, isError, data, error } = response; + const outputLogs = (data?.logs || []).map((l: ILog) => ({ + ...l, + // log timestamps in query service API are unix nanos + timestamp: (l.timestamp as number) / 10 ** 6, + })); + return { isLoading: isFetching, - outputLogs: data?.logs || [], + outputLogs, isError, errorMsg: error?.response?.data?.error || '', }; 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/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/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 @@