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