mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 05:55:59 +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';
|
import Loadable from 'components/Loadable';
|
||||||
|
|
||||||
export const ServicesTablePage = Loadable(
|
export const ServicesTablePage = Loadable(
|
||||||
() => import(/* webpackChunkName: "ServicesTablePage" */ 'pages/Metrics'),
|
() => import(/* webpackChunkName: "ServicesTablePage" */ 'pages/Services'),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ServiceMetricsPage = Loadable(
|
export const ServiceMetricsPage = Loadable(
|
||||||
|
@ -1,28 +1,13 @@
|
|||||||
import axios from 'api';
|
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';
|
import { PayloadProps, Props } from 'types/api/metrics/getService';
|
||||||
|
|
||||||
const getService = async (
|
const getService = async (props: Props): Promise<PayloadProps> => {
|
||||||
props: Props,
|
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post(`/services`, {
|
const response = await axios.post(`/services`, {
|
||||||
start: `${props.start}`,
|
start: `${props.start}`,
|
||||||
end: `${props.end}`,
|
end: `${props.end}`,
|
||||||
tags: props.selectedTags,
|
tags: props.selectedTags,
|
||||||
});
|
});
|
||||||
|
return response.data;
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
error: null,
|
|
||||||
message: response.data.status,
|
|
||||||
payload: response.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getService;
|
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 getService from 'api/metrics/getService';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import GetMinMax from 'lib/getMinMax';
|
import GetMinMax from 'lib/getMinMax';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@ -38,16 +39,16 @@ export const GetService = (
|
|||||||
selectedTags: props.selectedTags,
|
selectedTags: props.selectedTags,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
if (response.length > 0) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'GET_SERVICE_LIST_SUCCESS',
|
type: 'GET_SERVICE_LIST_SUCCESS',
|
||||||
payload: response.payload,
|
payload: response,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'GET_SERVICE_LIST_ERROR',
|
type: 'GET_SERVICE_LIST_ERROR',
|
||||||
payload: {
|
payload: {
|
||||||
errorMessage: response.error || 'Something went wrong',
|
errorMessage: SOMETHING_WENT_WRONG,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -55,7 +56,7 @@ export const GetService = (
|
|||||||
dispatch({
|
dispatch({
|
||||||
type: 'GET_SERVICE_LIST_ERROR',
|
type: 'GET_SERVICE_LIST_ERROR',
|
||||||
payload: {
|
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