mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-29 16:14:42 +00:00
358 lines
9.0 KiB
TypeScript
358 lines
9.0 KiB
TypeScript
/* eslint-disable no-nested-ternary */
|
|
import './NodeEvents.styles.scss';
|
|
|
|
import { Color } from '@signozhq/design-tokens';
|
|
import { Button, Table, TableColumnsType } from 'antd';
|
|
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
|
import { EventContents } from 'container/InfraMonitoringK8s/commonUtils';
|
|
import LoadingContainer from 'container/InfraMonitoringK8s/LoadingContainer';
|
|
import LogsError from 'container/LogsError/LogsError';
|
|
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
|
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
|
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
|
import {
|
|
CustomTimeType,
|
|
Time,
|
|
} from 'container/TopNav/DateTimeSelectionV2/config';
|
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
|
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
|
import { isArray } from 'lodash-es';
|
|
import { ChevronDown, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react';
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import { useQuery } from 'react-query';
|
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
|
import { DataSource } from 'types/common/queryBuilder';
|
|
import { v4 } from 'uuid';
|
|
|
|
import { getNodesEventsQueryPayload } from './constants';
|
|
import NoEventsContainer from './NoEventsContainer';
|
|
|
|
interface EventDataType {
|
|
key: string;
|
|
timestamp: string;
|
|
body: string;
|
|
id: string;
|
|
attributes_bool?: Record<string, boolean>;
|
|
attributes_number?: Record<string, number>;
|
|
attributes_string?: Record<string, string>;
|
|
resources_string?: Record<string, string>;
|
|
scope_name?: string;
|
|
scope_string?: Record<string, string>;
|
|
scope_version?: string;
|
|
severity_number?: number;
|
|
severity_text?: string;
|
|
span_id?: string;
|
|
trace_flags?: number;
|
|
trace_id?: string;
|
|
severity?: string;
|
|
}
|
|
|
|
interface INodeEventsProps {
|
|
timeRange: {
|
|
startTime: number;
|
|
endTime: number;
|
|
};
|
|
handleChangeEventFilters: (filters: IBuilderQuery['filters']) => void;
|
|
filters: IBuilderQuery['filters'];
|
|
isModalTimeSelection: boolean;
|
|
handleTimeChange: (
|
|
interval: Time | CustomTimeType,
|
|
dateTimeRange?: [number, number],
|
|
) => void;
|
|
selectedInterval: Time;
|
|
}
|
|
|
|
const EventsPageSize = 10;
|
|
|
|
export default function Events({
|
|
timeRange,
|
|
handleChangeEventFilters,
|
|
filters,
|
|
isModalTimeSelection,
|
|
handleTimeChange,
|
|
selectedInterval,
|
|
}: INodeEventsProps): JSX.Element {
|
|
const { currentQuery } = useQueryBuilder();
|
|
|
|
const [formattedNodeEvents, setFormattedNodeEvents] = useState<
|
|
EventDataType[]
|
|
>([]);
|
|
|
|
const [hasReachedEndOfEvents, setHasReachedEndOfEvents] = useState(false);
|
|
|
|
const [page, setPage] = useState(1);
|
|
|
|
const updatedCurrentQuery = useMemo(
|
|
() => ({
|
|
...currentQuery,
|
|
builder: {
|
|
...currentQuery.builder,
|
|
queryData: [
|
|
{
|
|
...currentQuery.builder.queryData[0],
|
|
dataSource: DataSource.LOGS,
|
|
aggregateOperator: 'noop',
|
|
aggregateAttribute: {
|
|
...currentQuery.builder.queryData[0].aggregateAttribute,
|
|
},
|
|
filters: {
|
|
items: [],
|
|
op: 'AND',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
[currentQuery],
|
|
);
|
|
|
|
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
|
|
|
const queryPayload = useMemo(() => {
|
|
const basePayload = getNodesEventsQueryPayload(
|
|
timeRange.startTime,
|
|
timeRange.endTime,
|
|
filters,
|
|
);
|
|
|
|
basePayload.query.builder.queryData[0].pageSize = 10;
|
|
basePayload.query.builder.queryData[0].orderBy = [
|
|
{ columnName: 'timestamp', order: ORDERBY_FILTERS.DESC },
|
|
];
|
|
|
|
return basePayload;
|
|
}, [timeRange.startTime, timeRange.endTime, filters]);
|
|
|
|
const { data: eventsData, isLoading, isFetching, isError } = useQuery({
|
|
queryKey: ['nodeEvents', timeRange.startTime, timeRange.endTime, filters],
|
|
queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION),
|
|
enabled: !!queryPayload,
|
|
});
|
|
|
|
const columns: TableColumnsType<EventDataType> = [
|
|
{ title: 'Severity', dataIndex: 'severity', key: 'severity', width: 100 },
|
|
{
|
|
title: 'Timestamp',
|
|
dataIndex: 'timestamp',
|
|
width: 200,
|
|
ellipsis: true,
|
|
key: 'timestamp',
|
|
},
|
|
{ title: 'Body', dataIndex: 'body', key: 'body' },
|
|
];
|
|
|
|
useEffect(() => {
|
|
if (eventsData?.payload?.data?.newResult?.data?.result) {
|
|
const responsePayload =
|
|
eventsData?.payload.data.newResult.data.result[0].list || [];
|
|
|
|
const formattedData = responsePayload?.map(
|
|
(event): EventDataType => ({
|
|
timestamp: event.timestamp,
|
|
severity: event.data.severity_text,
|
|
body: event.data.body,
|
|
id: event.data.id,
|
|
key: event.data.id,
|
|
resources_string: event.data.resources_string,
|
|
}),
|
|
);
|
|
|
|
setFormattedNodeEvents(formattedData);
|
|
|
|
if (
|
|
!responsePayload ||
|
|
(responsePayload &&
|
|
isArray(responsePayload) &&
|
|
responsePayload.length < EventsPageSize)
|
|
) {
|
|
setHasReachedEndOfEvents(true);
|
|
} else {
|
|
setHasReachedEndOfEvents(false);
|
|
}
|
|
}
|
|
}, [eventsData]);
|
|
|
|
const handleExpandRow = (record: EventDataType): JSX.Element => (
|
|
<EventContents data={record.resources_string} />
|
|
);
|
|
|
|
const handlePrev = (): void => {
|
|
if (!formattedNodeEvents.length) return;
|
|
|
|
setPage(page - 1);
|
|
|
|
const firstEvent = formattedNodeEvents[0];
|
|
|
|
const newItems = [
|
|
...filters.items.filter((item) => item.key?.key !== 'id'),
|
|
{
|
|
id: v4(),
|
|
key: {
|
|
key: 'id',
|
|
type: '',
|
|
dataType: DataTypes.String,
|
|
isColumn: true,
|
|
},
|
|
op: '>',
|
|
value: firstEvent.id,
|
|
},
|
|
];
|
|
|
|
const newFilters = {
|
|
op: 'AND',
|
|
items: newItems,
|
|
} as IBuilderQuery['filters'];
|
|
|
|
handleChangeEventFilters(newFilters);
|
|
};
|
|
|
|
const handleNext = (): void => {
|
|
if (!formattedNodeEvents.length) return;
|
|
|
|
setPage(page + 1);
|
|
const lastEvent = formattedNodeEvents[formattedNodeEvents.length - 1];
|
|
|
|
const newItems = [
|
|
...filters.items.filter((item) => item.key?.key !== 'id'),
|
|
{
|
|
id: v4(),
|
|
key: {
|
|
key: 'id',
|
|
type: '',
|
|
dataType: DataTypes.String,
|
|
isColumn: true,
|
|
},
|
|
op: '<',
|
|
value: lastEvent.id,
|
|
},
|
|
];
|
|
|
|
const newFilters = {
|
|
op: 'AND',
|
|
items: newItems,
|
|
} as IBuilderQuery['filters'];
|
|
|
|
handleChangeEventFilters(newFilters);
|
|
};
|
|
|
|
const handleExpandRowIcon = ({
|
|
expanded,
|
|
onExpand,
|
|
record,
|
|
}: {
|
|
expanded: boolean;
|
|
onExpand: (
|
|
record: EventDataType,
|
|
e: React.MouseEvent<HTMLElement, MouseEvent>,
|
|
) => void;
|
|
record: EventDataType;
|
|
}): JSX.Element =>
|
|
expanded ? (
|
|
<ChevronDown
|
|
className="periscope-btn-icon"
|
|
size={14}
|
|
onClick={(e): void =>
|
|
onExpand(
|
|
record,
|
|
(e as unknown) as React.MouseEvent<HTMLElement, MouseEvent>,
|
|
)
|
|
}
|
|
/>
|
|
) : (
|
|
<ChevronRight
|
|
className="periscope-btn-icon"
|
|
size={14}
|
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
|
onClick={(e): void =>
|
|
onExpand(
|
|
record,
|
|
(e as unknown) as React.MouseEvent<HTMLElement, MouseEvent>,
|
|
)
|
|
}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<div className="node-events-container">
|
|
<div className="node-events-header">
|
|
<div className="filter-section">
|
|
{query && (
|
|
<QueryBuilderSearch
|
|
query={query}
|
|
onChange={handleChangeEventFilters}
|
|
disableNavigationShortcuts
|
|
/>
|
|
)}
|
|
</div>
|
|
<div className="datetime-section">
|
|
<DateTimeSelectionV2
|
|
showAutoRefresh={false}
|
|
showRefreshText={false}
|
|
hideShareModal
|
|
isModalTimeSelection={isModalTimeSelection}
|
|
onTimeChange={handleTimeChange}
|
|
defaultRelativeTime="5m"
|
|
modalSelectedInterval={selectedInterval}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{isLoading && <LoadingContainer />}
|
|
|
|
{!isLoading && !isError && formattedNodeEvents.length === 0 && (
|
|
<NoEventsContainer />
|
|
)}
|
|
|
|
{isError && !isLoading && <LogsError />}
|
|
|
|
{!isLoading && !isError && formattedNodeEvents.length > 0 && (
|
|
<div className="node-events-list-container">
|
|
<div className="node-events-list-card">
|
|
<Table<EventDataType>
|
|
loading={isLoading && page > 1}
|
|
columns={columns}
|
|
expandable={{
|
|
expandedRowRender: handleExpandRow,
|
|
rowExpandable: (record): boolean => record.body !== 'Not Expandable',
|
|
expandIcon: handleExpandRowIcon,
|
|
}}
|
|
dataSource={formattedNodeEvents}
|
|
pagination={false}
|
|
rowKey={(record): string => record.id}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{!isError && formattedNodeEvents.length > 0 && (
|
|
<div className="node-events-footer">
|
|
<Button
|
|
className="node-events-footer-button periscope-btn ghost"
|
|
type="link"
|
|
onClick={handlePrev}
|
|
disabled={page === 1 || isFetching || isLoading}
|
|
>
|
|
{!isFetching && <ChevronLeft size={14} />}
|
|
Prev
|
|
</Button>
|
|
|
|
<Button
|
|
className="node-events-footer-button periscope-btn ghost"
|
|
type="link"
|
|
onClick={handleNext}
|
|
disabled={hasReachedEndOfEvents || isFetching || isLoading}
|
|
>
|
|
Next
|
|
{!isFetching && <ChevronRight size={14} />}
|
|
</Button>
|
|
|
|
{(isFetching || isLoading) && (
|
|
<Loader2 className="animate-spin" size={16} color={Color.BG_ROBIN_500} />
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|