From b9409820ccabcae14caf33a2c31df5c1b8baa497 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Tue, 25 Jul 2023 20:45:12 +0530 Subject: [PATCH] refactor: remove the dependency of services from redux (#2998) * refactor: remove the dependency of services using redux * refactor: seperated columns and unit test case * refactor: move the constant to other file * refactor: updated test case * refactor: removed the duplicate enum * fix: removed the inline function * fix: removed the inline function * refactor: removed the magic string * fix: change the name from matrics to metrics * fix: one on one mapping of props * refactor: created a hook to getting services through api call * fix: linter error * refactor: renamed the file according to functionality * refactor: renamed more file according to functionality * refactor: removed unwanted interfaces and renamed files * refactor: separated types * refactor: shifted mock data and completed review changes * chore: updated test cases * refactor: added useEffect in errornotification * chore: updated service test * chore: shifted loading to table level * chore: updated test cases --------- Co-authored-by: Vishal Sharma --- frontend/src/AppRoutes/pageComponents.ts | 2 +- frontend/src/api/metrics/getService.ts | 29 +-- .../container/MetricsTable/Metrics.test.tsx | 70 -------- frontend/src/container/MetricsTable/index.tsx | 169 ------------------ .../ServiceTable/Columns/ColumnContants.ts | 26 +++ .../Columns/GetColumnSearchProps.tsx | 34 ++++ .../ServiceTable/Columns/ServiceColumn.ts | 46 +++++ .../ServiceTable/Filter/FilterDropdown.tsx | 41 +++++ .../container/ServiceTable/Service.test.tsx | 50 ++++++ .../SkipOnBoardModal/index.tsx | 0 .../ServiceTable/__mock__/servicesListMock.ts | 22 +++ frontend/src/container/ServiceTable/index.tsx | 26 +++ .../{MetricsTable => ServiceTable}/styles.ts | 0 frontend/src/container/ServiceTable/types.ts | 6 + frontend/src/hooks/useErrorNotification.ts | 17 ++ frontend/src/hooks/useQueryService.ts | 29 +++ frontend/src/pages/Metrics/index.tsx | 116 ------------ frontend/src/pages/Services/index.tsx | 70 ++++++++ .../src/store/actions/metrics/getService.ts | 9 +- 19 files changed, 380 insertions(+), 382 deletions(-) delete mode 100644 frontend/src/container/MetricsTable/Metrics.test.tsx delete mode 100644 frontend/src/container/MetricsTable/index.tsx create mode 100644 frontend/src/container/ServiceTable/Columns/ColumnContants.ts create mode 100644 frontend/src/container/ServiceTable/Columns/GetColumnSearchProps.tsx create mode 100644 frontend/src/container/ServiceTable/Columns/ServiceColumn.ts create mode 100644 frontend/src/container/ServiceTable/Filter/FilterDropdown.tsx create mode 100644 frontend/src/container/ServiceTable/Service.test.tsx rename frontend/src/container/{MetricsTable => ServiceTable}/SkipOnBoardModal/index.tsx (100%) create mode 100644 frontend/src/container/ServiceTable/__mock__/servicesListMock.ts create mode 100644 frontend/src/container/ServiceTable/index.tsx rename frontend/src/container/{MetricsTable => ServiceTable}/styles.ts (100%) create mode 100644 frontend/src/container/ServiceTable/types.ts create mode 100644 frontend/src/hooks/useErrorNotification.ts create mode 100644 frontend/src/hooks/useQueryService.ts delete mode 100644 frontend/src/pages/Metrics/index.tsx create mode 100644 frontend/src/pages/Services/index.tsx diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index f7bc5e1704..f3d40ccbcd 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -1,7 +1,7 @@ import Loadable from 'components/Loadable'; export const ServicesTablePage = Loadable( - () => import(/* webpackChunkName: "ServicesTablePage" */ 'pages/Metrics'), + () => import(/* webpackChunkName: "ServicesTablePage" */ 'pages/Services'), ); export const ServiceMetricsPage = Loadable( diff --git a/frontend/src/api/metrics/getService.ts b/frontend/src/api/metrics/getService.ts index d3bb27c741..731da11c81 100644 --- a/frontend/src/api/metrics/getService.ts +++ b/frontend/src/api/metrics/getService.ts @@ -1,28 +1,13 @@ import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; import { PayloadProps, Props } from 'types/api/metrics/getService'; -const getService = async ( - props: Props, -): Promise | ErrorResponse> => { - try { - const response = await axios.post(`/services`, { - start: `${props.start}`, - end: `${props.end}`, - tags: props.selectedTags, - }); - - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } +const getService = async (props: Props): Promise => { + const response = await axios.post(`/services`, { + start: `${props.start}`, + end: `${props.end}`, + tags: props.selectedTags, + }); + return response.data; }; export default getService; diff --git a/frontend/src/container/MetricsTable/Metrics.test.tsx b/frontend/src/container/MetricsTable/Metrics.test.tsx deleted file mode 100644 index 74239c19f7..0000000000 --- a/frontend/src/container/MetricsTable/Metrics.test.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { render, RenderResult, screen, waitFor } from '@testing-library/react'; -import { ReactElement } from 'react'; -import { Provider } from 'react-redux'; -import { BrowserRouter } from 'react-router-dom'; -import { - combineReducers, - legacy_createStore as createStore, - Store, -} from 'redux'; - -import { InitialValue } from '../../store/reducers/metric'; -import Metrics from './index'; - -const rootReducer = combineReducers({ - metrics: (state = InitialValue) => state, -}); - -const mockStore = createStore(rootReducer); - -const renderWithReduxAndRouter = (mockStore: Store) => ( - component: ReactElement, -): RenderResult => - render( - - {component} - , - ); - -describe('Metrics Component', () => { - it('renders without errors', async () => { - renderWithReduxAndRouter(mockStore)(); - - await waitFor(() => { - expect(screen.getByText(/application/i)).toBeInTheDocument(); - expect(screen.getByText(/p99 latency \(in ms\)/i)).toBeInTheDocument(); - expect(screen.getByText(/error rate \(% of total\)/i)).toBeInTheDocument(); - expect(screen.getByText(/operations per second/i)).toBeInTheDocument(); - }); - }); - - it('renders loading when required conditions are met', async () => { - const customStore = createStore(rootReducer, { - metrics: { - services: [], - loading: true, - error: false, - }, - }); - - const { container } = renderWithReduxAndRouter(customStore)(); - - const spinner = container.querySelector('.ant-spin-nested-loading'); - - expect(spinner).toBeInTheDocument(); - }); - - it('renders no data when required conditions are met', async () => { - const customStore = createStore(rootReducer, { - metrics: { - services: [], - loading: false, - error: false, - }, - }); - - renderWithReduxAndRouter(customStore)(); - - expect(screen.getByText('No data')).toBeInTheDocument(); - }); -}); diff --git a/frontend/src/container/MetricsTable/index.tsx b/frontend/src/container/MetricsTable/index.tsx deleted file mode 100644 index 9fee92c811..0000000000 --- a/frontend/src/container/MetricsTable/index.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { blue } from '@ant-design/colors'; -import { SearchOutlined } from '@ant-design/icons'; -import { Button, Card, Input, Space } from 'antd'; -import type { ColumnsType, ColumnType } from 'antd/es/table'; -import type { - FilterConfirmProps, - FilterDropdownProps, -} from 'antd/es/table/interface'; -import localStorageGet from 'api/browser/localstorage/get'; -import localStorageSet from 'api/browser/localstorage/set'; -import { ResizeTable } from 'components/ResizeTable'; -import { SKIP_ONBOARDING } from 'constants/onboarding'; -import ROUTES from 'constants/routes'; -import { routeConfig } from 'container/SideNav/config'; -import { getQueryString } from 'container/SideNav/helper'; -import { useCallback, useMemo, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { Link, useLocation } from 'react-router-dom'; -import { AppState } from 'store/reducers'; -import { ServicesList } from 'types/api/metrics/getService'; -import MetricReducer from 'types/reducer/metrics'; - -import SkipBoardModal from './SkipOnBoardModal'; -import { Container, Name } from './styles'; - -function Metrics(): JSX.Element { - const { search } = useLocation(); - const [skipOnboarding, setSkipOnboarding] = useState( - localStorageGet(SKIP_ONBOARDING) === 'true', - ); - - const { services, loading, error } = useSelector( - (state) => state.metrics, - ); - - const onContinueClick = (): void => { - localStorageSet(SKIP_ONBOARDING, 'true'); - setSkipOnboarding(true); - }; - const handleSearch = (confirm: (param?: FilterConfirmProps) => void): void => { - confirm(); - }; - - const FilterIcon: ColumnType['filterIcon'] = useCallback( - (filtered: boolean) => ( - - ), - [], - ); - - const filterDropdown = useCallback( - ({ setSelectedKeys, selectedKeys, confirm }: FilterDropdownProps) => ( - - - - setSelectedKeys(e.target.value ? [e.target.value] : []) - } - allowClear - onPressEnter={(): void => handleSearch(confirm)} - /> - - - - ), - [], - ); - - type DataIndex = keyof ServicesList; - - const getColumnSearchProps = useCallback( - (dataIndex: DataIndex): ColumnType => ({ - filterDropdown, - filterIcon: FilterIcon, - onFilter: (value: string | number | boolean, record: DataProps): boolean => - record[dataIndex] - .toString() - .toLowerCase() - .includes(value.toString().toLowerCase()), - render: (metrics: string): JSX.Element => { - const urlParams = new URLSearchParams(search); - const avialableParams = routeConfig[ROUTES.SERVICE_METRICS]; - const queryString = getQueryString(avialableParams, urlParams); - - return ( - - {metrics} - - ); - }, - }), - [filterDropdown, FilterIcon, search], - ); - - const columns: ColumnsType = useMemo( - () => [ - { - title: 'Application', - dataIndex: 'serviceName', - width: 200, - key: 'serviceName', - ...getColumnSearchProps('serviceName'), - }, - { - title: 'P99 latency (in ms)', - dataIndex: 'p99', - key: 'p99', - width: 150, - defaultSortOrder: 'descend', - sorter: (a: DataProps, b: DataProps): number => a.p99 - b.p99, - render: (value: number): string => (value / 1000000).toFixed(2), - }, - { - title: 'Error Rate (% of total)', - dataIndex: 'errorRate', - key: 'errorRate', - width: 150, - sorter: (a: DataProps, b: DataProps): number => a.errorRate - b.errorRate, - render: (value: number): string => value.toFixed(2), - }, - { - title: 'Operations Per Second', - dataIndex: 'callRate', - key: 'callRate', - width: 150, - sorter: (a: DataProps, b: DataProps): number => a.callRate - b.callRate, - render: (value: number): string => value.toFixed(2), - }, - ], - [getColumnSearchProps], - ); - - if ( - services.length === 0 && - loading === false && - !skipOnboarding && - error === true - ) { - return ; - } - - return ( - - - - ); -} - -type DataProps = ServicesList; - -export default Metrics; diff --git a/frontend/src/container/ServiceTable/Columns/ColumnContants.ts b/frontend/src/container/ServiceTable/Columns/ColumnContants.ts new file mode 100644 index 0000000000..69e96a1ae3 --- /dev/null +++ b/frontend/src/container/ServiceTable/Columns/ColumnContants.ts @@ -0,0 +1,26 @@ +export enum ColumnKey { + Application = 'serviceName', + P99 = 'p99', + ErrorRate = 'errorRate', + Operations = 'callRate', +} + +export const ColumnTitle: { + [key in ColumnKey]: string; +} = { + [ColumnKey.Application]: 'Application', + [ColumnKey.P99]: 'P99 latency (in ms)', + [ColumnKey.ErrorRate]: 'Error Rate (% of total)', + [ColumnKey.Operations]: 'Operations Per Second', +}; + +export enum ColumnWidth { + Application = 200, + P99 = 150, + ErrorRate = 150, + Operations = 150, +} + +export const SORTING_ORDER = 'descend'; + +export const SEARCH_PLACEHOLDER = 'Search by service'; diff --git a/frontend/src/container/ServiceTable/Columns/GetColumnSearchProps.tsx b/frontend/src/container/ServiceTable/Columns/GetColumnSearchProps.tsx new file mode 100644 index 0000000000..4257dc57ec --- /dev/null +++ b/frontend/src/container/ServiceTable/Columns/GetColumnSearchProps.tsx @@ -0,0 +1,34 @@ +import { SearchOutlined } from '@ant-design/icons'; +import type { ColumnType } from 'antd/es/table'; +import ROUTES from 'constants/routes'; +import { routeConfig } from 'container/SideNav/config'; +import { getQueryString } from 'container/SideNav/helper'; +import { Link } from 'react-router-dom'; +import { ServicesList } from 'types/api/metrics/getService'; + +import { filterDropdown } from '../Filter/FilterDropdown'; +import { Name } from '../styles'; + +export const getColumnSearchProps = ( + dataIndex: keyof ServicesList, + search: string, +): ColumnType => ({ + filterDropdown, + filterIcon: , + onFilter: (value: string | number | boolean, record: ServicesList): boolean => + record[dataIndex] + .toString() + .toLowerCase() + .includes(value.toString().toLowerCase()), + render: (metrics: string): JSX.Element => { + const urlParams = new URLSearchParams(search); + const avialableParams = routeConfig[ROUTES.SERVICE_METRICS]; + const queryString = getQueryString(avialableParams, urlParams); + + return ( + + {metrics} + + ); + }, +}); diff --git a/frontend/src/container/ServiceTable/Columns/ServiceColumn.ts b/frontend/src/container/ServiceTable/Columns/ServiceColumn.ts new file mode 100644 index 0000000000..f613a9dab4 --- /dev/null +++ b/frontend/src/container/ServiceTable/Columns/ServiceColumn.ts @@ -0,0 +1,46 @@ +import type { ColumnsType } from 'antd/es/table'; +import { ServicesList } from 'types/api/metrics/getService'; + +import { + ColumnKey, + ColumnTitle, + ColumnWidth, + SORTING_ORDER, +} from './ColumnContants'; +import { getColumnSearchProps } from './GetColumnSearchProps'; + +export const getColumns = (search: string): ColumnsType => [ + { + title: ColumnTitle[ColumnKey.Application], + dataIndex: ColumnKey.Application, + width: ColumnWidth.Application, + key: ColumnKey.Application, + ...getColumnSearchProps('serviceName', search), + }, + { + title: ColumnTitle[ColumnKey.P99], + dataIndex: ColumnKey.P99, + key: ColumnKey.P99, + width: ColumnWidth.P99, + defaultSortOrder: SORTING_ORDER, + sorter: (a: ServicesList, b: ServicesList): number => a.p99 - b.p99, + render: (value: number): string => (value / 1000000).toFixed(2), + }, + { + title: ColumnTitle[ColumnKey.ErrorRate], + dataIndex: ColumnKey.ErrorRate, + key: ColumnKey.ErrorRate, + width: 150, + sorter: (a: ServicesList, b: ServicesList): number => + a.errorRate - b.errorRate, + render: (value: number): string => value.toFixed(2), + }, + { + title: ColumnTitle[ColumnKey.Operations], + dataIndex: ColumnKey.Operations, + key: ColumnKey.Operations, + width: ColumnWidth.Operations, + sorter: (a: ServicesList, b: ServicesList): number => a.callRate - b.callRate, + render: (value: number): string => value.toFixed(2), + }, +]; diff --git a/frontend/src/container/ServiceTable/Filter/FilterDropdown.tsx b/frontend/src/container/ServiceTable/Filter/FilterDropdown.tsx new file mode 100644 index 0000000000..1dc4a12d89 --- /dev/null +++ b/frontend/src/container/ServiceTable/Filter/FilterDropdown.tsx @@ -0,0 +1,41 @@ +import { SearchOutlined } from '@ant-design/icons'; +import { Button, Card, Input, Space } from 'antd'; +import type { FilterDropdownProps } from 'antd/es/table/interface'; + +import { SEARCH_PLACEHOLDER } from '../Columns/ColumnContants'; + +export const filterDropdown = ({ + setSelectedKeys, + selectedKeys, + confirm, +}: FilterDropdownProps): JSX.Element => { + const handleSearch = (): void => { + confirm(); + }; + + const selectedKeysHandler = (e: React.ChangeEvent): void => { + setSelectedKeys(e.target.value ? [e.target.value] : []); + }; + + return ( + + + + + + + ); +}; diff --git a/frontend/src/container/ServiceTable/Service.test.tsx b/frontend/src/container/ServiceTable/Service.test.tsx new file mode 100644 index 0000000000..4fc9231a78 --- /dev/null +++ b/frontend/src/container/ServiceTable/Service.test.tsx @@ -0,0 +1,50 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import ROUTES from 'constants/routes'; +import { BrowserRouter } from 'react-router-dom'; + +import { Services } from './__mock__/servicesListMock'; +import Metrics from './index'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: (): { pathname: string } => ({ + pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.APPLICATION}/`, + }), +})); + +describe('Metrics Component', () => { + it('renders without errors', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(/application/i)).toBeInTheDocument(); + expect(screen.getByText(/p99 latency \(in ms\)/i)).toBeInTheDocument(); + expect(screen.getByText(/error rate \(% of total\)/i)).toBeInTheDocument(); + expect(screen.getByText(/operations per second/i)).toBeInTheDocument(); + }); + }); + + it('renders if the data is loaded in the table', async () => { + render( + + + , + ); + + expect(screen.getByText('frontend')).toBeInTheDocument(); + }); + + it('renders no data when required conditions are met', async () => { + render( + + + , + ); + + expect(screen.getByText('No data')).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/container/MetricsTable/SkipOnBoardModal/index.tsx b/frontend/src/container/ServiceTable/SkipOnBoardModal/index.tsx similarity index 100% rename from frontend/src/container/MetricsTable/SkipOnBoardModal/index.tsx rename to frontend/src/container/ServiceTable/SkipOnBoardModal/index.tsx diff --git a/frontend/src/container/ServiceTable/__mock__/servicesListMock.ts b/frontend/src/container/ServiceTable/__mock__/servicesListMock.ts new file mode 100644 index 0000000000..283ba7718b --- /dev/null +++ b/frontend/src/container/ServiceTable/__mock__/servicesListMock.ts @@ -0,0 +1,22 @@ +import { ServicesList } from 'types/api/metrics/getService'; + +export const Services: ServicesList[] = [ + { + serviceName: 'frontend', + p99: 1261498140, + avgDuration: 768497850.9803921, + numCalls: 255, + callRate: 0.9444444444444444, + numErrors: 0, + errorRate: 0, + }, + { + serviceName: 'customer', + p99: 890150740.0000001, + avgDuration: 369612035.2941176, + numCalls: 255, + callRate: 0.9444444444444444, + numErrors: 0, + errorRate: 0, + }, +]; diff --git a/frontend/src/container/ServiceTable/index.tsx b/frontend/src/container/ServiceTable/index.tsx new file mode 100644 index 0000000000..6c9fa1aede --- /dev/null +++ b/frontend/src/container/ServiceTable/index.tsx @@ -0,0 +1,26 @@ +import { ResizeTable } from 'components/ResizeTable'; +import { useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; + +import { getColumns } from './Columns/ServiceColumn'; +import { Container } from './styles'; +import ServiceTableProp from './types'; + +function Services({ services, isLoading }: ServiceTableProp): JSX.Element { + const { search } = useLocation(); + + const tableColumns = useMemo(() => getColumns(search), [search]); + + return ( + + + + ); +} + +export default Services; diff --git a/frontend/src/container/MetricsTable/styles.ts b/frontend/src/container/ServiceTable/styles.ts similarity index 100% rename from frontend/src/container/MetricsTable/styles.ts rename to frontend/src/container/ServiceTable/styles.ts diff --git a/frontend/src/container/ServiceTable/types.ts b/frontend/src/container/ServiceTable/types.ts new file mode 100644 index 0000000000..7118bfa2fe --- /dev/null +++ b/frontend/src/container/ServiceTable/types.ts @@ -0,0 +1,6 @@ +import { ServicesList } from 'types/api/metrics/getService'; + +export default interface ServiceTableProp { + services: ServicesList[]; + isLoading: boolean; +} diff --git a/frontend/src/hooks/useErrorNotification.ts b/frontend/src/hooks/useErrorNotification.ts new file mode 100644 index 0000000000..b2961f66cc --- /dev/null +++ b/frontend/src/hooks/useErrorNotification.ts @@ -0,0 +1,17 @@ +import { AxiosError } from 'axios'; +import { useEffect } from 'react'; + +import { useNotifications } from './useNotifications'; + +const useErrorNotification = (error: AxiosError | null): void => { + const { notifications } = useNotifications(); + useEffect(() => { + if (error) { + notifications.error({ + message: error.message, + }); + } + }, [error, notifications]); +}; + +export default useErrorNotification; diff --git a/frontend/src/hooks/useQueryService.ts b/frontend/src/hooks/useQueryService.ts new file mode 100644 index 0000000000..0307460ccc --- /dev/null +++ b/frontend/src/hooks/useQueryService.ts @@ -0,0 +1,29 @@ +import getService from 'api/metrics/getService'; +import { AxiosError } from 'axios'; +import { Time } from 'container/TopNav/DateTimeSelection/config'; +import { useQuery, UseQueryResult } from 'react-query'; +import { PayloadProps } from 'types/api/metrics/getService'; +import { Tags } from 'types/reducer/trace'; + +export const useQueryService = ({ + minTime, + maxTime, + selectedTime, + selectedTags, +}: UseQueryServiceProps): UseQueryResult => { + const queryKey = [minTime, maxTime, selectedTime, selectedTags]; + return useQuery(queryKey, () => + getService({ + end: maxTime, + start: minTime, + selectedTags, + }), + ); +}; + +interface UseQueryServiceProps { + minTime: number; + maxTime: number; + selectedTime: Time; + selectedTags: Tags[]; +} diff --git a/frontend/src/pages/Metrics/index.tsx b/frontend/src/pages/Metrics/index.tsx deleted file mode 100644 index 1d83d28b33..0000000000 --- a/frontend/src/pages/Metrics/index.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { Space } from 'antd'; -import getLocalStorageKey from 'api/browser/localstorage/get'; -import ReleaseNote from 'components/ReleaseNote'; -import Spinner from 'components/Spinner'; -import { SKIP_ONBOARDING } from 'constants/onboarding'; -import MetricTable from 'container/MetricsTable'; -import ResourceAttributesFilter from 'container/ResourceAttributesFilter'; -import { useNotifications } from 'hooks/useNotifications'; -import useResourceAttribute from 'hooks/useResourceAttribute'; -import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils'; -import { useEffect, useMemo } from 'react'; -import { connect, useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { GetService, GetServiceProps } from 'store/actions/metrics'; -import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import { GlobalReducer } from 'types/reducer/globalTime'; -import MetricReducer from 'types/reducer/metrics'; -import { Tags } from 'types/reducer/trace'; - -function Metrics({ getService }: MetricsProps): JSX.Element { - const { minTime, maxTime, loading, selectedTime } = useSelector< - AppState, - GlobalReducer - >((state) => state.globalTime); - const location = useLocation(); - const { services, error, errorMessage } = useSelector( - (state) => state.metrics, - ); - const { notifications } = useNotifications(); - - useEffect(() => { - if (error) { - notifications.error({ - message: errorMessage, - }); - } - }, [error, errorMessage, notifications]); - - const { queries } = useResourceAttribute(); - - const selectedTags = useMemo( - () => (convertRawQueriesToTraceSelectedTags(queries, '') as Tags[]) || [], - [queries], - ); - - const isSkipped = getLocalStorageKey(SKIP_ONBOARDING) === 'true'; - - useEffect(() => { - if (loading === false) { - getService({ - maxTime, - minTime, - selectedTags, - }); - } - }, [getService, loading, maxTime, minTime, selectedTags]); - - useEffect(() => { - let timeInterval: NodeJS.Timeout; - - if (loading === false && !isSkipped && services.length === 0) { - timeInterval = setInterval(() => { - getService({ - maxTime, - minTime, - selectedTags, - }); - }, 50000); - } - - return (): void => { - clearInterval(timeInterval); - }; - }, [ - getService, - isSkipped, - loading, - maxTime, - minTime, - services, - selectedTime, - selectedTags, - ]); - - if (loading) { - return ; - } - - return ( - - - - - - - ); -} - -interface DispatchProps { - getService: ( - props: GetServiceProps, - ) => (dispatch: Dispatch, getState: () => AppState) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - getService: bindActionCreators(GetService, dispatch), -}); - -type MetricsProps = DispatchProps; - -export default connect(null, mapDispatchToProps)(Metrics); diff --git a/frontend/src/pages/Services/index.tsx b/frontend/src/pages/Services/index.tsx new file mode 100644 index 0000000000..87c0720efb --- /dev/null +++ b/frontend/src/pages/Services/index.tsx @@ -0,0 +1,70 @@ +import { Space } from 'antd'; +import localStorageGet from 'api/browser/localstorage/get'; +import localStorageSet from 'api/browser/localstorage/set'; +import ReleaseNote from 'components/ReleaseNote'; +import { SKIP_ONBOARDING } from 'constants/onboarding'; +import ResourceAttributesFilter from 'container/ResourceAttributesFilter'; +import ServicesTable from 'container/ServiceTable'; +import SkipOnBoardingModal from 'container/ServiceTable/SkipOnBoardModal'; +import useErrorNotification from 'hooks/useErrorNotification'; +import { useQueryService } from 'hooks/useQueryService'; +import useResourceAttribute from 'hooks/useResourceAttribute'; +import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils'; +import { useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { useLocation } from 'react-router-dom'; +import { AppState } from 'store/reducers'; +import { GlobalReducer } from 'types/reducer/globalTime'; +import { Tags } from 'types/reducer/trace'; + +function Metrics(): JSX.Element { + const { minTime, maxTime, selectedTime } = useSelector< + AppState, + GlobalReducer + >((state) => state.globalTime); + + const location = useLocation(); + const { queries } = useResourceAttribute(); + const [skipOnboarding, setSkipOnboarding] = useState( + localStorageGet(SKIP_ONBOARDING) === 'true', + ); + + const onContinueClick = (): void => { + localStorageSet(SKIP_ONBOARDING, 'true'); + setSkipOnboarding(true); + }; + + const selectedTags = useMemo( + () => (convertRawQueriesToTraceSelectedTags(queries, '') as Tags[]) || [], + [queries], + ); + + const { data, error, isLoading, isError } = useQueryService({ + minTime, + maxTime, + selectedTime, + selectedTags, + }); + + useErrorNotification(error); + + if ( + data?.length === 0 && + isLoading === false && + !skipOnboarding && + isError === true + ) { + return ; + } + + return ( + + + + + + + ); +} + +export default Metrics; diff --git a/frontend/src/store/actions/metrics/getService.ts b/frontend/src/store/actions/metrics/getService.ts index 90d65c0c43..8de8f3c134 100644 --- a/frontend/src/store/actions/metrics/getService.ts +++ b/frontend/src/store/actions/metrics/getService.ts @@ -1,5 +1,6 @@ import getService from 'api/metrics/getService'; import { AxiosError } from 'axios'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; import GetMinMax from 'lib/getMinMax'; import { Dispatch } from 'redux'; import { AppState } from 'store/reducers'; @@ -38,16 +39,16 @@ export const GetService = ( selectedTags: props.selectedTags, }); - if (response.statusCode === 200) { + if (response.length > 0) { dispatch({ type: 'GET_SERVICE_LIST_SUCCESS', - payload: response.payload, + payload: response, }); } else { dispatch({ type: 'GET_SERVICE_LIST_ERROR', payload: { - errorMessage: response.error || 'Something went wrong', + errorMessage: SOMETHING_WENT_WRONG, }, }); } @@ -55,7 +56,7 @@ export const GetService = ( dispatch({ type: 'GET_SERVICE_LIST_ERROR', payload: { - errorMessage: (error as AxiosError).toString() || 'Something went wrong', + errorMessage: (error as AxiosError).toString() || SOMETHING_WENT_WRONG, }, }); }