Compare commits
4 Commits
fix/pass_t
...
tvats-hand
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
509a1cfb85 | ||
|
|
fd118d386a | ||
|
|
8752022cef | ||
|
|
c7e4a9c45d |
@@ -10,6 +10,10 @@ import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { useHandleLogsPagination } from 'hooks/infraMonitoring/useHandleLogsPagination';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import {
|
||||
LOG_FIELD_BODY_KEY,
|
||||
LOG_FIELD_TIMESTAMP_KEY,
|
||||
} from 'lib/logs/flatLogData';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
@@ -85,11 +89,15 @@ function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
|
||||
dataType: 'string',
|
||||
type: '',
|
||||
name: 'body',
|
||||
displayName: 'Body',
|
||||
key: LOG_FIELD_BODY_KEY,
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
type: '',
|
||||
name: 'timestamp',
|
||||
displayName: 'Timestamp',
|
||||
key: LOG_FIELD_TIMESTAMP_KEY,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -13,6 +13,10 @@ import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
// utils
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import {
|
||||
LOG_FIELD_BODY_KEY,
|
||||
LOG_FIELD_TIMESTAMP_KEY,
|
||||
} from 'lib/logs/flatLogData';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
// interfaces
|
||||
@@ -42,7 +46,9 @@ interface LogFieldProps {
|
||||
}
|
||||
|
||||
type LogSelectedFieldProps = Omit<LogFieldProps, 'linesPerRow'> &
|
||||
Pick<AddToQueryHOCProps, 'onAddToQuery'>;
|
||||
Pick<AddToQueryHOCProps, 'onAddToQuery'> & {
|
||||
fieldKeyDisplay: string;
|
||||
};
|
||||
|
||||
function LogGeneralField({
|
||||
fieldKey,
|
||||
@@ -74,6 +80,7 @@ function LogGeneralField({
|
||||
function LogSelectedField({
|
||||
fieldKey = '',
|
||||
fieldValue = '',
|
||||
fieldKeyDisplay = '',
|
||||
onAddToQuery,
|
||||
fontSize,
|
||||
}: LogSelectedFieldProps): JSX.Element {
|
||||
@@ -90,7 +97,7 @@ function LogSelectedField({
|
||||
style={{ color: blue[4] }}
|
||||
className={cx('selected-log-field-key', fontSize)}
|
||||
>
|
||||
{fieldKey}
|
||||
{fieldKeyDisplay}
|
||||
</span>
|
||||
</Typography.Text>
|
||||
</AddToQueryHOC>
|
||||
@@ -162,7 +169,7 @@ function ListLogView({
|
||||
);
|
||||
|
||||
const updatedSelecedFields = useMemo(
|
||||
() => selectedFields.filter((e) => e.name !== 'id'),
|
||||
() => selectedFields.filter((e) => e.key !== 'id'),
|
||||
[selectedFields],
|
||||
);
|
||||
|
||||
@@ -170,16 +177,16 @@ function ListLogView({
|
||||
|
||||
const timestampValue = useMemo(
|
||||
() =>
|
||||
typeof flattenLogData.timestamp === 'string'
|
||||
typeof flattenLogData[LOG_FIELD_TIMESTAMP_KEY] === 'string'
|
||||
? formatTimezoneAdjustedTimestamp(
|
||||
flattenLogData.timestamp,
|
||||
flattenLogData[LOG_FIELD_TIMESTAMP_KEY],
|
||||
DATE_TIME_FORMATS.ISO_DATETIME_MS,
|
||||
)
|
||||
: formatTimezoneAdjustedTimestamp(
|
||||
flattenLogData.timestamp / 1e6,
|
||||
flattenLogData[LOG_FIELD_TIMESTAMP_KEY] / 1e6,
|
||||
DATE_TIME_FORMATS.ISO_DATETIME_MS,
|
||||
),
|
||||
[flattenLogData.timestamp, formatTimezoneAdjustedTimestamp],
|
||||
[flattenLogData, formatTimezoneAdjustedTimestamp],
|
||||
);
|
||||
|
||||
const logType = getLogIndicatorType(logData);
|
||||
@@ -215,10 +222,12 @@ function ListLogView({
|
||||
/>
|
||||
<div>
|
||||
<LogContainer fontSize={fontSize}>
|
||||
{updatedSelecedFields.some((field) => field.name === 'body') && (
|
||||
{updatedSelecedFields.some(
|
||||
(field) => field.key === LOG_FIELD_BODY_KEY,
|
||||
) && (
|
||||
<LogGeneralField
|
||||
fieldKey="Log"
|
||||
fieldValue={flattenLogData.body}
|
||||
fieldValue={flattenLogData[LOG_FIELD_BODY_KEY]}
|
||||
linesPerRow={linesPerRow}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
@@ -230,7 +239,9 @@ function ListLogView({
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
)}
|
||||
{updatedSelecedFields.some((field) => field.name === 'timestamp') && (
|
||||
{updatedSelecedFields.some(
|
||||
(field) => field.key === LOG_FIELD_TIMESTAMP_KEY,
|
||||
) && (
|
||||
<LogGeneralField
|
||||
fieldKey="Timestamp"
|
||||
fieldValue={timestampValue}
|
||||
@@ -239,13 +250,17 @@ function ListLogView({
|
||||
)}
|
||||
|
||||
{updatedSelecedFields
|
||||
.filter((field) => !['timestamp', 'body'].includes(field.name))
|
||||
.filter(
|
||||
(field) =>
|
||||
![LOG_FIELD_TIMESTAMP_KEY, LOG_FIELD_BODY_KEY].includes(field.key),
|
||||
)
|
||||
.map((field) =>
|
||||
isValidLogField(flattenLogData[field.name] as never) ? (
|
||||
isValidLogField(flattenLogData[field.key] as never) ? (
|
||||
<LogSelectedField
|
||||
key={field.name}
|
||||
fieldKey={field.name}
|
||||
fieldValue={flattenLogData[field.name] as never}
|
||||
key={field.key}
|
||||
fieldKey={field.key}
|
||||
fieldKeyDisplay={field.displayName}
|
||||
fieldValue={flattenLogData[field.key] as never}
|
||||
onAddToQuery={onAddToQuery}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
|
||||
@@ -73,16 +73,25 @@ function RawLogView({
|
||||
);
|
||||
|
||||
const attributesValues = updatedSelecedFields
|
||||
.filter((field) => !['timestamp', 'body'].includes(field.name))
|
||||
.map((field) => flattenLogData[field.name])
|
||||
.filter((attribute) => {
|
||||
.filter(
|
||||
(field) => !['log.timestamp:string', 'log.body:string'].includes(field.key),
|
||||
)
|
||||
.map((field) => {
|
||||
const value = flattenLogData[field.key];
|
||||
const label = field.displayName;
|
||||
|
||||
// loadash isEmpty doesnot work with numbers
|
||||
if (isNumber(attribute)) {
|
||||
return true;
|
||||
if (isNumber(value)) {
|
||||
return `${label}: ${value}`;
|
||||
}
|
||||
|
||||
return !isUndefined(attribute) && !isEmpty(attribute);
|
||||
});
|
||||
if (!isUndefined(value) && !isEmpty(value)) {
|
||||
return `${label}: ${value}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((attribute) => attribute !== null);
|
||||
|
||||
let attributesText = attributesValues.join(' | ');
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ import cx from 'classnames';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import {
|
||||
FlatLogData,
|
||||
LOG_FIELD_BODY_KEY,
|
||||
LOG_FIELD_TIMESTAMP_KEY,
|
||||
} from 'lib/logs/flatLogData';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
@@ -51,28 +55,33 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
|
||||
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
|
||||
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
|
||||
.filter((e) => !['id', 'body', 'timestamp'].includes(e.name))
|
||||
.map(({ name }) => ({
|
||||
title: name,
|
||||
dataIndex: name,
|
||||
accessorKey: name,
|
||||
id: name.toLowerCase().replace(/\./g, '_'),
|
||||
key: name,
|
||||
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
|
||||
props: {
|
||||
style: isListViewPanel
|
||||
? defaultListViewPanelStyle
|
||||
: getDefaultCellStyle(isDarkMode),
|
||||
},
|
||||
children: (
|
||||
<Typography.Paragraph
|
||||
ellipsis={{ rows: linesPerRow }}
|
||||
className={cx('paragraph', fontSize)}
|
||||
>
|
||||
{field}
|
||||
</Typography.Paragraph>
|
||||
),
|
||||
}),
|
||||
.filter(
|
||||
(e) => !['id', LOG_FIELD_BODY_KEY, LOG_FIELD_TIMESTAMP_KEY].includes(e.key),
|
||||
)
|
||||
.map((field) => ({
|
||||
title: field.displayName,
|
||||
dataIndex: field.key,
|
||||
accessorKey: field.key,
|
||||
id: field.key.toLowerCase().replace(/\./g, '_').replace(/:/g, '_'),
|
||||
key: field.key,
|
||||
render: (fieldValue, record): ColumnTypeRender<Record<string, unknown>> => {
|
||||
const value = record[field.key] || fieldValue;
|
||||
return {
|
||||
props: {
|
||||
style: isListViewPanel
|
||||
? defaultListViewPanelStyle
|
||||
: getDefaultCellStyle(isDarkMode),
|
||||
},
|
||||
children: (
|
||||
<Typography.Paragraph
|
||||
ellipsis={{ rows: linesPerRow }}
|
||||
className={cx('paragraph', fontSize)}
|
||||
>
|
||||
{value}
|
||||
</Typography.Paragraph>
|
||||
),
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
if (isListViewPanel) {
|
||||
@@ -100,26 +109,29 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
),
|
||||
}),
|
||||
},
|
||||
...(fields.some((field) => field.name === 'timestamp')
|
||||
...(fields.some((field) => field.key === LOG_FIELD_TIMESTAMP_KEY)
|
||||
? [
|
||||
{
|
||||
title: 'timestamp',
|
||||
dataIndex: 'timestamp',
|
||||
dataIndex: LOG_FIELD_TIMESTAMP_KEY,
|
||||
key: 'timestamp',
|
||||
accessorKey: 'timestamp',
|
||||
accessorKey: LOG_FIELD_TIMESTAMP_KEY,
|
||||
id: 'timestamp',
|
||||
// https://github.com/ant-design/ant-design/discussions/36886
|
||||
render: (
|
||||
field: string | number,
|
||||
record: Record<string, unknown>,
|
||||
): ColumnTypeRender<Record<string, unknown>> => {
|
||||
const timestampValue =
|
||||
(record[LOG_FIELD_TIMESTAMP_KEY] as string | number) || field;
|
||||
const date =
|
||||
typeof field === 'string'
|
||||
typeof timestampValue === 'string'
|
||||
? formatTimezoneAdjustedTimestamp(
|
||||
field,
|
||||
timestampValue,
|
||||
DATE_TIME_FORMATS.ISO_DATETIME_MS,
|
||||
)
|
||||
: formatTimezoneAdjustedTimestamp(
|
||||
field / 1e6,
|
||||
timestampValue / 1e6,
|
||||
DATE_TIME_FORMATS.ISO_DATETIME_MS,
|
||||
);
|
||||
return {
|
||||
@@ -136,33 +148,37 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
]
|
||||
: []),
|
||||
...(appendTo === 'center' ? fieldColumns : []),
|
||||
...(fields.some((field) => field.name === 'body')
|
||||
...(fields.some((field) => field.key === LOG_FIELD_BODY_KEY)
|
||||
? [
|
||||
{
|
||||
title: 'body',
|
||||
dataIndex: 'body',
|
||||
dataIndex: LOG_FIELD_BODY_KEY,
|
||||
key: 'body',
|
||||
accessorKey: 'body',
|
||||
accessorKey: LOG_FIELD_BODY_KEY,
|
||||
id: 'body',
|
||||
render: (
|
||||
field: string | number,
|
||||
): ColumnTypeRender<Record<string, unknown>> => ({
|
||||
props: {
|
||||
style: bodyColumnStyle,
|
||||
},
|
||||
children: (
|
||||
<TableBodyContent
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: getSanitizedLogBody(field as string, {
|
||||
shouldEscapeHtml: true,
|
||||
}),
|
||||
}}
|
||||
fontSize={fontSize}
|
||||
linesPerRow={linesPerRow}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
record: Record<string, unknown>,
|
||||
): ColumnTypeRender<Record<string, unknown>> => {
|
||||
const bodyValue = (record[LOG_FIELD_BODY_KEY] as string) || '';
|
||||
return {
|
||||
props: {
|
||||
style: bodyColumnStyle,
|
||||
},
|
||||
children: (
|
||||
<TableBodyContent
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: getSanitizedLogBody(bodyValue, {
|
||||
shouldEscapeHtml: true,
|
||||
}),
|
||||
}}
|
||||
fontSize={fontSize}
|
||||
linesPerRow={linesPerRow}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
@@ -416,18 +416,21 @@ function OptionsMenu({
|
||||
)}
|
||||
|
||||
<div className="column-format">
|
||||
{addColumn?.value?.map(({ name }) => (
|
||||
<div className="column-name" key={name}>
|
||||
{addColumn?.value?.map((column) => (
|
||||
<div className="column-name" key={column.key}>
|
||||
<div className="name">
|
||||
<Tooltip placement="left" title={name}>
|
||||
{name}
|
||||
<Tooltip
|
||||
placement="left"
|
||||
title={column.displayName || column.name}
|
||||
>
|
||||
{column.displayName || column.name}
|
||||
</Tooltip>
|
||||
</div>
|
||||
{addColumn?.value?.length > 1 && (
|
||||
<X
|
||||
className="delete-btn"
|
||||
size={14}
|
||||
onClick={(): void => addColumn.onRemove(name)}
|
||||
onClick={(): void => addColumn.onRemove(column.key)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -17,12 +17,6 @@ export const Card = styled(CardComponent)<CardProps>`
|
||||
overflow: hidden;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(171, 189, 255, 0) 0%,
|
||||
rgba(171, 189, 255, 0) 100%
|
||||
),
|
||||
#0b0c0e;
|
||||
|
||||
${({ isDarkMode }): StyledCSS =>
|
||||
!isDarkMode &&
|
||||
|
||||
@@ -11,6 +11,10 @@ import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { useHandleLogsPagination } from 'hooks/infraMonitoring/useHandleLogsPagination';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import {
|
||||
LOG_FIELD_BODY_KEY,
|
||||
LOG_FIELD_TIMESTAMP_KEY,
|
||||
} from 'lib/logs/flatLogData';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
@@ -75,11 +79,15 @@ function EntityLogs({
|
||||
dataType: 'string',
|
||||
type: '',
|
||||
name: 'body',
|
||||
displayName: 'Body',
|
||||
key: LOG_FIELD_BODY_KEY,
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
type: '',
|
||||
name: 'timestamp',
|
||||
displayName: 'Timestamp',
|
||||
key: LOG_FIELD_TIMESTAMP_KEY,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,6 @@ import InfinityTableView from 'container/LogsExplorerList/InfinityTableView';
|
||||
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 { useEventSource } from 'providers/EventSource';
|
||||
@@ -57,10 +56,7 @@ function LiveLogsList({ logs, isLoading }: LiveLogsListProps): JSX.Element {
|
||||
[formattedLogs, activeLogId],
|
||||
);
|
||||
|
||||
const selectedFields = convertKeysToColumnFields([
|
||||
...defaultLogsSelectedColumns,
|
||||
...options.selectColumns,
|
||||
]);
|
||||
const selectedFields = convertKeysToColumnFields(options.selectColumns);
|
||||
|
||||
const getItemContent = useCallback(
|
||||
(_: number, log: ILog): JSX.Element => {
|
||||
|
||||
@@ -13,7 +13,9 @@ export const convertKeysToColumnFields = (
|
||||
.filter((item) => !isEmpty(item.name))
|
||||
.map((item) => ({
|
||||
dataType: item.fieldDataType ?? '',
|
||||
name: item.name,
|
||||
name: item.name ?? '',
|
||||
key: item.key ?? '',
|
||||
displayName: item.displayName ?? '',
|
||||
type: item.fieldContext ?? '',
|
||||
}));
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import { Typography } from 'antd/lib';
|
||||
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
|
||||
import {
|
||||
LOG_FIELD_BODY_KEY,
|
||||
LOG_FIELD_TIMESTAMP_KEY,
|
||||
} from 'lib/logs/flatLogData';
|
||||
// import Typography from 'antd/es/typography/Typography';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { ReactNode } from 'react';
|
||||
@@ -18,15 +22,15 @@ export const getLogPanelColumnsList = (
|
||||
|
||||
const columns: ColumnsType<RowData> =
|
||||
selectedLogFields?.map((field: IField) => {
|
||||
const { name } = field;
|
||||
const { name, key, displayName } = field;
|
||||
|
||||
return {
|
||||
title: name,
|
||||
dataIndex: name,
|
||||
key: name,
|
||||
width: name === 'body' ? 350 : 100,
|
||||
title: displayName,
|
||||
dataIndex: key,
|
||||
key,
|
||||
width: key === LOG_FIELD_BODY_KEY ? 350 : 100,
|
||||
render: (value: ReactNode): JSX.Element => {
|
||||
if (name === 'timestamp') {
|
||||
if (key === LOG_FIELD_TIMESTAMP_KEY) {
|
||||
return (
|
||||
<Typography.Text>
|
||||
{formatTimezoneAdjustedTimestamp(value as string)}
|
||||
@@ -34,7 +38,7 @@ export const getLogPanelColumnsList = (
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'body') {
|
||||
if (key === LOG_FIELD_BODY_KEY) {
|
||||
return (
|
||||
<Typography.Paragraph ellipsis={{ rows: 1 }} data-testid={name}>
|
||||
{value}
|
||||
|
||||
@@ -558,8 +558,10 @@ export const getDefaultWidgetData = (
|
||||
decimalPrecision: PrecisionOptionsEnum.TWO, // default decimal precision
|
||||
selectedLogFields: defaultLogsSelectedColumns.map((field) => ({
|
||||
...field,
|
||||
key: field.key ?? '',
|
||||
type: field.fieldContext ?? '',
|
||||
dataType: field.fieldDataType ?? '',
|
||||
displayName: field.displayName || field.name,
|
||||
})),
|
||||
selectedTracesFields: defaultTraceSelectedColumns,
|
||||
});
|
||||
|
||||
@@ -42,10 +42,12 @@ function AddColumnField({ config }: AddColumnFieldProps): JSX.Element | null {
|
||||
</SearchIconWrapper>
|
||||
</Input.Group>
|
||||
|
||||
{config.value?.map(({ name }) => (
|
||||
<AddColumnItem direction="horizontal" key={name}>
|
||||
<Typography>{name}</Typography>
|
||||
<DeleteOutlinedIcon onClick={(): void => config.onRemove(name)} />
|
||||
{config.value?.map((column) => (
|
||||
<AddColumnItem direction="horizontal" key={column.key}>
|
||||
<Typography>{column.displayName || column.name}</Typography>
|
||||
<DeleteOutlinedIcon
|
||||
onClick={(): void => config.onRemove(column.key || column.name)}
|
||||
/>
|
||||
</AddColumnItem>
|
||||
))}
|
||||
</AddColumnWrapper>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import {
|
||||
LOG_FIELD_BODY_KEY,
|
||||
LOG_FIELD_TIMESTAMP_KEY,
|
||||
} from 'lib/logs/flatLogData';
|
||||
|
||||
import { FontSize, OptionsQuery } from './types';
|
||||
|
||||
@@ -16,15 +20,19 @@ export const defaultLogsSelectedColumns: TelemetryFieldKey[] = [
|
||||
name: 'timestamp',
|
||||
signal: 'logs',
|
||||
fieldContext: 'log',
|
||||
fieldDataType: '',
|
||||
fieldDataType: 'string',
|
||||
isIndexed: false,
|
||||
key: LOG_FIELD_TIMESTAMP_KEY,
|
||||
displayName: 'Timestamp',
|
||||
},
|
||||
{
|
||||
name: 'body',
|
||||
signal: 'logs',
|
||||
fieldContext: 'log',
|
||||
fieldDataType: '',
|
||||
fieldDataType: 'string',
|
||||
isIndexed: false,
|
||||
key: LOG_FIELD_BODY_KEY,
|
||||
displayName: 'Body',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -34,29 +42,47 @@ export const defaultTraceSelectedColumns: TelemetryFieldKey[] = [
|
||||
signal: 'traces',
|
||||
fieldContext: 'resource',
|
||||
fieldDataType: 'string',
|
||||
key: 'resource.service.name:string',
|
||||
displayName: 'Service Name',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
signal: 'traces',
|
||||
fieldContext: 'span',
|
||||
fieldDataType: 'string',
|
||||
key: 'span.name:string',
|
||||
displayName: 'Span Name',
|
||||
},
|
||||
{
|
||||
name: 'duration_nano',
|
||||
signal: 'traces',
|
||||
fieldContext: 'span',
|
||||
fieldDataType: '',
|
||||
fieldDataType: 'number',
|
||||
key: 'span.duration_nano:number',
|
||||
displayName: 'Duration',
|
||||
},
|
||||
{
|
||||
name: 'http_method',
|
||||
signal: 'traces',
|
||||
fieldContext: 'span',
|
||||
fieldDataType: '',
|
||||
fieldDataType: 'string',
|
||||
key: 'span.http_method:string',
|
||||
displayName: 'HTTP Method',
|
||||
},
|
||||
{
|
||||
name: 'response_status_code',
|
||||
signal: 'traces',
|
||||
fieldContext: 'span',
|
||||
fieldDataType: '',
|
||||
fieldDataType: 'string',
|
||||
key: 'span.response_status_code:string',
|
||||
displayName: 'Status Code',
|
||||
},
|
||||
{
|
||||
name: 'timestamp',
|
||||
signal: 'traces',
|
||||
fieldContext: 'span',
|
||||
fieldDataType: 'string',
|
||||
key: 'span.timestamp:string',
|
||||
displayName: 'Timestamp',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -16,11 +16,6 @@ import {
|
||||
QueryKeyRequestProps,
|
||||
QueryKeySuggestionsResponseProps,
|
||||
} from 'types/api/querySuggestions/types';
|
||||
import {
|
||||
FieldContext,
|
||||
FieldDataType,
|
||||
SignalType,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
@@ -35,7 +30,11 @@ import {
|
||||
OptionsMenuConfig,
|
||||
OptionsQuery,
|
||||
} from './types';
|
||||
import { getOptionsFromKeys } from './utils';
|
||||
import {
|
||||
createTelemetryFieldKey,
|
||||
getOptionsFromKeys,
|
||||
resolveColumnConflicts,
|
||||
} from './utils';
|
||||
|
||||
interface UseOptionsMenuProps {
|
||||
storageKey?: string;
|
||||
@@ -104,28 +103,19 @@ const useOptionsMenu = ({
|
||||
return [];
|
||||
}
|
||||
|
||||
const attributesData = initialAttributesResult?.reduce(
|
||||
(acc: TelemetryFieldKey[], attributeResponse): TelemetryFieldKey[] => {
|
||||
const suggestions =
|
||||
Object.values(attributeResponse?.data?.data?.data?.keys || {}).flat() ||
|
||||
[];
|
||||
// Collect all suggestions from all API responses first
|
||||
const allSuggestions =
|
||||
initialAttributesResult?.flatMap((attributeResponse) =>
|
||||
Object.values(attributeResponse?.data?.data?.data?.keys || {})
|
||||
.flat()
|
||||
.map((suggestion) => createTelemetryFieldKey(suggestion)),
|
||||
) || [];
|
||||
|
||||
const mappedSuggestions: TelemetryFieldKey[] = suggestions.map(
|
||||
(suggestion) => ({
|
||||
name: suggestion.name,
|
||||
signal: suggestion.signal as SignalType,
|
||||
fieldDataType: suggestion.fieldDataType as FieldDataType,
|
||||
fieldContext: suggestion.fieldContext as FieldContext,
|
||||
}),
|
||||
);
|
||||
|
||||
return [...acc, ...mappedSuggestions];
|
||||
},
|
||||
[],
|
||||
);
|
||||
// Resolve conflicts and deduplicate once at the end for better performance
|
||||
const attributesData = resolveColumnConflicts(allSuggestions);
|
||||
|
||||
let initialSelected: TelemetryFieldKey[] = (initialOptions?.selectColumns
|
||||
?.map((column) => attributesData.find(({ name }) => name === column))
|
||||
?.map((column) => attributesData.find(({ key }) => key === column))
|
||||
.filter((e) => !!e) || []) as TelemetryFieldKey[];
|
||||
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
@@ -133,13 +123,15 @@ const useOptionsMenu = ({
|
||||
?.map((col) => {
|
||||
if (col && Object.keys(AllTraceFilterKeyValue).includes(col?.name)) {
|
||||
const metaData = defaultTraceSelectedColumns.find(
|
||||
(coln) => coln.name === col.name,
|
||||
(coln) => coln.key === col.key,
|
||||
);
|
||||
|
||||
return {
|
||||
...metaData,
|
||||
name: metaData?.name || '',
|
||||
};
|
||||
if (metaData) {
|
||||
return {
|
||||
...metaData,
|
||||
name: metaData.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
return col;
|
||||
})
|
||||
@@ -187,40 +179,23 @@ const useOptionsMenu = ({
|
||||
const searchedAttributesDataList = Object.values(
|
||||
searchedAttributesDataV5?.data.data.keys || {},
|
||||
).flat();
|
||||
|
||||
if (searchedAttributesDataList.length) {
|
||||
if (dataSource === DataSource.LOGS) {
|
||||
const logsSelectedColumns: TelemetryFieldKey[] = defaultLogsSelectedColumns.map(
|
||||
(e) => ({
|
||||
...e,
|
||||
name: e.name,
|
||||
signal: e.signal as SignalType,
|
||||
fieldContext: e.fieldContext as FieldContext,
|
||||
fieldDataType: e.fieldDataType as FieldDataType,
|
||||
}),
|
||||
);
|
||||
return [
|
||||
...logsSelectedColumns,
|
||||
...searchedAttributesDataList
|
||||
.filter((attribute) => attribute.name !== 'body')
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
.map((e) => ({
|
||||
...e,
|
||||
name: e.name,
|
||||
signal: e.signal as SignalType,
|
||||
fieldContext: e.fieldContext as FieldContext,
|
||||
fieldDataType: e.fieldDataType as FieldDataType,
|
||||
})),
|
||||
];
|
||||
}
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
return searchedAttributesDataList.map((e) => ({
|
||||
...e,
|
||||
name: e.name,
|
||||
signal: e.signal as SignalType,
|
||||
fieldContext: e.fieldContext as FieldContext,
|
||||
fieldDataType: e.fieldDataType as FieldDataType,
|
||||
}));
|
||||
// Map all attributes with proper key and displayName
|
||||
const mappedAttributes = searchedAttributesDataList.map((e) =>
|
||||
createTelemetryFieldKey(e),
|
||||
);
|
||||
|
||||
// Combine with default columns and resolve conflicts
|
||||
const allColumns =
|
||||
dataSource === DataSource.LOGS
|
||||
? [...defaultLogsSelectedColumns, ...mappedAttributes]
|
||||
: mappedAttributes;
|
||||
|
||||
// Resolve conflicts with deduplication
|
||||
return resolveColumnConflicts(allColumns);
|
||||
}
|
||||
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
return defaultTraceSelectedColumns.map((e) => ({
|
||||
...e,
|
||||
@@ -234,19 +209,10 @@ const useOptionsMenu = ({
|
||||
const initialOptionsQuery: OptionsQuery = useMemo(() => {
|
||||
let defaultColumns: TelemetryFieldKey[] = defaultOptionsQuery.selectColumns;
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
defaultColumns = defaultTraceSelectedColumns.map((e) => ({
|
||||
...e,
|
||||
name: e.name,
|
||||
}));
|
||||
defaultColumns = defaultTraceSelectedColumns;
|
||||
} else if (dataSource === DataSource.LOGS) {
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
defaultColumns = defaultLogsSelectedColumns.map((e) => ({
|
||||
...e,
|
||||
name: e.name,
|
||||
signal: e.signal as SignalType,
|
||||
fieldContext: e.fieldContext as FieldContext,
|
||||
fieldDataType: e.fieldDataType as FieldDataType,
|
||||
}));
|
||||
defaultColumns = defaultLogsSelectedColumns;
|
||||
}
|
||||
|
||||
const finalSelectColumns = initialOptions?.selectColumns
|
||||
@@ -261,7 +227,7 @@ const useOptionsMenu = ({
|
||||
}, [dataSource, initialOptions, initialSelectedColumns]);
|
||||
|
||||
const selectedColumnKeys = useMemo(
|
||||
() => preferences?.columns?.map(({ name }) => name) || [],
|
||||
() => preferences?.columns?.map(({ key }) => key) || [],
|
||||
[preferences?.columns],
|
||||
);
|
||||
|
||||
@@ -290,7 +256,7 @@ const useOptionsMenu = ({
|
||||
const column = [
|
||||
...searchedAttributeKeys,
|
||||
...(preferences?.columns || []),
|
||||
].find(({ name }) => name === key);
|
||||
].find((column) => column.key === key);
|
||||
|
||||
if (!column) return acc;
|
||||
return [...acc, column];
|
||||
@@ -319,7 +285,7 @@ const useOptionsMenu = ({
|
||||
const handleRemoveSelectedColumn = useCallback(
|
||||
(columnKey: string) => {
|
||||
const newSelectedColumns = preferences?.columns?.filter(
|
||||
({ name }) => name !== columnKey,
|
||||
(column) => column.key !== columnKey,
|
||||
);
|
||||
|
||||
if (!newSelectedColumns?.length && dataSource !== DataSource.LOGS) {
|
||||
|
||||
@@ -1,13 +1,163 @@
|
||||
import { SelectProps } from 'antd';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import {
|
||||
FieldContext,
|
||||
FieldDataType,
|
||||
SignalType,
|
||||
} from 'types/api/v5/queryRange';
|
||||
|
||||
/**
|
||||
* Display name mapping for log fieldContext columns
|
||||
* Provides user-friendly names for standard log fields
|
||||
*/
|
||||
const LOG_FIELD_DISPLAY_NAMES: Record<string, string> = {
|
||||
body: 'Body',
|
||||
severity_number: 'Severity Number',
|
||||
severity_text: 'Severity Text',
|
||||
span_id: 'Span ID',
|
||||
trace_flags: 'Trace Flags',
|
||||
trace_id: 'Trace ID',
|
||||
scope_name: 'Scope Name',
|
||||
scope_version: 'Scope Version',
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to create a TelemetryFieldKey with properly formatted key and displayName
|
||||
* Ensures consistent key format: fieldContext.name:fieldDataType
|
||||
* Uses display name map for log fieldContext columns
|
||||
* @param suggestion - The raw suggestion data from the API
|
||||
* @returns A TelemetryFieldKey with key and displayName fields properly set
|
||||
*/
|
||||
export function createTelemetryFieldKey(suggestion: any): TelemetryFieldKey {
|
||||
let displayName = suggestion.displayName || suggestion.name;
|
||||
|
||||
// Use mapped display name for log fieldContext columns
|
||||
// We need to check the fieldContext to avoid overriding non-log fields coming from attributes
|
||||
if (
|
||||
(suggestion.fieldContext === 'log' || suggestion.fieldContext === 'scope') &&
|
||||
LOG_FIELD_DISPLAY_NAMES[suggestion.name]
|
||||
) {
|
||||
displayName = LOG_FIELD_DISPLAY_NAMES[suggestion.name];
|
||||
}
|
||||
|
||||
return {
|
||||
name: suggestion.name,
|
||||
displayName,
|
||||
key: `${suggestion.fieldContext}.${suggestion.name}:${suggestion.fieldDataType}`,
|
||||
signal: suggestion.signal as SignalType,
|
||||
fieldDataType: suggestion.fieldDataType as FieldDataType,
|
||||
fieldContext: suggestion.fieldContext as FieldContext,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a suffix for a column display name based on conflicts
|
||||
* @param column - The column to generate suffix for
|
||||
* @param hasContextConflict - Whether there's a conflict in fieldContext
|
||||
* @param hasDataTypeConflict - Whether there's a conflict in fieldDataType
|
||||
* @returns The suffix string to append to the column name
|
||||
*/
|
||||
function generateColumnSuffix(
|
||||
column: TelemetryFieldKey,
|
||||
hasContextConflict: boolean,
|
||||
hasDataTypeConflict: boolean,
|
||||
): string {
|
||||
if (hasContextConflict && column.fieldContext) {
|
||||
return ` (${column.fieldContext})`;
|
||||
}
|
||||
if (!hasContextConflict && hasDataTypeConflict && column.fieldDataType) {
|
||||
return ` (${column.fieldDataType})`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates display names for conflicting columns in the columnsByKey map
|
||||
* @param columns - Array of columns with the same name
|
||||
* @param columnsByKey - Map to update with new display names
|
||||
*/
|
||||
function updateConflictingDisplayNames(
|
||||
columns: TelemetryFieldKey[],
|
||||
columnsByKey: Map<string, TelemetryFieldKey>,
|
||||
): void {
|
||||
const contexts = new Set(columns.map((c) => c.fieldContext));
|
||||
const dataTypes = new Set(columns.map((c) => c.fieldDataType));
|
||||
const hasContextConflict = contexts.size > 1;
|
||||
const hasDataTypeConflict = dataTypes.size > 1;
|
||||
|
||||
if (!hasContextConflict && !hasDataTypeConflict) {
|
||||
return;
|
||||
}
|
||||
|
||||
columns.forEach((column) => {
|
||||
// Skip if already has a custom displayName (not just the name)
|
||||
if (column.displayName && column.displayName !== column.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const suffix = generateColumnSuffix(
|
||||
column,
|
||||
hasContextConflict,
|
||||
hasDataTypeConflict,
|
||||
);
|
||||
|
||||
if (suffix) {
|
||||
columnsByKey.set(column.key || column.name, {
|
||||
...column,
|
||||
displayName: `${column.name}${suffix}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a list of TelemetryFieldKeys and updates displayName for conflicting columns
|
||||
* Adds suffix with fieldContext and/or fieldDataType when columns have the same name
|
||||
* but different context or data type.
|
||||
* Note: 'log' & 'scope' fieldContext suffix is hidden as it's the default context for logs.
|
||||
* Also deduplicates columns with the same key.
|
||||
* @param suggestions - Array of TelemetryFieldKey objects to process
|
||||
* @returns Array with updated displayNames for conflicting columns and no duplicates
|
||||
*/
|
||||
export function resolveColumnConflicts(
|
||||
suggestions: TelemetryFieldKey[],
|
||||
): TelemetryFieldKey[] {
|
||||
// Use Map for O(1) deduplication by key
|
||||
const columnsByKey = new Map<string, TelemetryFieldKey>();
|
||||
// Track columns by name to detect conflicts
|
||||
const columnsByName = new Map<string, TelemetryFieldKey[]>();
|
||||
|
||||
// First pass: deduplicate by key and group by name
|
||||
suggestions.forEach((suggestion) => {
|
||||
// Skip duplicates (same key)
|
||||
if (columnsByKey.has(suggestion.key || suggestion.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
columnsByKey.set(suggestion.key || suggestion.name, suggestion);
|
||||
|
||||
// Group by name for conflict detection
|
||||
const existing = columnsByName.get(suggestion.name) || [];
|
||||
columnsByName.set(suggestion.name, [...existing, suggestion]);
|
||||
});
|
||||
|
||||
// Second pass: resolve conflicts for columns with same name
|
||||
columnsByName.forEach((columns) => {
|
||||
if (columns.length > 1) {
|
||||
updateConflictingDisplayNames(columns, columnsByKey);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(columnsByKey.values());
|
||||
}
|
||||
|
||||
export const getOptionsFromKeys = (
|
||||
keys: TelemetryFieldKey[],
|
||||
selectedKeys: (string | undefined)[],
|
||||
): SelectProps['options'] => {
|
||||
const options = keys.map(({ name }) => ({
|
||||
label: name,
|
||||
value: name,
|
||||
const options = keys.map(({ key, displayName, name }) => ({
|
||||
label: displayName || name,
|
||||
value: key,
|
||||
}));
|
||||
|
||||
return options.filter(
|
||||
|
||||
@@ -90,8 +90,9 @@ export function QueryTable({
|
||||
column: any,
|
||||
tableColumns: any,
|
||||
): void => {
|
||||
e.stopPropagation();
|
||||
if (isQueryTypeBuilder && enableDrillDown) {
|
||||
e.stopPropagation();
|
||||
|
||||
onClick({ x: e.clientX, y: e.clientY }, { record, column, tableColumns });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -20,6 +20,10 @@ import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import {
|
||||
LOG_FIELD_BODY_KEY,
|
||||
LOG_FIELD_TIMESTAMP_KEY,
|
||||
} from 'lib/logs/flatLogData';
|
||||
import { Compass } from 'lucide-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
@@ -182,12 +186,16 @@ function SpanLogs({
|
||||
{
|
||||
dataType: 'string',
|
||||
type: '',
|
||||
displayName: 'Body',
|
||||
name: 'body',
|
||||
key: LOG_FIELD_BODY_KEY,
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
type: '',
|
||||
name: 'timestamp',
|
||||
key: LOG_FIELD_TIMESTAMP_KEY,
|
||||
displayName: 'Timestamp',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -1,17 +1,61 @@
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
// Exported constants for top-level field mappings
|
||||
export const LOG_FIELD_ID_KEY = 'id';
|
||||
export const LOG_FIELD_TIMESTAMP_KEY = 'log.timestamp:string';
|
||||
export const LOG_FIELD_BODY_KEY = 'log.body:string';
|
||||
export const LOG_FIELD_SPAN_ID_KEY = 'log.span_id:string';
|
||||
export const LOG_FIELD_TRACE_ID_KEY = 'log.trace_id:string';
|
||||
export const LOG_FIELD_TRACE_FLAGS_KEY = 'log.trace_flags:number';
|
||||
export const LOG_FIELD_SEVERITY_TEXT_KEY = 'log.severity_text:string';
|
||||
export const LOG_FIELD_SEVERITY_NUMBER_KEY = 'log.severity_number:number';
|
||||
export const LOG_FIELD_SCOPE_NAME_KEY = 'scope.scope_name:string';
|
||||
export const LOG_FIELD_SCOPE_VERSION_KEY = 'scope.scope_version:string';
|
||||
|
||||
export function FlatLogData(log: ILog): Record<string, string> {
|
||||
const flattenLogObject: Record<string, string> = {};
|
||||
|
||||
Object.keys(log).forEach((key: string): void => {
|
||||
if (typeof log[key as never] !== 'object') {
|
||||
flattenLogObject[key] = log[key as never];
|
||||
} else {
|
||||
Object.keys(defaultTo(log[key as never], {})).forEach((childKey) => {
|
||||
flattenLogObject[childKey] = log[key as never][childKey];
|
||||
// Map of field names to their contexts and data types
|
||||
const fieldMappings: Record<string, { context: string; datatype: string }> = {
|
||||
resources_string: { context: 'resource', datatype: 'string' },
|
||||
scope_string: { context: 'scope', datatype: 'string' },
|
||||
attributes_string: { context: 'attribute', datatype: 'string' },
|
||||
attributes_number: { context: 'attribute', datatype: 'number' },
|
||||
attributes_bool: { context: 'attribute', datatype: 'bool' },
|
||||
};
|
||||
|
||||
// Flatten specific fields with context and datatype
|
||||
Object.entries(fieldMappings).forEach(([fieldKey, { context, datatype }]) => {
|
||||
const fieldData = log[fieldKey as keyof ILog];
|
||||
if (fieldData && typeof fieldData === 'object') {
|
||||
Object.entries(defaultTo(fieldData, {})).forEach(([key, value]) => {
|
||||
const flatKey = `${context}.${key}:${datatype}`;
|
||||
flattenLogObject[flatKey] = String(value);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add top-level fields
|
||||
const topLevelFieldsToContextMapping: Record<string, string> = {
|
||||
id: LOG_FIELD_ID_KEY,
|
||||
timestamp: LOG_FIELD_TIMESTAMP_KEY,
|
||||
body: LOG_FIELD_BODY_KEY,
|
||||
span_id: LOG_FIELD_SPAN_ID_KEY,
|
||||
trace_id: LOG_FIELD_TRACE_ID_KEY,
|
||||
trace_flags: LOG_FIELD_TRACE_FLAGS_KEY,
|
||||
severity_text: LOG_FIELD_SEVERITY_TEXT_KEY,
|
||||
severity_number: LOG_FIELD_SEVERITY_NUMBER_KEY,
|
||||
scope_name: LOG_FIELD_SCOPE_NAME_KEY,
|
||||
scope_version: LOG_FIELD_SCOPE_VERSION_KEY,
|
||||
};
|
||||
|
||||
Object.entries(topLevelFieldsToContextMapping).forEach(([field, key]) => {
|
||||
const value = log[field as keyof ILog];
|
||||
if (value !== undefined && value !== null) {
|
||||
flattenLogObject[key] = String(value);
|
||||
}
|
||||
});
|
||||
|
||||
return flattenLogObject;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,20 @@
|
||||
|
||||
// https://tobyzerner.github.io/placement.js/dist/index.js
|
||||
|
||||
/**
|
||||
* Positions an element (tooltip/popover) relative to a reference element.
|
||||
* Automatically flips to the opposite side if there's insufficient space.
|
||||
*
|
||||
* @param element - The HTMLElement to position
|
||||
* @param reference - Reference element/Range or bounding rect
|
||||
* @param side - Preferred side: 'top', 'bottom', 'left', 'right' (default: 'bottom')
|
||||
* @param align - Alignment: 'start', 'center', 'end' (default: 'center')
|
||||
* @param options - Optional bounds for constraining the element
|
||||
* - bound: Custom boundary rect/element
|
||||
* - followCursor: { x, y } - If provided, tooltip follows cursor with smart positioning
|
||||
*/
|
||||
export const placement = (function () {
|
||||
const e = {
|
||||
const AXIS_PROPS = {
|
||||
size: ['height', 'width'],
|
||||
clientSize: ['clientHeight', 'clientWidth'],
|
||||
offsetSize: ['offsetHeight', 'offsetWidth'],
|
||||
@@ -28,87 +40,241 @@ export const placement = (function () {
|
||||
marginAfter: ['marginBottom', 'marginRight'],
|
||||
scrollOffset: ['pageYOffset', 'pageXOffset'],
|
||||
};
|
||||
function t(e) {
|
||||
return { top: e.top, bottom: e.bottom, left: e.left, right: e.right };
|
||||
}
|
||||
return function (o, r, f, a, i) {
|
||||
void 0 === f && (f = 'bottom'),
|
||||
void 0 === a && (a = 'center'),
|
||||
void 0 === i && (i = {}),
|
||||
(r instanceof Element || r instanceof Range) &&
|
||||
(r = t(r.getBoundingClientRect()));
|
||||
const n = {
|
||||
top: r.bottom,
|
||||
bottom: r.top,
|
||||
left: r.right,
|
||||
right: r.left,
|
||||
...r,
|
||||
|
||||
function extractRect(source) {
|
||||
return {
|
||||
top: source.top,
|
||||
bottom: source.bottom,
|
||||
left: source.left,
|
||||
right: source.right,
|
||||
};
|
||||
const s = {
|
||||
}
|
||||
|
||||
return function (element, reference, side, align, options) {
|
||||
// Default parameters
|
||||
void 0 === side && (side = 'bottom');
|
||||
void 0 === align && (align = 'center');
|
||||
void 0 === options && (options = {});
|
||||
|
||||
// Handle cursor following mode
|
||||
if (options.followCursor) {
|
||||
const cursorX = options.followCursor.x;
|
||||
const cursorY = options.followCursor.y;
|
||||
const offset = options.followCursor.offset || 10; // Default 10px offset from cursor
|
||||
|
||||
element.style.position = 'absolute';
|
||||
element.style.maxWidth = '';
|
||||
element.style.maxHeight = '';
|
||||
|
||||
const elementWidth = element.offsetWidth;
|
||||
const elementHeight = element.offsetHeight;
|
||||
|
||||
// Use viewport bounds for cursor following (not chart bounds)
|
||||
const viewportBounds = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: window.innerHeight,
|
||||
right: window.innerWidth,
|
||||
};
|
||||
|
||||
// Vertical positioning: follow cursor Y with offset, clamped to viewport
|
||||
const topPosition = cursorY + offset;
|
||||
const clampedTop = Math.max(
|
||||
viewportBounds.top,
|
||||
Math.min(topPosition, viewportBounds.bottom - elementHeight),
|
||||
);
|
||||
element.style.top = `${clampedTop}px`;
|
||||
element.style.bottom = 'auto';
|
||||
|
||||
// Horizontal positioning: auto-detect left or right based on available space
|
||||
const spaceOnRight = viewportBounds.right - cursorX;
|
||||
const spaceOnLeft = cursorX - viewportBounds.left;
|
||||
|
||||
if (spaceOnRight >= elementWidth + offset) {
|
||||
// Enough space on the right
|
||||
element.style.left = `${cursorX + offset}px`;
|
||||
element.style.right = 'auto';
|
||||
element.dataset.side = 'right';
|
||||
} else if (spaceOnLeft >= elementWidth + offset) {
|
||||
// Not enough space on right, use left
|
||||
element.style.left = `${cursorX - elementWidth - offset}px`;
|
||||
element.style.right = 'auto';
|
||||
element.dataset.side = 'left';
|
||||
} else if (spaceOnRight > spaceOnLeft) {
|
||||
// Not enough space on either side, pick the side with more space
|
||||
const leftPos = cursorX + offset;
|
||||
const clampedLeft = Math.max(
|
||||
viewportBounds.left,
|
||||
Math.min(leftPos, viewportBounds.right - elementWidth),
|
||||
);
|
||||
element.style.left = `${clampedLeft}px`;
|
||||
element.style.right = 'auto';
|
||||
element.dataset.side = 'right';
|
||||
} else {
|
||||
const leftPos = cursorX - elementWidth - offset;
|
||||
const clampedLeft = Math.max(
|
||||
viewportBounds.left,
|
||||
Math.min(leftPos, viewportBounds.right - elementWidth),
|
||||
);
|
||||
element.style.left = `${clampedLeft}px`;
|
||||
element.style.right = 'auto';
|
||||
element.dataset.side = 'left';
|
||||
}
|
||||
|
||||
element.dataset.align = 'cursor';
|
||||
return; // Exit early, don't run normal positioning logic
|
||||
}
|
||||
|
||||
// Normalize reference to rect object
|
||||
(reference instanceof Element || reference instanceof Range) &&
|
||||
(reference = extractRect(reference.getBoundingClientRect()));
|
||||
|
||||
// Create anchor rect with swapped opposite edges for positioning
|
||||
const anchorRect = {
|
||||
top: reference.bottom,
|
||||
bottom: reference.top,
|
||||
left: reference.right,
|
||||
right: reference.left,
|
||||
...reference,
|
||||
};
|
||||
|
||||
// Viewport bounds (can be overridden via options.bound)
|
||||
const bounds = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: window.innerHeight,
|
||||
right: window.innerWidth,
|
||||
};
|
||||
i.bound &&
|
||||
((i.bound instanceof Element || i.bound instanceof Range) &&
|
||||
(i.bound = t(i.bound.getBoundingClientRect())),
|
||||
Object.assign(s, i.bound));
|
||||
const l = getComputedStyle(o);
|
||||
const m = {};
|
||||
const b = {};
|
||||
for (const g in e)
|
||||
(m[g] = e[g][f === 'top' || f === 'bottom' ? 0 : 1]),
|
||||
(b[g] = e[g][f === 'top' || f === 'bottom' ? 1 : 0]);
|
||||
(o.style.position = 'absolute'),
|
||||
(o.style.maxWidth = ''),
|
||||
(o.style.maxHeight = '');
|
||||
const d = parseInt(l[b.marginBefore]);
|
||||
const c = parseInt(l[b.marginAfter]);
|
||||
const u = d + c;
|
||||
const p = s[b.after] - s[b.before] - u;
|
||||
const h = parseInt(l[b.maxSize]);
|
||||
(!h || p < h) && (o.style[b.maxSize] = `${p}px`);
|
||||
const x = parseInt(l[m.marginBefore]) + parseInt(l[m.marginAfter]);
|
||||
const y = n[m.before] - s[m.before] - x;
|
||||
const z = s[m.after] - n[m.after] - x;
|
||||
((f === m.before && o[m.offsetSize] > y) ||
|
||||
(f === m.after && o[m.offsetSize] > z)) &&
|
||||
(f = y > z ? m.before : m.after);
|
||||
const S = f === m.before ? y : z;
|
||||
const v = parseInt(l[m.maxSize]);
|
||||
(!v || S < v) && (o.style[m.maxSize] = `${S}px`);
|
||||
const w = window[m.scrollOffset];
|
||||
const O = function (e) {
|
||||
return Math.max(s[m.before], Math.min(e, s[m.after] - o[m.offsetSize] - x));
|
||||
|
||||
options.bound &&
|
||||
((options.bound instanceof Element || options.bound instanceof Range) &&
|
||||
(options.bound = extractRect(options.bound.getBoundingClientRect())),
|
||||
Object.assign(bounds, options.bound));
|
||||
|
||||
const styles = getComputedStyle(element);
|
||||
const isVertical = side === 'top' || side === 'bottom';
|
||||
|
||||
// Build axis property maps based on orientation
|
||||
const mainAxis = {}; // Properties for the main positioning axis
|
||||
const crossAxis = {}; // Properties for the perpendicular axis
|
||||
|
||||
for (const prop in AXIS_PROPS) {
|
||||
mainAxis[prop] = AXIS_PROPS[prop][isVertical ? 0 : 1];
|
||||
crossAxis[prop] = AXIS_PROPS[prop][isVertical ? 1 : 0];
|
||||
}
|
||||
|
||||
// Reset element positioning
|
||||
element.style.position = 'absolute';
|
||||
element.style.maxWidth = '';
|
||||
element.style.maxHeight = '';
|
||||
|
||||
// Cross-axis: calculate and apply max size constraint
|
||||
const crossMarginBefore = parseInt(styles[crossAxis.marginBefore]);
|
||||
const crossMarginAfter = parseInt(styles[crossAxis.marginAfter]);
|
||||
const crossMarginTotal = crossMarginBefore + crossMarginAfter;
|
||||
const crossAvailableSpace =
|
||||
bounds[crossAxis.after] - bounds[crossAxis.before] - crossMarginTotal;
|
||||
const crossMaxSize = parseInt(styles[crossAxis.maxSize]);
|
||||
|
||||
(!crossMaxSize || crossAvailableSpace < crossMaxSize) &&
|
||||
(element.style[crossAxis.maxSize] = `${crossAvailableSpace}px`);
|
||||
|
||||
// Main-axis: calculate space on both sides
|
||||
const mainMarginTotal =
|
||||
parseInt(styles[mainAxis.marginBefore]) +
|
||||
parseInt(styles[mainAxis.marginAfter]);
|
||||
const spaceBefore =
|
||||
anchorRect[mainAxis.before] - bounds[mainAxis.before] - mainMarginTotal;
|
||||
const spaceAfter =
|
||||
bounds[mainAxis.after] - anchorRect[mainAxis.after] - mainMarginTotal;
|
||||
|
||||
// Auto-flip to the side with more space if needed
|
||||
((side === mainAxis.before && element[mainAxis.offsetSize] > spaceBefore) ||
|
||||
(side === mainAxis.after && element[mainAxis.offsetSize] > spaceAfter)) &&
|
||||
(side = spaceBefore > spaceAfter ? mainAxis.before : mainAxis.after);
|
||||
|
||||
// Apply main-axis max size constraint
|
||||
const mainAvailableSpace =
|
||||
side === mainAxis.before ? spaceBefore : spaceAfter;
|
||||
const mainMaxSize = parseInt(styles[mainAxis.maxSize]);
|
||||
|
||||
(!mainMaxSize || mainAvailableSpace < mainMaxSize) &&
|
||||
(element.style[mainAxis.maxSize] = `${mainAvailableSpace}px`);
|
||||
|
||||
// Position on main axis
|
||||
const mainScrollOffset = window[mainAxis.scrollOffset];
|
||||
const clampMainPosition = function (pos) {
|
||||
return Math.max(
|
||||
bounds[mainAxis.before],
|
||||
Math.min(
|
||||
pos,
|
||||
bounds[mainAxis.after] - element[mainAxis.offsetSize] - mainMarginTotal,
|
||||
),
|
||||
);
|
||||
};
|
||||
f === m.before
|
||||
? ((o.style[m.before] = `${w + O(n[m.before] - o[m.offsetSize] - x)}px`),
|
||||
(o.style[m.after] = 'auto'))
|
||||
: ((o.style[m.before] = `${w + O(n[m.after])}px`),
|
||||
(o.style[m.after] = 'auto'));
|
||||
const B = window[b.scrollOffset];
|
||||
const I = function (e) {
|
||||
return Math.max(s[b.before], Math.min(e, s[b.after] - o[b.offsetSize] - u));
|
||||
|
||||
side === mainAxis.before
|
||||
? ((element.style[mainAxis.before] = `${
|
||||
mainScrollOffset +
|
||||
clampMainPosition(
|
||||
anchorRect[mainAxis.before] -
|
||||
element[mainAxis.offsetSize] -
|
||||
mainMarginTotal,
|
||||
)
|
||||
}px`),
|
||||
(element.style[mainAxis.after] = 'auto'))
|
||||
: ((element.style[mainAxis.before] = `${
|
||||
mainScrollOffset + clampMainPosition(anchorRect[mainAxis.after])
|
||||
}px`),
|
||||
(element.style[mainAxis.after] = 'auto'));
|
||||
|
||||
// Position on cross axis based on alignment
|
||||
const crossScrollOffset = window[crossAxis.scrollOffset];
|
||||
const clampCrossPosition = function (pos) {
|
||||
return Math.max(
|
||||
bounds[crossAxis.before],
|
||||
Math.min(
|
||||
pos,
|
||||
bounds[crossAxis.after] - element[crossAxis.offsetSize] - crossMarginTotal,
|
||||
),
|
||||
);
|
||||
};
|
||||
switch (a) {
|
||||
|
||||
switch (align) {
|
||||
case 'start':
|
||||
(o.style[b.before] = `${B + I(n[b.before] - d)}px`),
|
||||
(o.style[b.after] = 'auto');
|
||||
(element.style[crossAxis.before] = `${
|
||||
crossScrollOffset +
|
||||
clampCrossPosition(anchorRect[crossAxis.before] - crossMarginBefore)
|
||||
}px`),
|
||||
(element.style[crossAxis.after] = 'auto');
|
||||
break;
|
||||
case 'end':
|
||||
(o.style[b.before] = 'auto'),
|
||||
(o.style[b.after] = `${
|
||||
B + I(document.documentElement[b.clientSize] - n[b.after] - c)
|
||||
(element.style[crossAxis.before] = 'auto'),
|
||||
(element.style[crossAxis.after] = `${
|
||||
crossScrollOffset +
|
||||
clampCrossPosition(
|
||||
document.documentElement[crossAxis.clientSize] -
|
||||
anchorRect[crossAxis.after] -
|
||||
crossMarginAfter,
|
||||
)
|
||||
}px`);
|
||||
break;
|
||||
default:
|
||||
var H = n[b.after] - n[b.before];
|
||||
(o.style[b.before] = `${
|
||||
B + I(n[b.before] + H / 2 - o[b.offsetSize] / 2 - d)
|
||||
// 'center'
|
||||
var crossSize = anchorRect[crossAxis.after] - anchorRect[crossAxis.before];
|
||||
(element.style[crossAxis.before] = `${
|
||||
crossScrollOffset +
|
||||
clampCrossPosition(
|
||||
anchorRect[crossAxis.before] +
|
||||
crossSize / 2 -
|
||||
element[crossAxis.offsetSize] / 2 -
|
||||
crossMarginBefore,
|
||||
)
|
||||
}px`),
|
||||
(o.style[b.after] = 'auto');
|
||||
(element.style[crossAxis.after] = 'auto');
|
||||
}
|
||||
(o.dataset.side = f), (o.dataset.align = a);
|
||||
|
||||
// Store final placement as data attributes
|
||||
(element.dataset.side = side), (element.dataset.align = align);
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -3,7 +3,71 @@ import { themeColors } from 'constants/theme';
|
||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
function isSeriesValueValid(seriesValue: number | undefined | null): boolean {
|
||||
return (
|
||||
seriesValue !== undefined &&
|
||||
seriesValue !== null &&
|
||||
!Number.isNaN(seriesValue)
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to get the focused/highlighted series at a specific position
|
||||
function resolveSeriesColor(series: uPlot.Series, index: number): string {
|
||||
let color = '#000000';
|
||||
if (typeof series.stroke === 'string') {
|
||||
color = series.stroke;
|
||||
} else if (typeof series.fill === 'string') {
|
||||
color = series.fill;
|
||||
} else {
|
||||
const seriesLabel = series.label || `Series ${index}`;
|
||||
const isDarkMode = !document.body.classList.contains('lightMode');
|
||||
color = generateColor(
|
||||
seriesLabel,
|
||||
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||
);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
function getPreferredSeriesIndex(
|
||||
u: uPlot,
|
||||
timestampIndex: number,
|
||||
e: MouseEvent,
|
||||
): number {
|
||||
const bbox = u.over.getBoundingClientRect();
|
||||
const top = e.clientY - bbox.top;
|
||||
// Prefer series explicitly marked as focused
|
||||
for (let i = 1; i < u.series.length; i++) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const isSeriesFocused = u.series[i]?._focus === true;
|
||||
const isSeriesShown = u.series[i].show !== false;
|
||||
const seriesValue = u.data[i]?.[timestampIndex];
|
||||
if (isSeriesFocused && isSeriesShown && isSeriesValueValid(seriesValue)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: choose series with Y closest to mouse position
|
||||
let focusedSeriesIndex = -1;
|
||||
let closestPixelDiff = Infinity;
|
||||
for (let i = 1; i < u.series.length; i++) {
|
||||
const series = u.data[i];
|
||||
const seriesValue = series?.[timestampIndex];
|
||||
|
||||
if (isSeriesValueValid(seriesValue) && u.series[i].show !== false) {
|
||||
const yPx = u.valToPos(seriesValue as number, 'y');
|
||||
const diff = Math.abs(yPx - top);
|
||||
if (diff < closestPixelDiff) {
|
||||
closestPixelDiff = diff;
|
||||
focusedSeriesIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return focusedSeriesIndex;
|
||||
}
|
||||
|
||||
export const getFocusedSeriesAtPosition = (
|
||||
e: MouseEvent,
|
||||
u: uPlot,
|
||||
@@ -17,74 +81,28 @@ export const getFocusedSeriesAtPosition = (
|
||||
} | null => {
|
||||
const bbox = u.over.getBoundingClientRect();
|
||||
const left = e.clientX - bbox.left;
|
||||
const top = e.clientY - bbox.top;
|
||||
|
||||
const timestampIndex = u.posToIdx(left);
|
||||
let focusedSeriesIndex = -1;
|
||||
let closestPixelDiff = Infinity;
|
||||
|
||||
// Check all series (skip index 0 which is the x-axis)
|
||||
for (let i = 1; i < u.data.length; i++) {
|
||||
const series = u.data[i];
|
||||
const seriesValue = series[timestampIndex];
|
||||
|
||||
if (
|
||||
seriesValue !== undefined &&
|
||||
seriesValue !== null &&
|
||||
!Number.isNaN(seriesValue)
|
||||
) {
|
||||
const seriesYPx = u.valToPos(seriesValue, 'y');
|
||||
const pixelDiff = Math.abs(seriesYPx - top);
|
||||
|
||||
if (pixelDiff < closestPixelDiff) {
|
||||
closestPixelDiff = pixelDiff;
|
||||
focusedSeriesIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we found a focused series, return its data
|
||||
if (focusedSeriesIndex > 0) {
|
||||
const series = u.series[focusedSeriesIndex];
|
||||
const seriesValue = u.data[focusedSeriesIndex][timestampIndex];
|
||||
|
||||
// Ensure we have a valid value
|
||||
if (
|
||||
seriesValue !== undefined &&
|
||||
seriesValue !== null &&
|
||||
!Number.isNaN(seriesValue)
|
||||
) {
|
||||
// Get color - try series stroke first, then generate based on label
|
||||
let color = '#000000';
|
||||
if (typeof series.stroke === 'string') {
|
||||
color = series.stroke;
|
||||
} else if (typeof series.fill === 'string') {
|
||||
color = series.fill;
|
||||
} else {
|
||||
// Generate color based on series label (like the tooltip plugin does)
|
||||
const seriesLabel = series.label || `Series ${focusedSeriesIndex}`;
|
||||
// Detect theme mode by checking body class
|
||||
const isDarkMode = !document.body.classList.contains('lightMode');
|
||||
color = generateColor(
|
||||
seriesLabel,
|
||||
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||
);
|
||||
}
|
||||
const preferredIndex = getPreferredSeriesIndex(u, timestampIndex, e);
|
||||
|
||||
if (preferredIndex > 0) {
|
||||
const series = u.series[preferredIndex];
|
||||
const seriesValue = u.data[preferredIndex][timestampIndex];
|
||||
if (isSeriesValueValid(seriesValue)) {
|
||||
const color = resolveSeriesColor(series, preferredIndex);
|
||||
return {
|
||||
seriesIndex: focusedSeriesIndex,
|
||||
seriesName: series.label || `Series ${focusedSeriesIndex}`,
|
||||
seriesIndex: preferredIndex,
|
||||
seriesName: series.label || `Series ${preferredIndex}`,
|
||||
value: seriesValue as number,
|
||||
color,
|
||||
show: series.show !== false,
|
||||
isFocused: true, // This indicates it's the highlighted/bold one
|
||||
isFocused: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export interface OnClickPluginOpts {
|
||||
onClick: (
|
||||
xValue: number,
|
||||
@@ -137,50 +155,31 @@ function onClickPlugin(opts: OnClickPluginOpts): uPlot.Plugin {
|
||||
const yValue = u.posToVal(event.offsetY, 'y');
|
||||
|
||||
// Get the focused/highlighted series (the one that would be bold in hover)
|
||||
const focusedSeries = getFocusedSeriesAtPosition(event, u);
|
||||
const focusedSeriesData = getFocusedSeriesAtPosition(event, u);
|
||||
|
||||
let metric = {};
|
||||
const { series } = u;
|
||||
const apiResult = opts.apiResponse?.data?.result || [];
|
||||
const outputMetric = {
|
||||
queryName: '',
|
||||
inFocusOrNot: false,
|
||||
};
|
||||
|
||||
// this is to get the metric value of the focused series
|
||||
if (Array.isArray(series) && series.length > 0) {
|
||||
series.forEach((item, index) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if (item?.show && item?._focus) {
|
||||
const { metric: focusedMetric, queryName } = apiResult[index - 1] || [];
|
||||
metric = focusedMetric;
|
||||
outputMetric.queryName = queryName;
|
||||
outputMetric.inFocusOrNot = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!outputMetric.queryName) {
|
||||
// Get the focused series data
|
||||
const focusedSeriesData = getFocusedSeriesAtPosition(event, u);
|
||||
|
||||
// If we found a valid focused series, get its data
|
||||
if (
|
||||
focusedSeriesData &&
|
||||
focusedSeriesData.seriesIndex <= apiResult.length
|
||||
) {
|
||||
const { metric: focusedMetric, queryName } =
|
||||
apiResult[focusedSeriesData.seriesIndex - 1] || [];
|
||||
metric = focusedMetric;
|
||||
outputMetric.queryName = queryName;
|
||||
outputMetric.inFocusOrNot = true;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if (
|
||||
focusedSeriesData &&
|
||||
focusedSeriesData.seriesIndex <= apiResult.length
|
||||
) {
|
||||
const { metric: focusedMetric, queryName } =
|
||||
apiResult[focusedSeriesData.seriesIndex - 1] || {};
|
||||
metric = focusedMetric;
|
||||
outputMetric.queryName = queryName;
|
||||
outputMetric.inFocusOrNot = true;
|
||||
}
|
||||
|
||||
// Get the actual data point timestamp from the focused series
|
||||
let actualDataTimestamp = xValue; // fallback to click position timestamp
|
||||
if (focusedSeries) {
|
||||
if (focusedSeriesData) {
|
||||
// Get the data index from the focused series
|
||||
const dataIndex = u.posToIdx(event.offsetX);
|
||||
// Get the actual timestamp from the x-axis data (u.data[0])
|
||||
@@ -209,7 +208,7 @@ function onClickPlugin(opts: OnClickPluginOpts): uPlot.Plugin {
|
||||
absoluteMouseX,
|
||||
absoluteMouseY,
|
||||
axesData,
|
||||
focusedSeries,
|
||||
focusedSeriesData,
|
||||
);
|
||||
};
|
||||
u.over.addEventListener('click', handleClick);
|
||||
|
||||
@@ -415,7 +415,11 @@ ToolTipPluginProps): any => {
|
||||
}
|
||||
// Clear and set new content in one operation
|
||||
overlay.replaceChildren(content);
|
||||
placement(overlay, anchor, 'right', 'start', { bound });
|
||||
placement(overlay, anchor, 'right', 'start', {
|
||||
bound,
|
||||
followCursor: { x: anchor.left, y: anchor.top, offset: 4 },
|
||||
});
|
||||
|
||||
showOverlay();
|
||||
} else {
|
||||
hideOverlay();
|
||||
|
||||
@@ -401,14 +401,14 @@ body {
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
margin: 0.5rem;
|
||||
background: rgba(0, 0, 0);
|
||||
background: var(--bg-ink-300);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
color: #fff;
|
||||
color: var(--bg-vanilla-100);
|
||||
z-index: 10000;
|
||||
// pointer-events: none;
|
||||
overflow: auto;
|
||||
max-height: 480px !important;
|
||||
max-width: 240px !important;
|
||||
max-width: 300px !important;
|
||||
border-radius: 5px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
@@ -571,6 +571,12 @@ body {
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
#overlay {
|
||||
color: var(--bg-ink-500);
|
||||
background: var(--bg-vanilla-100);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.ant-dropdown-menu {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export interface IField {
|
||||
name: string;
|
||||
displayName: string;
|
||||
key: string;
|
||||
type: string;
|
||||
dataType: string;
|
||||
}
|
||||
|
||||
@@ -128,7 +128,11 @@ export interface VariableItem {
|
||||
|
||||
export interface TelemetryFieldKey {
|
||||
name: string;
|
||||
displayName?: string;
|
||||
// display name can change dynamically depending on if there's a conflicting field with same name and in only meant for UI display
|
||||
key?: string;
|
||||
// key is a unique identifier generated for each field, used for comparisons and selections
|
||||
// key = fieldContext.name:fieldDataType
|
||||
description?: string;
|
||||
unit?: string;
|
||||
signal?: SignalType;
|
||||
|
||||
@@ -54,8 +54,6 @@ func (r *aggExprRewriter) Rewrite(
|
||||
expr string,
|
||||
rateInterval uint64,
|
||||
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
startNs uint64,
|
||||
endNs uint64,
|
||||
) (string, []any, error) {
|
||||
|
||||
wrapped := fmt.Sprintf("SELECT %s", expr)
|
||||
@@ -85,8 +83,6 @@ func (r *aggExprRewriter) Rewrite(
|
||||
r.conditionBuilder,
|
||||
r.jsonBodyPrefix,
|
||||
r.jsonKeyToKey,
|
||||
startNs,
|
||||
endNs,
|
||||
)
|
||||
// Rewrite the first select item (our expression)
|
||||
if err := sel.SelectItems[0].Accept(visitor); err != nil {
|
||||
@@ -105,14 +101,12 @@ func (r *aggExprRewriter) RewriteMulti(
|
||||
exprs []string,
|
||||
rateInterval uint64,
|
||||
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
||||
startNs uint64,
|
||||
endNs uint64,
|
||||
) ([]string, [][]any, error) {
|
||||
out := make([]string, len(exprs))
|
||||
var errs []error
|
||||
var chArgsList [][]any
|
||||
for i, e := range exprs {
|
||||
w, chArgs, err := r.Rewrite(ctx, e, rateInterval, keys, startNs, endNs)
|
||||
w, chArgs, err := r.Rewrite(ctx, e, rateInterval, keys)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
out[i] = e
|
||||
@@ -140,8 +134,6 @@ type exprVisitor struct {
|
||||
Modified bool
|
||||
chArgs []any
|
||||
isRate bool
|
||||
startNs uint64
|
||||
endNs uint64
|
||||
}
|
||||
|
||||
func newExprVisitor(
|
||||
@@ -152,8 +144,6 @@ func newExprVisitor(
|
||||
conditionBuilder qbtypes.ConditionBuilder,
|
||||
jsonBodyPrefix string,
|
||||
jsonKeyToKey qbtypes.JsonKeyToFieldFunc,
|
||||
startNs uint64,
|
||||
endNs uint64,
|
||||
) *exprVisitor {
|
||||
return &exprVisitor{
|
||||
logger: logger,
|
||||
@@ -163,8 +153,6 @@ func newExprVisitor(
|
||||
conditionBuilder: conditionBuilder,
|
||||
jsonBodyPrefix: jsonBodyPrefix,
|
||||
jsonKeyToKey: jsonKeyToKey,
|
||||
startNs: startNs,
|
||||
endNs: endNs,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +190,7 @@ func (v *exprVisitor) VisitFunctionExpr(fn *chparser.FunctionExpr) error {
|
||||
if aggFunc.FuncCombinator {
|
||||
// Map the predicate (last argument)
|
||||
origPred := args[len(args)-1].String()
|
||||
whereClause, err := PrepareWhereClause(
|
||||
whereClause, err := PrepareWhereClause(
|
||||
origPred,
|
||||
FilterExprVisitorOpts{
|
||||
Logger: v.logger,
|
||||
@@ -212,7 +200,7 @@ func (v *exprVisitor) VisitFunctionExpr(fn *chparser.FunctionExpr) error {
|
||||
FullTextColumn: v.fullTextColumn,
|
||||
JsonBodyPrefix: v.jsonBodyPrefix,
|
||||
JsonKeyToKey: v.jsonKeyToKey,
|
||||
}, v.startNs, v.endNs,
|
||||
}, 0, 0,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -350,8 +350,6 @@ func (b *logQueryStatementBuilder) buildTimeSeriesQuery(
|
||||
ctx, agg.Expression,
|
||||
uint64(query.StepInterval.Seconds()),
|
||||
keys,
|
||||
start,
|
||||
end,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -501,8 +499,6 @@ func (b *logQueryStatementBuilder) buildScalarQuery(
|
||||
ctx, aggExpr.Expression,
|
||||
rateInterval,
|
||||
keys,
|
||||
start,
|
||||
end,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -596,7 +592,7 @@ func (b *logQueryStatementBuilder) addFilterCondition(
|
||||
JsonBodyPrefix: b.jsonBodyPrefix,
|
||||
JsonKeyToKey: b.jsonKeyToKey,
|
||||
Variables: variables,
|
||||
}, start, end)
|
||||
}, start, end)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -512,8 +512,6 @@ func (b *traceQueryStatementBuilder) buildTimeSeriesQuery(
|
||||
ctx, agg.Expression,
|
||||
uint64(query.StepInterval.Seconds()),
|
||||
keys,
|
||||
start,
|
||||
end,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -659,8 +657,6 @@ func (b *traceQueryStatementBuilder) buildScalarQuery(
|
||||
ctx, aggExpr.Expression,
|
||||
rateInterval,
|
||||
keys,
|
||||
start,
|
||||
end,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -750,7 +746,7 @@ func (b *traceQueryStatementBuilder) addFilterCondition(
|
||||
FieldKeys: keys,
|
||||
SkipResourceFilter: true,
|
||||
Variables: variables,
|
||||
}, start, end)
|
||||
}, start, end)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -237,7 +237,7 @@ func (b *traceOperatorCTEBuilder) buildQueryCTE(ctx context.Context, queryName s
|
||||
ConditionBuilder: b.stmtBuilder.cb,
|
||||
FieldKeys: keys,
|
||||
SkipResourceFilter: true,
|
||||
}, b.start, b.end,
|
||||
}, b.start, b.end,
|
||||
)
|
||||
if err != nil {
|
||||
b.stmtBuilder.logger.ErrorContext(ctx, "Failed to prepare where clause", "error", err, "filter", query.Filter.Expression)
|
||||
@@ -575,8 +575,6 @@ func (b *traceOperatorCTEBuilder) buildTimeSeriesQuery(ctx context.Context, sele
|
||||
agg.Expression,
|
||||
uint64(b.operator.StepInterval.Seconds()),
|
||||
keys,
|
||||
b.start,
|
||||
b.end,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.NewInvalidInputf(
|
||||
@@ -689,8 +687,6 @@ func (b *traceOperatorCTEBuilder) buildTraceQuery(ctx context.Context, selectFro
|
||||
agg.Expression,
|
||||
rateInterval,
|
||||
keys,
|
||||
b.start,
|
||||
b.end,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.NewInvalidInputf(
|
||||
@@ -829,8 +825,6 @@ func (b *traceOperatorCTEBuilder) buildScalarQuery(ctx context.Context, selectFr
|
||||
agg.Expression,
|
||||
uint64((b.end-b.start)/querybuilder.NsToSeconds),
|
||||
keys,
|
||||
b.start,
|
||||
b.end,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.NewInvalidInputf(
|
||||
|
||||
@@ -37,8 +37,8 @@ type ConditionBuilder interface {
|
||||
|
||||
type AggExprRewriter interface {
|
||||
// Rewrite rewrites the aggregation expression to be used in the query.
|
||||
Rewrite(ctx context.Context, expr string, rateInterval uint64, keys map[string][]*telemetrytypes.TelemetryFieldKey, startNs uint64, endNs uint64) (string, []any, error)
|
||||
RewriteMulti(ctx context.Context, exprs []string, rateInterval uint64, keys map[string][]*telemetrytypes.TelemetryFieldKey, startNs uint64, endNs uint64) ([]string, [][]any, error)
|
||||
Rewrite(ctx context.Context, expr string, rateInterval uint64, keys map[string][]*telemetrytypes.TelemetryFieldKey) (string, []any, error)
|
||||
RewriteMulti(ctx context.Context, exprs []string, rateInterval uint64, keys map[string][]*telemetrytypes.TelemetryFieldKey) ([]string, [][]any, error)
|
||||
}
|
||||
|
||||
type Statement struct {
|
||||
|
||||
Reference in New Issue
Block a user