mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-10-12 07:11:31 +08:00

* feat: time picker hint and timezone picker UI with basic functionality + helper to get timezones * feat: add support for esc keypress to close the timezone picker * chore: add the selected timezone as url param and close timezone picker on select * fix: overall improvement + add searchIndex to timezone * feat: timezone preferences UI * chore: improve timezone utils * chore: change timezone item from div to button * feat: display timezone in timepicker input * chore: fix the typo * fix: don't focus on time picker when timezone is clicked * fix: fix the issue of timezone breaking for browser and utc timezones * fix: display the timezone in timepicker hint 'You are at' * feat: timezone basic functionality (#6492) * chore: change div to fragment + change type to any as the ESLint complains otherwise * chore: manage etc timezone filtering with an arg * chore: update timezone wrapper class name * fix: add timezone support to downloaded logs * feat: add current timezone to dashboard list and configure metadata modal * fix: add pencil icon next to timezone hint + change the copy to Current timezone * fix: properly handle the escape button behavior for timezone picker * chore: replace @vvo/tzdb with native Intl API for timezones * feat: lightmode for timezone picker and timezone adaptation components * fix: use normald tz in browser timezone * fix: timezone picker lightmode fixes * feat: display selected time range in 12 hour format * chore: remove unnecessary optional chaining * fix: fix the typo in css variable * chore: add em dash and change icon for timezone hint in date/time picker * chore: move pen line icon to the right of timezone offset * fix: fix the failing tests * feat: handle switching off the timezone adaptation
211 lines
5.6 KiB
TypeScript
211 lines
5.6 KiB
TypeScript
import './styles.scss';
|
|
|
|
import { Button, Divider, Space, Typography } from 'antd';
|
|
import logEvent from 'api/common/logEvent';
|
|
import getNextPrevId from 'api/errors/getNextPrevId';
|
|
import Editor from 'components/Editor';
|
|
import { ResizeTable } from 'components/ResizeTable';
|
|
import { getNanoSeconds } from 'container/AllError/utils';
|
|
import { useNotifications } from 'hooks/useNotifications';
|
|
import createQueryParams from 'lib/createQueryParams';
|
|
import history from 'lib/history';
|
|
import { isUndefined } from 'lodash-es';
|
|
import { urlKey } from 'pages/ErrorDetails/utils';
|
|
import { useTimezone } from 'providers/Timezone';
|
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useQuery } from 'react-query';
|
|
import { useLocation } from 'react-router-dom';
|
|
import { PayloadProps as GetByErrorTypeAndServicePayload } from 'types/api/errors/getByErrorTypeAndService';
|
|
|
|
import { keyToExclude } from './config';
|
|
import { DashedContainer, EditorContainer, EventContainer } from './styles';
|
|
|
|
function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
|
const { idPayload } = props;
|
|
const { t } = useTranslation(['errorDetails', 'common']);
|
|
const { search, pathname } = useLocation();
|
|
|
|
const params = useMemo(() => new URLSearchParams(search), [search]);
|
|
|
|
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),
|
|
}),
|
|
},
|
|
);
|
|
|
|
const errorDetail = idPayload;
|
|
|
|
const [stackTraceValue] = useState(errorDetail.exceptionStacktrace);
|
|
|
|
const columns = useMemo(
|
|
() => [
|
|
{
|
|
title: 'Key',
|
|
width: 100,
|
|
dataIndex: 'key',
|
|
key: 'key',
|
|
},
|
|
{
|
|
title: 'Value',
|
|
dataIndex: 'value',
|
|
width: 100,
|
|
key: 'value',
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
|
|
const { notifications } = useNotifications();
|
|
|
|
const onClickErrorIdHandler = async (
|
|
id: string,
|
|
timestamp: string,
|
|
): Promise<void> => {
|
|
try {
|
|
if (id.length === 0) {
|
|
notifications.error({
|
|
message: 'Error Id cannot be empty',
|
|
});
|
|
return;
|
|
}
|
|
|
|
const queryParams = {
|
|
groupId: idPayload.groupID,
|
|
timestamp: getNanoSeconds(timestamp),
|
|
errorId: id,
|
|
};
|
|
|
|
history.replace(`${pathname}?${createQueryParams(queryParams)}`);
|
|
} catch (error) {
|
|
notifications.error({
|
|
message: t('something_went_wrong'),
|
|
});
|
|
}
|
|
};
|
|
|
|
const data: { key: string; value: string }[] = Object.keys(errorDetail)
|
|
.filter((e) => !keyToExclude.includes(e))
|
|
.map((key) => ({
|
|
key,
|
|
value: errorDetail[key as keyof GetByErrorTypeAndServicePayload],
|
|
}));
|
|
|
|
const onClickTraceHandler = (): void => {
|
|
logEvent('Exception: Navigate to trace detail page', {
|
|
groupId: errorDetail?.groupID,
|
|
spanId: errorDetail.spanID,
|
|
traceId: errorDetail.traceID,
|
|
exceptionId: errorDetail?.errorId,
|
|
});
|
|
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
|
|
};
|
|
|
|
const logEventCalledRef = useRef(false);
|
|
useEffect(() => {
|
|
if (!logEventCalledRef.current && !isUndefined(data)) {
|
|
logEvent('Exception: Detail page visited', {
|
|
groupId: errorDetail?.groupID,
|
|
spanId: errorDetail.spanID,
|
|
traceId: errorDetail.traceID,
|
|
exceptionId: errorDetail?.errorId,
|
|
});
|
|
logEventCalledRef.current = true;
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [data]);
|
|
|
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
|
|
|
return (
|
|
<>
|
|
<Typography>{errorDetail.exceptionType}</Typography>
|
|
<Typography>{errorDetail.exceptionMessage}</Typography>
|
|
<Divider />
|
|
|
|
<EventContainer>
|
|
<div>
|
|
<Typography>Event {errorDetail.errorId}</Typography>
|
|
<Typography>
|
|
{formatTimezoneAdjustedTimestamp(
|
|
errorDetail.timestamp,
|
|
'DD/MM/YYYY hh:mm:ss A (UTC Z)',
|
|
)}
|
|
</Typography>
|
|
</div>
|
|
<div>
|
|
<Space align="end" direction="horizontal">
|
|
<Button
|
|
loading={nextPrevStatus === 'loading'}
|
|
disabled={nextPrevData?.payload?.prevErrorID.length === 0}
|
|
onClick={(): Promise<void> =>
|
|
onClickErrorIdHandler(
|
|
nextPrevData?.payload?.prevErrorID || '',
|
|
nextPrevData?.payload?.prevTimestamp || '',
|
|
)
|
|
}
|
|
>
|
|
{t('older')}
|
|
</Button>
|
|
<Button
|
|
loading={nextPrevStatus === 'loading'}
|
|
disabled={nextPrevData?.payload?.nextErrorID.length === 0}
|
|
onClick={(): Promise<void> =>
|
|
onClickErrorIdHandler(
|
|
nextPrevData?.payload?.nextErrorID || '',
|
|
nextPrevData?.payload?.nextTimestamp || '',
|
|
)
|
|
}
|
|
>
|
|
{t('newer')}
|
|
</Button>
|
|
</Space>
|
|
</div>
|
|
</EventContainer>
|
|
|
|
<DashedContainer>
|
|
<Typography>{t('see_trace_graph')}</Typography>
|
|
<Button onClick={onClickTraceHandler} type="primary">
|
|
{t('see_error_in_trace_graph')}
|
|
</Button>
|
|
</DashedContainer>
|
|
|
|
<Typography.Title level={4}>{t('stack_trace')}</Typography.Title>
|
|
<div className="error-container">
|
|
<Editor value={stackTraceValue} readOnly />
|
|
</div>
|
|
|
|
<EditorContainer>
|
|
<Space direction="vertical">
|
|
<ResizeTable columns={columns} tableLayout="fixed" dataSource={data} />
|
|
</Space>
|
|
</EditorContainer>
|
|
</>
|
|
);
|
|
}
|
|
|
|
interface ErrorDetailsProps {
|
|
idPayload: GetByErrorTypeAndServicePayload;
|
|
}
|
|
|
|
export default ErrorDetails;
|