mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 17:48:59 +08:00
feat: exception page is updated (#1376)
* chore: all error utils is added * chore: error page list is added with total page and other handlings * test: unit test case for order is added
This commit is contained in:
parent
4d1516e3fc
commit
5554cce379
@ -81,6 +81,7 @@
|
|||||||
"style-loader": "1.3.0",
|
"style-loader": "1.3.0",
|
||||||
"styled-components": "^5.2.1",
|
"styled-components": "^5.2.1",
|
||||||
"terser-webpack-plugin": "^5.2.5",
|
"terser-webpack-plugin": "^5.2.5",
|
||||||
|
"timestamp-nano": "^1.0.0",
|
||||||
"ts-node": "^10.2.1",
|
"ts-node": "^10.2.1",
|
||||||
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
||||||
"typescript": "^4.0.5",
|
"typescript": "^4.0.5",
|
||||||
|
@ -10,9 +10,8 @@ const getAll = async (
|
|||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(
|
const response = await axios.get(
|
||||||
`/errors?${createQueryParams({
|
`/listErrors?${createQueryParams({
|
||||||
start: props.start.toString(),
|
...props,
|
||||||
end: props.end.toString(),
|
|
||||||
})}`,
|
})}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -10,11 +10,8 @@ const getByErrorType = async (
|
|||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(
|
const response = await axios.get(
|
||||||
`/errorWithType?${createQueryParams({
|
`/errorFromGroupID?${createQueryParams({
|
||||||
start: props.start.toString(),
|
...props,
|
||||||
end: props.end.toString(),
|
|
||||||
serviceName: props.serviceName,
|
|
||||||
errorType: props.errorType,
|
|
||||||
})}`,
|
})}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -3,17 +3,15 @@ import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { PayloadProps, Props } from 'types/api/errors/getById';
|
import { PayloadProps, Props } from 'types/api/errors/getByErrorId';
|
||||||
|
|
||||||
const getById = async (
|
const getById = async (
|
||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(
|
const response = await axios.get(
|
||||||
`/errorWithId?${createQueryParams({
|
`/errorFromErrorID?${createQueryParams({
|
||||||
start: props.start.toString(),
|
...props,
|
||||||
end: props.end.toString(),
|
|
||||||
errorId: props.errorId,
|
|
||||||
})}`,
|
})}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
29
frontend/src/api/errors/getErrorCounts.ts
Normal file
29
frontend/src/api/errors/getErrorCounts.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { PayloadProps, Props } from 'types/api/errors/getErrorCounts';
|
||||||
|
|
||||||
|
const getErrorCounts = async (
|
||||||
|
props: Props,
|
||||||
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(
|
||||||
|
`/countErrors?${createQueryParams({
|
||||||
|
...props,
|
||||||
|
})}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.message,
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getErrorCounts;
|
29
frontend/src/api/errors/getNextPrevId.ts
Normal file
29
frontend/src/api/errors/getNextPrevId.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { PayloadProps, Props } from 'types/api/errors/getNextPrevId';
|
||||||
|
|
||||||
|
const getErrorCounts = async (
|
||||||
|
props: Props,
|
||||||
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(
|
||||||
|
`/nextPrevErrorIDs?${createQueryParams({
|
||||||
|
...props,
|
||||||
|
})}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.message,
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getErrorCounts;
|
@ -1,31 +1,85 @@
|
|||||||
import { notification, Table, Tooltip, Typography } from 'antd';
|
import { notification, Table, TableProps, Tooltip, Typography } from 'antd';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import getAll from 'api/errors/getAll';
|
import getAll from 'api/errors/getAll';
|
||||||
|
import getErrorCounts from 'api/errors/getErrorCounts';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import React, { useEffect } from 'react';
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from 'react-query';
|
import { useQueries } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { Exception } from 'types/api/errors/getAll';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { Exception, PayloadProps } from 'types/api/errors/getAll';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getDefaultOrder,
|
||||||
|
getNanoSeconds,
|
||||||
|
getOffSet,
|
||||||
|
getOrder,
|
||||||
|
getOrderParams,
|
||||||
|
getUpdatePageSize,
|
||||||
|
urlKey,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
function AllErrors(): JSX.Element {
|
function AllErrors(): JSX.Element {
|
||||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
const { maxTime, minTime, loading } = useSelector<AppState, GlobalReducer>(
|
||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
|
const { search, pathname } = useLocation();
|
||||||
|
const params = useMemo(() => new URLSearchParams(search), [search]);
|
||||||
|
|
||||||
const { t } = useTranslation(['common']);
|
const { t } = useTranslation(['common']);
|
||||||
|
|
||||||
const { isLoading, data } = useQuery(['getAllError', [maxTime, minTime]], {
|
const updatedOrder = getOrder(params.get(urlKey.order));
|
||||||
queryFn: () =>
|
const getUpdatedOffset = getOffSet(params.get(urlKey.offset));
|
||||||
|
const getUpdatedParams = getOrderParams(params.get(urlKey.orderParam));
|
||||||
|
const getUpdatedPageSize = getUpdatePageSize(params.get(urlKey.pageSize));
|
||||||
|
|
||||||
|
const updatedPath = useMemo(
|
||||||
|
() =>
|
||||||
|
`${pathname}?${createQueryParams({
|
||||||
|
order: updatedOrder,
|
||||||
|
offset: getUpdatedOffset,
|
||||||
|
orderParam: getUpdatedParams,
|
||||||
|
pageSize: getUpdatedPageSize,
|
||||||
|
})}`,
|
||||||
|
[
|
||||||
|
pathname,
|
||||||
|
updatedOrder,
|
||||||
|
getUpdatedOffset,
|
||||||
|
getUpdatedParams,
|
||||||
|
getUpdatedPageSize,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [{ isLoading, data }, errorCountResponse] = useQueries([
|
||||||
|
{
|
||||||
|
queryKey: ['getAllErrors', updatedPath, maxTime, minTime],
|
||||||
|
queryFn: (): Promise<SuccessResponse<PayloadProps> | ErrorResponse> =>
|
||||||
getAll({
|
getAll({
|
||||||
end: maxTime,
|
end: maxTime,
|
||||||
start: minTime,
|
start: minTime,
|
||||||
|
order: updatedOrder,
|
||||||
|
limit: getUpdatedPageSize,
|
||||||
|
offset: getUpdatedOffset,
|
||||||
|
orderParam: getUpdatedParams,
|
||||||
}),
|
}),
|
||||||
});
|
enabled: !loading,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
queryKey: ['getErrorCounts', maxTime, minTime],
|
||||||
|
queryFn: (): Promise<ErrorResponse | SuccessResponse<number>> =>
|
||||||
|
getErrorCounts({
|
||||||
|
end: maxTime,
|
||||||
|
start: minTime,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.error) {
|
if (data?.error) {
|
||||||
@ -35,11 +89,9 @@ function AllErrors(): JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [data?.error, data?.payload, t]);
|
}, [data?.error, data?.payload, t]);
|
||||||
|
|
||||||
const getDateValue = (value: string): JSX.Element => {
|
const getDateValue = (value: string): JSX.Element => (
|
||||||
return (
|
|
||||||
<Typography>{dayjs(value).format('DD/MM/YYYY HH:mm:ss A')}</Typography>
|
<Typography>{dayjs(value).format('DD/MM/YYYY HH:mm:ss A')}</Typography>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const columns: ColumnsType<Exception> = [
|
const columns: ColumnsType<Exception> = [
|
||||||
{
|
{
|
||||||
@ -49,14 +101,22 @@ function AllErrors(): JSX.Element {
|
|||||||
render: (value, record): JSX.Element => (
|
render: (value, record): JSX.Element => (
|
||||||
<Tooltip overlay={(): JSX.Element => value}>
|
<Tooltip overlay={(): JSX.Element => value}>
|
||||||
<Link
|
<Link
|
||||||
to={`${ROUTES.ERROR_DETAIL}?serviceName=${record.serviceName}&errorType=${record.exceptionType}`}
|
to={`${ROUTES.ERROR_DETAIL}?serviceName=${
|
||||||
|
record.serviceName
|
||||||
|
}&exceptionType=${record.exceptionType}&groupId=${
|
||||||
|
record.groupID
|
||||||
|
}×tamp=${getNanoSeconds(record.lastSeen)}`}
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
),
|
),
|
||||||
sorter: (a, b): number =>
|
sorter: true,
|
||||||
a.exceptionType.charCodeAt(0) - b.exceptionType.charCodeAt(0),
|
defaultSortOrder: getDefaultOrder(
|
||||||
|
getUpdatedParams,
|
||||||
|
updatedOrder,
|
||||||
|
'exceptionType',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Error Message',
|
title: 'Error Message',
|
||||||
@ -78,39 +138,86 @@ function AllErrors(): JSX.Element {
|
|||||||
title: 'Count',
|
title: 'Count',
|
||||||
dataIndex: 'exceptionCount',
|
dataIndex: 'exceptionCount',
|
||||||
key: 'exceptionCount',
|
key: 'exceptionCount',
|
||||||
sorter: (a, b): number => a.exceptionCount - b.exceptionCount,
|
sorter: true,
|
||||||
|
defaultSortOrder: getDefaultOrder(
|
||||||
|
getUpdatedParams,
|
||||||
|
updatedOrder,
|
||||||
|
'exceptionCount',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Last Seen',
|
title: 'Last Seen',
|
||||||
dataIndex: 'lastSeen',
|
dataIndex: 'lastSeen',
|
||||||
key: 'lastSeen',
|
key: 'lastSeen',
|
||||||
render: getDateValue,
|
render: getDateValue,
|
||||||
sorter: (a, b): number =>
|
sorter: true,
|
||||||
dayjs(b.lastSeen).isBefore(dayjs(a.lastSeen)) === true ? 1 : 0,
|
defaultSortOrder: getDefaultOrder(
|
||||||
|
getUpdatedParams,
|
||||||
|
updatedOrder,
|
||||||
|
'lastSeen',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'First Seen',
|
title: 'First Seen',
|
||||||
dataIndex: 'firstSeen',
|
dataIndex: 'firstSeen',
|
||||||
key: 'firstSeen',
|
key: 'firstSeen',
|
||||||
render: getDateValue,
|
render: getDateValue,
|
||||||
sorter: (a, b): number =>
|
sorter: true,
|
||||||
dayjs(b.firstSeen).isBefore(dayjs(a.firstSeen)) === true ? 1 : 0,
|
defaultSortOrder: getDefaultOrder(
|
||||||
|
getUpdatedParams,
|
||||||
|
updatedOrder,
|
||||||
|
'firstSeen',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Application',
|
title: 'Application',
|
||||||
dataIndex: 'serviceName',
|
dataIndex: 'serviceName',
|
||||||
key: 'serviceName',
|
key: 'serviceName',
|
||||||
sorter: (a, b): number =>
|
sorter: true,
|
||||||
a.serviceName.charCodeAt(0) - b.serviceName.charCodeAt(0),
|
defaultSortOrder: getDefaultOrder(
|
||||||
|
getUpdatedParams,
|
||||||
|
updatedOrder,
|
||||||
|
'serviceName',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const onChangeHandler: TableProps<Exception>['onChange'] = (
|
||||||
|
paginations,
|
||||||
|
_,
|
||||||
|
sorter,
|
||||||
|
) => {
|
||||||
|
if (!Array.isArray(sorter)) {
|
||||||
|
const { current = 0, pageSize = 0 } = paginations;
|
||||||
|
const { columnKey = '', order } = sorter;
|
||||||
|
const updatedOrder = order === 'ascend' ? 'ascending' : 'descending';
|
||||||
|
|
||||||
|
history.replace(
|
||||||
|
`${pathname}?${createQueryParams({
|
||||||
|
order: updatedOrder,
|
||||||
|
offset: current - 1,
|
||||||
|
orderParam: columnKey,
|
||||||
|
pageSize,
|
||||||
|
})}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<Table
|
||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
dataSource={data?.payload as Exception[]}
|
dataSource={data?.payload as Exception[]}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
loading={isLoading || false}
|
rowKey="firstSeen"
|
||||||
|
loading={isLoading || false || errorCountResponse.status === 'loading'}
|
||||||
|
pagination={{
|
||||||
|
pageSize: getUpdatedPageSize,
|
||||||
|
responsive: true,
|
||||||
|
current: getUpdatedOffset + 1,
|
||||||
|
position: ['bottomLeft'],
|
||||||
|
total: errorCountResponse.data?.payload || 0,
|
||||||
|
}}
|
||||||
|
onChange={onChangeHandler}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
28
frontend/src/container/AllError/utils.test.ts
Normal file
28
frontend/src/container/AllError/utils.test.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { isOrder, isOrderParams } from './utils';
|
||||||
|
|
||||||
|
describe('Error utils', () => {
|
||||||
|
test('Valid OrderBy Params', () => {
|
||||||
|
expect(isOrderParams('serviceName')).toBe(true);
|
||||||
|
expect(isOrderParams('exceptionCount')).toBe(true);
|
||||||
|
expect(isOrderParams('lastSeen')).toBe(true);
|
||||||
|
expect(isOrderParams('firstSeen')).toBe(true);
|
||||||
|
expect(isOrderParams('exceptionType')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Invalid OrderBy Params', () => {
|
||||||
|
expect(isOrderParams('invalid')).toBe(false);
|
||||||
|
expect(isOrderParams(null)).toBe(false);
|
||||||
|
expect(isOrderParams('')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Valid Order', () => {
|
||||||
|
expect(isOrder('ascending')).toBe(true);
|
||||||
|
expect(isOrder('descending')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Invalid Order', () => {
|
||||||
|
expect(isOrder('invalid')).toBe(false);
|
||||||
|
expect(isOrder(null)).toBe(false);
|
||||||
|
expect(isOrder('')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
89
frontend/src/container/AllError/utils.ts
Normal file
89
frontend/src/container/AllError/utils.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { SortOrder } from 'antd/lib/table/interface';
|
||||||
|
import Timestamp from 'timestamp-nano';
|
||||||
|
import { Order, OrderBy } from 'types/api/errors/getAll';
|
||||||
|
|
||||||
|
export const isOrder = (order: string | null): order is Order =>
|
||||||
|
!!(order === 'ascending' || order === 'descending');
|
||||||
|
|
||||||
|
export const urlKey = {
|
||||||
|
order: 'order',
|
||||||
|
offset: 'offset',
|
||||||
|
orderParam: 'orderParam',
|
||||||
|
pageSize: 'pageSize',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isOrderParams = (orderBy: string | null): orderBy is OrderBy => {
|
||||||
|
return !!(
|
||||||
|
orderBy === 'serviceName' ||
|
||||||
|
orderBy === 'exceptionCount' ||
|
||||||
|
orderBy === 'lastSeen' ||
|
||||||
|
orderBy === 'firstSeen' ||
|
||||||
|
orderBy === 'exceptionType'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOrder = (order: string | null): Order => {
|
||||||
|
if (isOrder(order)) {
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
return 'ascending';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLimit = (limit: string | null): number => {
|
||||||
|
if (limit) {
|
||||||
|
return parseInt(limit, 10);
|
||||||
|
}
|
||||||
|
return 10;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOffSet = (offset: string | null): number => {
|
||||||
|
if (offset && typeof offset === 'string') {
|
||||||
|
return parseInt(offset, 10);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOrderParams = (order: string | null): OrderBy => {
|
||||||
|
if (isOrderParams(order)) {
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
return 'serviceName';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDefaultOrder = (
|
||||||
|
orderBy: OrderBy,
|
||||||
|
order: Order,
|
||||||
|
data: OrderBy,
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
): SortOrder | undefined => {
|
||||||
|
if (orderBy === 'exceptionType' && data === 'exceptionType') {
|
||||||
|
return order === 'ascending' ? 'ascend' : 'descend';
|
||||||
|
}
|
||||||
|
if (orderBy === 'serviceName' && data === 'serviceName') {
|
||||||
|
return order === 'ascending' ? 'ascend' : 'descend';
|
||||||
|
}
|
||||||
|
if (orderBy === 'exceptionCount' && data === 'exceptionCount') {
|
||||||
|
return order === 'ascending' ? 'ascend' : 'descend';
|
||||||
|
}
|
||||||
|
if (orderBy === 'lastSeen' && data === 'lastSeen') {
|
||||||
|
return order === 'ascending' ? 'ascend' : 'descend';
|
||||||
|
}
|
||||||
|
if (orderBy === 'firstSeen' && data === 'firstSeen') {
|
||||||
|
return order === 'ascending' ? 'ascend' : 'descend';
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNanoSeconds = (date: string): number => {
|
||||||
|
return (
|
||||||
|
parseInt((new Date(date).getTime() / 1e3).toString(), 10) * 1e9 +
|
||||||
|
Timestamp.fromString(date).getNano()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUpdatePageSize = (pageSize: string | null): number => {
|
||||||
|
if (pageSize) {
|
||||||
|
return parseInt(pageSize, 10);
|
||||||
|
}
|
||||||
|
return 10;
|
||||||
|
};
|
@ -1,25 +1,49 @@
|
|||||||
import { Button, Divider, notification, Space, Table, Typography } from 'antd';
|
import { Button, Divider, notification, Space, Table, Typography } from 'antd';
|
||||||
|
import getNextPrevId from 'api/errors/getNextPrevId';
|
||||||
import Editor from 'components/Editor';
|
import Editor from 'components/Editor';
|
||||||
|
import { getNanoSeconds } from 'container/AllError/utils';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
|
import { urlKey } from 'pages/ErrorDetails/utils';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { PayloadProps as GetByErrorTypeAndServicePayload } from 'types/api/errors/getByErrorTypeAndService';
|
import { PayloadProps as GetByErrorTypeAndServicePayload } from 'types/api/errors/getByErrorTypeAndService';
|
||||||
import { PayloadProps } from 'types/api/errors/getById';
|
|
||||||
|
|
||||||
import { DashedContainer, EditorContainer, EventContainer } from './styles';
|
import { DashedContainer, EditorContainer, EventContainer } from './styles';
|
||||||
|
|
||||||
function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
||||||
const { idPayload } = props;
|
const { idPayload } = props;
|
||||||
const [isLoading, setLoading] = useState<boolean>(false);
|
|
||||||
const { t } = useTranslation(['errorDetails', 'common']);
|
const { t } = useTranslation(['errorDetails', 'common']);
|
||||||
|
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const params = new URLSearchParams(search);
|
|
||||||
const queryErrorId = params.get('errorId');
|
const params = useMemo(() => new URLSearchParams(search), [search]);
|
||||||
const serviceName = params.get('serviceName');
|
|
||||||
const errorType = params.get('errorType');
|
const errorId = params.get(urlKey.errorId);
|
||||||
|
const serviceName = params.get(urlKey.serviceName);
|
||||||
|
const errorType = params.get(urlKey.exceptionType);
|
||||||
|
const timestamp = params.get(urlKey.timestamp);
|
||||||
|
|
||||||
|
const { data: nextPrevData, status: nextPrevStatus } = useQuery(
|
||||||
|
[
|
||||||
|
idPayload.errorId,
|
||||||
|
idPayload.groupID,
|
||||||
|
idPayload.timestamp,
|
||||||
|
errorId,
|
||||||
|
serviceName,
|
||||||
|
errorType,
|
||||||
|
timestamp,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
queryFn: () =>
|
||||||
|
getNextPrevId({
|
||||||
|
errorID: errorId || idPayload.errorId,
|
||||||
|
groupID: idPayload.groupID,
|
||||||
|
timestamp: timestamp || getNanoSeconds(idPayload.timestamp).toString(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const errorDetail = idPayload;
|
const errorDetail = idPayload;
|
||||||
|
|
||||||
@ -48,34 +72,34 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
|||||||
'errorId',
|
'errorId',
|
||||||
'timestamp',
|
'timestamp',
|
||||||
'exceptionMessage',
|
'exceptionMessage',
|
||||||
'newerErrorId',
|
'exceptionEscaped',
|
||||||
'olderErrorId',
|
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onClickErrorIdHandler = async (id: string): Promise<void> => {
|
const onClickErrorIdHandler = async (
|
||||||
|
id: string,
|
||||||
|
timespamp: string,
|
||||||
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
if (id.length === 0) {
|
if (id.length === 0) {
|
||||||
notification.error({
|
notification.error({
|
||||||
message: 'Error Id cannot be empty',
|
message: 'Error Id cannot be empty',
|
||||||
});
|
});
|
||||||
setLoading(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
history.replace(
|
||||||
|
`${history.location.pathname}?${urlKey.serviceName}=${serviceName}&${
|
||||||
history.push(
|
urlKey.exceptionType
|
||||||
`${history.location.pathname}?errorId=${id}&serviceName=${serviceName}&errorType=${errorType}`,
|
}=${errorType}&groupId=${idPayload.groupID}×tamp=${getNanoSeconds(
|
||||||
|
timespamp,
|
||||||
|
)}&errorId=${id}`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notification.error({
|
notification.error({
|
||||||
message: t('something_went_wrong'),
|
message: t('something_went_wrong'),
|
||||||
});
|
});
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -106,25 +130,25 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
|||||||
<div>
|
<div>
|
||||||
<Space align="end" direction="horizontal">
|
<Space align="end" direction="horizontal">
|
||||||
<Button
|
<Button
|
||||||
loading={isLoading}
|
loading={nextPrevStatus === 'loading'}
|
||||||
disabled={
|
disabled={nextPrevData?.payload?.prevErrorID.length === 0}
|
||||||
errorDetail.olderErrorId.length === 0 ||
|
|
||||||
queryErrorId === errorDetail.olderErrorId
|
|
||||||
}
|
|
||||||
onClick={(): Promise<void> =>
|
onClick={(): Promise<void> =>
|
||||||
onClickErrorIdHandler(errorDetail.olderErrorId)
|
onClickErrorIdHandler(
|
||||||
|
nextPrevData?.payload?.prevErrorID || '',
|
||||||
|
nextPrevData?.payload?.prevTimestamp || '',
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('older')}
|
{t('older')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
loading={isLoading}
|
loading={nextPrevStatus === 'loading'}
|
||||||
disabled={
|
disabled={nextPrevData?.payload?.nextErrorID.length === 0}
|
||||||
errorDetail.newerErrorId.length === 0 ||
|
|
||||||
queryErrorId === errorDetail.newerErrorId
|
|
||||||
}
|
|
||||||
onClick={(): Promise<void> =>
|
onClick={(): Promise<void> =>
|
||||||
onClickErrorIdHandler(errorDetail.newerErrorId)
|
onClickErrorIdHandler(
|
||||||
|
nextPrevData?.payload?.nextErrorID || '',
|
||||||
|
nextPrevData?.payload?.nextTimestamp || '',
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('newer')}
|
{t('newer')}
|
||||||
@ -153,7 +177,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ErrorDetailsProps {
|
interface ErrorDetailsProps {
|
||||||
idPayload: PayloadProps;
|
idPayload: GetByErrorTypeAndServicePayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ErrorDetails;
|
export default ErrorDetails;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const createQueryParams = (params: { [x: string]: string }): string =>
|
const createQueryParams = (params: { [x: string]: string | number }): string =>
|
||||||
Object.keys(params)
|
Object.keys(params)
|
||||||
.map((k) => `${k}=${encodeURI(params[k])}`)
|
.map((k) => `${k}=${encodeURI(String(params[k]))}`)
|
||||||
.join('&');
|
.join('&');
|
||||||
|
|
||||||
export default createQueryParams;
|
export default createQueryParams;
|
||||||
|
@ -4,107 +4,106 @@ import getById from 'api/errors/getById';
|
|||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import ErrorDetailsContainer from 'container/ErrorDetails';
|
import ErrorDetailsContainer from 'container/ErrorDetails';
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Redirect, useLocation } from 'react-router-dom';
|
import { Redirect, useLocation } from 'react-router-dom';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { PayloadProps } from 'types/api/errors/getById';
|
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
|
import { urlKey } from './utils';
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
function ErrorDetails(): JSX.Element {
|
function ErrorDetails(): JSX.Element {
|
||||||
const { t } = useTranslation(['common']);
|
const { t } = useTranslation(['common']);
|
||||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const params = new URLSearchParams(search);
|
const params = useMemo(() => new URLSearchParams(search), [search]);
|
||||||
|
|
||||||
|
const serviceName = params.get(urlKey.serviceName);
|
||||||
|
const expectionType = params.get(urlKey.exceptionType);
|
||||||
|
const groupId = params.get(urlKey.groupId);
|
||||||
|
const errorId = params.get(urlKey.errorId);
|
||||||
|
const timestamp = params.get(urlKey.timestamp);
|
||||||
|
|
||||||
const errorId = params.get('errorId');
|
|
||||||
const errorType = params.get('errorType');
|
|
||||||
const serviceName = params.get('serviceName');
|
|
||||||
const defaultError = t('something_went_wrong');
|
const defaultError = t('something_went_wrong');
|
||||||
|
|
||||||
|
const { data: IdData, status: IdStatus } = useQuery(
|
||||||
|
[errorId, timestamp, groupId],
|
||||||
|
{
|
||||||
|
queryFn: () =>
|
||||||
|
getById({
|
||||||
|
errorID: errorId || '',
|
||||||
|
groupID: groupId || '',
|
||||||
|
timestamp: timestamp || '',
|
||||||
|
}),
|
||||||
|
enabled:
|
||||||
|
errorId !== null &&
|
||||||
|
groupId !== null &&
|
||||||
|
timestamp !== null &&
|
||||||
|
errorId.length !== 0 &&
|
||||||
|
groupId.length !== 0 &&
|
||||||
|
timestamp.length !== 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const { data, status } = useQuery(
|
const { data, status } = useQuery(
|
||||||
[
|
[
|
||||||
'errorByType',
|
'expectionType',
|
||||||
errorType,
|
expectionType,
|
||||||
'serviceName',
|
'serviceName',
|
||||||
serviceName,
|
serviceName,
|
||||||
maxTime,
|
maxTime,
|
||||||
minTime,
|
minTime,
|
||||||
errorId,
|
groupId,
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
getByErrorType({
|
getByErrorType({
|
||||||
end: maxTime,
|
groupID: groupId || '',
|
||||||
errorType: errorType || '',
|
timestamp: timestamp || '',
|
||||||
serviceName: serviceName || '',
|
|
||||||
start: minTime,
|
|
||||||
}),
|
|
||||||
enabled: errorId === null && errorType !== null && serviceName !== null,
|
|
||||||
cacheTime: 5000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { status: ErrorIdStatus, data: errorIdPayload } = useQuery(
|
|
||||||
[
|
|
||||||
'errorByType',
|
|
||||||
errorType,
|
|
||||||
'serviceName',
|
|
||||||
serviceName,
|
|
||||||
maxTime,
|
|
||||||
minTime,
|
|
||||||
'errorId',
|
|
||||||
errorId,
|
|
||||||
],
|
|
||||||
{
|
|
||||||
queryFn: () =>
|
|
||||||
getById({
|
|
||||||
end: maxTime,
|
|
||||||
errorId: errorId || data?.payload?.errorId || '',
|
|
||||||
start: minTime,
|
|
||||||
}),
|
}),
|
||||||
enabled:
|
enabled:
|
||||||
(errorId !== null || status === 'success') &&
|
!!expectionType && !!serviceName && !!groupId && IdStatus !== 'success',
|
||||||
errorType !== null &&
|
|
||||||
serviceName !== null,
|
|
||||||
cacheTime: 5000,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// if errorType and serviceName is null redirecting to the ALL_ERROR page not now
|
// if errorType and serviceName is null redirecting to the ALL_ERROR page not now
|
||||||
if (errorType === null || serviceName === null) {
|
if (
|
||||||
|
serviceName === null ||
|
||||||
|
expectionType === null ||
|
||||||
|
groupId === null ||
|
||||||
|
timestamp === null
|
||||||
|
) {
|
||||||
return <Redirect to={ROUTES.ALL_ERROR} />;
|
return <Redirect to={ROUTES.ALL_ERROR} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// when the api is in loading state
|
// when the api is in loading state
|
||||||
if (status === 'loading' || ErrorIdStatus === 'loading') {
|
if (status === 'loading' || IdStatus === 'loading') {
|
||||||
return <Spinner tip="Loading.." />;
|
return <Spinner tip="Loading.." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if any error occurred while loading
|
// if any error occurred while loading
|
||||||
if (status === 'error' || ErrorIdStatus === 'error') {
|
if (status === 'error' || IdStatus === 'error') {
|
||||||
return (
|
return <Typography>{data?.error || defaultError}</Typography>;
|
||||||
<Typography>
|
|
||||||
{data?.error || errorIdPayload?.error || defaultError}
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const idPayload = data?.payload || IdData?.payload;
|
||||||
|
|
||||||
// if API is successfully but there is an error
|
// if API is successfully but there is an error
|
||||||
if (
|
if (
|
||||||
(status === 'success' && data?.statusCode >= 400) ||
|
(status === 'success' && data?.statusCode >= 400) ||
|
||||||
(ErrorIdStatus === 'success' && errorIdPayload.statusCode >= 400)
|
(IdStatus === 'success' && IdData.statusCode >= 400) ||
|
||||||
|
idPayload === null ||
|
||||||
|
idPayload === undefined
|
||||||
) {
|
) {
|
||||||
return <Typography>{data?.error || defaultError}</Typography>;
|
return <Typography>{data?.error || defaultError}</Typography>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <ErrorDetailsContainer idPayload={idPayload} />;
|
||||||
<ErrorDetailsContainer idPayload={errorIdPayload?.payload as PayloadProps} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorDetailsParams {
|
export interface ErrorDetailsParams {
|
||||||
|
8
frontend/src/pages/ErrorDetails/utils.ts
Normal file
8
frontend/src/pages/ErrorDetails/utils.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export const urlKey = {
|
||||||
|
serviceName: 'serviceName',
|
||||||
|
exceptionType: 'exceptionType',
|
||||||
|
groupId: 'groupId',
|
||||||
|
lastSeen: 'lastSeen',
|
||||||
|
errorId: 'errorId',
|
||||||
|
timestamp: 'timestamp',
|
||||||
|
};
|
@ -1,8 +1,20 @@
|
|||||||
import { GlobalTime } from 'types/actions/globalTime';
|
import { GlobalTime } from 'types/actions/globalTime';
|
||||||
|
|
||||||
|
export type Order = 'ascending' | 'descending';
|
||||||
|
export type OrderBy =
|
||||||
|
| 'serviceName'
|
||||||
|
| 'exceptionCount'
|
||||||
|
| 'lastSeen'
|
||||||
|
| 'firstSeen'
|
||||||
|
| 'exceptionType';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
start: GlobalTime['minTime'];
|
start: GlobalTime['minTime'];
|
||||||
end: GlobalTime['maxTime'];
|
end: GlobalTime['maxTime'];
|
||||||
|
order?: Order;
|
||||||
|
orderParam?: OrderBy;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Exception {
|
export interface Exception {
|
||||||
@ -12,6 +24,7 @@ export interface Exception {
|
|||||||
lastSeen: string;
|
lastSeen: string;
|
||||||
firstSeen: string;
|
firstSeen: string;
|
||||||
serviceName: string;
|
serviceName: string;
|
||||||
|
groupID: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PayloadProps = Exception[];
|
export type PayloadProps = Exception[];
|
||||||
|
9
frontend/src/types/api/errors/getByErrorId.ts
Normal file
9
frontend/src/types/api/errors/getByErrorId.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { PayloadProps as Prop } from './getByErrorTypeAndService';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
groupID: string;
|
||||||
|
errorID: string;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PayloadProps = Prop;
|
@ -1,10 +1,6 @@
|
|||||||
import { GlobalTime } from 'types/actions/globalTime';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
start: GlobalTime['minTime'];
|
timestamp: string;
|
||||||
end: GlobalTime['maxTime'];
|
groupID: string;
|
||||||
serviceName: string;
|
|
||||||
errorType: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PayloadProps {
|
export interface PayloadProps {
|
||||||
@ -16,7 +12,6 @@ export interface PayloadProps {
|
|||||||
timestamp: string;
|
timestamp: string;
|
||||||
spanID: string;
|
spanID: string;
|
||||||
traceID: string;
|
traceID: string;
|
||||||
serviceName: Props['serviceName'];
|
serviceName: string;
|
||||||
newerErrorId: string;
|
groupID: string;
|
||||||
olderErrorId: string;
|
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
import { GlobalTime } from 'types/actions/globalTime';
|
import { GlobalTime } from 'types/actions/globalTime';
|
||||||
|
|
||||||
import { PayloadProps as Payload } from './getByErrorTypeAndService';
|
|
||||||
|
|
||||||
export type PayloadProps = Payload;
|
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
start: GlobalTime['minTime'];
|
start: GlobalTime['minTime'];
|
||||||
end: GlobalTime['minTime'];
|
end: GlobalTime['minTime'];
|
||||||
errorId: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PayloadProps = number;
|
13
frontend/src/types/api/errors/getNextPrevId.ts
Normal file
13
frontend/src/types/api/errors/getNextPrevId.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export type Props = {
|
||||||
|
errorID: string;
|
||||||
|
timestamp: string;
|
||||||
|
groupID: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PayloadProps = {
|
||||||
|
prevErrorID: string;
|
||||||
|
nextErrorID: string;
|
||||||
|
groupID: string;
|
||||||
|
nextTimestamp: string;
|
||||||
|
prevTimestamp: string;
|
||||||
|
};
|
@ -12408,6 +12408,11 @@ timed-out@^4.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
|
resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
|
||||||
integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=
|
integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=
|
||||||
|
|
||||||
|
timestamp-nano@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/timestamp-nano/-/timestamp-nano-1.0.0.tgz#03bf0b43c2bdcb913a6a02fbaae6f97d68650f3a"
|
||||||
|
integrity sha512-NO/1CZigzlCWQiWdIGv8ebXt6Uk77zdLz2NE7KcZRU5Egj2+947lzUpk30xQUQlq5dRY25j7ZulG4RfA2DHYfA==
|
||||||
|
|
||||||
tiny-invariant@^1.0.2:
|
tiny-invariant@^1.0.2:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9"
|
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user