mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-13 06:29:04 +08:00
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 <makeavish786@gmail.com>
This commit is contained in:
parent
e6fa1383f3
commit
b9409820cc
@ -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(
|
||||
|
@ -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<SuccessResponse<PayloadProps> | 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<PayloadProps> => {
|
||||
const response = await axios.post(`/services`, {
|
||||
start: `${props.start}`,
|
||||
end: `${props.end}`,
|
||||
tags: props.selectedTags,
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export default getService;
|
||||
|
@ -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(
|
||||
<BrowserRouter>
|
||||
<Provider store={mockStore}>{component}</Provider>
|
||||
</BrowserRouter>,
|
||||
);
|
||||
|
||||
describe('Metrics Component', () => {
|
||||
it('renders without errors', async () => {
|
||||
renderWithReduxAndRouter(mockStore)(<Metrics />);
|
||||
|
||||
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)(<Metrics />);
|
||||
|
||||
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)(<Metrics />);
|
||||
|
||||
expect(screen.getByText('No data')).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -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<AppState, MetricReducer>(
|
||||
(state) => state.metrics,
|
||||
);
|
||||
|
||||
const onContinueClick = (): void => {
|
||||
localStorageSet(SKIP_ONBOARDING, 'true');
|
||||
setSkipOnboarding(true);
|
||||
};
|
||||
const handleSearch = (confirm: (param?: FilterConfirmProps) => void): void => {
|
||||
confirm();
|
||||
};
|
||||
|
||||
const FilterIcon: ColumnType<DataProps>['filterIcon'] = useCallback(
|
||||
(filtered: boolean) => (
|
||||
<SearchOutlined
|
||||
style={{
|
||||
color: filtered ? blue[6] : undefined,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const filterDropdown = useCallback(
|
||||
({ setSelectedKeys, selectedKeys, confirm }: FilterDropdownProps) => (
|
||||
<Card size="small">
|
||||
<Space align="start" direction="vertical">
|
||||
<Input
|
||||
placeholder="Search by service"
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e): void =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
allowClear
|
||||
onPressEnter={(): void => handleSearch(confirm)}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={(): void => handleSearch(confirm)}
|
||||
icon={<SearchOutlined />}
|
||||
size="small"
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
type DataIndex = keyof ServicesList;
|
||||
|
||||
const getColumnSearchProps = useCallback(
|
||||
(dataIndex: DataIndex): ColumnType<DataProps> => ({
|
||||
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 (
|
||||
<Link to={`${ROUTES.APPLICATION}/${metrics}?${queryString.join('')}`}>
|
||||
<Name>{metrics}</Name>
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
}),
|
||||
[filterDropdown, FilterIcon, search],
|
||||
);
|
||||
|
||||
const columns: ColumnsType<DataProps> = 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 <SkipBoardModal onContinueClick={onContinueClick} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ResizeTable
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
dataSource={services}
|
||||
rowKey="serviceName"
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
type DataProps = ServicesList;
|
||||
|
||||
export default Metrics;
|
@ -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';
|
@ -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<ServicesList> => ({
|
||||
filterDropdown,
|
||||
filterIcon: <SearchOutlined />,
|
||||
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 (
|
||||
<Link to={`${ROUTES.APPLICATION}/${metrics}?${queryString.join('')}`}>
|
||||
<Name>{metrics}</Name>
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
});
|
46
frontend/src/container/ServiceTable/Columns/ServiceColumn.ts
Normal file
46
frontend/src/container/ServiceTable/Columns/ServiceColumn.ts
Normal file
@ -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<ServicesList> => [
|
||||
{
|
||||
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),
|
||||
},
|
||||
];
|
@ -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<HTMLInputElement>): void => {
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : []);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card size="small">
|
||||
<Space align="start" direction="vertical">
|
||||
<Input
|
||||
placeholder={SEARCH_PLACEHOLDER}
|
||||
value={selectedKeys[0]}
|
||||
onChange={selectedKeysHandler}
|
||||
allowClear
|
||||
onPressEnter={handleSearch}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSearch}
|
||||
icon={<SearchOutlined />}
|
||||
size="small"
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
);
|
||||
};
|
50
frontend/src/container/ServiceTable/Service.test.tsx
Normal file
50
frontend/src/container/ServiceTable/Service.test.tsx
Normal file
@ -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(
|
||||
<BrowserRouter>
|
||||
<Metrics services={Services} isLoading={false} />
|
||||
</BrowserRouter>,
|
||||
);
|
||||
|
||||
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(
|
||||
<BrowserRouter>
|
||||
<Metrics services={Services} isLoading={false} />
|
||||
</BrowserRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('frontend')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders no data when required conditions are met', async () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Metrics services={[]} isLoading={false} />
|
||||
</BrowserRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('No data')).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -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,
|
||||
},
|
||||
];
|
26
frontend/src/container/ServiceTable/index.tsx
Normal file
26
frontend/src/container/ServiceTable/index.tsx
Normal file
@ -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 (
|
||||
<Container>
|
||||
<ResizeTable
|
||||
columns={tableColumns}
|
||||
dataSource={services}
|
||||
loading={isLoading}
|
||||
rowKey="serviceName"
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default Services;
|
6
frontend/src/container/ServiceTable/types.ts
Normal file
6
frontend/src/container/ServiceTable/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { ServicesList } from 'types/api/metrics/getService';
|
||||
|
||||
export default interface ServiceTableProp {
|
||||
services: ServicesList[];
|
||||
isLoading: boolean;
|
||||
}
|
17
frontend/src/hooks/useErrorNotification.ts
Normal file
17
frontend/src/hooks/useErrorNotification.ts
Normal file
@ -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;
|
29
frontend/src/hooks/useQueryService.ts
Normal file
29
frontend/src/hooks/useQueryService.ts
Normal file
@ -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<PayloadProps, AxiosError> => {
|
||||
const queryKey = [minTime, maxTime, selectedTime, selectedTags];
|
||||
return useQuery<PayloadProps, AxiosError>(queryKey, () =>
|
||||
getService({
|
||||
end: maxTime,
|
||||
start: minTime,
|
||||
selectedTags,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
interface UseQueryServiceProps {
|
||||
minTime: number;
|
||||
maxTime: number;
|
||||
selectedTime: Time;
|
||||
selectedTags: Tags[];
|
||||
}
|
@ -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<AppState, MetricReducer>(
|
||||
(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 <Spinner tip="Loading..." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<ReleaseNote path={location.pathname} />
|
||||
|
||||
<ResourceAttributesFilter />
|
||||
<MetricTable />
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
getService: (
|
||||
props: GetServiceProps,
|
||||
) => (dispatch: Dispatch<AppActions>, getState: () => AppState) => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
getService: bindActionCreators(GetService, dispatch),
|
||||
});
|
||||
|
||||
type MetricsProps = DispatchProps;
|
||||
|
||||
export default connect(null, mapDispatchToProps)(Metrics);
|
70
frontend/src/pages/Services/index.tsx
Normal file
70
frontend/src/pages/Services/index.tsx
Normal file
@ -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 <SkipOnBoardingModal onContinueClick={onContinueClick} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<ReleaseNote path={location.pathname} />
|
||||
|
||||
<ResourceAttributesFilter />
|
||||
<ServicesTable services={data || []} isLoading={isLoading} />
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default Metrics;
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user