Compare commits

..

4 Commits

Author SHA1 Message Date
Abhi Kumar
fd19739cac fix: minor changes 2026-02-18 18:25:49 +05:30
Abhi Kumar
17c2c609ce fix: added fix for rendering single point 2026-02-18 18:20:24 +05:30
primus-bot[bot]
04643264ff chore(release): bump to v0.112.0 (#10340)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-02-18 15:50:23 +05:30
Ishan
3aa0d8a7fd feat: Improve logs browsing when the side-drawer is open (#10250)
* feat: logs keyboard handle feature

* feat: raw logs code optimised

* feat: listlogs and table view optimised

* feat: added chevron arrows in log details

* feat: added bg table scrolling

* feat: entity logs bg click bug

* feat: pr comment fixes

* feat: pr optimised

* feat: removed unwanted code

* feat: removed unwanted code
2026-02-18 14:12:00 +05:30
41 changed files with 1157 additions and 833 deletions

View File

@@ -176,7 +176,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.111.0
image: signoz/signoz:v0.112.0
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.111.0
image: signoz/signoz:v0.112.0
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -179,7 +179,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.111.0}
image: signoz/signoz:${VERSION:-v0.112.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -111,7 +111,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.111.0}
image: signoz/signoz:${VERSION:-v0.112.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -1,8 +1,9 @@
/* eslint-disable no-nested-ternary */
import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useQuery } from 'react-query';
import { Virtuoso } from 'react-virtuoso';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { Card } from 'antd';
import LogDetail from 'components/LogDetail';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
@@ -10,6 +11,8 @@ import LogsError from 'container/LogsError/LogsError';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { FontSize } from 'container/OptionsMenu/types';
import { useHandleLogsPagination } from 'hooks/infraMonitoring/useHandleLogsPagination';
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
import useScrollToLog from 'hooks/logs/useScrollToLog';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { ILog } from 'types/api/logs/log';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
@@ -28,6 +31,15 @@ interface Props {
}
function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
const virtuosoRef = useRef<VirtuosoHandle>(null);
const {
activeLog,
onAddToQuery,
selectedTab,
handleSetActiveLog,
handleCloseLogDetail,
} = useLogDetailHandlers();
const basePayload = getHostLogsQueryPayload(
timeRange.startTime,
timeRange.endTime,
@@ -72,29 +84,40 @@ function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
setIsPaginating(false);
}, [data, setIsPaginating]);
const handleScrollToLog = useScrollToLog({
logs,
virtuosoRef,
});
const getItemContent = useCallback(
(_: number, logToRender: ILog): JSX.Element => (
<RawLogView
isTextOverflowEllipsisDisabled
key={logToRender.id}
data={logToRender}
linesPerRow={5}
fontSize={FontSize.MEDIUM}
selectedFields={[
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
]}
/>
),
[],
(_: number, logToRender: ILog): JSX.Element => {
return (
<div key={logToRender.id}>
<RawLogView
isTextOverflowEllipsisDisabled
data={logToRender}
linesPerRow={5}
fontSize={FontSize.MEDIUM}
selectedFields={[
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
]}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
isActiveLog={activeLog?.id === logToRender.id}
/>
</div>
);
},
[activeLog, handleSetActiveLog, handleCloseLogDetail],
);
const renderFooter = useCallback(
@@ -118,6 +141,7 @@ function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
<Virtuoso
className="host-metrics-logs-virtuoso"
key="host-metrics-logs-virtuoso"
ref={virtuosoRef}
data={logs}
endReached={loadMoreLogs}
totalCount={logs.length}
@@ -139,7 +163,24 @@ function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
{!isLoading && !isError && logs.length === 0 && <NoLogsContainer />}
{isError && !isLoading && <LogsError />}
{!isLoading && !isError && logs.length > 0 && (
<div className="host-metrics-logs-list-container">{renderContent}</div>
<div
className="host-metrics-logs-list-container"
data-log-detail-ignore="true"
>
{renderContent}
</div>
)}
{selectedTab && activeLog && (
<LogDetail
log={activeLog}
onClose={handleCloseLogDetail}
logs={logs}
onNavigateLog={handleSetActiveLog}
selectedTab={selectedTab}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onScrollToLog={handleScrollToLog}
/>
)}
</div>
);

View File

@@ -13,6 +13,9 @@ export type LogDetailProps = {
handleChangeSelectedView?: ChangeViewFunctionType;
isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null;
logs?: ILog[];
onNavigateLog?: (log: ILog) => void;
onScrollToLog?: (logId: string) => void;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
Pick<DrawerProps, 'onClose'>;

View File

@@ -15,6 +15,8 @@
}
.log-detail-drawer__title-right {
display: flex;
align-items: center;
.ant-btn {
display: flex;
align-items: center;
@@ -66,6 +68,10 @@
margin-bottom: 16px;
}
.log-detail-drawer__content {
height: 100%;
}
.log-detail-drawer__log {
width: 100%;
display: flex;
@@ -183,9 +189,115 @@
.ant-drawer-close {
padding: 0px;
}
.log-detail-drawer__footer-hint {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 8px 16px;
text-align: left;
color: var(--text-vanilla-200);
background: var(--bg-ink-400);
z-index: 10;
.log-detail-drawer__footer-hint-content {
display: flex;
align-items: center;
gap: 4px;
}
.log-detail-drawer__footer-hint-icon {
display: inline;
vertical-align: middle;
color: var(--text-vanilla-200);
}
.log-detail-drawer__footer-hint-text {
font-size: 13px;
margin: 0;
}
}
.log-arrows {
display: flex;
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1);
border-radius: 6px;
padding: 2px 6px;
align-items: center;
margin-left: 8px;
}
.log-arrow-btn {
padding: 0;
min-width: 28px;
height: 28px;
border-radius: 4px;
background: var(--bg-ink-400);
color: var(--text-vanilla-400);
border: 1px solid var(--bg-ink-300);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease-in-out;
}
.log-arrow-btn-up,
.log-arrow-btn-down {
background: var(--bg-ink-400);
}
.log-arrow-btn:active,
.log-arrow-btn:focus {
background: var(--bg-ink-300);
color: var(--text-vanilla-100);
}
.log-arrow-btn[disabled] {
opacity: 0.5;
cursor: not-allowed;
background: var(--bg-ink-500);
color: var(--text-vanilla-200);
.log-arrow-btn:hover:not([disabled]) {
background: var(--bg-ink-300);
color: var(--text-vanilla-100);
}
}
}
.lightMode {
.log-arrows {
background: var(--bg-vanilla-100);
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.04);
}
.log-arrow-btn {
background: var(--bg-vanilla-100);
color: var(--text-ink-400);
border: 1px solid var(--bg-vanilla-300);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.04);
}
.log-arrow-btn-up,
.log-arrow-btn-down {
background: var(--bg-vanilla-100);
}
.log-arrow-btn:active,
.log-arrow-btn:focus {
background: var(--bg-vanilla-200);
color: var(--text-ink-500);
}
.log-arrow-btn:hover:not([disabled]) {
background: var(--bg-vanilla-200);
color: var(--text-ink-500);
}
.log-arrow-btn[disabled] {
background: var(--bg-vanilla-100);
color: var(--text-ink-200);
}
.ant-drawer-header {
border-bottom: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
@@ -252,4 +364,33 @@
color: var(--text-ink-300);
}
}
.log-detail-drawer__footer-hint {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 8px 16px;
text-align: left;
color: var(--text-vanilla-700);
background: var(--bg-vanilla-100);
z-index: 10;
.log-detail-drawer__footer-hint-content {
display: flex;
align-items: center;
gap: 4px;
}
.log-detail-drawer__footer-hint-icon {
display: inline;
vertical-align: middle;
color: var(--text-vanilla-700);
}
.log-detail-drawer__footer-hint-text {
font-size: 13px;
margin: 0;
}
}
}

View File

@@ -1,5 +1,5 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useCopyToClipboard, useLocation } from 'react-use';
import { Color, Spacing } from '@signozhq/design-tokens';
@@ -32,8 +32,12 @@ import { useSafeNavigate } from 'hooks/useSafeNavigate';
import createQueryParams from 'lib/createQueryParams';
import { cloneDeep } from 'lodash-es';
import {
ArrowDown,
ArrowUp,
BarChart2,
Braces,
ChevronDown,
ChevronUp,
Compass,
Copy,
Filter,
@@ -60,6 +64,9 @@ function LogDetailInner({
isListViewPanel = false,
listViewPanelSelectedFields,
handleChangeSelectedView,
logs,
onNavigateLog,
onScrollToLog,
}: LogDetailInnerProps): JSX.Element {
const initialContextQuery = useInitialQuery(log);
const [contextQuery, setContextQuery] = useState<Query | undefined>(
@@ -74,6 +81,78 @@ function LogDetailInner({
const [isEdit, setIsEdit] = useState<boolean>(false);
const { stagedQuery, updateAllQueriesOperators } = useQueryBuilder();
// Handle clicks outside to close drawer, except on explicitly ignored regions
useEffect(() => {
const handleClickOutside = (e: MouseEvent): void => {
const target = e.target as HTMLElement;
// Don't close if clicking on explicitly ignored regions
if (target.closest('[data-log-detail-ignore="true"]')) {
return;
}
// Close the drawer for any other outside click
onClose?.(e as any);
};
document.addEventListener('mousedown', handleClickOutside);
return (): void => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [onClose]);
// Keyboard navigation - handle up/down arrow keys
// Only listen when in OVERVIEW tab
useEffect(() => {
if (
!logs ||
!onNavigateLog ||
logs.length === 0 ||
selectedView !== VIEW_TYPES.OVERVIEW
) {
return;
}
const handleKeyDown = (e: KeyboardEvent): void => {
const currentIndex = logs.findIndex((l) => l.id === log.id);
if (currentIndex === -1) {
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
e.stopPropagation();
// Navigate to previous log
if (currentIndex > 0) {
const prevLog = logs[currentIndex - 1];
onNavigateLog(prevLog);
// Trigger scroll to the log element
if (onScrollToLog) {
onScrollToLog(prevLog.id);
}
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
e.stopPropagation();
// Navigate to next log
if (currentIndex < logs.length - 1) {
const nextLog = logs[currentIndex + 1];
onNavigateLog(nextLog);
// Trigger scroll to the log element
if (onScrollToLog) {
onScrollToLog(nextLog.id);
}
}
}
};
document.addEventListener('keydown', handleKeyDown);
return (): void => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [log.id, logs, onNavigateLog, onScrollToLog, selectedView]);
const listQuery = useMemo(() => {
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) {
return null;
@@ -227,32 +306,87 @@ function LogDetailInner({
);
const logType = log?.attributes_string?.log_level || LogType.INFO;
const currentLogIndex = logs ? logs.findIndex((l) => l.id === log.id) : -1;
const isPrevDisabled =
!logs || !onNavigateLog || logs.length === 0 || currentLogIndex <= 0;
const isNextDisabled =
!logs ||
!onNavigateLog ||
logs.length === 0 ||
currentLogIndex === logs.length - 1;
type HandleNavigateLogParams = {
direction: 'next' | 'previous';
};
const handleNavigateLog = ({ direction }: HandleNavigateLogParams): void => {
if (!logs || !onNavigateLog || currentLogIndex === -1) {
return;
}
if (direction === 'previous' && !isPrevDisabled) {
const prevLog = logs[currentLogIndex - 1];
onNavigateLog(prevLog);
onScrollToLog?.(prevLog.id);
} else if (direction === 'next' && !isNextDisabled) {
const nextLog = logs[currentLogIndex + 1];
onNavigateLog(nextLog);
onScrollToLog?.(nextLog.id);
}
};
return (
<Drawer
width="60%"
maskStyle={{ background: 'none' }}
mask={false}
maskClosable={false}
title={
<div className="log-detail-drawer__title">
<div className="log-detail-drawer__title" data-log-detail-ignore="true">
<div className="log-detail-drawer__title-left">
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />
<Typography.Text className="title">Log details</Typography.Text>
</div>
{showOpenInExplorerBtn && (
<div className="log-detail-drawer__title-right">
<Button
className="open-in-explorer-btn"
icon={<Compass size={16} />}
onClick={handleOpenInExplorer}
<div className="log-detail-drawer__title-right">
<div className="log-arrows">
<Tooltip
title={isPrevDisabled ? '' : 'Move to previous log'}
placement="top"
mouseLeaveDelay={0}
>
Open in Explorer
</Button>
<Button
icon={<ChevronUp size={14} />}
className="log-arrow-btn log-arrow-btn-up"
disabled={isPrevDisabled}
onClick={(): void => handleNavigateLog({ direction: 'previous' })}
/>
</Tooltip>
<Tooltip
title={isNextDisabled ? '' : 'Move to next log'}
placement="top"
mouseLeaveDelay={0}
>
<Button
icon={<ChevronDown size={14} />}
className="log-arrow-btn log-arrow-btn-down"
disabled={isNextDisabled}
onClick={(): void => handleNavigateLog({ direction: 'next' })}
/>
</Tooltip>
</div>
)}
{showOpenInExplorerBtn && (
<div>
<Button
className="open-in-explorer-btn"
icon={<Compass size={16} />}
onClick={handleOpenInExplorer}
>
Open in Explorer
</Button>
</div>
)}
</div>
</div>
}
placement="right"
// closable
onClose={drawerCloseHandler}
open={log !== null}
style={{
@@ -263,138 +397,164 @@ function LogDetailInner({
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
>
<div className="log-detail-drawer__log">
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
<Tooltip title={removeEscapeCharacters(log?.body)} placement="left">
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
</Tooltip>
<div className="log-detail-drawer__content" data-log-detail-ignore="true">
<div className="log-detail-drawer__log">
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
<Tooltip title={removeEscapeCharacters(log?.body)} placement="left">
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
</Tooltip>
<div className="log-overflow-shadow">&nbsp;</div>
</div>
<div className="log-overflow-shadow">&nbsp;</div>
</div>
<div className="tabs-and-search">
<Radio.Group
className="views-tabs"
onChange={handleModeChange}
value={selectedView}
>
<Radio.Button
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedView === VIEW_TYPES.OVERVIEW ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.OVERVIEW}
<div className="tabs-and-search">
<Radio.Group
className="views-tabs"
onChange={handleModeChange}
value={selectedView}
>
<div className="view-title">
<Table size={14} />
Overview
</div>
</Radio.Button>
<Radio.Button
className={selectedView === VIEW_TYPES.JSON ? 'selected_view tab' : 'tab'}
value={VIEW_TYPES.JSON}
>
<div className="view-title">
<Braces size={14} />
JSON
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.CONTEXT ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.CONTEXT}
>
<div className="view-title">
<TextSelect size={14} />
Context
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.INFRAMETRICS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.INFRAMETRICS}
>
<div className="view-title">
<BarChart2 size={14} />
Metrics
</div>
</Radio.Button>
</Radio.Group>
<Radio.Button
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedView === VIEW_TYPES.OVERVIEW ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.OVERVIEW}
>
<div className="view-title">
<Table size={14} />
Overview
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.JSON ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.JSON}
>
<div className="view-title">
<Braces size={14} />
JSON
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.CONTEXT ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.CONTEXT}
>
<div className="view-title">
<TextSelect size={14} />
Context
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === VIEW_TYPES.INFRAMETRICS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.INFRAMETRICS}
>
<div className="view-title">
<BarChart2 size={14} />
Metrics
</div>
</Radio.Button>
</Radio.Group>
<div className="log-detail-drawer__actions">
{selectedView === VIEW_TYPES.CONTEXT && (
<Tooltip
title="Show Filters"
placement="topLeft"
aria-label="Show Filters"
>
<Button
className="action-btn"
icon={<Filter size={16} />}
onClick={handleFilterVisible}
/>
</Tooltip>
)}
<div className="log-detail-drawer__actions">
{selectedView === VIEW_TYPES.CONTEXT && (
<Tooltip
title="Show Filters"
title={selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'}
placement="topLeft"
aria-label="Show Filters"
aria-label={
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
}
>
<Button
className="action-btn"
icon={<Filter size={16} />}
onClick={handleFilterVisible}
icon={<Copy size={16} />}
onClick={selectedView === VIEW_TYPES.JSON ? handleJSONCopy : onLogCopy}
/>
</Tooltip>
)}
<Tooltip
title={selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'}
placement="topLeft"
aria-label={
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
}
>
<Button
className="action-btn"
icon={<Copy size={16} />}
onClick={selectedView === VIEW_TYPES.JSON ? handleJSONCopy : onLogCopy}
</div>
</div>
{isFilterVisible && contextQuery?.builder.queryData[0] && (
<div className="log-detail-drawer-query-container">
<QuerySearch
onChange={(value): void => handleQueryExpressionChange(value, 0)}
dataSource={DataSource.LOGS}
queryData={contextQuery?.builder.queryData[0]}
onRun={handleRunQuery}
/>
</Tooltip>
</div>
</div>
{isFilterVisible && contextQuery?.builder.queryData[0] && (
<div className="log-detail-drawer-query-container">
<QuerySearch
onChange={(value): void => handleQueryExpressionChange(value, 0)}
dataSource={DataSource.LOGS}
queryData={contextQuery?.builder.queryData[0]}
onRun={handleRunQuery}
</div>
)}
{selectedView === VIEW_TYPES.OVERVIEW && (
<Overview
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
isListViewPanel={isListViewPanel}
selectedOptions={options}
listViewPanelSelectedFields={listViewPanelSelectedFields}
handleChangeSelectedView={handleChangeSelectedView}
/>
</div>
)}
)}
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}
{selectedView === VIEW_TYPES.OVERVIEW && (
<Overview
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
isListViewPanel={isListViewPanel}
selectedOptions={options}
listViewPanelSelectedFields={listViewPanelSelectedFields}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}
{selectedView === VIEW_TYPES.CONTEXT && (
<ContextView
log={log}
filters={filters}
contextQuery={contextQuery}
isEdit={isEdit}
/>
)}
{selectedView === VIEW_TYPES.INFRAMETRICS && (
<InfraMetrics
clusterName={log.resources_string?.[RESOURCE_KEYS.CLUSTER_NAME] || ''}
podName={log.resources_string?.[RESOURCE_KEYS.POD_NAME] || ''}
nodeName={log.resources_string?.[RESOURCE_KEYS.NODE_NAME] || ''}
hostName={log.resources_string?.[RESOURCE_KEYS.HOST_NAME] || ''}
timestamp={log.timestamp.toString()}
dataSource={DataSource.LOGS}
/>
)}
{selectedView === VIEW_TYPES.CONTEXT && (
<ContextView
log={log}
filters={filters}
contextQuery={contextQuery}
isEdit={isEdit}
/>
)}
{selectedView === VIEW_TYPES.INFRAMETRICS && (
<InfraMetrics
clusterName={log.resources_string?.[RESOURCE_KEYS.CLUSTER_NAME] || ''}
podName={log.resources_string?.[RESOURCE_KEYS.POD_NAME] || ''}
nodeName={log.resources_string?.[RESOURCE_KEYS.NODE_NAME] || ''}
hostName={log.resources_string?.[RESOURCE_KEYS.HOST_NAME] || ''}
timestamp={log.timestamp.toString()}
dataSource={DataSource.LOGS}
/>
)}
{selectedView === VIEW_TYPES.OVERVIEW && (
<div className="log-detail-drawer__footer-hint">
<div className="log-detail-drawer__footer-hint-content">
<Typography.Text
type="secondary"
className="log-detail-drawer__footer-hint-text"
>
Use
</Typography.Text>
<ArrowUp size={14} className="log-detail-drawer__footer-hint-icon" />
<span>/</span>
<ArrowDown size={14} className="log-detail-drawer__footer-hint-icon" />
<Typography.Text
type="secondary"
className="log-detail-drawer__footer-hint-text"
>
to view previous/next log
</Typography.Text>
</div>
</div>
)}
</div>
</Drawer>
);
}

View File

@@ -2,13 +2,11 @@ import { memo, useCallback, useMemo } from 'react';
import { blue } from '@ant-design/colors';
import { Typography } from 'antd';
import cx from 'classnames';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode';
// utils
@@ -104,12 +102,17 @@ function LogSelectedField({
type ListLogViewProps = {
logData: ILog;
selectedFields: IField[];
onSetActiveLog: (log: ILog) => void;
onSetActiveLog: (
log: ILog,
selectedTab?: typeof VIEW_TYPES[keyof typeof VIEW_TYPES],
) => void;
onAddToQuery: AddToQueryHOCProps['onAddToQuery'];
activeLog?: ILog | null;
linesPerRow: number;
fontSize: FontSize;
handleChangeSelectedView?: ChangeViewFunctionType;
isActiveLog?: boolean;
onClearActiveLog?: () => void;
};
function ListLogView({
@@ -120,7 +123,8 @@ function ListLogView({
activeLog,
linesPerRow,
fontSize,
handleChangeSelectedView,
isActiveLog,
onClearActiveLog,
}: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
@@ -129,35 +133,24 @@ function ListLogView({
);
const isReadOnlyLog = !isLogsExplorerPage;
const {
activeLog: activeContextLog,
onAddToQuery: handleAddToQuery,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
} = useActiveLog();
const isDarkMode = useIsDarkMode();
const handlerClearActiveContextLog = useCallback(
(event: React.MouseEvent | React.KeyboardEvent) => {
event.preventDefault();
event.stopPropagation();
handleClearActiveContextLog();
},
[handleClearActiveContextLog],
);
const handleDetailedView = useCallback(() => {
if (isActiveLog) {
onClearActiveLog?.();
return;
}
onSetActiveLog(logData);
}, [logData, onSetActiveLog]);
}, [logData, onSetActiveLog, isActiveLog, onClearActiveLog]);
const handleShowContext = useCallback(
(event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
handleSetActiveContextLog(logData);
onSetActiveLog(logData, VIEW_TYPES.CONTEXT);
},
[logData, handleSetActiveContextLog],
[logData, onSetActiveLog],
);
const updatedSelecedFields = useMemo(
@@ -186,11 +179,7 @@ function ListLogView({
return (
<>
<Container
$isActiveLog={
isHighlighted ||
activeLog?.id === logData.id ||
activeContextLog?.id === logData.id
}
$isActiveLog={isHighlighted || activeLog?.id === logData.id}
$isDarkMode={isDarkMode}
$logType={logType}
onClick={handleDetailedView}
@@ -251,15 +240,6 @@ function ListLogView({
/>
)}
</Container>
{activeContextLog && (
<LogDetail
log={activeContextLog}
onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT}
onClose={handlerClearActiveContextLog}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
</>
);
}

View File

@@ -1,19 +1,15 @@
import {
KeyboardEvent,
memo,
MouseEvent,
MouseEventHandler,
useCallback,
useMemo,
useState,
} from 'react';
import { Color } from '@signozhq/design-tokens';
import { DrawerProps, Tooltip } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
import { Tooltip } from 'antd';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
// hooks
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -39,7 +35,8 @@ function RawLogView({
selectedFields = [],
fontSize,
onLogClick,
handleChangeSelectedView,
onSetActiveLog,
onClearActiveLog,
}: RawLogViewProps): JSX.Element {
const {
isHighlighted: isUrlHighlighted,
@@ -48,15 +45,6 @@ function RawLogView({
} = useCopyLogLink(data.id);
const flattenLogData = useMemo(() => FlatLogData(data), [data]);
const {
activeLog,
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
} = useActiveLog();
const [selectedTab, setSelectedTab] = useState<VIEWS | undefined>();
const isDarkMode = useIsDarkMode();
const isReadOnlyLog = !isLogsExplorerPage || isReadOnly;
@@ -134,34 +122,24 @@ function RawLogView({
// Use custom click handler if provided, otherwise use default behavior
if (onLogClick) {
onLogClick(data, event);
} else {
onSetActiveLog(data);
setSelectedTab(VIEW_TYPES.OVERVIEW);
return;
}
if (isActiveLog) {
onClearActiveLog?.();
return;
}
},
[isReadOnly, data, onSetActiveLog, onLogClick],
);
const handleCloseLogDetail: DrawerProps['onClose'] = useCallback(
(
event: MouseEvent<Element, globalThis.MouseEvent> | KeyboardEvent<Element>,
) => {
event.preventDefault();
event.stopPropagation();
onClearActiveLog();
setSelectedTab(undefined);
onSetActiveLog?.(data);
},
[onClearActiveLog],
[isReadOnly, onLogClick, isActiveLog, onSetActiveLog, data, onClearActiveLog],
);
const handleShowContext: MouseEventHandler<HTMLElement> = useCallback(
(event) => {
event.preventDefault();
event.stopPropagation();
// handleSetActiveContextLog(data);
setSelectedTab(VIEW_TYPES.CONTEXT);
onSetActiveLog(data);
onSetActiveLog?.(data, VIEW_TYPES.CONTEXT);
},
[data, onSetActiveLog],
);
@@ -181,7 +159,7 @@ function RawLogView({
$isDarkMode={isDarkMode}
$isReadOnly={isReadOnly}
$isHightlightedLog={isUrlHighlighted}
$isActiveLog={activeLog?.id === data.id || isActiveLog}
$isActiveLog={isActiveLog}
$isCustomHighlighted={isHighlighted}
$logType={logType}
fontSize={fontSize}
@@ -218,17 +196,6 @@ function RawLogView({
onLogCopy={onLogCopy}
/>
)}
{selectedTab && (
<LogDetail
selectedTab={selectedTab}
log={activeLog}
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
</RawLogViewContainer>
);
}

View File

@@ -45,9 +45,6 @@ export const RawLogViewContainer = styled(Row)<{
: `margin: 2px 0;`}
}
${({ $isActiveLog, $logType }): string =>
getActiveLogBackground($isActiveLog, true, $logType)}
${({ $isReadOnly, $isActiveLog, $isDarkMode, $logType }): string =>
$isActiveLog
? getActiveLogBackground($isActiveLog, $isDarkMode, $logType)

View File

@@ -1,4 +1,5 @@
import { MouseEvent } from 'react';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { FontSize } from 'container/OptionsMenu/types';
import { IField } from 'types/api/logs/fields';
@@ -16,6 +17,11 @@ export interface RawLogViewProps {
selectedFields?: IField[];
onLogClick?: (log: ILog, event: MouseEvent) => void;
handleChangeSelectedView?: ChangeViewFunctionType;
onSetActiveLog?: (
log: ILog,
selectedTab?: typeof VIEW_TYPES[keyof typeof VIEW_TYPES],
) => void;
onClearActiveLog?: () => void;
}
export interface RawLogContentProps {

View File

@@ -86,7 +86,6 @@ interface QuerySearchProps {
signalSource?: string;
hardcodedAttributeKeys?: QueryKeyDataSuggestionsProps[];
onRun?: (query: string) => void;
showFilterSuggestionsWithoutMetric?: boolean;
}
function QuerySearch({
@@ -97,7 +96,6 @@ function QuerySearch({
onRun,
signalSource,
hardcodedAttributeKeys,
showFilterSuggestionsWithoutMetric,
}: QuerySearchProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
@@ -254,8 +252,7 @@ function QuerySearch({
async (searchText?: string): Promise<void> => {
if (
dataSource === DataSource.METRICS &&
!queryData.aggregateAttribute?.key &&
!showFilterSuggestionsWithoutMetric
!queryData.aggregateAttribute?.key
) {
setKeySuggestions([]);
return;
@@ -304,7 +301,6 @@ function QuerySearch({
queryData.aggregateAttribute?.key,
signalSource,
hardcodedAttributeKeys,
showFilterSuggestionsWithoutMetric,
],
);
@@ -1566,7 +1562,6 @@ QuerySearch.defaultProps = {
hardcodedAttributeKeys: undefined,
placeholder:
"Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')",
showFilterSuggestionsWithoutMetric: false,
};
export default QuerySearch;

View File

@@ -14,9 +14,11 @@ import {
VisibilityMode,
} from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { isInvalidPlotValue } from 'lib/uPlotV2/utils/dataUtils';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { QueryData } from 'types/api/widgets/getQuery';
import { PanelMode } from '../types';
import { buildBaseConfig } from '../utils/baseConfigBuilder';
@@ -31,6 +33,22 @@ export const prepareChartData = (
return [timestampArr, ...yAxisValuesArr];
};
function hasSingleVisiblePointForSeries(series: QueryData): boolean {
const rawValues = series.values ?? [];
let validPointCount = 0;
for (const [, rawValue] of rawValues) {
if (!isInvalidPlotValue(rawValue)) {
validPointCount += 1;
if (validPointCount > 1) {
return false;
}
}
}
return true;
}
export const prepareUPlotConfig = ({
widget,
isDarkMode,
@@ -68,6 +86,7 @@ export const prepareUPlotConfig = ({
});
apiResponse.data?.result?.forEach((series) => {
const hasSingleVisiblePoint = hasSingleVisiblePointForSeries(series);
const baseLabelName = getLabelName(
series.metric,
series.queryName || '', // query
@@ -80,13 +99,15 @@ export const prepareUPlotConfig = ({
builder.addSeries({
scaleKey: 'y',
drawStyle: DrawStyle.Line,
drawStyle: hasSingleVisiblePoint ? DrawStyle.Points : DrawStyle.Line,
label: label,
colorMapping: widget.customLegendColors ?? {},
spanGaps: true,
lineStyle: LineStyle.Solid,
lineInterpolation: LineInterpolation.Spline,
showPoints: VisibilityMode.Never,
showPoints: hasSingleVisiblePoint
? VisibilityMode.Always
: VisibilityMode.Never,
pointSize: 5,
isDarkMode,
panelType: PANEL_TYPES.TIME_SERIES,

View File

@@ -1,8 +1,9 @@
/* eslint-disable no-nested-ternary */
import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useQuery } from 'react-query';
import { Virtuoso } from 'react-virtuoso';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { Card } from 'antd';
import LogDetail from 'components/LogDetail';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
@@ -11,6 +12,8 @@ import LogsError from 'container/LogsError/LogsError';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { FontSize } from 'container/OptionsMenu/types';
import { useHandleLogsPagination } from 'hooks/infraMonitoring/useHandleLogsPagination';
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
import useScrollToLog from 'hooks/logs/useScrollToLog';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { ILog } from 'types/api/logs/log';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
@@ -40,6 +43,15 @@ function EntityLogs({
category,
queryKeyFilters,
}: Props): JSX.Element {
const virtuosoRef = useRef<VirtuosoHandle>(null);
const {
activeLog,
onAddToQuery,
selectedTab,
handleSetActiveLog,
handleCloseLogDetail,
} = useLogDetailHandlers();
const basePayload = getEntityEventsOrLogsQueryPayload(
timeRange.startTime,
timeRange.endTime,
@@ -62,29 +74,40 @@ function EntityLogs({
basePayload,
});
const handleScrollToLog = useScrollToLog({
logs,
virtuosoRef,
});
const getItemContent = useCallback(
(_: number, logToRender: ILog): JSX.Element => (
<RawLogView
isTextOverflowEllipsisDisabled
key={logToRender.id}
data={logToRender}
linesPerRow={5}
fontSize={FontSize.MEDIUM}
selectedFields={[
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
]}
/>
),
[],
(_: number, logToRender: ILog): JSX.Element => {
return (
<div key={logToRender.id}>
<RawLogView
isTextOverflowEllipsisDisabled
data={logToRender}
linesPerRow={5}
fontSize={FontSize.MEDIUM}
selectedFields={[
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
]}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
isActiveLog={activeLog?.id === logToRender.id}
/>
</div>
);
},
[activeLog, handleSetActiveLog, handleCloseLogDetail],
);
const { data, isLoading, isFetching, isError } = useQuery({
@@ -131,6 +154,7 @@ function EntityLogs({
<Virtuoso
className="entity-logs-virtuoso"
key="entity-logs-virtuoso"
ref={virtuosoRef}
data={logs}
endReached={loadMoreLogs}
totalCount={logs.length}
@@ -154,7 +178,21 @@ function EntityLogs({
)}
{isError && !isLoading && <LogsError />}
{!isLoading && !isError && logs.length > 0 && (
<div className="entity-logs-list-container">{renderContent}</div>
<div className="entity-logs-list-container" data-log-detail-ignore="true">
{renderContent}
</div>
)}
{selectedTab && activeLog && (
<LogDetail
log={activeLog}
onClose={handleCloseLogDetail}
logs={logs}
onNavigateLog={handleSetActiveLog}
selectedTab={selectedTab}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onScrollToLog={handleScrollToLog}
/>
)}
</div>
);

View File

@@ -2,7 +2,6 @@ import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { Card, Typography } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
@@ -14,8 +13,9 @@ import { InfinityWrapperStyled } from 'container/LogsExplorerList/styles';
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
import { useOptionsMenu } from 'container/OptionsMenu';
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
import useScrollToLog from 'hooks/logs/useScrollToLog';
import { useEventSource } from 'providers/EventSource';
// interfaces
import { ILog } from 'types/api/logs/log';
@@ -38,10 +38,11 @@ function LiveLogsList({
const {
activeLog,
onClearActiveLog,
onAddToQuery,
onSetActiveLog,
} = useActiveLog();
selectedTab,
handleSetActiveLog,
handleCloseLogDetail,
} = useLogDetailHandlers();
// get only data from the logs object
const formattedLogs: ILog[] = useMemo(
@@ -65,42 +66,56 @@ function LiveLogsList({
...options.selectColumns,
]);
const handleScrollToLog = useScrollToLog({
logs: formattedLogs,
virtuosoRef: ref,
});
const getItemContent = useCallback(
(_: number, log: ILog): JSX.Element => {
if (options.format === 'raw') {
return (
<RawLogView
key={log.id}
data={log}
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
<div key={log.id}>
<RawLogView
data={log}
isActiveLog={activeLog?.id === log.id}
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
/>
</div>
);
}
return (
<ListLogView
key={log.id}
logData={log}
selectedFields={selectedFields}
linesPerRow={options.maxLines}
onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
<div key={log.id}>
<ListLogView
logData={log}
isActiveLog={activeLog?.id === log.id}
selectedFields={selectedFields}
linesPerRow={options.maxLines}
onAddToQuery={onAddToQuery}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
</div>
);
},
[
handleChangeSelectedView,
onAddToQuery,
onSetActiveLog,
options.fontSize,
options.format,
options.maxLines,
options.fontSize,
activeLog?.id,
selectedFields,
onAddToQuery,
handleSetActiveLog,
handleCloseLogDetail,
handleChangeSelectedView,
],
);
@@ -156,6 +171,10 @@ function LiveLogsList({
activeLogIndex,
}}
handleChangeSelectedView={handleChangeSelectedView}
logs={formattedLogs}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
activeLog={activeLog}
/>
) : (
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>
@@ -173,14 +192,17 @@ function LiveLogsList({
</InfinityWrapperStyled>
)}
{activeLog && (
{activeLog && selectedTab && (
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
selectedTab={selectedTab}
log={activeLog}
onClose={onClearActiveLog}
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
logs={formattedLogs}
onNavigateLog={handleSetActiveLog}
onScrollToLog={handleScrollToLog}
/>
)}
</div>

View File

@@ -395,7 +395,7 @@ export default function TableViewActions(
onOpenChange={setIsOpen}
arrow={false}
content={
<div>
<div data-log-detail-ignore="true">
<Button
className="more-filter-actions"
type="text"
@@ -481,7 +481,7 @@ export default function TableViewActions(
onOpenChange={setIsOpen}
arrow={false}
content={
<div>
<div data-log-detail-ignore="true">
<Button
className="more-filter-actions"
type="text"

View File

@@ -7,6 +7,7 @@ import {
useMemo,
} from 'react';
import { ColumnsType } from 'antd/es/table';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import LogLinesActionButtons from 'components/Logs/LogLinesActionButtons/LogLinesActionButtons';
import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { FontSize } from 'container/OptionsMenu/types';
@@ -22,22 +23,27 @@ interface TableRowProps {
tableColumns: ColumnsType<Record<string, unknown>>;
index: number;
log: Record<string, unknown>;
handleSetActiveContextLog: (log: ILog) => void;
onShowLogDetails: (log: ILog) => void;
onShowLogDetails?: (
log: ILog,
selectedTab?: typeof VIEW_TYPES[keyof typeof VIEW_TYPES],
) => void;
logs: ILog[];
hasActions: boolean;
fontSize: FontSize;
isActiveLog?: boolean;
onClearActiveLog?: () => void;
}
export default function TableRow({
tableColumns,
index,
log,
handleSetActiveContextLog,
onShowLogDetails,
logs,
hasActions,
fontSize,
isActiveLog,
onClearActiveLog,
}: TableRowProps): JSX.Element {
const isDarkMode = useIsDarkMode();
@@ -52,21 +58,31 @@ export default function TableRow({
(event) => {
event.preventDefault();
event.stopPropagation();
if (!handleSetActiveContextLog || !currentLog) {
if (!currentLog) {
return;
}
handleSetActiveContextLog(currentLog);
onShowLogDetails?.(currentLog, VIEW_TYPES.CONTEXT);
},
[currentLog, handleSetActiveContextLog],
[currentLog, onShowLogDetails],
);
const handleShowLogDetails = useCallback(() => {
if (!onShowLogDetails || !currentLog) {
if (!currentLog) {
return;
}
onShowLogDetails(currentLog);
}, [currentLog, onShowLogDetails]);
// If this log is already active, close the detail drawer
if (isActiveLog && onClearActiveLog) {
onClearActiveLog();
return;
}
// Otherwise, open the detail drawer for this log
if (onShowLogDetails) {
onShowLogDetails(currentLog);
}
}, [currentLog, onShowLogDetails, isActiveLog, onClearActiveLog]);
const hasSingleColumn =
tableColumns.filter((column) => column.key !== 'state-indicator').length ===

View File

@@ -4,7 +4,6 @@ import {
TableVirtuoso,
TableVirtuosoHandle,
} from 'react-virtuoso';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { getLogIndicatorType } from 'components/Logs/LogStateIndicator/utils';
import { useTableView } from 'components/Logs/TableView/useTableView';
@@ -58,26 +57,40 @@ const CustomTableRow: TableComponents<ILog>['TableRow'] = ({
const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
function InfinityTableView(
{ isLoading, tableViewProps, infitiyTableProps, handleChangeSelectedView },
ref,
): JSX.Element | null {
const {
activeLog: activeContextLog,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
onAddToQuery: handleAddToQuery,
} = useActiveLog();
const {
activeLog,
{
isLoading,
tableViewProps,
infitiyTableProps,
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
} = useActiveLog();
activeLog,
},
ref,
): JSX.Element | null {
const { activeLog: activeContextLog } = useActiveLog();
const onSetActiveLogExpand = useCallback(
(log: ILog) => {
onSetActiveLog?.(log);
},
[onSetActiveLog],
);
const onSetActiveLogContext = useCallback(
(log: ILog) => {
onSetActiveLog?.(log, VIEW_TYPES.CONTEXT);
},
[onSetActiveLog],
);
const onCloseActiveLog = useCallback(() => {
onClearActiveLog?.();
}, [onClearActiveLog]);
const { dataSource, columns } = useTableView({
...tableViewProps,
onClickExpand: onSetActiveLog,
onOpenLogsContext: handleSetActiveContextLog,
onClickExpand: onSetActiveLogExpand,
onOpenLogsContext: onSetActiveLogContext,
});
const { draggedColumns, onDragColumns } = useDragColumns<
@@ -98,27 +111,32 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
);
const itemContent = useCallback(
(index: number, log: Record<string, unknown>): JSX.Element => (
<TableRow
tableColumns={tableColumns}
index={index}
log={log}
handleSetActiveContextLog={handleSetActiveContextLog}
logs={tableViewProps.logs}
hasActions
fontSize={tableViewProps.fontSize}
onShowLogDetails={onSetActiveLog}
/>
),
(index: number, log: Record<string, unknown>): JSX.Element => {
return (
<div key={log.id as string}>
<TableRow
tableColumns={tableColumns}
index={index}
log={log}
logs={tableViewProps.logs}
hasActions
fontSize={tableViewProps.fontSize}
onShowLogDetails={onSetActiveLog}
isActiveLog={activeLog?.id === log.id}
onClearActiveLog={onCloseActiveLog}
/>
</div>
);
},
[
handleSetActiveContextLog,
tableColumns,
tableViewProps.fontSize,
tableViewProps.logs,
onSetActiveLog,
tableViewProps.logs,
tableViewProps.fontSize,
activeLog?.id,
onCloseActiveLog,
],
);
const tableHeader = useCallback(
() => (
<tr>
@@ -179,24 +197,6 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
? { endReached: infitiyTableProps.onEndReached }
: {})}
/>
{activeContextLog && (
<LogDetail
log={activeContextLog}
onClose={handleClearActiveContextLog}
onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
/>
</>
);
},

View File

@@ -1,5 +1,7 @@
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { UseTableViewProps } from 'components/Logs/TableView/types';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ILog } from 'types/api/logs/log';
export type InfinityTableProps = {
isLoading?: boolean;
@@ -8,4 +10,11 @@ export type InfinityTableProps = {
onEndReached: (index: number) => void;
};
handleChangeSelectedView?: ChangeViewFunctionType;
logs?: ILog[];
onSetActiveLog?: (
log: ILog,
selectedTab?: typeof VIEW_TYPES[keyof typeof VIEW_TYPES],
) => void;
onClearActiveLog?: () => void;
activeLog?: ILog | null;
};

View File

@@ -4,7 +4,6 @@ import { Card } from 'antd';
import logEvent from 'api/common/logEvent';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
// components
import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
@@ -16,8 +15,9 @@ import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import { useOptionsMenu } from 'container/OptionsMenu';
import { FontSize } from 'container/OptionsMenu/types';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
import useScrollToLog from 'hooks/logs/useScrollToLog';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import APIError from 'types/api/error';
// interfaces
@@ -55,10 +55,11 @@ function LogsExplorerList({
const {
activeLog,
onClearActiveLog,
onAddToQuery,
onSetActiveLog,
} = useActiveLog();
selectedTab,
handleSetActiveLog,
handleCloseLogDetail,
} = useLogDetailHandlers();
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
@@ -82,6 +83,12 @@ function LogsExplorerList({
() => convertKeysToColumnFields(options.selectColumns),
[options],
);
const handleScrollToLog = useScrollToLog({
logs,
virtuosoRef: ref,
});
useEffect(() => {
if (!isLoading && !isFetching && !isError && logs.length !== 0) {
logEvent('Logs Explorer: Data present', {
@@ -94,40 +101,48 @@ function LogsExplorerList({
(_: number, log: ILog): JSX.Element => {
if (options.format === 'raw') {
return (
<RawLogView
key={log.id}
data={log}
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
/>
<div key={log.id}>
<RawLogView
data={log}
isActiveLog={activeLog?.id === log.id}
linesPerRow={options.maxLines}
selectedFields={selectedFields}
fontSize={options.fontSize}
handleChangeSelectedView={handleChangeSelectedView}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
/>
</div>
);
}
return (
<ListLogView
key={log.id}
logData={log}
selectedFields={selectedFields}
onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog}
activeLog={activeLog}
fontSize={options.fontSize}
linesPerRow={options.maxLines}
handleChangeSelectedView={handleChangeSelectedView}
/>
<div key={log.id}>
<ListLogView
logData={log}
isActiveLog={activeLog?.id === log.id}
selectedFields={selectedFields}
onAddToQuery={onAddToQuery}
onSetActiveLog={handleSetActiveLog}
activeLog={activeLog}
fontSize={options.fontSize}
linesPerRow={options.maxLines}
handleChangeSelectedView={handleChangeSelectedView}
onClearActiveLog={handleCloseLogDetail}
/>
</div>
);
},
[
activeLog,
handleChangeSelectedView,
onAddToQuery,
onSetActiveLog,
options.fontSize,
options.format,
options.fontSize,
options.maxLines,
activeLog,
selectedFields,
onAddToQuery,
handleSetActiveLog,
handleChangeSelectedView,
handleCloseLogDetail,
],
);
@@ -153,6 +168,10 @@ function LogsExplorerList({
}}
infitiyTableProps={{ onEndReached }}
handleChangeSelectedView={handleChangeSelectedView}
logs={logs}
onSetActiveLog={handleSetActiveLog}
onClearActiveLog={handleCloseLogDetail}
activeLog={activeLog}
/>
);
}
@@ -199,6 +218,9 @@ function LogsExplorerList({
getItemContent,
selectedFields,
handleChangeSelectedView,
handleSetActiveLog,
handleCloseLogDetail,
activeLog,
]);
const isTraceToLogsNavigation = useMemo(() => {
@@ -278,14 +300,19 @@ function LogsExplorerList({
{renderContent}
</InfinityWrapperStyled>
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
/>
{selectedTab && activeLog && (
<LogDetail
selectedTab={selectedTab}
log={activeLog}
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
handleChangeSelectedView={handleChangeSelectedView}
logs={logs}
onNavigateLog={handleSetActiveLog}
onScrollToLog={handleScrollToLog}
/>
)}
</>
)}
</div>

View File

@@ -466,7 +466,10 @@ function LogsExplorerViewsContainer({
</div>
)}
<div className="logs-explorer-views-type-content">
<div
className="logs-explorer-views-type-content"
data-log-detail-ignore="true"
>
{showLiveLogs && (
<LiveLogs handleChangeSelectedView={handleChangeSelectedView} />
)}

View File

@@ -8,7 +8,6 @@ import {
} from 'react';
import { UseQueryResult } from 'react-query';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { ResizeTable } from 'components/ResizeTable';
import { SOMETHING_WENT_WRONG } from 'constants/api';
@@ -16,7 +15,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import Controls from 'container/Controls';
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
import { tableStyles } from 'container/TracesExplorer/ListView/styles';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
import { useLogsData } from 'hooks/useLogsData';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { FlatLogData } from 'lib/logs/flatLogData';
@@ -83,24 +82,24 @@ function LogsPanelComponent({
() => logs.map((log) => FlatLogData(log) as RowData),
[logs],
);
const {
activeLog,
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
} = useActiveLog();
selectedTab,
handleSetActiveLog,
handleCloseLogDetail,
} = useLogDetailHandlers();
const handleRow = useCallback(
(record: RowData): HTMLAttributes<RowData> => ({
onClick: (): void => {
const log = logs.find((item) => item.id === record.id);
if (log) {
onSetActiveLog(log);
handleSetActiveLog(log);
}
},
}),
[logs, onSetActiveLog],
[handleSetActiveLog, logs],
);
const handleRequestData = (newOffset: number): void => {
@@ -132,7 +131,7 @@ function LogsPanelComponent({
return (
<>
<div className="logs-table">
<div className="logs-table" data-log-detail-ignore="true">
<div className="resize-table">
<OverlayScrollbar>
<ResizeTable
@@ -166,15 +165,19 @@ function LogsPanelComponent({
</div>
)}
</div>
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
isListViewPanel
listViewPanelSelectedFields={widget?.selectedLogFields}
/>
{selectedTab && activeLog && (
<LogDetail
selectedTab={selectedTab}
log={activeLog}
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
isListViewPanel
listViewPanelSelectedFields={widget?.selectedLogFields}
logs={logs}
onNavigateLog={handleSetActiveLog}
/>
)}
</>
);
}

View File

@@ -33,7 +33,7 @@ function ExpandedView({
options,
spaceAggregationSeriesMap,
step,
appliedMetricInspectionOptions,
metricInspectionOptions,
timeAggregatedSeriesMap,
}: ExpandedViewProps): JSX.Element {
const [
@@ -44,17 +44,17 @@ function ExpandedView({
useEffect(() => {
logEvent(MetricsExplorerEvents.InspectPointClicked, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
[MetricsExplorerEventKeys.Filters]: appliedMetricInspectionOptions.filters,
[MetricsExplorerEventKeys.Filters]: metricInspectionOptions.filters,
[MetricsExplorerEventKeys.TimeAggregationInterval]:
appliedMetricInspectionOptions.timeAggregationInterval,
metricInspectionOptions.timeAggregationInterval,
[MetricsExplorerEventKeys.TimeAggregationOption]:
appliedMetricInspectionOptions.timeAggregationOption,
metricInspectionOptions.timeAggregationOption,
[MetricsExplorerEventKeys.SpaceAggregationOption]:
appliedMetricInspectionOptions.spaceAggregationOption,
metricInspectionOptions.spaceAggregationOption,
[MetricsExplorerEventKeys.SpaceAggregationLabels]:
appliedMetricInspectionOptions.spaceAggregationLabels,
metricInspectionOptions.spaceAggregationLabels,
});
}, [appliedMetricInspectionOptions]);
}, [metricInspectionOptions]);
useEffect(() => {
if (step !== InspectionStep.COMPLETED) {
@@ -167,7 +167,7 @@ function ExpandedView({
<Typography.Text strong>
{`${absoluteValue} is the ${
SPACE_AGGREGATION_OPTIONS_FOR_EXPANDED_VIEW[
appliedMetricInspectionOptions.spaceAggregationOption ??
metricInspectionOptions.spaceAggregationOption ??
SpaceAggregationOptions.SUM_BY
]
} of`}
@@ -240,7 +240,7 @@ function ExpandedView({
)?.value ?? options?.value
} is the ${
TIME_AGGREGATION_OPTIONS[
appliedMetricInspectionOptions.timeAggregationOption ??
metricInspectionOptions.timeAggregationOption ??
TimeAggregationOptions.SUM
]
} of`
@@ -299,7 +299,7 @@ function ExpandedView({
<Typography.Text strong>
{`${absoluteValue} is the ${
TIME_AGGREGATION_OPTIONS[
appliedMetricInspectionOptions.timeAggregationOption ??
metricInspectionOptions.timeAggregationOption ??
TimeAggregationOptions.SUM
]
} of`}

View File

@@ -29,7 +29,7 @@ function GraphView({
popoverOptions,
setShowExpandedView,
setExpandedViewOptions,
appliedMetricInspectionOptions,
metricInspectionOptions,
isInspectMetricsRefetching,
}: GraphViewProps): JSX.Element {
const isDarkMode = useIsDarkMode();
@@ -233,7 +233,7 @@ function GraphView({
inspectMetricsTimeSeries={inspectMetricsTimeSeries}
setShowExpandedView={setShowExpandedView}
setExpandedViewOptions={setExpandedViewOptions}
appliedMetricInspectionOptions={appliedMetricInspectionOptions}
metricInspectionOptions={metricInspectionOptions}
isInspectMetricsRefetching={isInspectMetricsRefetching}
/>
)}
@@ -255,7 +255,7 @@ function GraphView({
<HoverPopover
options={hoverPopoverOptions}
step={inspectionStep}
appliedMetricInspectionOptions={appliedMetricInspectionOptions}
metricInspectionOptions={metricInspectionOptions}
/>
)}
</div>

View File

@@ -122,10 +122,6 @@
gap: 4px;
.inspect-metrics-query-builder-header {
display: flex;
align-items: center;
justify-content: space-between;
.query-builder-button-label {
display: flex;
align-items: center;
@@ -261,21 +257,6 @@
.completed-checklist-container {
margin-left: 20px;
.completed-checklist-item,
.whats-next-checklist-item {
.completed-checklist-item-title,
.whats-next-checklist-item-title {
display: flex;
align-items: center;
gap: 4px;
.ant-typography {
display: flex;
align-items: center;
gap: 4px;
}
}
}
}
.completed-message-container {

View File

@@ -9,7 +9,6 @@ import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Compass } from 'lucide-react';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
@@ -23,7 +22,6 @@ import {
MetricInspectionAction,
} from './types';
import { useInspectMetrics } from './useInspectMetrics';
import { useMetricName } from './utils';
import './Inspect.styles.scss';
@@ -33,12 +31,7 @@ function Inspect({
onClose,
}: InspectProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const {
currentMetricName,
setCurrentMetricName,
appliedMetricName,
setAppliedMetricName,
} = useMetricName(defaultMetricName);
const [metricName, setMetricName] = useState<string | null>(defaultMetricName);
const [
popoverOptions,
setPopoverOptions,
@@ -49,12 +42,9 @@ function Inspect({
] = useState<GraphPopoverOptions | null>(null);
const [showExpandedView, setShowExpandedView] = useState(false);
const { data: metricDetailsData } = useGetMetricDetails(
appliedMetricName ?? '',
{
enabled: !!appliedMetricName,
},
);
const { data: metricDetailsData } = useGetMetricDetails(metricName ?? '', {
enabled: !!metricName,
});
const { currentQuery } = useQueryBuilder();
const { handleChangeQueryData } = useQueryOperations({
@@ -107,16 +97,25 @@ function Inspect({
aggregatedTimeSeries,
timeAggregatedSeriesMap,
reset,
} = useInspectMetrics(appliedMetricName);
} = useInspectMetrics(metricName);
const handleDispatchMetricInspectionOptions = useCallback(
(action: MetricInspectionAction): void => {
dispatchMetricInspectionOptions(action);
logEvent(MetricsExplorerEvents.InspectQueryChanged, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
[MetricsExplorerEventKeys.Filters]: metricInspectionOptions.filters,
[MetricsExplorerEventKeys.TimeAggregationInterval]:
metricInspectionOptions.timeAggregationInterval,
[MetricsExplorerEventKeys.TimeAggregationOption]:
metricInspectionOptions.timeAggregationOption,
[MetricsExplorerEventKeys.SpaceAggregationOption]:
metricInspectionOptions.spaceAggregationOption,
[MetricsExplorerEventKeys.SpaceAggregationLabels]:
metricInspectionOptions.spaceAggregationLabels,
});
},
[dispatchMetricInspectionOptions],
[dispatchMetricInspectionOptions, metricInspectionOptions],
);
const selectedMetricType = useMemo(
@@ -129,39 +128,18 @@ function Inspect({
[metricDetailsData],
);
const aggregateAttribute = useMemo(
() => ({
key: currentMetricName ?? '',
dataType: DataTypes.String,
type: selectedMetricType as string,
isColumn: true,
isJSON: false,
id: `${currentMetricName}--${DataTypes.String}--${selectedMetricType}--true`,
}),
[currentMetricName, selectedMetricType],
);
const [currentQueryData, setCurrentQueryData] = useState<IBuilderQuery>({
...searchQuery,
aggregateAttribute,
});
useEffect(() => {
if (searchQuery) {
setCurrentQueryData({
...searchQuery,
aggregateAttribute,
});
}
}, [aggregateAttribute, searchQuery]);
const resetInspection = useCallback(() => {
setShowExpandedView(false);
setPopoverOptions(null);
setExpandedViewOptions(null);
setCurrentQueryData(searchQuery as IBuilderQuery);
reset();
}, [reset, searchQuery]);
}, [reset]);
// Reset inspection when the selected metric changes
useEffect(() => {
resetInspection();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [metricName]);
// Hide expanded view whenever inspection step changes
useEffect(() => {
@@ -215,7 +193,7 @@ function Inspect({
inspectMetricsTimeSeries={aggregatedTimeSeries}
formattedInspectMetricsTimeSeries={formattedInspectMetricsTimeSeries}
resetInspection={resetInspection}
metricName={appliedMetricName}
metricName={metricName}
metricUnit={selectedMetricUnit}
metricType={selectedMetricType}
spaceAggregationSeriesMap={spaceAggregationSeriesMap}
@@ -225,20 +203,19 @@ function Inspect({
showExpandedView={showExpandedView}
setExpandedViewOptions={setExpandedViewOptions}
popoverOptions={popoverOptions}
appliedMetricInspectionOptions={metricInspectionOptions.appliedOptions}
metricInspectionOptions={metricInspectionOptions}
isInspectMetricsRefetching={isInspectMetricsRefetching}
/>
<QueryBuilder
currentMetricName={currentMetricName}
setCurrentMetricName={setCurrentMetricName}
setAppliedMetricName={setAppliedMetricName}
metricName={metricName}
metricType={selectedMetricType}
setMetricName={setMetricName}
spaceAggregationLabels={spaceAggregationLabels}
currentMetricInspectionOptions={metricInspectionOptions.currentOptions}
metricInspectionOptions={metricInspectionOptions}
dispatchMetricInspectionOptions={handleDispatchMetricInspectionOptions}
inspectionStep={inspectionStep}
inspectMetricsTimeSeries={inspectMetricsTimeSeries}
currentQuery={currentQueryData}
setCurrentQuery={setCurrentQueryData}
searchQuery={searchQuery as IBuilderQuery}
/>
</div>
<div className="inspect-metrics-content-second-col">
@@ -251,7 +228,7 @@ function Inspect({
options={expandedViewOptions}
spaceAggregationSeriesMap={spaceAggregationSeriesMap}
step={inspectionStep}
appliedMetricInspectionOptions={metricInspectionOptions.appliedOptions}
metricInspectionOptions={metricInspectionOptions}
timeAggregatedSeriesMap={timeAggregatedSeriesMap}
/>
)}
@@ -267,21 +244,17 @@ function Inspect({
aggregatedTimeSeries,
formattedInspectMetricsTimeSeries,
resetInspection,
appliedMetricName,
metricName,
selectedMetricUnit,
selectedMetricType,
spaceAggregationSeriesMap,
inspectionStep,
showExpandedView,
popoverOptions,
metricInspectionOptions.appliedOptions,
metricInspectionOptions.currentOptions,
currentMetricName,
setCurrentMetricName,
setAppliedMetricName,
metricInspectionOptions,
spaceAggregationLabels,
handleDispatchMetricInspectionOptions,
currentQueryData,
searchQuery,
expandedViewOptions,
timeAggregatedSeriesMap,
]);

View File

@@ -1,6 +1,5 @@
import { useCallback } from 'react';
import { Button, Card } from 'antd';
import { Atom, Play } from 'lucide-react';
import { Atom } from 'lucide-react';
import { QueryBuilderProps } from './types';
import {
@@ -11,24 +10,16 @@ import {
} from './utils';
function QueryBuilder({
currentMetricName,
setCurrentMetricName,
setAppliedMetricName,
metricName,
setMetricName,
spaceAggregationLabels,
currentMetricInspectionOptions,
metricInspectionOptions,
dispatchMetricInspectionOptions,
inspectionStep,
inspectMetricsTimeSeries,
currentQuery,
setCurrentQuery,
searchQuery,
metricType,
}: QueryBuilderProps): JSX.Element {
const applyInspectionOptions = useCallback(() => {
setAppliedMetricName(currentMetricName ?? '');
dispatchMetricInspectionOptions({
type: 'APPLY_INSPECTION_OPTIONS',
});
}, [currentMetricName, setAppliedMetricName, dispatchMetricInspectionOptions]);
return (
<div className="inspect-metrics-query-builder">
<div className="inspect-metrics-query-builder-header">
@@ -40,36 +31,25 @@ function QueryBuilder({
>
Query Builder
</Button>
<Button
type="primary"
className="stage-run-query"
icon={<Play size={14} />}
onClick={applyInspectionOptions}
data-testid="apply-query-button"
>
Stage & Run Query
</Button>
</div>
<Card className="inspect-metrics-query-builder-content">
<MetricNameSearch
currentMetricName={currentMetricName}
setCurrentMetricName={setCurrentMetricName}
/>
<MetricNameSearch metricName={metricName} setMetricName={setMetricName} />
<MetricFilters
dispatchMetricInspectionOptions={dispatchMetricInspectionOptions}
currentQuery={currentQuery}
setCurrentQuery={setCurrentQuery}
searchQuery={searchQuery}
metricName={metricName}
metricType={metricType || null}
/>
<MetricTimeAggregation
inspectionStep={inspectionStep}
currentMetricInspectionOptions={currentMetricInspectionOptions}
metricInspectionOptions={metricInspectionOptions}
dispatchMetricInspectionOptions={dispatchMetricInspectionOptions}
inspectMetricsTimeSeries={inspectMetricsTimeSeries}
/>
<MetricSpaceAggregation
inspectionStep={inspectionStep}
spaceAggregationLabels={spaceAggregationLabels}
currentMetricInspectionOptions={currentMetricInspectionOptions}
metricInspectionOptions={metricInspectionOptions}
dispatchMetricInspectionOptions={dispatchMetricInspectionOptions}
/>
</Card>

View File

@@ -11,13 +11,13 @@ function TableView({
setShowExpandedView,
setExpandedViewOptions,
isInspectMetricsRefetching,
appliedMetricInspectionOptions,
metricInspectionOptions,
}: TableViewProps): JSX.Element {
const isSpaceAggregatedWithoutLabel = useMemo(
() =>
!!appliedMetricInspectionOptions.spaceAggregationOption &&
appliedMetricInspectionOptions.spaceAggregationLabels.length === 0,
[appliedMetricInspectionOptions],
!!metricInspectionOptions.spaceAggregationOption &&
metricInspectionOptions.spaceAggregationLabels.length === 0,
[metricInspectionOptions],
);
const labelKeys = useMemo(() => {
if (isSpaceAggregatedWithoutLabel) {

View File

@@ -10,7 +10,7 @@ import ExpandedView from '../ExpandedView';
import {
GraphPopoverData,
InspectionStep,
InspectOptions,
MetricInspectionOptions,
SpaceAggregationOptions,
TimeAggregationOptions,
} from '../types';
@@ -62,7 +62,7 @@ describe('ExpandedView', () => {
],
]);
const mockMetricInspectionOptions: InspectOptions = {
const mockMetricInspectionOptions: MetricInspectionOptions = {
timeAggregationOption: TimeAggregationOptions.MAX,
timeAggregationInterval: 60,
spaceAggregationOption: SpaceAggregationOptions.MAX_BY,
@@ -79,7 +79,7 @@ describe('ExpandedView', () => {
options={mockOptions}
spaceAggregationSeriesMap={mockSpaceAggregationSeriesMap}
step={InspectionStep.TIME_AGGREGATION}
appliedMetricInspectionOptions={mockMetricInspectionOptions}
metricInspectionOptions={mockMetricInspectionOptions}
timeAggregatedSeriesMap={mockTimeAggregatedSeriesMap}
/>,
);
@@ -96,7 +96,7 @@ describe('ExpandedView', () => {
options={mockOptions}
spaceAggregationSeriesMap={mockSpaceAggregationSeriesMap}
step={InspectionStep.SPACE_AGGREGATION}
appliedMetricInspectionOptions={{
metricInspectionOptions={{
...mockMetricInspectionOptions,
timeAggregationInterval: TIME_AGGREGATION_INTERVAL,
}}
@@ -127,7 +127,7 @@ describe('ExpandedView', () => {
options={mockOptions}
spaceAggregationSeriesMap={mockSpaceAggregationSeriesMap}
step={InspectionStep.COMPLETED}
appliedMetricInspectionOptions={mockMetricInspectionOptions}
metricInspectionOptions={mockMetricInspectionOptions}
timeAggregatedSeriesMap={mockTimeAggregatedSeriesMap}
/>,
);
@@ -153,7 +153,7 @@ describe('ExpandedView', () => {
options={mockOptions}
spaceAggregationSeriesMap={mockSpaceAggregationSeriesMap}
step={InspectionStep.TIME_AGGREGATION}
appliedMetricInspectionOptions={mockMetricInspectionOptions}
metricInspectionOptions={mockMetricInspectionOptions}
timeAggregatedSeriesMap={mockTimeAggregatedSeriesMap}
/>,
);

View File

@@ -54,7 +54,7 @@ describe('GraphView', () => {
setExpandedViewOptions: jest.fn(),
resetInspection: jest.fn(),
showExpandedView: false,
appliedMetricInspectionOptions: {
metricInspectionOptions: {
timeAggregationInterval: 60,
spaceAggregationOption: SpaceAggregationOptions.MAX_BY,
spaceAggregationLabels: ['host_name'],

View File

@@ -1,7 +1,7 @@
/* eslint-disable react/jsx-props-no-spreading */
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { fireEvent, render, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import * as appContextHooks from 'providers/App/App';
import store from 'store';
@@ -22,27 +22,6 @@ jest.mock('react-router-dom', () => ({
}),
}));
jest.mock('container/QueryBuilder/filters', () => ({
AggregatorFilter: ({ onSelect, onChange, defaultValue }: any): JSX.Element => (
<div data-testid="mock-aggregator-filter">
<input
data-testid="metric-name-input"
defaultValue={defaultValue}
onChange={(e: React.ChangeEvent<HTMLInputElement>): void =>
onChange({ key: e.target.value })
}
/>
<button
type="button"
data-testid="select-metric-button"
onClick={(): void => onSelect({ key: 'test_metric_2' })}
>
Select Metric
</button>
</div>
),
}));
jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
user: {
role: 'admin',
@@ -69,16 +48,12 @@ jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
const queryClient = new QueryClient();
const mockSetCurrentMetricName = jest.fn();
const mockSetAppliedMetricName = jest.fn();
describe('QueryBuilder', () => {
const defaultProps = {
currentMetricName: 'test_metric',
setCurrentMetricName: mockSetCurrentMetricName,
setAppliedMetricName: mockSetAppliedMetricName,
metricName: 'test_metric',
setMetricName: jest.fn(),
spaceAggregationLabels: ['label1', 'label2'],
currentMetricInspectionOptions: {
metricInspectionOptions: {
timeAggregationInterval: 60,
timeAggregationOption: TimeAggregationOptions.AVG,
spaceAggregationLabels: [],
@@ -92,13 +67,12 @@ describe('QueryBuilder', () => {
metricType: MetricType.SUM,
inspectionStep: InspectionStep.TIME_AGGREGATION,
inspectMetricsTimeSeries: [],
currentQuery: {
searchQuery: {
filters: {
items: [],
op: 'and',
},
} as any,
setCurrentQuery: jest.fn(),
};
beforeEach(() => {
@@ -159,57 +133,4 @@ describe('QueryBuilder', () => {
);
expect(screen.getByTestId('metric-space-aggregation')).toBeInTheDocument();
});
it('should call setCurrentMetricName when metric name is selected', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<QueryBuilder {...defaultProps} />
</Provider>
</QueryClientProvider>,
);
const metricNameSearch = screen.getByTestId('metric-name-search');
expect(metricNameSearch).toBeInTheDocument();
expect(screen.getByText('From')).toBeInTheDocument();
const selectButton = screen.getByTestId('select-metric-button');
fireEvent.click(selectButton);
expect(mockSetCurrentMetricName).toHaveBeenCalledWith('test_metric_2');
});
it('should call setAppliedMetricName when query is applied', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<QueryBuilder {...defaultProps} />
</Provider>
</QueryClientProvider>,
);
const applyQueryButton = screen.getByTestId('apply-query-button');
fireEvent.click(applyQueryButton);
expect(mockSetCurrentMetricName).toHaveBeenCalledTimes(0);
expect(mockSetAppliedMetricName).toHaveBeenCalledWith('test_metric');
});
it('should apply inspect options when query is applied', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<QueryBuilder {...defaultProps} />
</Provider>
</QueryClientProvider>,
);
const applyQueryButton = screen.getByTestId('apply-query-button');
fireEvent.click(applyQueryButton);
expect(defaultProps.dispatchMetricInspectionOptions).toHaveBeenCalledWith({
type: 'APPLY_INSPECTION_OPTIONS',
});
});
});

View File

@@ -49,7 +49,7 @@ describe('TableView', () => {
inspectMetricsTimeSeries: mockTimeSeries,
setShowExpandedView: jest.fn(),
setExpandedViewOptions: jest.fn(),
appliedMetricInspectionOptions: {
metricInspectionOptions: {
timeAggregationInterval: 60,
timeAggregationOption: TimeAggregationOptions.MAX,
spaceAggregationOption: SpaceAggregationOptions.MAX_BY,

View File

@@ -72,25 +72,13 @@ export const SPACE_AGGREGATION_OPTIONS_FOR_EXPANDED_VIEW: Record<
};
export const INITIAL_INSPECT_METRICS_OPTIONS: MetricInspectionOptions = {
currentOptions: {
timeAggregationOption: undefined,
timeAggregationInterval: undefined,
spaceAggregationOption: undefined,
spaceAggregationLabels: [],
filters: {
items: [],
op: 'AND',
},
},
appliedOptions: {
timeAggregationOption: undefined,
timeAggregationInterval: undefined,
spaceAggregationOption: undefined,
spaceAggregationLabels: [],
filters: {
items: [],
op: 'AND',
},
timeAggregationOption: undefined,
timeAggregationInterval: undefined,
spaceAggregationOption: undefined,
spaceAggregationLabels: [],
filters: {
items: [],
op: 'AND',
},
};

View File

@@ -43,36 +43,36 @@ export interface GraphViewProps {
showExpandedView: boolean;
setShowExpandedView: (showExpandedView: boolean) => void;
setExpandedViewOptions: (options: GraphPopoverOptions | null) => void;
appliedMetricInspectionOptions: InspectOptions;
metricInspectionOptions: MetricInspectionOptions;
isInspectMetricsRefetching: boolean;
}
export interface QueryBuilderProps {
currentMetricName: string | null;
setCurrentMetricName: (metricName: string) => void;
setAppliedMetricName: (metricName: string) => void;
metricName: string | null;
setMetricName: (metricName: string) => void;
metricType: MetricType | undefined;
spaceAggregationLabels: string[];
currentMetricInspectionOptions: InspectOptions;
metricInspectionOptions: MetricInspectionOptions;
dispatchMetricInspectionOptions: (action: MetricInspectionAction) => void;
inspectionStep: InspectionStep;
inspectMetricsTimeSeries: InspectMetricsSeries[];
currentQuery: IBuilderQuery;
setCurrentQuery: (query: IBuilderQuery) => void;
searchQuery: IBuilderQuery;
}
export interface MetricNameSearchProps {
currentMetricName: string | null;
setCurrentMetricName: (metricName: string) => void;
metricName: string | null;
setMetricName: (metricName: string) => void;
}
export interface MetricFiltersProps {
searchQuery: IBuilderQuery;
dispatchMetricInspectionOptions: (action: MetricInspectionAction) => void;
currentQuery: IBuilderQuery;
setCurrentQuery: (query: IBuilderQuery) => void;
metricName: string | null;
metricType: MetricType | null;
}
export interface MetricTimeAggregationProps {
currentMetricInspectionOptions: InspectOptions;
metricInspectionOptions: MetricInspectionOptions;
dispatchMetricInspectionOptions: (action: MetricInspectionAction) => void;
inspectionStep: InspectionStep;
inspectMetricsTimeSeries: InspectMetricsSeries[];
@@ -80,7 +80,7 @@ export interface MetricTimeAggregationProps {
export interface MetricSpaceAggregationProps {
spaceAggregationLabels: string[];
currentMetricInspectionOptions: InspectOptions;
metricInspectionOptions: MetricInspectionOptions;
dispatchMetricInspectionOptions: (action: MetricInspectionAction) => void;
inspectionStep: InspectionStep;
}
@@ -101,7 +101,7 @@ export enum SpaceAggregationOptions {
AVG_BY = 'avg_by',
}
export interface InspectOptions {
export interface MetricInspectionOptions {
timeAggregationOption: TimeAggregationOptions | undefined;
timeAggregationInterval: number | undefined;
spaceAggregationOption: SpaceAggregationOptions | undefined;
@@ -109,19 +109,13 @@ export interface InspectOptions {
filters: TagFilter;
}
export interface MetricInspectionOptions {
currentOptions: InspectOptions;
appliedOptions: InspectOptions;
}
export type MetricInspectionAction =
| { type: 'SET_TIME_AGGREGATION_OPTION'; payload: TimeAggregationOptions }
| { type: 'SET_TIME_AGGREGATION_INTERVAL'; payload: number }
| { type: 'SET_SPACE_AGGREGATION_OPTION'; payload: SpaceAggregationOptions }
| { type: 'SET_SPACE_AGGREGATION_LABELS'; payload: string[] }
| { type: 'SET_FILTERS'; payload: TagFilter }
| { type: 'RESET_INSPECTION' }
| { type: 'APPLY_INSPECTION_OPTIONS' };
| { type: 'RESET_INSPECTION' };
export enum InspectionStep {
TIME_AGGREGATION = 1,
@@ -162,7 +156,7 @@ export interface ExpandedViewProps {
options: GraphPopoverOptions | null;
spaceAggregationSeriesMap: Map<string, InspectMetricsSeries[]>;
step: InspectionStep;
appliedMetricInspectionOptions: InspectOptions;
metricInspectionOptions: MetricInspectionOptions;
timeAggregatedSeriesMap: Map<number, GraphPopoverData[]>;
}
@@ -171,7 +165,7 @@ export interface TableViewProps {
inspectMetricsTimeSeries: InspectMetricsSeries[];
setShowExpandedView: (showExpandedView: boolean) => void;
setExpandedViewOptions: (options: GraphPopoverOptions | null) => void;
appliedMetricInspectionOptions: InspectOptions;
metricInspectionOptions: MetricInspectionOptions;
isInspectMetricsRefetching: boolean;
}

View File

@@ -27,55 +27,30 @@ const metricInspectionReducer = (
case 'SET_TIME_AGGREGATION_OPTION':
return {
...state,
currentOptions: {
...state.currentOptions,
timeAggregationOption: action.payload,
},
timeAggregationOption: action.payload,
};
case 'SET_TIME_AGGREGATION_INTERVAL':
return {
...state,
currentOptions: {
...state.currentOptions,
timeAggregationInterval: action.payload,
},
timeAggregationInterval: action.payload,
};
case 'SET_SPACE_AGGREGATION_OPTION':
return {
...state,
currentOptions: {
...state.currentOptions,
spaceAggregationOption: action.payload,
},
spaceAggregationOption: action.payload,
};
case 'SET_SPACE_AGGREGATION_LABELS':
return {
...state,
currentOptions: {
...state.currentOptions,
spaceAggregationLabels: action.payload,
},
spaceAggregationLabels: action.payload,
};
case 'SET_FILTERS':
return {
...state,
currentOptions: {
...state.currentOptions,
filters: action.payload,
},
};
case 'APPLY_INSPECTION_OPTIONS':
return {
...state,
appliedOptions: {
...state.appliedOptions,
...state.currentOptions,
},
filters: action.payload,
};
case 'RESET_INSPECTION':
return {
...INITIAL_INSPECT_METRICS_OPTIONS,
};
return { ...INITIAL_INSPECT_METRICS_OPTIONS };
default:
return state;
}
@@ -109,7 +84,7 @@ export function useInspectMetrics(
metricName: metricName ?? '',
start,
end,
filters: metricInspectionOptions.appliedOptions.filters,
filters: metricInspectionOptions.filters,
},
{
enabled: !!metricName,
@@ -142,26 +117,13 @@ export function useInspectMetrics(
);
// Evaluate inspection step
const currentInspectionStep = useMemo(() => {
if (metricInspectionOptions.currentOptions.spaceAggregationOption) {
const inspectionStep = useMemo(() => {
if (metricInspectionOptions.spaceAggregationOption) {
return InspectionStep.COMPLETED;
}
if (
metricInspectionOptions.currentOptions.timeAggregationOption &&
metricInspectionOptions.currentOptions.timeAggregationInterval
) {
return InspectionStep.SPACE_AGGREGATION;
}
return InspectionStep.TIME_AGGREGATION;
}, [metricInspectionOptions]);
const appliedInspectionStep = useMemo(() => {
if (metricInspectionOptions.appliedOptions.spaceAggregationOption) {
return InspectionStep.COMPLETED;
}
if (
metricInspectionOptions.appliedOptions.timeAggregationOption &&
metricInspectionOptions.appliedOptions.timeAggregationInterval
metricInspectionOptions.timeAggregationOption &&
metricInspectionOptions.timeAggregationInterval
) {
return InspectionStep.SPACE_AGGREGATION;
}
@@ -187,26 +149,23 @@ export function useInspectMetrics(
// Apply time aggregation once required options are set
if (
appliedInspectionStep >= InspectionStep.SPACE_AGGREGATION &&
metricInspectionOptions.appliedOptions.timeAggregationOption &&
metricInspectionOptions.appliedOptions.timeAggregationInterval
inspectionStep >= InspectionStep.SPACE_AGGREGATION &&
metricInspectionOptions.timeAggregationOption &&
metricInspectionOptions.timeAggregationInterval
) {
const {
timeAggregatedSeries,
timeAggregatedSeriesMap,
} = applyTimeAggregation(
inspectMetricsTimeSeries,
metricInspectionOptions.appliedOptions,
);
} = applyTimeAggregation(inspectMetricsTimeSeries, metricInspectionOptions);
timeSeries = timeAggregatedSeries;
setTimeAggregatedSeriesMap(timeAggregatedSeriesMap);
setAggregatedTimeSeries(timeSeries);
}
// Apply space aggregation
if (appliedInspectionStep === InspectionStep.COMPLETED) {
if (inspectionStep === InspectionStep.COMPLETED) {
const { aggregatedSeries, spaceAggregatedSeriesMap } = applySpaceAggregation(
timeSeries,
metricInspectionOptions.appliedOptions,
metricInspectionOptions,
);
timeSeries = aggregatedSeries;
setSpaceAggregatedSeriesMap(spaceAggregatedSeriesMap);
@@ -227,7 +186,7 @@ export function useInspectMetrics(
const rawData = [timestamps, ...timeseriesArray];
return rawData.map((series) => new Float64Array(series));
}, [inspectMetricsTimeSeries, appliedInspectionStep, metricInspectionOptions]);
}, [inspectMetricsTimeSeries, inspectionStep, metricInspectionOptions]);
const spaceAggregationLabels = useMemo(() => {
const labels = new Set<string>();
@@ -257,7 +216,7 @@ export function useInspectMetrics(
spaceAggregationLabels,
metricInspectionOptions,
dispatchMetricInspectionOptions,
inspectionStep: currentInspectionStep,
inspectionStep,
isInspectMetricsRefetching,
spaceAggregatedSeriesMap,
aggregatedTimeSeries,

View File

@@ -5,11 +5,15 @@ import logEvent from 'api/common/logEvent';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import classNames from 'classnames';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
import { convertExpressionToFilters } from 'components/QueryBuilderV2/utils';
import { initialQueriesMap } from 'constants/queryBuilder';
import { AggregatorFilter } from 'container/QueryBuilder/filters';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { HardHat } from 'lucide-react';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
@@ -22,8 +26,8 @@ import {
GraphPopoverData,
GraphPopoverOptions,
InspectionStep,
InspectOptions,
MetricFiltersProps,
MetricInspectionOptions,
MetricNameSearchProps,
MetricSpaceAggregationProps,
MetricTimeAggregationProps,
@@ -67,13 +71,13 @@ export function getDefaultTimeAggregationInterval(
}
export function MetricNameSearch({
currentMetricName,
setCurrentMetricName,
metricName,
setMetricName,
}: MetricNameSearchProps): JSX.Element {
const [searchText, setSearchText] = useState(currentMetricName);
const [searchText, setSearchText] = useState(metricName);
const handleSetMetricName = (value: BaseAutocompleteData): void => {
setCurrentMetricName(value.key);
setMetricName(value.key);
};
const handleChange = (value: BaseAutocompleteData): void => {
@@ -98,31 +102,25 @@ export function MetricNameSearch({
export function MetricFilters({
dispatchMetricInspectionOptions,
currentQuery,
setCurrentQuery,
searchQuery,
metricName,
metricType,
}: MetricFiltersProps): JSX.Element {
const handleOnChange = (expression: string): void => {
logEvent(MetricsExplorerEvents.FilterApplied, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
});
const tagFilter = {
items: convertExpressionToFilters(expression),
op: 'AND',
};
setCurrentQuery({
...currentQuery,
filters: tagFilter,
filter: {
...currentQuery.filter,
expression,
},
expression,
});
dispatchMetricInspectionOptions({
type: 'SET_FILTERS',
payload: tagFilter,
});
};
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: searchQuery,
entityVersion: '',
});
const aggregateAttribute = useMemo(
() => ({
key: metricName ?? '',
dataType: DataTypes.String,
type: metricType,
id: `${metricName}--${DataTypes.String}--${metricType}--true`,
}),
[metricName, metricType],
);
return (
<div
@@ -130,17 +128,30 @@ export function MetricFilters({
className="inspect-metrics-input-group metric-filters"
>
<Typography.Text>Where</Typography.Text>
<QuerySearch
queryData={currentQuery}
onChange={handleOnChange}
dataSource={DataSource.METRICS}
<QueryBuilderSearch
query={{
...searchQuery,
aggregateAttribute,
}}
onChange={(value): void => {
handleChangeQueryData('filters', value);
logEvent(MetricsExplorerEvents.FilterApplied, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
});
dispatchMetricInspectionOptions({
type: 'SET_FILTERS',
payload: value,
});
}}
suffixIcon={<HardHat size={16} />}
disableNavigationShortcuts
/>
</div>
);
}
export function MetricTimeAggregation({
currentMetricInspectionOptions,
metricInspectionOptions,
dispatchMetricInspectionOptions,
inspectionStep,
inspectMetricsTimeSeries,
@@ -161,14 +172,14 @@ export function MetricTimeAggregation({
<div className="inspect-metrics-input-group">
<Typography.Text>Align with</Typography.Text>
<Select
value={currentMetricInspectionOptions.timeAggregationOption}
value={metricInspectionOptions.timeAggregationOption}
onChange={(value): void => {
dispatchMetricInspectionOptions({
type: 'SET_TIME_AGGREGATION_OPTION',
payload: value,
});
// set the time aggregation interval to the default value if it is not set
if (!currentMetricInspectionOptions.timeAggregationInterval) {
if (!metricInspectionOptions.timeAggregationInterval) {
dispatchMetricInspectionOptions({
type: 'SET_TIME_AGGREGATION_INTERVAL',
payload: getDefaultTimeAggregationInterval(
@@ -192,7 +203,7 @@ export function MetricTimeAggregation({
<Input
type="number"
className="no-arrows-input"
value={currentMetricInspectionOptions.timeAggregationInterval}
value={metricInspectionOptions.timeAggregationInterval}
placeholder="Select interval..."
suffix="seconds"
onChange={(e): void => {
@@ -211,7 +222,7 @@ export function MetricTimeAggregation({
export function MetricSpaceAggregation({
spaceAggregationLabels,
currentMetricInspectionOptions,
metricInspectionOptions,
dispatchMetricInspectionOptions,
inspectionStep,
}: MetricSpaceAggregationProps): JSX.Element {
@@ -230,7 +241,7 @@ export function MetricSpaceAggregation({
<div className="metric-space-aggregation-content">
<div className="metric-space-aggregation-content-left">
<Select
value={currentMetricInspectionOptions.spaceAggregationOption}
value={metricInspectionOptions.spaceAggregationOption}
placeholder="Select option"
onChange={(value): void => {
dispatchMetricInspectionOptions({
@@ -253,7 +264,7 @@ export function MetricSpaceAggregation({
mode="multiple"
style={{ width: '100%' }}
placeholder="Search for attributes..."
value={currentMetricInspectionOptions.spaceAggregationLabels}
value={metricInspectionOptions.spaceAggregationLabels}
onChange={(value): void => {
dispatchMetricInspectionOptions({
type: 'SET_SPACE_AGGREGATION_LABELS',
@@ -309,7 +320,7 @@ export function applyFilters(
export function applyTimeAggregation(
inspectMetricsTimeSeries: InspectMetricsSeries[],
appliedMetricInspectionOptions: InspectOptions,
metricInspectionOptions: MetricInspectionOptions,
): {
timeAggregatedSeries: InspectMetricsSeries[];
timeAggregatedSeriesMap: Map<number, GraphPopoverData[]>;
@@ -317,7 +328,7 @@ export function applyTimeAggregation(
const {
timeAggregationOption,
timeAggregationInterval,
} = appliedMetricInspectionOptions;
} = metricInspectionOptions;
if (!timeAggregationInterval) {
return {
@@ -402,7 +413,7 @@ export function applyTimeAggregation(
export function applySpaceAggregation(
inspectMetricsTimeSeries: InspectMetricsSeries[],
appliedMetricInspectionOptions: InspectOptions,
metricInspectionOptions: MetricInspectionOptions,
): {
aggregatedSeries: InspectMetricsSeries[];
spaceAggregatedSeriesMap: Map<string, InspectMetricsSeries[]>;
@@ -412,7 +423,7 @@ export function applySpaceAggregation(
inspectMetricsTimeSeries.forEach((series) => {
// Create composite key from selected labels
const key = appliedMetricInspectionOptions.spaceAggregationLabels
const key = metricInspectionOptions.spaceAggregationLabels
.map((label) => `${label}:${series.labels[label]}`)
.join(',');
@@ -447,7 +458,7 @@ export function applySpaceAggregation(
([timestamp, values]) => {
let aggregatedValue: number;
switch (appliedMetricInspectionOptions.spaceAggregationOption) {
switch (metricInspectionOptions.spaceAggregationOption) {
case SpaceAggregationOptions.SUM_BY:
aggregatedValue = values.reduce((sum, val) => sum + val, 0);
break;
@@ -703,11 +714,11 @@ export function getTimeSeriesLabel(
export function HoverPopover({
options,
step,
appliedMetricInspectionOptions,
metricInspectionOptions,
}: {
options: GraphPopoverOptions;
step: InspectionStep;
appliedMetricInspectionOptions: InspectOptions;
metricInspectionOptions: MetricInspectionOptions;
}): JSX.Element {
const closestTimestamp = useMemo(() => {
if (!options.timeSeries) {
@@ -735,7 +746,7 @@ export function HoverPopover({
const title = useMemo(() => {
if (
step === InspectionStep.COMPLETED &&
appliedMetricInspectionOptions.spaceAggregationLabels.length === 0
metricInspectionOptions.spaceAggregationLabels.length === 0
) {
return undefined;
}
@@ -749,7 +760,7 @@ export function HoverPopover({
options.timeSeries,
options.timeSeries?.strokeColor,
);
}, [step, options.timeSeries, appliedMetricInspectionOptions]);
}, [step, options.timeSeries, metricInspectionOptions]);
return (
<Card
@@ -819,26 +830,3 @@ export function onGraphHover(
timeSeries: series,
});
}
export function useMetricName(
metricName: string | null,
): {
currentMetricName: string | null;
setCurrentMetricName: (metricName: string | null) => void;
appliedMetricName: string | null;
setAppliedMetricName: (metricName: string | null) => void;
} {
const [currentMetricName, setCurrentMetricName] = useState<string | null>(
metricName,
);
const [appliedMetricName, setAppliedMetricName] = useState<string | null>(
metricName,
);
return {
currentMetricName,
setCurrentMetricName,
appliedMetricName,
setAppliedMetricName,
};
}

View File

@@ -0,0 +1,59 @@
import { useCallback, useState } from 'react';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import type { UseActiveLog } from 'hooks/logs/types';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { ILog } from 'types/api/logs/log';
type SelectedTab = typeof VIEW_TYPES[keyof typeof VIEW_TYPES] | undefined;
type UseLogDetailHandlersParams = {
defaultTab?: SelectedTab;
};
type UseLogDetailHandlersResult = {
activeLog: UseActiveLog['activeLog'];
onAddToQuery: UseActiveLog['onAddToQuery'];
selectedTab: SelectedTab;
handleSetActiveLog: (log: ILog, selectedTab?: SelectedTab) => void;
handleCloseLogDetail: () => void;
};
function useLogDetailHandlers({
defaultTab = VIEW_TYPES.OVERVIEW,
}: UseLogDetailHandlersParams = {}): UseLogDetailHandlersResult {
const {
activeLog,
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
} = useActiveLog();
const [selectedTab, setSelectedTab] = useState<SelectedTab>(defaultTab);
const handleSetActiveLog = useCallback(
(log: ILog, nextTab: SelectedTab = defaultTab): void => {
if (activeLog?.id === log.id) {
onClearActiveLog();
setSelectedTab(undefined);
return;
}
onSetActiveLog(log);
setSelectedTab(nextTab ?? defaultTab);
},
[activeLog?.id, defaultTab, onClearActiveLog, onSetActiveLog],
);
const handleCloseLogDetail = useCallback((): void => {
onClearActiveLog();
setSelectedTab(undefined);
}, [onClearActiveLog]);
return {
activeLog,
onAddToQuery,
selectedTab,
handleSetActiveLog,
handleCloseLogDetail,
};
}
export default useLogDetailHandlers;

View File

@@ -0,0 +1,28 @@
import { useCallback } from 'react';
import type { VirtuosoHandle } from 'react-virtuoso';
type UseScrollToLogParams = {
logs: Array<{ id: string }>;
virtuosoRef: React.RefObject<VirtuosoHandle | null>;
};
function useScrollToLog({
logs,
virtuosoRef,
}: UseScrollToLogParams): (logId: string) => void {
return useCallback(
(logId: string): void => {
const logIndex = logs.findIndex(({ id }) => id === logId);
if (logIndex !== -1 && virtuosoRef.current) {
virtuosoRef.current.scrollToIndex({
index: logIndex,
align: 'center',
behavior: 'smooth',
});
}
},
[logs, virtuosoRef],
);
}
export default useScrollToLog;

View File

@@ -567,6 +567,15 @@ body {
border: 1px solid var(--bg-vanilla-300);
}
.ant-tooltip {
--antd-arrow-background-color: var(--bg-vanilla-100);
.ant-tooltip-inner {
background-color: var(--bg-vanilla-100);
color: var(---bg-ink-500);
}
}
.ant-dropdown-menu {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);

View File

@@ -9,10 +9,9 @@ export const getDefaultLogBackground = (
if (isReadOnly) {
return '';
}
// TODO handle the light mode here
return `&:hover {
background-color: ${
isDarkMode ? 'rgba(171, 189, 255, 0.04)' : 'var(--bg-vanilla-200)'
isDarkMode ? 'rgba(171, 189, 255, 0.04)' : 'rgba(0, 0, 0, 0.04)'
};
}`;
};
@@ -28,22 +27,38 @@ export const getActiveLogBackground = (
if (isDarkMode) {
switch (logType) {
case LogType.INFO:
return `background-color: ${Color.BG_ROBIN_500}10 !important;`;
return `background-color: ${Color.BG_ROBIN_500}40 !important;`;
case LogType.WARN:
return `background-color: ${Color.BG_AMBER_500}10 !important;`;
return `background-color: ${Color.BG_AMBER_500}40 !important;`;
case LogType.ERROR:
return `background-color: ${Color.BG_CHERRY_500}10 !important;`;
return `background-color: ${Color.BG_CHERRY_500}40 !important;`;
case LogType.TRACE:
return `background-color: ${Color.BG_FOREST_400}10 !important;`;
return `background-color: ${Color.BG_FOREST_400}40 !important;`;
case LogType.DEBUG:
return `background-color: ${Color.BG_AQUA_500}10 !important;`;
return `background-color: ${Color.BG_AQUA_500}40 !important;`;
case LogType.FATAL:
return `background-color: ${Color.BG_SAKURA_500}10 !important;`;
return `background-color: ${Color.BG_SAKURA_500}40 !important;`;
default:
return `background-color: ${Color.BG_SLATE_200} !important;`;
return `background-color: ${Color.BG_ROBIN_500}40 !important;`;
}
}
return `background-color: ${Color.BG_VANILLA_400}!important; color: ${Color.TEXT_SLATE_400} !important;`;
// Light mode - use lighter background colors
switch (logType) {
case LogType.INFO:
return `background-color: ${Color.BG_ROBIN_100} !important;`;
case LogType.WARN:
return `background-color: ${Color.BG_AMBER_100} !important;`;
case LogType.ERROR:
return `background-color: ${Color.BG_CHERRY_100} !important;`;
case LogType.TRACE:
return `background-color: ${Color.BG_FOREST_200} !important;`;
case LogType.DEBUG:
return `background-color: ${Color.BG_AQUA_100} !important;`;
case LogType.FATAL:
return `background-color: ${Color.BG_SAKURA_100} !important;`;
default:
return `background-color: ${Color.BG_VANILLA_300} !important;`;
}
};
export const getHightLightedLogBackground = (