mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 15:36:48 +00:00
feat: handle active log flow (#8946)
* feat: handle active log flow * feat: show live logs in logs explorer view * feat: enable live logs in logs explorer * feat: show live time option only in logs list view * chore: pass showLiveLogs as false in test cases * fix: handle live logs data format to open in log details * fix: use current query state for frequency chart in live logs view * fix: encode filter expression, show live option only in list view
This commit is contained in:
parent
f3569a9a02
commit
c0a9948146
@ -174,6 +174,31 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.time-input-prefix {
|
||||
.live-dot-icon {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--bg-forest-500);
|
||||
animation: ripple 1s infinite;
|
||||
|
||||
margin-right: 4px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ripple {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.4);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 6px rgba(245, 158, 11, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.time-input-suffix-icon-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -59,7 +59,9 @@ interface CustomTimePickerProps {
|
||||
customDateTimeVisible?: boolean;
|
||||
setCustomDTPickerVisible?: Dispatch<SetStateAction<boolean>>;
|
||||
onCustomDateHandler?: (dateTimeRange: DateTimeRangeType) => void;
|
||||
handleGoLive?: () => void;
|
||||
showLiveLogs?: boolean;
|
||||
onGoLive?: () => void;
|
||||
onExitLiveLogs?: () => void;
|
||||
}
|
||||
|
||||
function CustomTimePicker({
|
||||
@ -76,7 +78,9 @@ function CustomTimePicker({
|
||||
customDateTimeVisible,
|
||||
setCustomDTPickerVisible,
|
||||
onCustomDateHandler,
|
||||
handleGoLive,
|
||||
onGoLive,
|
||||
onExitLiveLogs,
|
||||
showLiveLogs,
|
||||
}: CustomTimePickerProps): JSX.Element {
|
||||
const [
|
||||
selectedTimePlaceholderValue,
|
||||
@ -165,9 +169,13 @@ function CustomTimePicker({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
|
||||
setSelectedTimePlaceholderValue(value);
|
||||
}, [selectedTime, selectedValue]);
|
||||
if (showLiveLogs) {
|
||||
setSelectedTimePlaceholderValue('Live');
|
||||
} else {
|
||||
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
|
||||
setSelectedTimePlaceholderValue(value);
|
||||
}
|
||||
}, [selectedTime, selectedValue, showLiveLogs]);
|
||||
|
||||
const hide = (): void => {
|
||||
setOpen(false);
|
||||
@ -338,6 +346,28 @@ function CustomTimePicker({
|
||||
return '';
|
||||
};
|
||||
|
||||
const getInputPrefix = (): JSX.Element => {
|
||||
if (showLiveLogs) {
|
||||
return (
|
||||
<div className="time-input-prefix">
|
||||
<div className="live-dot-icon" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="time-input-prefix">
|
||||
{inputValue && inputStatus === 'success' ? (
|
||||
<CheckCircle size={14} color="#51E7A8" />
|
||||
) : (
|
||||
<Tooltip title="Enter time in format (e.g., 1m, 2h, 3d, 4w)">
|
||||
<Clock size={14} className="cursor-pointer" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="custom-time-picker">
|
||||
<Tooltip title={getTooltipTitle()} placement="top">
|
||||
@ -357,7 +387,8 @@ function CustomTimePicker({
|
||||
setCustomDTPickerVisible={defaultTo(setCustomDTPickerVisible, noop)}
|
||||
onCustomDateHandler={defaultTo(onCustomDateHandler, noop)}
|
||||
onSelectHandler={handleSelect}
|
||||
handleGoLive={defaultTo(handleGoLive, noop)}
|
||||
onGoLive={defaultTo(onGoLive, noop)}
|
||||
onExitLiveLogs={defaultTo(onExitLiveLogs, noop)}
|
||||
options={items}
|
||||
selectedTime={selectedTime}
|
||||
activeView={activeView}
|
||||
@ -392,17 +423,7 @@ function CustomTimePicker({
|
||||
onBlur={handleBlur}
|
||||
onChange={handleInputChange}
|
||||
data-1p-ignore
|
||||
prefix={
|
||||
<div className="time-input-prefix">
|
||||
{inputValue && inputStatus === 'success' ? (
|
||||
<CheckCircle size={14} color="#51E7A8" />
|
||||
) : (
|
||||
<Tooltip title="Enter time in format (e.g., 1m, 2h, 3d, 4w)">
|
||||
<Clock size={14} className="cursor-pointer" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
prefix={getInputPrefix()}
|
||||
suffix={
|
||||
<div className="time-input-suffix">
|
||||
{!!isTimezoneOverridden && activeTimezoneOffset && (
|
||||
@ -439,6 +460,8 @@ CustomTimePicker.defaultProps = {
|
||||
customDateTimeVisible: false,
|
||||
setCustomDTPickerVisible: noop,
|
||||
onCustomDateHandler: noop,
|
||||
handleGoLive: noop,
|
||||
onGoLive: noop,
|
||||
onCustomTimeStatusUpdate: noop,
|
||||
onExitLiveLogs: noop,
|
||||
showLiveLogs: false,
|
||||
};
|
||||
|
||||
@ -6,6 +6,7 @@ import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import DatePickerV2 from 'components/DatePickerV2/DatePickerV2';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
||||
import {
|
||||
@ -16,7 +17,14 @@ import {
|
||||
import dayjs from 'dayjs';
|
||||
import { Clock, PenLine } from 'lucide-react';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { getCustomTimeRanges } from 'utils/customTimeRangeUtils';
|
||||
|
||||
@ -32,12 +40,13 @@ interface CustomTimePickerPopoverContentProps {
|
||||
lexicalContext?: LexicalContext,
|
||||
) => void;
|
||||
onSelectHandler: (label: string, value: string) => void;
|
||||
handleGoLive: () => void;
|
||||
onGoLive: () => void;
|
||||
selectedTime: string;
|
||||
activeView: 'datetime' | 'timezone';
|
||||
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
|
||||
isOpenedFromFooter: boolean;
|
||||
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
|
||||
onExitLiveLogs: () => void;
|
||||
}
|
||||
|
||||
interface RecentlyUsedDateTimeRange {
|
||||
@ -56,12 +65,13 @@ function CustomTimePickerPopoverContent({
|
||||
setCustomDTPickerVisible,
|
||||
onCustomDateHandler,
|
||||
onSelectHandler,
|
||||
handleGoLive,
|
||||
onGoLive,
|
||||
selectedTime,
|
||||
activeView,
|
||||
setActiveView,
|
||||
isOpenedFromFooter,
|
||||
setIsOpenedFromFooter,
|
||||
onExitLiveLogs,
|
||||
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
@ -69,6 +79,19 @@ function CustomTimePickerPopoverContent({
|
||||
pathname,
|
||||
]);
|
||||
|
||||
const url = new URLSearchParams(window.location.search);
|
||||
|
||||
let panelTypeFromURL = url.get(QueryParams.panelTypes);
|
||||
|
||||
try {
|
||||
panelTypeFromURL = JSON.parse(panelTypeFromURL as string);
|
||||
} catch {
|
||||
// fallback → leave as-is
|
||||
}
|
||||
|
||||
const isLogsListView =
|
||||
panelTypeFromURL !== 'table' && panelTypeFromURL !== 'graph'; // we do not select list view in the url
|
||||
|
||||
const { timezone } = useTimezone();
|
||||
const activeTimezoneOffset = timezone.offset;
|
||||
|
||||
@ -76,6 +99,12 @@ function CustomTimePickerPopoverContent({
|
||||
RecentlyUsedDateTimeRange[]
|
||||
>([]);
|
||||
|
||||
const handleExitLiveLogs = useCallback((): void => {
|
||||
if (isLogsExplorerPage) {
|
||||
onExitLiveLogs();
|
||||
}
|
||||
}, [isLogsExplorerPage, onExitLiveLogs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!customDateTimeVisible) {
|
||||
const customTimeRanges = getCustomTimeRanges();
|
||||
@ -107,6 +136,7 @@ function CustomTimePickerPopoverContent({
|
||||
className="time-btns"
|
||||
key={option.label + option.value}
|
||||
onClick={(): void => {
|
||||
handleExitLiveLogs();
|
||||
onSelectHandler(option.label, option.value);
|
||||
}}
|
||||
>
|
||||
@ -140,12 +170,17 @@ function CustomTimePickerPopoverContent({
|
||||
);
|
||||
}
|
||||
|
||||
const handleGoLive = (): void => {
|
||||
onGoLive();
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="date-time-popover">
|
||||
{!customDateTimeVisible && (
|
||||
<div className="date-time-options">
|
||||
{isLogsExplorerPage && (
|
||||
{isLogsExplorerPage && isLogsListView && (
|
||||
<Button className="data-time-live" type="text" onClick={handleGoLive}>
|
||||
Live
|
||||
</Button>
|
||||
@ -155,6 +190,7 @@ function CustomTimePickerPopoverContent({
|
||||
type="text"
|
||||
key={option.label + option.value}
|
||||
onClick={(): void => {
|
||||
handleExitLiveLogs();
|
||||
onSelectHandler(option.label, option.value);
|
||||
}}
|
||||
className={cx(
|
||||
@ -169,7 +205,6 @@ function CustomTimePickerPopoverContent({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cx(
|
||||
'relative-date-time',
|
||||
@ -199,12 +234,14 @@ function CustomTimePickerPopoverContent({
|
||||
tabIndex={0}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleExitLiveLogs();
|
||||
onCustomDateHandler([dayjs(range.from), dayjs(range.to)]);
|
||||
setIsOpen(false);
|
||||
}
|
||||
}}
|
||||
key={range.value}
|
||||
onClick={(): void => {
|
||||
handleExitLiveLogs();
|
||||
onCustomDateHandler([dayjs(range.from), dayjs(range.to)]);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
|
||||
@ -13,7 +13,7 @@ import { useCallback } from 'react';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { SpinnerWrapper, Wrapper } from './styles';
|
||||
import { SpinnerWrapper } from './styles';
|
||||
|
||||
function ListViewPanel(): JSX.Element {
|
||||
const { config } = useOptionsMenu({
|
||||
@ -42,7 +42,7 @@ function ListViewPanel(): JSX.Element {
|
||||
}, [config]);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<div className="live-logs-settings-panel">
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
style={defaultSelectStyle}
|
||||
@ -68,7 +68,7 @@ function ListViewPanel(): JSX.Element {
|
||||
<Spinner style={{ height: 'auto' }} />
|
||||
</SpinnerWrapper>
|
||||
)}
|
||||
</Wrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
.live-logs-chart-container {
|
||||
height: 200px;
|
||||
min-height: 200px;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.live-logs-settings-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--bg-ink-300);
|
||||
|
||||
.live-logs-frequency-chart-view-controller {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.live-logs-settings-panel {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
@ -1,46 +1,89 @@
|
||||
import { Col } from 'antd';
|
||||
import Spinner from 'components/Spinner';
|
||||
import './LiveLogsContainer.styles.scss';
|
||||
|
||||
import { Button, Switch, Typography } from 'antd';
|
||||
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
||||
import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import GoToTop from 'container/GoToTop';
|
||||
import FiltersInput from 'container/LiveLogs/FiltersInput';
|
||||
import LiveLogsTopNav from 'container/LiveLogsTopNav';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useClickOutside from 'hooks/useClickOutside';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { useEventSourceEvent } from 'hooks/useEventSourceEvent';
|
||||
import { prepareQueryRangePayload } from 'lib/dashboard/prepareQueryRangePayload';
|
||||
import { Sliders } from 'lucide-react';
|
||||
import { useEventSource } from 'providers/EventSource';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
import { validateQuery } from 'utils/queryValidationUtils';
|
||||
|
||||
import { idObject } from '../constants';
|
||||
import ListViewPanel from '../ListViewPanel';
|
||||
import LiveLogsList from '../LiveLogsList';
|
||||
import { ILiveLogsLog } from '../LiveLogsList/types';
|
||||
import LiveLogsListChart from '../LiveLogsListChart';
|
||||
import { QueryHistoryState } from '../types';
|
||||
import { prepareQueryByFilter } from '../utils';
|
||||
import { ContentWrapper, LiveLogsChart, Wrapper } from './styles';
|
||||
|
||||
function LiveLogsContainer(): JSX.Element {
|
||||
const location = useLocation();
|
||||
const [logs, setLogs] = useState<ILog[]>([]);
|
||||
const [logs, setLogs] = useState<ILiveLogsLog[]>([]);
|
||||
const { currentQuery, stagedQuery } = useQueryBuilder();
|
||||
const [showLiveLogsFrequencyChart, setShowLiveLogsFrequencyChart] = useState(
|
||||
true,
|
||||
);
|
||||
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
const listQuery = useMemo(() => {
|
||||
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
||||
|
||||
return stagedQuery.builder.queryData.find((item) => !item.disabled) || null;
|
||||
}, [stagedQuery]);
|
||||
|
||||
const queryLocationState = location.state as QueryHistoryState;
|
||||
|
||||
const batchedEventsRef = useRef<ILog[]>([]);
|
||||
const batchedEventsRef = useRef<ILiveLogsLog[]>([]);
|
||||
|
||||
const { selectedTime: globalSelectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
const [showFormatMenuItems, setShowFormatMenuItems] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const prevFilterExpressionRef = useRef<string | null>(null);
|
||||
|
||||
const { options, config } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
|
||||
});
|
||||
|
||||
const formatItems = [
|
||||
{
|
||||
key: 'raw',
|
||||
label: 'Raw',
|
||||
data: {
|
||||
title: 'max lines per row',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'list',
|
||||
label: 'Default',
|
||||
},
|
||||
{
|
||||
key: 'table',
|
||||
label: 'Column',
|
||||
data: {
|
||||
title: 'columns',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const handleToggleShowFormatOptions = (): void =>
|
||||
setShowFormatMenuItems(!showFormatMenuItems);
|
||||
|
||||
useClickOutside({
|
||||
ref: menuRef,
|
||||
onClickOutside: () => {
|
||||
if (showFormatMenuItems) {
|
||||
setShowFormatMenuItems(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
handleStartOpenConnection,
|
||||
@ -53,7 +96,7 @@ function LiveLogsContainer(): JSX.Element {
|
||||
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
|
||||
const updateLogs = useCallback((newLogs: ILog[]) => {
|
||||
const updateLogs = useCallback((newLogs: ILiveLogsLog[]) => {
|
||||
setLogs((prevState) =>
|
||||
[...newLogs, ...prevState].slice(0, MAX_LOGS_LIST_SIZE),
|
||||
);
|
||||
@ -67,7 +110,7 @@ function LiveLogsContainer(): JSX.Element {
|
||||
}, 500);
|
||||
|
||||
const batchLiveLog = useCallback(
|
||||
(log: ILog): void => {
|
||||
(log: ILiveLogsLog): void => {
|
||||
batchedEventsRef.current.push(log);
|
||||
|
||||
debouncedUpdateLogs();
|
||||
@ -77,7 +120,7 @@ function LiveLogsContainer(): JSX.Element {
|
||||
|
||||
const handleGetLiveLogs = useCallback(
|
||||
(event: MessageEvent<string>) => {
|
||||
const data: ILog = JSON.parse(event.data);
|
||||
const data: ILiveLogsLog = JSON.parse(event?.data);
|
||||
|
||||
batchLiveLog(data);
|
||||
},
|
||||
@ -91,72 +134,65 @@ function LiveLogsContainer(): JSX.Element {
|
||||
useEventSourceEvent('message', handleGetLiveLogs);
|
||||
useEventSourceEvent('error', handleError);
|
||||
|
||||
const getPreparedQuery = useCallback(
|
||||
(query: Query): Query => {
|
||||
const firstLogId: string | null = logs.length ? logs[0].id : null;
|
||||
|
||||
const preparedQuery: Query = prepareQueryByFilter(
|
||||
query,
|
||||
idObject,
|
||||
firstLogId,
|
||||
);
|
||||
|
||||
return preparedQuery;
|
||||
},
|
||||
[logs],
|
||||
);
|
||||
|
||||
const openConnection = useCallback(
|
||||
(query: Query) => {
|
||||
const { queryPayload } = prepareQueryRangePayload({
|
||||
query,
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
});
|
||||
|
||||
const encodedQueryPayload = encodeURIComponent(JSON.stringify(queryPayload));
|
||||
const queryString = `q=${encodedQueryPayload}`;
|
||||
|
||||
handleStartOpenConnection({ queryString });
|
||||
(filterExpression?: string | null) => {
|
||||
handleStartOpenConnection(filterExpression || '');
|
||||
},
|
||||
[globalSelectedTime, handleStartOpenConnection],
|
||||
[handleStartOpenConnection],
|
||||
);
|
||||
|
||||
const handleStartNewConnection = useCallback(
|
||||
(query: Query) => {
|
||||
(filterExpression?: string | null) => {
|
||||
handleCloseConnection();
|
||||
|
||||
const preparedQuery = getPreparedQuery(query);
|
||||
|
||||
openConnection(preparedQuery);
|
||||
openConnection(filterExpression);
|
||||
},
|
||||
[getPreparedQuery, handleCloseConnection, openConnection],
|
||||
[handleCloseConnection, openConnection],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!compositeQuery) return;
|
||||
const currentFilterExpression =
|
||||
currentQuery?.builder.queryData[0]?.filter?.expression?.trim() || '';
|
||||
|
||||
// Check if filterExpression has actually changed
|
||||
if (
|
||||
(initialLoading && !isConnectionLoading) ||
|
||||
compositeQuery.id !== stagedQuery?.id
|
||||
!prevFilterExpressionRef.current ||
|
||||
prevFilterExpressionRef.current !== currentFilterExpression
|
||||
) {
|
||||
handleStartNewConnection(compositeQuery);
|
||||
const validationResult = validateQuery(currentFilterExpression || '');
|
||||
|
||||
if (validationResult.isValid) {
|
||||
setLogs([]);
|
||||
batchedEventsRef.current = [];
|
||||
handleStartNewConnection(currentFilterExpression);
|
||||
}
|
||||
|
||||
prevFilterExpressionRef.current = currentFilterExpression || null;
|
||||
}
|
||||
}, [
|
||||
compositeQuery,
|
||||
initialLoading,
|
||||
stagedQuery,
|
||||
isConnectionLoading,
|
||||
openConnection,
|
||||
handleStartNewConnection,
|
||||
]);
|
||||
}, [currentQuery, handleStartNewConnection]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialLoading && !isConnectionLoading) {
|
||||
const currentFilterExpression =
|
||||
currentQuery?.builder.queryData[0]?.filter?.expression?.trim() || '';
|
||||
|
||||
const validationResult = validateQuery(currentFilterExpression || '');
|
||||
|
||||
if (validationResult.isValid) {
|
||||
handleStartNewConnection(currentFilterExpression);
|
||||
prevFilterExpressionRef.current = currentFilterExpression || null;
|
||||
} else {
|
||||
handleStartNewConnection(null);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialLoading, isConnectionLoading, handleStartNewConnection]);
|
||||
|
||||
useEffect((): (() => void) | undefined => {
|
||||
if (isConnectionError && reconnectDueToError && compositeQuery) {
|
||||
if (isConnectionError && reconnectDueToError) {
|
||||
// Small delay to prevent immediate reconnection attempts
|
||||
const reconnectTimer = setTimeout(() => {
|
||||
handleStartNewConnection(compositeQuery);
|
||||
handleStartNewConnection();
|
||||
}, 1000);
|
||||
|
||||
return (): void => clearTimeout(reconnectTimer);
|
||||
@ -169,50 +205,70 @@ function LiveLogsContainer(): JSX.Element {
|
||||
handleStartNewConnection,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const prefetchedList = queryLocationState?.listQueryPayload[0]?.list;
|
||||
// clean up the connection when the component unmounts
|
||||
useEffect(
|
||||
() => (): void => {
|
||||
handleCloseConnection();
|
||||
},
|
||||
[handleCloseConnection],
|
||||
);
|
||||
|
||||
if (prefetchedList) {
|
||||
const prefetchedLogs: ILog[] = prefetchedList
|
||||
.map((item) => ({
|
||||
...item.data,
|
||||
timestamp: item.timestamp,
|
||||
}))
|
||||
.reverse();
|
||||
|
||||
updateLogs(prefetchedLogs);
|
||||
}
|
||||
}, [queryLocationState, updateLogs]);
|
||||
const handleToggleFrequencyChart = useCallback(() => {
|
||||
setShowLiveLogsFrequencyChart(!showLiveLogsFrequencyChart);
|
||||
}, [showLiveLogsFrequencyChart]);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<LiveLogsTopNav />
|
||||
<ContentWrapper gutter={[0, 20]} style={{ color: themeColors.lightWhite }}>
|
||||
<Col span={24}>
|
||||
<FiltersInput />
|
||||
</Col>
|
||||
{initialLoading && logs.length === 0 ? (
|
||||
<Col span={24}>
|
||||
<Spinner style={{ height: 'auto' }} tip="Fetching Logs" />
|
||||
</Col>
|
||||
) : (
|
||||
<>
|
||||
<Col span={24}>
|
||||
<LiveLogsChart
|
||||
initialData={queryLocationState?.graphQueryPayload || null}
|
||||
<div className="live-logs-container">
|
||||
<div className="live-logs-content">
|
||||
<div className="live-logs-settings-panel">
|
||||
<div className="live-logs-frequency-chart-view-controller">
|
||||
<Typography>Frequency chart</Typography>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={showLiveLogsFrequencyChart}
|
||||
defaultChecked
|
||||
onChange={handleToggleFrequencyChart}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="format-options-container" ref={menuRef}>
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={handleToggleShowFormatOptions}
|
||||
icon={<Sliders size={14} />}
|
||||
/>
|
||||
|
||||
{showFormatMenuItems && (
|
||||
<LogsFormatOptionsMenu
|
||||
title="FORMAT"
|
||||
items={formatItems}
|
||||
selectedOptionFormat={options.format}
|
||||
config={config}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<ListViewPanel />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<LiveLogsList logs={logs} />
|
||||
</Col>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showLiveLogsFrequencyChart && (
|
||||
<div className="live-logs-chart-container">
|
||||
<LiveLogsListChart
|
||||
initialData={queryLocationState?.graphQueryPayload || null}
|
||||
className="live-logs-chart"
|
||||
isShowingLiveLogs
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<GoToTop />
|
||||
</ContentWrapper>
|
||||
</Wrapper>
|
||||
|
||||
<div className="live-logs-list-container">
|
||||
<LiveLogsList
|
||||
logs={logs}
|
||||
isLoading={initialLoading && logs.length === 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GoToTop />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
.live-logs-container {
|
||||
.live-logs-content {
|
||||
.live-logs-chart-container {
|
||||
padding: 0px 8px;
|
||||
|
||||
.logs-frequency-chart {
|
||||
.ant-card-body {
|
||||
height: 140px;
|
||||
min-height: 140px;
|
||||
padding: 0 16px 22px 16px;
|
||||
font-family: 'Geist Mono';
|
||||
}
|
||||
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.live-logs-list {
|
||||
.live-logs-list-loading {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: var(--text-vanilla-100);
|
||||
}
|
||||
|
||||
.live-logs-list-loading {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.loading-live-logs-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
|
||||
.loading-gif {
|
||||
height: 72px;
|
||||
margin-left: -24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.live-logs-list-loading {
|
||||
.loading-live-logs-content {
|
||||
.ant-typography {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,24 +1,23 @@
|
||||
import './LiveLogsList.styles.scss';
|
||||
|
||||
import { Card, Typography } from 'antd';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import ListLogView from 'components/Logs/ListLogView';
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { CARD_BODY_STYLE } from 'constants/card';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { OptionFormatTypes } from 'constants/optionsFormatTypes';
|
||||
import InfinityTableView from 'container/LogsExplorerList/InfinityTableView';
|
||||
import { InfinityWrapperStyled } from 'container/LogsExplorerList/styles';
|
||||
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
|
||||
import { Heading } from 'container/LogsTable/styles';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useEventSource } from 'providers/EventSource';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||
// interfaces
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
@ -26,11 +25,9 @@ import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
|
||||
import { LiveLogsListProps } from './types';
|
||||
|
||||
function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
|
||||
const ref = useRef<VirtuosoHandle>(null);
|
||||
|
||||
const { t } = useTranslation(['logs']);
|
||||
|
||||
const { isConnectionLoading } = useEventSource();
|
||||
|
||||
const { activeLogId } = useCopyLogLink();
|
||||
@ -43,6 +40,12 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
onSetActiveLog,
|
||||
} = useActiveLog();
|
||||
|
||||
// get only data from the logs object
|
||||
const formattedLogs: ILog[] = useMemo(
|
||||
() => logs.map((log) => log?.data).flat(),
|
||||
[logs],
|
||||
);
|
||||
|
||||
const { options } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: DataSource.LOGS,
|
||||
@ -50,8 +53,8 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
});
|
||||
|
||||
const activeLogIndex = useMemo(
|
||||
() => logs.findIndex(({ id }) => id === activeLogId),
|
||||
[logs, activeLogId],
|
||||
() => formattedLogs.findIndex(({ id }) => id === activeLogId),
|
||||
[formattedLogs, activeLogId],
|
||||
);
|
||||
|
||||
const selectedFields = convertKeysToColumnFields([
|
||||
@ -105,30 +108,39 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
});
|
||||
}, [activeLogId, activeLogIndex]);
|
||||
|
||||
const isLoadingList = isConnectionLoading && logs.length === 0;
|
||||
const isLoadingList = isConnectionLoading && formattedLogs.length === 0;
|
||||
|
||||
if (isLoadingList) {
|
||||
return <Spinner style={{ height: 'auto' }} tip="Fetching Logs" />;
|
||||
}
|
||||
const renderLoading = useCallback(
|
||||
() => (
|
||||
<div className="live-logs-list-loading">
|
||||
<div className="loading-live-logs-content">
|
||||
<img
|
||||
className="loading-gif"
|
||||
src="/Icons/loading-plane.gif"
|
||||
alt="wait-icon"
|
||||
/>
|
||||
|
||||
<Typography>Fetching live logs...</Typography>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{options.format !== OptionFormatTypes.TABLE && (
|
||||
<Heading>
|
||||
<Typography.Text>Event</Typography.Text>
|
||||
</Heading>
|
||||
)}
|
||||
<div className="live-logs-list">
|
||||
{(formattedLogs.length === 0 || isLoading || isLoadingList) &&
|
||||
renderLoading()}
|
||||
|
||||
{logs.length === 0 && <Typography>{t('fetching_log_lines')}</Typography>}
|
||||
|
||||
{logs.length !== 0 && (
|
||||
{formattedLogs.length !== 0 && (
|
||||
<InfinityWrapperStyled>
|
||||
{options.format === OptionFormatTypes.TABLE ? (
|
||||
<InfinityTableView
|
||||
ref={ref}
|
||||
isLoading={false}
|
||||
tableViewProps={{
|
||||
logs,
|
||||
logs: formattedLogs,
|
||||
fields: selectedFields,
|
||||
linesPerRow: options.maxLines,
|
||||
fontSize: options.fontSize,
|
||||
@ -142,8 +154,8 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
<Virtuoso
|
||||
ref={ref}
|
||||
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
||||
data={logs}
|
||||
totalCount={logs.length}
|
||||
data={formattedLogs}
|
||||
totalCount={formattedLogs.length}
|
||||
itemContent={getItemContent}
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
@ -151,15 +163,18 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
)}
|
||||
</InfinityWrapperStyled>
|
||||
)}
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
log={activeLog}
|
||||
onClose={onClearActiveLog}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
onClickActionItem={onAddToQuery}
|
||||
/>
|
||||
</>
|
||||
|
||||
{activeLog && (
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
log={activeLog}
|
||||
onClose={onClearActiveLog}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onGroupByAttribute={onGroupByAttribute}
|
||||
onClickActionItem={onAddToQuery}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
export interface ILiveLogsLog {
|
||||
data: ILog[];
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export type LiveLogsListProps = {
|
||||
logs: ILog[];
|
||||
logs: ILiveLogsLog[];
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
@ -9,24 +9,33 @@ import { useMemo } from 'react';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
import { DataSource, LogsAggregatorOperator } from 'types/common/queryBuilder';
|
||||
import { validateQuery } from 'utils/queryValidationUtils';
|
||||
|
||||
import { LiveLogsListChartProps } from './types';
|
||||
|
||||
function LiveLogsListChart({
|
||||
className,
|
||||
initialData,
|
||||
isShowingLiveLogs = false,
|
||||
}: LiveLogsListChartProps): JSX.Element {
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { isConnectionOpen } = useEventSource();
|
||||
|
||||
const listChartQuery: Query | null = useMemo(() => {
|
||||
if (!stagedQuery) return null;
|
||||
if (!currentQuery) return null;
|
||||
|
||||
const currentFilterExpression =
|
||||
currentQuery?.builder.queryData[0]?.filter?.expression?.trim() || '';
|
||||
|
||||
const validationResult = validateQuery(currentFilterExpression || '');
|
||||
|
||||
if (!validationResult.isValid) return null;
|
||||
|
||||
return {
|
||||
...stagedQuery,
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...stagedQuery.builder,
|
||||
queryData: stagedQuery.builder.queryData.map((item) => ({
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
disabled: false,
|
||||
aggregateOperator: LogsAggregatorOperator.COUNT,
|
||||
@ -39,7 +48,7 @@ function LiveLogsListChart({
|
||||
})),
|
||||
},
|
||||
};
|
||||
}, [stagedQuery]);
|
||||
}, [currentQuery]);
|
||||
|
||||
const { data, isFetching } = useGetExplorerQueryRange(
|
||||
listChartQuery,
|
||||
@ -62,12 +71,15 @@ function LiveLogsListChart({
|
||||
}, [data, initialData]);
|
||||
|
||||
return (
|
||||
<LogsExplorerChart
|
||||
isLoading={initialData ? false : isFetching}
|
||||
data={chartData}
|
||||
isLabelEnabled={false}
|
||||
className={className}
|
||||
/>
|
||||
<div className="live-logs-chart-container">
|
||||
<LogsExplorerChart
|
||||
isLoading={initialData ? false : isFetching}
|
||||
data={chartData}
|
||||
isLabelEnabled={false}
|
||||
className={className}
|
||||
isLogsExplorerViews={isShowingLiveLogs}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -3,4 +3,5 @@ import { QueryData } from 'types/api/widgets/getQuery';
|
||||
export type LiveLogsListChartProps = {
|
||||
className?: string;
|
||||
initialData: QueryData[] | null;
|
||||
isShowingLiveLogs: boolean;
|
||||
};
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
import { PauseCircleFilled, PlayCircleFilled } from '@ant-design/icons';
|
||||
import { Button } from 'antd';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useEventSource } from 'providers/EventSource';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { validateQuery } from 'utils/queryValidationUtils';
|
||||
|
||||
function LiveLogsPauseResume(): JSX.Element {
|
||||
const {
|
||||
isConnectionOpen,
|
||||
isConnectionLoading,
|
||||
initialLoading,
|
||||
handleCloseConnection,
|
||||
handleStartOpenConnection,
|
||||
handleSetInitialLoading,
|
||||
} = useEventSource();
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const isPlaying = isConnectionOpen || isConnectionLoading || initialLoading;
|
||||
|
||||
const openConnection = useCallback(
|
||||
(filterExpression?: string | null) => {
|
||||
handleStartOpenConnection(filterExpression || '');
|
||||
},
|
||||
[handleStartOpenConnection],
|
||||
);
|
||||
|
||||
const handleStartNewConnection = useCallback(
|
||||
(filterExpression?: string | null) => {
|
||||
handleCloseConnection();
|
||||
|
||||
openConnection(filterExpression);
|
||||
},
|
||||
[handleCloseConnection, openConnection],
|
||||
);
|
||||
|
||||
const onLiveButtonClick = useCallback(() => {
|
||||
if (initialLoading) {
|
||||
handleSetInitialLoading(false);
|
||||
}
|
||||
|
||||
if ((!isConnectionOpen && isConnectionLoading) || isConnectionOpen) {
|
||||
handleCloseConnection();
|
||||
} else {
|
||||
const currentFilterExpression =
|
||||
currentQuery?.builder.queryData[0]?.filter?.expression?.trim() || '';
|
||||
|
||||
const validationResult = validateQuery(currentFilterExpression || '');
|
||||
|
||||
if (validationResult.isValid) {
|
||||
handleStartNewConnection(currentFilterExpression);
|
||||
} else {
|
||||
handleStartNewConnection(null);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
initialLoading,
|
||||
isConnectionOpen,
|
||||
isConnectionLoading,
|
||||
currentQuery,
|
||||
handleSetInitialLoading,
|
||||
handleCloseConnection,
|
||||
handleStartNewConnection,
|
||||
]);
|
||||
|
||||
// clean up the connection when the component unmounts
|
||||
useEffect(
|
||||
() => (): void => {
|
||||
handleCloseConnection();
|
||||
},
|
||||
[handleCloseConnection],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="live-logs-pause-resume">
|
||||
<Button
|
||||
icon={isPlaying ? <PauseCircleFilled /> : <PlayCircleFilled />}
|
||||
danger={isPlaying}
|
||||
onClick={onLiveButtonClick}
|
||||
type="primary"
|
||||
className={`periscope-btn ${isPlaying ? 'warning' : 'success'}`}
|
||||
>
|
||||
{isPlaying ? 'Pause' : 'Resume'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LiveLogsPauseResume;
|
||||
@ -6,4 +6,5 @@ export type LogsExplorerChartProps = {
|
||||
isLogsExplorerViews?: boolean;
|
||||
isLabelEnabled?: boolean;
|
||||
className?: string;
|
||||
isShowingLiveLogs?: boolean;
|
||||
};
|
||||
|
||||
@ -25,6 +25,7 @@ function LogsExplorerChart({
|
||||
isLabelEnabled = true,
|
||||
className,
|
||||
isLogsExplorerViews = false,
|
||||
isShowingLiveLogs = false,
|
||||
}: LogsExplorerChartProps): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const urlQuery = useUrlQuery();
|
||||
@ -55,6 +56,11 @@ function LogsExplorerChart({
|
||||
|
||||
const onDragSelect = useCallback(
|
||||
(start: number, end: number): void => {
|
||||
// Do not allow dragging on live logs chart
|
||||
if (isShowingLiveLogs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startTimestamp = Math.trunc(start);
|
||||
const endTimestamp = Math.trunc(end);
|
||||
|
||||
@ -75,7 +81,7 @@ function LogsExplorerChart({
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
},
|
||||
[dispatch, location.pathname, safeNavigate, urlQuery],
|
||||
[dispatch, location.pathname, safeNavigate, urlQuery, isShowingLiveLogs],
|
||||
);
|
||||
|
||||
const graphData = useMemo(
|
||||
|
||||
@ -17,7 +17,7 @@ import { getDraggedColumns } from 'hooks/useDragColumns/utils';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { isEmpty, isEqual } from 'lodash-es';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
interface ColumnViewProps {
|
||||
@ -51,6 +51,8 @@ function ColumnView({
|
||||
onGroupByAttribute: handleGroupByAttribute,
|
||||
} = useActiveLog();
|
||||
|
||||
const [showActiveLog, setShowActiveLog] = useState<boolean>(false);
|
||||
|
||||
const { queryData: activeLogId } = useUrlQueryData<string | null>(
|
||||
QueryParams.activeLogId,
|
||||
null,
|
||||
@ -72,9 +74,10 @@ function ColumnView({
|
||||
|
||||
if (log) {
|
||||
handleSetActiveLog(log);
|
||||
setShowActiveLog(true);
|
||||
}
|
||||
}
|
||||
}, [activeLogId, logs, handleSetActiveLog]);
|
||||
}, []);
|
||||
|
||||
const tableViewProps = {
|
||||
logs,
|
||||
@ -88,7 +91,6 @@ function ColumnView({
|
||||
const { dataSource, columns } = useTableView({
|
||||
...tableViewProps,
|
||||
onClickExpand: handleSetActiveLog,
|
||||
onOpenLogsContext: handleClearActiveLog,
|
||||
});
|
||||
|
||||
const { draggedColumns, onColumnOrderChange } = useDragColumns<
|
||||
@ -222,9 +224,22 @@ function ColumnView({
|
||||
const handleRowClick = (row: Row<Record<string, unknown>>): void => {
|
||||
const currentLog = logs.find(({ id }) => id === row.original.id);
|
||||
|
||||
setShowActiveLog(true);
|
||||
handleSetActiveLog(currentLog as ILog);
|
||||
};
|
||||
|
||||
const removeQueryParam = (key: string): void => {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete(key);
|
||||
window.history.replaceState({}, '', url);
|
||||
};
|
||||
|
||||
const handleLogDetailClose = (): void => {
|
||||
removeQueryParam(QueryParams.activeLogId);
|
||||
handleClearActiveLog();
|
||||
setShowActiveLog(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`logs-list-table-view-container ${
|
||||
@ -246,11 +261,11 @@ function ColumnView({
|
||||
scrollToIndexRef={scrollToIndexRef}
|
||||
/>
|
||||
|
||||
{activeLog && (
|
||||
{showActiveLog && activeLog && (
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
log={activeLog}
|
||||
onClose={handleClearActiveLog}
|
||||
onClose={handleLogDetailClose}
|
||||
onAddToQuery={handleAddToQuery}
|
||||
onClickActionItem={handleAddToQuery}
|
||||
onGroupByAttribute={handleGroupByAttribute}
|
||||
|
||||
@ -141,6 +141,7 @@ describe('LogsExplorerList - empty states', () => {
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
setWarning={(): void => {}}
|
||||
showLiveLogs={false}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</QueryBuilderContext.Provider>,
|
||||
@ -205,6 +206,7 @@ describe('LogsExplorerList - empty states', () => {
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
setWarning={(): void => {}}
|
||||
showLiveLogs={false}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</QueryBuilderContext.Provider>,
|
||||
|
||||
@ -0,0 +1,174 @@
|
||||
import { Button, Switch, Typography } from 'antd';
|
||||
import { WsDataEvent } from 'api/common/getQueryStats';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
||||
import ListViewOrderBy from 'components/OrderBy/ListViewOrderBy';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import Download from 'container/DownloadV2/DownloadV2';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import useClickOutside from 'hooks/useClickOutside';
|
||||
import { ArrowUp10, Minus, Sliders } from 'lucide-react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
|
||||
import QueryStatus from './QueryStatus';
|
||||
|
||||
function LogsActionsContainer({
|
||||
listQuery,
|
||||
queryStats,
|
||||
selectedPanelType,
|
||||
showFrequencyChart,
|
||||
handleToggleFrequencyChart,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
flattenLogData,
|
||||
isFetching,
|
||||
isLoading,
|
||||
isError,
|
||||
isSuccess,
|
||||
}: {
|
||||
listQuery: any;
|
||||
selectedPanelType: PANEL_TYPES;
|
||||
showFrequencyChart: boolean;
|
||||
handleToggleFrequencyChart: () => void;
|
||||
orderBy: string;
|
||||
setOrderBy: (value: string) => void;
|
||||
flattenLogData: any;
|
||||
isFetching: boolean;
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
isSuccess: boolean;
|
||||
queryStats: WsDataEvent | undefined;
|
||||
}): JSX.Element {
|
||||
const [showFormatMenuItems, setShowFormatMenuItems] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { options, config } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
|
||||
});
|
||||
|
||||
const formatItems = [
|
||||
{
|
||||
key: 'raw',
|
||||
label: 'Raw',
|
||||
data: {
|
||||
title: 'max lines per row',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'list',
|
||||
label: 'Default',
|
||||
},
|
||||
{
|
||||
key: 'table',
|
||||
label: 'Column',
|
||||
data: {
|
||||
title: 'columns',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const handleToggleShowFormatOptions = (): void =>
|
||||
setShowFormatMenuItems(!showFormatMenuItems);
|
||||
|
||||
useClickOutside({
|
||||
ref: menuRef,
|
||||
onClickOutside: () => {
|
||||
if (showFormatMenuItems) {
|
||||
setShowFormatMenuItems(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="logs-actions-container">
|
||||
<div className="tab-options">
|
||||
<div className="tab-options-left">
|
||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||
<div className="frequency-chart-view-controller">
|
||||
<Typography>Frequency chart</Typography>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={showFrequencyChart}
|
||||
defaultChecked
|
||||
onChange={handleToggleFrequencyChart}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="tab-options-right">
|
||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||
<>
|
||||
<div className="order-by-container">
|
||||
<div className="order-by-label">
|
||||
Order by <Minus size={14} /> <ArrowUp10 size={14} />
|
||||
</div>
|
||||
|
||||
<ListViewOrderBy
|
||||
value={orderBy}
|
||||
onChange={(value): void => setOrderBy(value)}
|
||||
dataSource={DataSource.LOGS}
|
||||
/>
|
||||
</div>
|
||||
<Download
|
||||
data={flattenLogData}
|
||||
isLoading={isFetching}
|
||||
fileName="log_data"
|
||||
/>
|
||||
<div className="format-options-container" ref={menuRef}>
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={handleToggleShowFormatOptions}
|
||||
icon={<Sliders size={14} />}
|
||||
data-testid="periscope-btn"
|
||||
/>
|
||||
|
||||
{showFormatMenuItems && (
|
||||
<LogsFormatOptionsMenu
|
||||
title="FORMAT"
|
||||
items={formatItems}
|
||||
selectedOptionFormat={options.format}
|
||||
config={config}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(selectedPanelType === PANEL_TYPES.TIME_SERIES ||
|
||||
selectedPanelType === PANEL_TYPES.TABLE) && (
|
||||
<div className="query-stats">
|
||||
<QueryStatus
|
||||
loading={isLoading || isFetching}
|
||||
error={isError}
|
||||
success={isSuccess}
|
||||
/>
|
||||
|
||||
{queryStats?.read_rows && (
|
||||
<Typography.Text className="rows">
|
||||
{getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '}
|
||||
rows
|
||||
</Typography.Text>
|
||||
)}
|
||||
|
||||
{queryStats?.elapsed_ms && (
|
||||
<>
|
||||
<div className="divider" />
|
||||
<Typography.Text className="time">
|
||||
{getYAxisFormattedValue(queryStats?.elapsed_ms?.toString(), 'ms')}
|
||||
</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogsActionsContainer;
|
||||
@ -1,14 +1,10 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import './LogsExplorerViews.styles.scss';
|
||||
|
||||
import { Button, Switch, Typography } from 'antd';
|
||||
import getFromLocalstorage from 'api/browser/localstorage/get';
|
||||
import setToLocalstorage from 'api/browser/localstorage/set';
|
||||
import { getQueryStats, WsDataEvent } from 'api/common/getQueryStats';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
||||
import ListViewOrderBy from 'components/OrderBy/ListViewOrderBy';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
@ -22,20 +18,18 @@ import {
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
|
||||
import Download from 'container/DownloadV2/DownloadV2';
|
||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||
import GoToTop from 'container/GoToTop';
|
||||
import {} from 'container/LiveLogs/constants';
|
||||
import LogsExplorerChart from 'container/LogsExplorerChart';
|
||||
import LogsExplorerList from 'container/LogsExplorerList';
|
||||
import LogsExplorerTable from 'container/LogsExplorerTable';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useClickOutside from 'hooks/useClickOutside';
|
||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
@ -49,7 +43,7 @@ import {
|
||||
omit,
|
||||
set,
|
||||
} from 'lodash-es';
|
||||
import { ArrowUp10, Minus, Sliders } from 'lucide-react';
|
||||
import LiveLogs from 'pages/LiveLogs';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import {
|
||||
@ -77,16 +71,12 @@ import {
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
import {
|
||||
DataSource,
|
||||
LogsAggregatorOperator,
|
||||
StringOperators,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { DataSource, LogsAggregatorOperator } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import QueryStatus from './QueryStatus';
|
||||
import LogsActionsContainer from './LogsActionsContainer';
|
||||
|
||||
function LogsExplorerViewsContainer({
|
||||
selectedView,
|
||||
@ -94,6 +84,7 @@ function LogsExplorerViewsContainer({
|
||||
listQueryKeyRef,
|
||||
chartQueryKeyRef,
|
||||
setWarning,
|
||||
showLiveLogs,
|
||||
}: {
|
||||
selectedView: ExplorerViews;
|
||||
setIsLoadingQueries: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
@ -102,6 +93,7 @@ function LogsExplorerViewsContainer({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
chartQueryKeyRef: MutableRefObject<any>;
|
||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||
showLiveLogs: boolean;
|
||||
}): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const dispatch = useDispatch();
|
||||
@ -149,7 +141,6 @@ function LogsExplorerViewsContainer({
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [logs, setLogs] = useState<ILog[]>([]);
|
||||
const [requestData, setRequestData] = useState<Query | null>(null);
|
||||
const [showFormatMenuItems, setShowFormatMenuItems] = useState(false);
|
||||
const [queryId, setQueryId] = useState<string>(v4());
|
||||
const [queryStats, setQueryStats] = useState<WsDataEvent>();
|
||||
const [listChartQuery, setListChartQuery] = useState<Query | null>(null);
|
||||
@ -162,12 +153,6 @@ function LogsExplorerViewsContainer({
|
||||
return stagedQuery.builder.queryData.find((item) => !item.disabled) || null;
|
||||
}, [stagedQuery]);
|
||||
|
||||
const { options, config } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
|
||||
});
|
||||
|
||||
const isMultipleQueries = useMemo(
|
||||
() =>
|
||||
currentQuery?.builder?.queryData?.length > 1 ||
|
||||
@ -603,41 +588,6 @@ function LogsExplorerViewsContainer({
|
||||
return isGroupByExist ? data.payload.data.result : firstPayloadQueryArray;
|
||||
}, [stagedQuery, panelType, data, listChartData, listQuery]);
|
||||
|
||||
const formatItems = [
|
||||
{
|
||||
key: 'raw',
|
||||
label: 'Raw',
|
||||
data: {
|
||||
title: 'max lines per row',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'list',
|
||||
label: 'Default',
|
||||
},
|
||||
{
|
||||
key: 'table',
|
||||
label: 'Column',
|
||||
data: {
|
||||
title: 'columns',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const handleToggleShowFormatOptions = (): void =>
|
||||
setShowFormatMenuItems(!showFormatMenuItems);
|
||||
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useClickOutside({
|
||||
ref: menuRef,
|
||||
onClickOutside: () => {
|
||||
if (showFormatMenuItems) {
|
||||
setShowFormatMenuItems(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isLoading ||
|
||||
@ -695,104 +645,40 @@ function LogsExplorerViewsContainer({
|
||||
return (
|
||||
<div className="logs-explorer-views-container">
|
||||
<div className="logs-explorer-views-types">
|
||||
<div className="logs-actions-container">
|
||||
<div className="tab-options">
|
||||
<div className="tab-options-left">
|
||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||
<div className="frequency-chart-view-controller">
|
||||
<Typography>Frequency chart</Typography>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={showFrequencyChart}
|
||||
defaultChecked
|
||||
onChange={handleToggleFrequencyChart}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="tab-options-right">
|
||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||
<>
|
||||
<div className="order-by-container">
|
||||
<div className="order-by-label">
|
||||
Order by <Minus size={14} /> <ArrowUp10 size={14} />
|
||||
</div>
|
||||
|
||||
<ListViewOrderBy
|
||||
value={orderBy}
|
||||
onChange={(value): void => setOrderBy(value)}
|
||||
dataSource={DataSource.LOGS}
|
||||
/>
|
||||
</div>
|
||||
<Download
|
||||
data={flattenLogData}
|
||||
isLoading={isFetching}
|
||||
fileName="log_data"
|
||||
/>
|
||||
<div className="format-options-container" ref={menuRef}>
|
||||
<Button
|
||||
className="periscope-btn ghost"
|
||||
onClick={handleToggleShowFormatOptions}
|
||||
icon={<Sliders size={14} />}
|
||||
data-testid="periscope-btn"
|
||||
/>
|
||||
|
||||
{showFormatMenuItems && (
|
||||
<LogsFormatOptionsMenu
|
||||
title="FORMAT"
|
||||
items={formatItems}
|
||||
selectedOptionFormat={options.format}
|
||||
config={config}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(selectedPanelType === PANEL_TYPES.TIME_SERIES ||
|
||||
selectedPanelType === PANEL_TYPES.TABLE) && (
|
||||
<div className="query-stats">
|
||||
<QueryStatus
|
||||
loading={isLoading || isFetching}
|
||||
error={isError}
|
||||
success={isSuccess}
|
||||
/>
|
||||
|
||||
{queryStats?.read_rows && (
|
||||
<Typography.Text className="rows">
|
||||
{getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '}
|
||||
rows
|
||||
</Typography.Text>
|
||||
)}
|
||||
|
||||
{queryStats?.elapsed_ms && (
|
||||
<>
|
||||
<div className="divider" />
|
||||
<Typography.Text className="time">
|
||||
{getYAxisFormattedValue(queryStats?.elapsed_ms?.toString(), 'ms')}
|
||||
</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedPanelType === PANEL_TYPES.LIST && showFrequencyChart && (
|
||||
<div className="logs-frequency-chart-container">
|
||||
<LogsExplorerChart
|
||||
className="logs-frequency-chart"
|
||||
isLoading={isFetchingListChartData || isLoadingListChartData}
|
||||
data={chartData}
|
||||
isLogsExplorerViews={panelType === PANEL_TYPES.LIST}
|
||||
/>
|
||||
</div>
|
||||
{!showLiveLogs && (
|
||||
<LogsActionsContainer
|
||||
listQuery={listQuery}
|
||||
queryStats={queryStats}
|
||||
selectedPanelType={selectedPanelType}
|
||||
showFrequencyChart={showFrequencyChart}
|
||||
handleToggleFrequencyChart={handleToggleFrequencyChart}
|
||||
orderBy={orderBy}
|
||||
setOrderBy={setOrderBy}
|
||||
flattenLogData={flattenLogData}
|
||||
isFetching={isFetching}
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
isSuccess={isSuccess}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedPanelType === PANEL_TYPES.LIST &&
|
||||
showFrequencyChart &&
|
||||
!showLiveLogs && (
|
||||
<div className="logs-frequency-chart-container">
|
||||
<LogsExplorerChart
|
||||
className="logs-frequency-chart"
|
||||
isLoading={isFetchingListChartData || isLoadingListChartData}
|
||||
data={chartData}
|
||||
isLogsExplorerViews={panelType === PANEL_TYPES.LIST}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="logs-explorer-views-type-content">
|
||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||
{showLiveLogs && <LiveLogs />}
|
||||
|
||||
{selectedPanelType === PANEL_TYPES.LIST && !showLiveLogs && (
|
||||
<LogsExplorerList
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
@ -805,7 +691,8 @@ function LogsExplorerViewsContainer({
|
||||
isFilterApplied={!isEmpty(listQuery?.filters?.items)}
|
||||
/>
|
||||
)}
|
||||
{selectedPanelType === PANEL_TYPES.TIME_SERIES && (
|
||||
|
||||
{selectedPanelType === PANEL_TYPES.TIME_SERIES && !showLiveLogs && (
|
||||
<TimeSeriesView
|
||||
isLoading={isLoading || isFetching}
|
||||
data={data}
|
||||
@ -817,7 +704,7 @@ function LogsExplorerViewsContainer({
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedPanelType === PANEL_TYPES.TABLE && (
|
||||
{selectedPanelType === PANEL_TYPES.TABLE && !showLiveLogs && (
|
||||
<LogsExplorerTable
|
||||
data={
|
||||
(data?.payload?.data?.newResult?.data?.result ||
|
||||
|
||||
@ -174,6 +174,7 @@ const renderer = (): RenderResult =>
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
setWarning={(): void => {}}
|
||||
showLiveLogs={false}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</VirtuosoMockContext.Provider>,
|
||||
@ -235,6 +236,7 @@ describe('LogsExplorerViews -', () => {
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
setWarning={(): void => {}}
|
||||
showLiveLogs={false}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</QueryBuilderContext.Provider>,
|
||||
|
||||
@ -20,7 +20,6 @@ const activeTab = 'active-tab';
|
||||
export default function LeftToolbarActions({
|
||||
items,
|
||||
selectedView,
|
||||
|
||||
onChangeSelectedView,
|
||||
showFilter,
|
||||
handleFilterVisibilityChange,
|
||||
|
||||
@ -12,6 +12,7 @@ interface RightToolbarActionsProps {
|
||||
isLoadingQueries?: boolean;
|
||||
listQueryKeyRef?: MutableRefObject<any>;
|
||||
chartQueryKeyRef?: MutableRefObject<any>;
|
||||
showLiveLogs?: boolean;
|
||||
}
|
||||
|
||||
export default function RightToolbarActions({
|
||||
@ -19,19 +20,25 @@ export default function RightToolbarActions({
|
||||
isLoadingQueries,
|
||||
listQueryKeyRef,
|
||||
chartQueryKeyRef,
|
||||
showLiveLogs,
|
||||
}: RightToolbarActionsProps): JSX.Element {
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (showLiveLogs) return;
|
||||
|
||||
registerShortcut(LogsExplorerShortcuts.StageAndRunQuery, onStageRunQuery);
|
||||
|
||||
return (): void => {
|
||||
deregisterShortcut(LogsExplorerShortcuts.StageAndRunQuery);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [onStageRunQuery]);
|
||||
}, [onStageRunQuery, showLiveLogs]);
|
||||
|
||||
if (showLiveLogs) return <div />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isLoadingQueries ? (
|
||||
@ -71,4 +78,5 @@ RightToolbarActions.defaultProps = {
|
||||
isLoadingQueries: false,
|
||||
listQueryKeyRef: null,
|
||||
chartQueryKeyRef: null,
|
||||
showLiveLogs: false,
|
||||
};
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
.timeRange {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import './Toolbar.styles.scss';
|
||||
|
||||
import ROUTES from 'constants/routes';
|
||||
import LiveLogsPauseResume from 'container/LiveLogs/LiveLogsPauseResume/LiveLogsPauseResume';
|
||||
import NewExplorerCTA from 'container/NewExplorerCTA';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { noop } from 'lodash-es';
|
||||
import { useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
@ -12,6 +14,9 @@ interface ToolbarProps {
|
||||
rightActions?: JSX.Element;
|
||||
showOldCTA?: boolean;
|
||||
warningElement?: JSX.Element;
|
||||
onGoLive?: () => void;
|
||||
onExitLiveLogs?: () => void;
|
||||
showLiveLogs?: boolean;
|
||||
}
|
||||
|
||||
export default function Toolbar({
|
||||
@ -20,6 +25,9 @@ export default function Toolbar({
|
||||
rightActions,
|
||||
showOldCTA,
|
||||
warningElement,
|
||||
showLiveLogs,
|
||||
onGoLive,
|
||||
onExitLiveLogs,
|
||||
}: ToolbarProps): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
@ -39,7 +47,11 @@ export default function Toolbar({
|
||||
<div className="timeRange">
|
||||
{warningElement}
|
||||
{showOldCTA && <NewExplorerCTA />}
|
||||
{showLiveLogs && <LiveLogsPauseResume />}
|
||||
<DateTimeSelectionV2
|
||||
showLiveLogs={showLiveLogs}
|
||||
onExitLiveLogs={onExitLiveLogs}
|
||||
onGoLive={onGoLive}
|
||||
showAutoRefresh={showAutoRefresh}
|
||||
showRefreshText={!isLogsExplorerPage && !isApiMonitoringPage}
|
||||
hideShareModal
|
||||
@ -57,4 +69,7 @@ Toolbar.defaultProps = {
|
||||
rightActions: <div />,
|
||||
showOldCTA: false,
|
||||
warningElement: <div />,
|
||||
showLiveLogs: false,
|
||||
onGoLive: (): void => noop(),
|
||||
onExitLiveLogs: (): void => {},
|
||||
};
|
||||
|
||||
@ -9,17 +9,7 @@ import CustomTimePicker from 'components/CustomTimePicker/CustomTimePicker';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
constructCompositeQuery,
|
||||
defaultLiveQueryDataConfig,
|
||||
} from 'container/LiveLogs/constants';
|
||||
import { QueryHistoryState } from 'container/LiveLogs/types';
|
||||
import NewExplorerCTA from 'container/NewExplorerCTA';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
@ -31,7 +21,6 @@ import { cloneDeep, isObject } from 'lodash-es';
|
||||
import { Check, Copy, Info, Send, Undo } from 'lucide-react';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import { useNavigationType, useSearchParams } from 'react-router-dom-v5-compat';
|
||||
@ -41,8 +30,6 @@ import { ThunkDispatch } from 'redux-thunk';
|
||||
import { GlobalTimeLoading, UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { normalizeTimeToMs } from 'utils/timeUtils';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@ -78,6 +65,9 @@ function DateTimeSelection({
|
||||
modalSelectedInterval,
|
||||
modalInitialStartTime,
|
||||
modalInitialEndTime,
|
||||
onGoLive,
|
||||
onExitLiveLogs,
|
||||
showLiveLogs,
|
||||
}: Props): JSX.Element {
|
||||
const [formSelector] = Form.useForm();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
@ -91,7 +81,6 @@ function DateTimeSelection({
|
||||
const searchStartTime = urlQuery.get('startTime');
|
||||
const searchEndTime = urlQuery.get('endTime');
|
||||
const relativeTimeFromUrl = urlQuery.get(QueryParams.relativeTime);
|
||||
const queryClient = useQueryClient();
|
||||
const [enableAbsoluteTime, setEnableAbsoluteTime] = useState(false);
|
||||
const [isValidteRelativeTime, setIsValidteRelativeTime] = useState(false);
|
||||
const [, handleCopyToClipboard] = useCopyToClipboard();
|
||||
@ -188,54 +177,7 @@ function DateTimeSelection({
|
||||
false,
|
||||
);
|
||||
|
||||
const {
|
||||
stagedQuery,
|
||||
currentQuery,
|
||||
initQueryBuilderData,
|
||||
panelType,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const handleGoLive = useCallback(() => {
|
||||
if (!stagedQuery) return;
|
||||
|
||||
setIsOpen(false);
|
||||
let queryHistoryState: QueryHistoryState | null = null;
|
||||
|
||||
const compositeQuery = constructCompositeQuery({
|
||||
query: stagedQuery,
|
||||
initialQueryData: initialQueryBuilderFormValuesMap.logs,
|
||||
customQueryData: defaultLiveQueryDataConfig,
|
||||
});
|
||||
|
||||
const isListView =
|
||||
panelType === PANEL_TYPES.LIST && stagedQuery.builder.queryData[0];
|
||||
|
||||
if (isListView) {
|
||||
const [graphQuery, listQuery] = queryClient.getQueriesData<
|
||||
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
|
||||
>({
|
||||
queryKey: REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
active: true,
|
||||
});
|
||||
|
||||
queryHistoryState = {
|
||||
graphQueryPayload:
|
||||
graphQuery && graphQuery[1]
|
||||
? graphQuery[1].payload?.data.result || []
|
||||
: [],
|
||||
listQueryPayload:
|
||||
listQuery && listQuery[1]
|
||||
? listQuery[1].payload?.data?.newResult?.data?.result || []
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
const JSONCompositeQuery = encodeURIComponent(JSON.stringify(compositeQuery));
|
||||
|
||||
const path = `${ROUTES.LIVE_LOGS}?${QueryParams.compositeQuery}=${JSONCompositeQuery}`;
|
||||
|
||||
safeNavigate(path, { state: queryHistoryState });
|
||||
}, [panelType, queryClient, safeNavigate, stagedQuery]);
|
||||
const { stagedQuery, currentQuery, initQueryBuilderData } = useQueryBuilder();
|
||||
|
||||
const { maxTime, minTime, selectedTime } = useSelector<
|
||||
AppState,
|
||||
@ -803,8 +745,12 @@ function DateTimeSelection({
|
||||
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
const getSelectedValue = (): string =>
|
||||
getInputLabel(
|
||||
const getSelectedValue = (): string => {
|
||||
if (showLiveLogs) {
|
||||
return 'live';
|
||||
}
|
||||
|
||||
return getInputLabel(
|
||||
dayjs(isModalTimeSelection ? modalStartTime : minTime / 1000000).tz(
|
||||
timezone.value,
|
||||
),
|
||||
@ -813,6 +759,7 @@ function DateTimeSelection({
|
||||
),
|
||||
isModalTimeSelection ? modalSelectedInterval : selectedTime,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="date-time-selector">
|
||||
@ -873,11 +820,13 @@ function DateTimeSelection({
|
||||
selectedValue={getSelectedValue()}
|
||||
data-testid="dropDown"
|
||||
items={options}
|
||||
showLiveLogs={showLiveLogs}
|
||||
newPopover
|
||||
handleGoLive={handleGoLive}
|
||||
onGoLive={onGoLive}
|
||||
onCustomDateHandler={onCustomDateHandler}
|
||||
customDateTimeVisible={customDateTimeVisible}
|
||||
setCustomDTPickerVisible={setCustomDTPickerVisible}
|
||||
onExitLiveLogs={onExitLiveLogs}
|
||||
/>
|
||||
|
||||
{showAutoRefresh && selectedTime !== 'custom' && (
|
||||
@ -933,6 +882,9 @@ interface DateTimeSelectionV2Props {
|
||||
modalSelectedInterval?: Time;
|
||||
modalInitialStartTime?: number;
|
||||
modalInitialEndTime?: number;
|
||||
showLiveLogs?: boolean;
|
||||
onGoLive?: () => void;
|
||||
onExitLiveLogs?: () => void;
|
||||
}
|
||||
|
||||
DateTimeSelection.defaultProps = {
|
||||
@ -946,6 +898,9 @@ DateTimeSelection.defaultProps = {
|
||||
modalSelectedInterval: RelativeTimeMap['5m'] as Time,
|
||||
modalInitialStartTime: undefined,
|
||||
modalInitialEndTime: undefined,
|
||||
onGoLive: (): void => {},
|
||||
onExitLiveLogs: (): void => {},
|
||||
showLiveLogs: false,
|
||||
};
|
||||
interface DispatchProps {
|
||||
updateTimeInterval: (
|
||||
|
||||
25
frontend/src/pages/LiveLogs/LiveLogs.tsx
Normal file
25
frontend/src/pages/LiveLogs/LiveLogs.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { liveLogsCompositeQuery } from 'container/LiveLogs/constants';
|
||||
import LiveLogsContainer from 'container/LiveLogs/LiveLogsContainer';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { useEffect } from 'react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
function LiveLogs(): JSX.Element {
|
||||
useShareBuilderUrl({ defaultValue: liveLogsCompositeQuery });
|
||||
const { handleSetConfig } = useQueryBuilder();
|
||||
|
||||
useEffect(() => {
|
||||
handleSetConfig(PANEL_TYPES.LIST, DataSource.LOGS);
|
||||
}, [handleSetConfig]);
|
||||
|
||||
return (
|
||||
<PreferenceContextProvider>
|
||||
<LiveLogsContainer />
|
||||
</PreferenceContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default LiveLogs;
|
||||
@ -1,28 +1,3 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { liveLogsCompositeQuery } from 'container/LiveLogs/constants';
|
||||
import LiveLogsContainer from 'container/LiveLogs/LiveLogsContainer';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { EventSourceProvider } from 'providers/EventSource';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { useEffect } from 'react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
function LiveLogs(): JSX.Element {
|
||||
useShareBuilderUrl({ defaultValue: liveLogsCompositeQuery });
|
||||
const { handleSetConfig } = useQueryBuilder();
|
||||
|
||||
useEffect(() => {
|
||||
handleSetConfig(PANEL_TYPES.LIST, DataSource.LOGS);
|
||||
}, [handleSetConfig]);
|
||||
|
||||
return (
|
||||
<EventSourceProvider>
|
||||
<PreferenceContextProvider>
|
||||
<LiveLogsContainer />
|
||||
</PreferenceContextProvider>
|
||||
</EventSourceProvider>
|
||||
);
|
||||
}
|
||||
import LiveLogs from './LiveLogs';
|
||||
|
||||
export default LiveLogs;
|
||||
|
||||
@ -30,6 +30,7 @@ import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { isEmpty, isEqual, isNull } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { EventSourceProvider } from 'providers/EventSource';
|
||||
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
@ -45,6 +46,7 @@ import { ExplorerViews } from './utils';
|
||||
|
||||
function LogsExplorer(): JSX.Element {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [showLiveLogs, setShowLiveLogs] = useState<boolean>(false);
|
||||
|
||||
// Get panel type from URL
|
||||
const panelTypesFromUrl = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
@ -144,6 +146,11 @@ function LogsExplorer(): JSX.Element {
|
||||
}
|
||||
|
||||
setSelectedView(view);
|
||||
|
||||
if (view !== ExplorerViews.LIST) {
|
||||
setShowLiveLogs(false);
|
||||
}
|
||||
|
||||
handleExplorerTabChange(
|
||||
view === ExplorerViews.TIMESERIES ? PANEL_TYPES.TIME_SERIES : view,
|
||||
);
|
||||
@ -335,62 +342,79 @@ function LogsExplorer(): JSX.Element {
|
||||
[],
|
||||
);
|
||||
|
||||
const handleShowLiveLogs = useCallback(() => {
|
||||
setShowLiveLogs(true);
|
||||
}, []);
|
||||
|
||||
const handleExitLiveLogs = useCallback(() => {
|
||||
setShowLiveLogs(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className={cx('logs-module-page', showFilters ? 'filter-visible' : '')}>
|
||||
{showFilters && (
|
||||
<section className={cx('log-quick-filter-left-section')}>
|
||||
<QuickFilters
|
||||
className="qf-logs-explorer"
|
||||
signal={SignalType.LOGS}
|
||||
source={QuickFiltersSource.LOGS_EXPLORER}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
<section className={cx('log-module-right-section')}>
|
||||
<Toolbar
|
||||
showAutoRefresh={false}
|
||||
leftActions={
|
||||
<LeftToolbarActions
|
||||
showFilter={showFilters}
|
||||
<EventSourceProvider>
|
||||
<div
|
||||
className={cx('logs-module-page', showFilters ? 'filter-visible' : '')}
|
||||
>
|
||||
{showFilters && (
|
||||
<section className={cx('log-quick-filter-left-section')}>
|
||||
<QuickFilters
|
||||
className="qf-logs-explorer"
|
||||
signal={SignalType.LOGS}
|
||||
source={QuickFiltersSource.LOGS_EXPLORER}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
items={toolbarViews}
|
||||
selectedView={selectedView}
|
||||
onChangeSelectedView={handleChangeSelectedView}
|
||||
/>
|
||||
}
|
||||
warningElement={
|
||||
!isEmpty(warning) ? <WarningPopover warningData={warning} /> : <div />
|
||||
}
|
||||
rightActions={
|
||||
<RightToolbarActions
|
||||
onStageRunQuery={(): void => handleRunQuery()}
|
||||
listQueryKeyRef={listQueryKeyRef}
|
||||
chartQueryKeyRef={chartQueryKeyRef}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
<section className={cx('log-module-right-section')}>
|
||||
<Toolbar
|
||||
showAutoRefresh={false}
|
||||
leftActions={
|
||||
<LeftToolbarActions
|
||||
showFilter={showFilters}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
items={toolbarViews}
|
||||
selectedView={selectedView}
|
||||
onChangeSelectedView={handleChangeSelectedView}
|
||||
/>
|
||||
}
|
||||
warningElement={
|
||||
!isEmpty(warning) ? <WarningPopover warningData={warning} /> : <div />
|
||||
}
|
||||
rightActions={
|
||||
<RightToolbarActions
|
||||
onStageRunQuery={(): void => handleRunQuery()}
|
||||
listQueryKeyRef={listQueryKeyRef}
|
||||
chartQueryKeyRef={chartQueryKeyRef}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
showLiveLogs={showLiveLogs}
|
||||
/>
|
||||
}
|
||||
showLiveLogs={showLiveLogs}
|
||||
onGoLive={handleShowLiveLogs}
|
||||
onExitLiveLogs={handleExitLiveLogs}
|
||||
/>
|
||||
|
||||
<div className="log-explorer-query-container">
|
||||
<div>
|
||||
<ExplorerCard sourcepage={DataSource.LOGS}>
|
||||
<LogExplorerQuerySection selectedView={selectedView} />
|
||||
</ExplorerCard>
|
||||
<div className="log-explorer-query-container">
|
||||
<div>
|
||||
<ExplorerCard sourcepage={DataSource.LOGS}>
|
||||
<LogExplorerQuerySection selectedView={selectedView} />
|
||||
</ExplorerCard>
|
||||
</div>
|
||||
<div className="logs-explorer-views">
|
||||
<LogsExplorerViewsContainer
|
||||
selectedView={selectedView}
|
||||
listQueryKeyRef={listQueryKeyRef}
|
||||
chartQueryKeyRef={chartQueryKeyRef}
|
||||
setIsLoadingQueries={setIsLoadingQueries}
|
||||
setWarning={setWarning}
|
||||
showLiveLogs={showLiveLogs}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="logs-explorer-views">
|
||||
<LogsExplorerViewsContainer
|
||||
selectedView={selectedView}
|
||||
listQueryKeyRef={listQueryKeyRef}
|
||||
chartQueryKeyRef={chartQueryKeyRef}
|
||||
setIsLoadingQueries={setIsLoadingQueries}
|
||||
setWarning={setWarning}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</EventSourceProvider>
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@ -81,6 +81,23 @@
|
||||
border-radius: 2px;
|
||||
border: 1px solid rgba(37, 225, 146, 0.1);
|
||||
background: rgba(37, 225, 146, 0.1) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: var(--bg-amber-500) !important;
|
||||
border-radius: 2px;
|
||||
border: 1px solid rgba(255, 184, 0, 0.1);
|
||||
background: rgba(255, 184, 0, 0.1) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: var(--bg-cherry-500) !important;
|
||||
border-radius: 2px;
|
||||
border: 1px solid rgba(255, 184, 0, 0.1);
|
||||
background: rgba(255, 184, 0, 0.1) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -27,10 +27,7 @@ interface IEventSourceContext {
|
||||
isConnectionError: boolean;
|
||||
initialLoading: boolean;
|
||||
reconnectDueToError: boolean;
|
||||
handleStartOpenConnection: (urlProps: {
|
||||
url?: string;
|
||||
queryString: string;
|
||||
}) => void;
|
||||
handleStartOpenConnection: (filterExpression?: string) => void;
|
||||
handleCloseConnection: () => void;
|
||||
handleSetInitialLoading: (value: boolean) => void;
|
||||
}
|
||||
@ -123,12 +120,10 @@ export function EventSourceProvider({
|
||||
}, [destroyEventSourceSession]);
|
||||
|
||||
const handleStartOpenConnection = useCallback(
|
||||
(urlProps: { url?: string; queryString: string }): void => {
|
||||
const { url, queryString } = urlProps;
|
||||
|
||||
const eventSourceUrl = url
|
||||
? `${url}/?${queryString}`
|
||||
: `${ENVIRONMENT.baseURL}${apiV3}logs/livetail?${queryString}`;
|
||||
(filterExpression?: string): void => {
|
||||
const eventSourceUrl = `${
|
||||
ENVIRONMENT.baseURL
|
||||
}${apiV3}logs/livetail?filter=${encodeURIComponent(filterExpression || '')}`;
|
||||
|
||||
eventSourceRef.current = new EventSourcePolyfill(eventSourceUrl, {
|
||||
headers: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user