mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-22 11:20:43 +01:00
Compare commits
10 Commits
issue_6021
...
fix/ui-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d7c08172a | ||
|
|
d879273e44 | ||
|
|
a4314382d1 | ||
|
|
9844e81c36 | ||
|
|
fa6197ced1 | ||
|
|
89bb599cbe | ||
|
|
e3bc7fa8e8 | ||
|
|
d61b558334 | ||
|
|
d39467f5ef | ||
|
|
d457ce6144 |
@@ -61,6 +61,7 @@
|
||||
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
|
||||
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
|
||||
"AI_OBSERVABILITY_OVERVIEW": "SigNoz | AI Observability Overview",
|
||||
"AI_OBSERVABILITY_EXPLORER": "SigNoz | AI Observability Explorer",
|
||||
"AI_OBSERVABILITY_CONFIGURATION": "SigNoz | AI Observability Configuration",
|
||||
"AI_OBSERVABILITY_ATTRIBUTE_MAPPING": "SigNoz | AI Observability Attribute Mapping"
|
||||
}
|
||||
@@ -42,7 +42,6 @@
|
||||
"NOT_FOUND": "SigNoz | Page Not Found",
|
||||
"LOGS": "SigNoz | Logs",
|
||||
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
|
||||
"OLD_LOGS_EXPLORER": "SigNoz | Old Logs Explorer",
|
||||
"LIVE_LOGS": "SigNoz | Live Logs",
|
||||
"LOGS_PIPELINES": "SigNoz | Logs Pipelines",
|
||||
"HOME_PAGE": "Open source Observability Platform | SigNoz",
|
||||
@@ -86,6 +85,7 @@
|
||||
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
|
||||
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
|
||||
"AI_OBSERVABILITY_OVERVIEW": "SigNoz | AI Observability Overview",
|
||||
"AI_OBSERVABILITY_EXPLORER": "SigNoz | AI Observability Explorer",
|
||||
"AI_OBSERVABILITY_CONFIGURATION": "SigNoz | AI Observability Configuration",
|
||||
"AI_OBSERVABILITY_ATTRIBUTE_MAPPING": "SigNoz | AI Observability Attribute Mapping"
|
||||
}
|
||||
@@ -1597,10 +1597,6 @@ describe('PrivateRoute', () => {
|
||||
LOGS: { path: ROUTES.LOGS, deniedRoles: DENIED_ROLES },
|
||||
LOGS_EXPLORER: { path: ROUTES.LOGS_EXPLORER, deniedRoles: DENIED_ROLES },
|
||||
LIVE_LOGS: { path: ROUTES.LIVE_LOGS, deniedRoles: DENIED_ROLES },
|
||||
OLD_LOGS_EXPLORER: {
|
||||
path: ROUTES.OLD_LOGS_EXPLORER,
|
||||
deniedRoles: DENIED_ROLES,
|
||||
},
|
||||
METRICS_EXPLORER: {
|
||||
path: ROUTES.METRICS_EXPLORER,
|
||||
deniedRoles: DENIED_ROLES,
|
||||
|
||||
@@ -154,14 +154,6 @@ export const Logs = Loadable(
|
||||
() => import(/* webpackChunkName: "Logs" */ 'pages/LogsModulePage'),
|
||||
);
|
||||
|
||||
export const LogsExplorer = Loadable(
|
||||
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/LogsModulePage'),
|
||||
);
|
||||
|
||||
export const OldLogsExplorer = Loadable(
|
||||
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/Logs'),
|
||||
);
|
||||
|
||||
export const LiveLogs = Loadable(
|
||||
() => import(/* webpackChunkName: "Live Logs" */ 'pages/LiveLogs'),
|
||||
);
|
||||
|
||||
@@ -26,13 +26,11 @@ import {
|
||||
LiveLogs,
|
||||
Login,
|
||||
Logs,
|
||||
LogsExplorer,
|
||||
LogsIndexToFields,
|
||||
LogsSaveViews,
|
||||
MessagingQueuesMainPage,
|
||||
MeterExplorerPage,
|
||||
MetricsExplorer,
|
||||
OldLogsExplorer,
|
||||
OnboardingV2,
|
||||
OrgOnboarding,
|
||||
PasswordReset,
|
||||
@@ -284,20 +282,6 @@ const routes: AppRoutes[] = [
|
||||
key: 'LOGS',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.LOGS_EXPLORER,
|
||||
exact: true,
|
||||
component: LogsExplorer,
|
||||
key: 'LOGS_EXPLORER',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.OLD_LOGS_EXPLORER,
|
||||
exact: true,
|
||||
component: OldLogsExplorer,
|
||||
key: 'OLD_LOGS_EXPLORER',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.LIVE_LOGS,
|
||||
exact: true,
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/logs/addToSelectedFields';
|
||||
|
||||
const addToSelectedFields = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const data = await axios.post(`/logs/fields`, props);
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return Promise.reject(ErrorResponseHandler(error as AxiosError));
|
||||
}
|
||||
};
|
||||
|
||||
export default addToSelectedFields;
|
||||
@@ -1,26 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/logs/getLogs';
|
||||
|
||||
const GetLogs = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const data = await axios.get(`/logs`, {
|
||||
params: props,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: data.data.results,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default GetLogs;
|
||||
@@ -1,26 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/logs/getLogsAggregate';
|
||||
|
||||
const GetLogsAggregate = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const data = await axios.get(`/logs/aggregate`, {
|
||||
params: props,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: data.data.items,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default GetLogsAggregate;
|
||||
@@ -1,24 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/logs/getSearchFields';
|
||||
|
||||
const GetSearchFields = async (): Promise<
|
||||
SuccessResponse<PayloadProps> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const data = await axios.get(`/logs/fields`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default GetSearchFields;
|
||||
@@ -1,23 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/logs/addToSelectedFields';
|
||||
|
||||
const removeSelectedField = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const data = await axios.post(`/logs/fields`, props);
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return Promise.reject(ErrorResponseHandler(error as AxiosError));
|
||||
}
|
||||
};
|
||||
|
||||
export default removeSelectedField;
|
||||
@@ -1,22 +0,0 @@
|
||||
import apiV1 from 'api/apiV1';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
import { withBasePath } from 'utils/basePath';
|
||||
|
||||
// 10 min in ms
|
||||
const TIMEOUT_IN_MS = 10 * 60 * 1000;
|
||||
|
||||
export const LiveTail = (queryParams: string): EventSourcePolyfill =>
|
||||
new EventSourcePolyfill(
|
||||
ENVIRONMENT.baseURL
|
||||
? `${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`
|
||||
: withBasePath(`${apiV1}logs/tail?${queryParams}`),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${getLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN)}`,
|
||||
},
|
||||
heartbeatTimeout: TIMEOUT_IN_MS,
|
||||
},
|
||||
);
|
||||
@@ -1,12 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { CategoryHeadingText } from './styles';
|
||||
|
||||
interface ICategoryHeadingProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
function CategoryHeading({ children }: ICategoryHeadingProps): JSX.Element {
|
||||
return <CategoryHeadingText color="muted">{children}</CategoryHeadingText>;
|
||||
}
|
||||
|
||||
export default CategoryHeading;
|
||||
@@ -1,6 +0,0 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const CategoryHeadingText = styled(Typography.Text)`
|
||||
font-size: 0.8rem;
|
||||
`;
|
||||
@@ -1,33 +0,0 @@
|
||||
import { CSSProperties } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { TableProps } from 'antd';
|
||||
|
||||
export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
|
||||
return {
|
||||
paddingTop: 4,
|
||||
paddingBottom: 6,
|
||||
paddingRight: 8,
|
||||
paddingLeft: 8,
|
||||
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_400,
|
||||
fontSize: '14px',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: 400,
|
||||
lineHeight: '18px',
|
||||
letterSpacing: '-0.07px',
|
||||
marginBottom: '0px',
|
||||
minWidth: '10rem',
|
||||
width: 'auto',
|
||||
};
|
||||
}
|
||||
|
||||
export const defaultTableStyle: CSSProperties = {
|
||||
minWidth: '40rem',
|
||||
};
|
||||
|
||||
export const defaultListViewPanelStyle: CSSProperties = {
|
||||
maxWidth: '40rem',
|
||||
};
|
||||
|
||||
export const tableScroll: TableProps<Record<string, unknown>>['scroll'] = {
|
||||
x: true,
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Table } from 'antd';
|
||||
|
||||
// config
|
||||
import { tableScroll } from './config';
|
||||
import { LogsTableViewProps } from './types';
|
||||
import { useTableView } from './useTableView';
|
||||
|
||||
function LogsTableView(props: LogsTableViewProps): JSX.Element {
|
||||
const { dataSource, columns } = useTableView(props);
|
||||
|
||||
return (
|
||||
<Table
|
||||
size="small"
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
bordered
|
||||
scroll={tableScroll}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogsTableView;
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface TableBodyContentProps {
|
||||
linesPerRow: number;
|
||||
fontSize: FontSize;
|
||||
isDarkMode?: boolean;
|
||||
}
|
||||
|
||||
export const TableBodyContent = styled.div<TableBodyContentProps>`
|
||||
margin-bottom: 0;
|
||||
color: ${(props): string =>
|
||||
props.isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_400};
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 128.571% */
|
||||
letter-spacing: -0.07px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: ${(props): number => props.linesPerRow};
|
||||
line-clamp: ${(props): number => props.linesPerRow};
|
||||
-webkit-box-orient: vertical;
|
||||
${({ fontSize }): string =>
|
||||
fontSize === FontSize.SMALL
|
||||
? `font-size:11px; line-height:16px;`
|
||||
: fontSize === FontSize.MEDIUM
|
||||
? `font-size:13px; line-height:20px;`
|
||||
: `font-size:14px; line-height:24px;`}
|
||||
`;
|
||||
@@ -1,40 +1,5 @@
|
||||
import {
|
||||
TableColumnsType as ColumnsType,
|
||||
TableColumnType as ColumnType,
|
||||
} from 'antd';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { TableColumnType as ColumnType } from 'antd';
|
||||
|
||||
export type ColumnTypeRender<T = unknown> = ReturnType<
|
||||
NonNullable<ColumnType<T>['render']>
|
||||
>;
|
||||
|
||||
export type LogsTableViewProps = {
|
||||
logs: ILog[];
|
||||
fields: IField[];
|
||||
linesPerRow: number;
|
||||
fontSize: FontSize;
|
||||
onClickExpand?: (log: ILog) => void;
|
||||
};
|
||||
|
||||
export type UseTableViewResult = {
|
||||
columns: ColumnsType<Record<string, unknown>>;
|
||||
dataSource: Record<string, string>[];
|
||||
};
|
||||
|
||||
export type UseTableViewProps = {
|
||||
appendTo?: 'center' | 'end';
|
||||
onOpenLogsContext?: (log: ILog) => void;
|
||||
onClickExpand?: (log: ILog) => void;
|
||||
activeLog?: ILog | null;
|
||||
activeLogIndex?: number;
|
||||
activeContextLog?: ILog | null;
|
||||
isListViewPanel?: boolean;
|
||||
} & LogsTableViewProps;
|
||||
|
||||
export type ActionsColumnProps = {
|
||||
logId: string;
|
||||
logs: ILog[];
|
||||
onOpenLogsContext?: (log: ILog) => void;
|
||||
};
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
.text {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 128.571% */
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
&.small {
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.state-indicator {
|
||||
width: 15px;
|
||||
.log-state-indicator {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-timestamp {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.timestamp-text {
|
||||
color: var(--l1-foreground);
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.paragraph {
|
||||
margin: 0;
|
||||
padding: 0px !important;
|
||||
&.small {
|
||||
font-size: 11px !important;
|
||||
line-height: 16px !important;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
font-size: 13px !important;
|
||||
line-height: 20px !important;
|
||||
}
|
||||
|
||||
&.large {
|
||||
font-size: 14px !important;
|
||||
line-height: 24px !important;
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { TableColumnsType as ColumnsType } from 'antd';
|
||||
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 { useTimezone } from 'providers/Timezone';
|
||||
|
||||
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
|
||||
import {
|
||||
defaultListViewPanelStyle,
|
||||
defaultTableStyle,
|
||||
getDefaultCellStyle,
|
||||
} from './config';
|
||||
import { TableBodyContent } from './styles';
|
||||
import {
|
||||
ColumnTypeRender,
|
||||
UseTableViewProps,
|
||||
UseTableViewResult,
|
||||
} from './types';
|
||||
|
||||
import './useTableView.styles.scss';
|
||||
|
||||
export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
const {
|
||||
logs,
|
||||
fields,
|
||||
linesPerRow,
|
||||
fontSize,
|
||||
appendTo = 'center',
|
||||
isListViewPanel,
|
||||
} = props;
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const flattenLogData = useMemo(
|
||||
() => logs.map((log) => FlatLogData(log)),
|
||||
[logs],
|
||||
);
|
||||
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
const bodyColumnStyle = useMemo(
|
||||
() => ({
|
||||
...defaultTableStyle,
|
||||
...(fields.length > 2 ? { width: 'auto' } : {}),
|
||||
}),
|
||||
[fields.length],
|
||||
);
|
||||
|
||||
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)),
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: linesPerRow,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
wordBreak: 'break-all',
|
||||
},
|
||||
},
|
||||
children: <p className={cx('paragraph', fontSize)}>{field}</p>,
|
||||
}),
|
||||
}));
|
||||
|
||||
if (isListViewPanel) {
|
||||
return [...fieldColumns];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
// We do not need any title and data index for the log state indicator
|
||||
title: '',
|
||||
dataIndex: '',
|
||||
key: 'state-indicator',
|
||||
accessorKey: 'state-indicator',
|
||||
id: 'state-indicator',
|
||||
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
|
||||
children: (
|
||||
<div className={cx('state-indicator', fontSize)}>
|
||||
<LogStateIndicator
|
||||
fontSize={fontSize}
|
||||
severityText={item.severity_text as string}
|
||||
severityNumber={item.severity_number as number}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
},
|
||||
...(fields.some((field) => field.name === 'timestamp')
|
||||
? [
|
||||
{
|
||||
title: 'timestamp',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
accessorKey: 'timestamp',
|
||||
id: 'timestamp',
|
||||
// https://github.com/ant-design/ant-design/discussions/36886
|
||||
render: (
|
||||
field: string | number,
|
||||
): ColumnTypeRender<Record<string, unknown>> => {
|
||||
const date =
|
||||
typeof field === 'string'
|
||||
? formatTimezoneAdjustedTimestamp(
|
||||
field,
|
||||
DATE_TIME_FORMATS.ISO_DATETIME_MS,
|
||||
)
|
||||
: formatTimezoneAdjustedTimestamp(
|
||||
field / 1e6,
|
||||
DATE_TIME_FORMATS.ISO_DATETIME_MS,
|
||||
);
|
||||
return {
|
||||
children: (
|
||||
<div className="table-timestamp">
|
||||
<p className={cx('timestamp-text text', fontSize)}>{date}</p>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(appendTo === 'center' ? fieldColumns : []),
|
||||
...(fields.some((field) => field.name === 'body')
|
||||
? [
|
||||
{
|
||||
title: 'body',
|
||||
dataIndex: 'body',
|
||||
key: 'body',
|
||||
accessorKey: 'body',
|
||||
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}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(appendTo === 'end' ? fieldColumns : []),
|
||||
];
|
||||
}, [
|
||||
fields,
|
||||
isListViewPanel,
|
||||
appendTo,
|
||||
isDarkMode,
|
||||
linesPerRow,
|
||||
fontSize,
|
||||
formatTimezoneAdjustedTimestamp,
|
||||
bodyColumnStyle,
|
||||
]);
|
||||
|
||||
return { columns, dataSource: flattenLogData };
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { Button, InputNumber, Popover, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { LogViewMode } from 'container/OptionsMenu/types';
|
||||
import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types';
|
||||
import {
|
||||
Check,
|
||||
|
||||
@@ -37,7 +37,6 @@ const ROUTES = {
|
||||
NOT_FOUND: '/not-found',
|
||||
LOGS_BASE: '/logs',
|
||||
LOGS: '/logs/logs-explorer',
|
||||
OLD_LOGS_EXPLORER: '/logs/old-logs-explorer',
|
||||
LOGS_EXPLORER: '/logs/logs-explorer',
|
||||
LIVE_LOGS: '/logs/logs-explorer/live',
|
||||
LOGS_PIPELINES: '/logs/pipelines',
|
||||
|
||||
@@ -20,7 +20,7 @@ export const SlackInitialConfig: Partial<SlackChannel> = {
|
||||
*Summary:* {{ .Annotations.summary }}
|
||||
*Description:* {{ .Annotations.description }}
|
||||
*RelatedLogs:* {{ if gt (len .Annotations.related_logs) 0 -}} View in <{{ .Annotations.related_logs }}|logs explorer> {{- end}}
|
||||
*RelatedTraces:* {{ if gt (len .Annotations.related_traces) 0 -}}{{ if match "/ai-observability" .Annotations.related_traces -}} View in <{{ .Annotations.related_traces }}|ai traces explorer> {{- else -}} View in <{{ .Annotations.related_traces }}|traces explorer> {{- end }}{{- end}}
|
||||
*RelatedTraces:* {{ if gt (len .Annotations.related_traces) 0 -}} View in <{{ .Annotations.related_traces }}|traces explorer> {{- end}}
|
||||
|
||||
*Details:*
|
||||
{{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }}
|
||||
@@ -137,7 +137,7 @@ export const JsmOpsInitialConfig: Partial<JsmOpsChannel> = {
|
||||
|
||||
{{ end }}{{ if .Annotations.related_logs }}[View related logs]({{ .Annotations.related_logs }})
|
||||
|
||||
{{ end }}{{ if .Annotations.related_traces }}{{ if match "/ai-observability" .Annotations.related_traces }}[View related AI traces]{{ else }}[View related traces]{{ end }}({{ .Annotations.related_traces }})
|
||||
{{ end }}{{ if .Annotations.related_traces }}[View related traces]({{ .Annotations.related_traces }})
|
||||
|
||||
{{ end }}{{ end }}`,
|
||||
priority:
|
||||
@@ -163,7 +163,7 @@ export const IncidentIOInitialConfig: Partial<IncidentIOChannel> = {
|
||||
|
||||
{{ end }}{{ if .Annotations.related_logs }}[View related logs]({{ .Annotations.related_logs }})
|
||||
|
||||
{{ end }}{{ if .Annotations.related_traces }}{{ if match "/ai-observability" .Annotations.related_traces }}[View related AI traces]{{ else }}[View related traces]{{ end }}({{ .Annotations.related_traces }})
|
||||
{{ end }}{{ if .Annotations.related_traces }}[View related traces]({{ .Annotations.related_traces }})
|
||||
|
||||
{{ end }}{{ end }}`,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.tableWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
padding: var(--spacing-2) var(--spacing-2);
|
||||
|
||||
--tabs-content-padding: 0;
|
||||
--tabs-text-color: var(--l1-foreground);
|
||||
--tabs-active-text-color: var(--l1-foreground);
|
||||
:global(.ant-tabs-tabpane) {
|
||||
padding: var(--spacing-0) var(--spacing-8);
|
||||
}
|
||||
}
|
||||
|
||||
.pageError {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Divider } from '@signozhq/ui/divider';
|
||||
import { Tabs } from '@signozhq/ui/tabs';
|
||||
import { Tabs } from 'antd';
|
||||
import { useConfirmableAction } from 'hooks/useConfirmableAction';
|
||||
|
||||
import AttributeMappingHeader from './components/AttributeMappingHeader/AttributeMappingHeader';
|
||||
import AttributeMappingActions from './components/AttributeMappingActions/AttributeMappingActions';
|
||||
import AttributeMappingsTab from './AttributeMappingsTab/AttributeMappingsTab';
|
||||
import DiscardChangesDialog from './components/DiscardChangesDialog/DiscardChangesDialog';
|
||||
import GroupFormDrawer from './components/GroupFormDrawer/GroupFormDrawer';
|
||||
@@ -59,24 +58,23 @@ function LLMObservabilityAttributeMapping(): JSX.Element {
|
||||
className={styles.llmObservabilityAttributeMapping}
|
||||
data-testid="llm-observability-attribute-mapping-page"
|
||||
>
|
||||
<AttributeMappingHeader
|
||||
isDirty={editor.isDirty}
|
||||
isSaving={editor.isSaving}
|
||||
onDiscard={discardConfirm.request}
|
||||
onSave={editor.save}
|
||||
/>
|
||||
|
||||
{editor.saveError && (
|
||||
<div className={styles.pageError} role="alert">
|
||||
{editor.saveError}
|
||||
</div>
|
||||
)}
|
||||
<Divider />
|
||||
|
||||
<Tabs
|
||||
testId="attribute-mapping-tabs"
|
||||
defaultValue={MAPPINGS_TAB_KEY}
|
||||
defaultActiveKey={MAPPINGS_TAB_KEY}
|
||||
items={tabItems}
|
||||
tabBarExtraContent={
|
||||
<AttributeMappingActions
|
||||
isDirty={editor.isDirty}
|
||||
isSaving={editor.isSaving}
|
||||
onDiscard={discardConfirm.request}
|
||||
onSave={editor.save}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{groupDrawer.isOpen && (
|
||||
<GroupFormDrawer
|
||||
|
||||
@@ -63,6 +63,26 @@ const EDITED_SPAN_JSON = `{
|
||||
}
|
||||
}`;
|
||||
|
||||
const SPAN_WITH_EXTRA_KEY_JSON = `{
|
||||
"attributes": {
|
||||
"input.value": "What is quantum computing?"
|
||||
},
|
||||
"resource": {
|
||||
"service.name": "llm-gateway"
|
||||
},
|
||||
"demo": {
|
||||
"name": "demo"
|
||||
}
|
||||
}`;
|
||||
|
||||
const EXTRA_KEY_RESULT_SPAN = {
|
||||
attributes: {
|
||||
'input.value': 'What is quantum computing?',
|
||||
[MAPPED_ATTRIBUTE_KEY]: 'What is quantum computing?',
|
||||
},
|
||||
resource: { 'service.name': 'llm-gateway' },
|
||||
};
|
||||
|
||||
const SPAN_INPUT_KEY = LOCALSTORAGE.LLM_ATTRIBUTE_MAPPING_TEST_SPAN;
|
||||
|
||||
describe('TestTab — sample-span flow', () => {
|
||||
@@ -104,6 +124,47 @@ describe('TestTab — sample-span flow', () => {
|
||||
expect(screen.queryByTestId('test-error')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('trims extra top-level keys and sends only the envelope', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
let body: { spans?: { attributes?: Record<string, unknown> }[] } | undefined;
|
||||
server.use(
|
||||
rest.post(TEST_ENDPOINT, async (req, res, ctx) => {
|
||||
body = await req.json();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(makeTestResponse([EXTRA_KEY_RESULT_SPAN])),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
render(<LLMObservabilityAttributeMapping />);
|
||||
|
||||
await user.click(screen.getByRole('tab', { name: 'Test' }));
|
||||
const runBtn = await screen.findByTestId('run-test-button');
|
||||
|
||||
await user.clear(screen.getByTestId('monaco'));
|
||||
await user.paste(SPAN_WITH_EXTRA_KEY_JSON);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('monaco')).toHaveValue(SPAN_WITH_EXTRA_KEY_JSON),
|
||||
);
|
||||
expect(screen.queryByTestId('test-input-error')).not.toBeInTheDocument();
|
||||
|
||||
await user.click(runBtn);
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('test-results'),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(body?.spans?.[0]?.attributes).toStrictEqual({
|
||||
'input.value': 'What is quantum computing?',
|
||||
});
|
||||
expect(screen.getByTestId('test-result-0-attributes')).toHaveTextContent(
|
||||
MAPPED_ATTRIBUTE_KEY,
|
||||
);
|
||||
expect(screen.getByTestId('test-result-0-resource')).toBeInTheDocument();
|
||||
expect(screen.getByText('populated')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('surfaces a backend error and renders no results', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
server.use(
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { parseSpanInput } from '../testPayload';
|
||||
|
||||
describe('parseSpanInput', () => {
|
||||
it('reads the envelope and trims extra top-level keys', () => {
|
||||
const span = parseSpanInput(`{
|
||||
"attributes": { "llm.model_name": "gpt-4o" },
|
||||
"resource": { "service.name": "llm-gateway" },
|
||||
"demo": { "name": "demo" }
|
||||
}`);
|
||||
|
||||
expect(span.attributes).toStrictEqual({ 'llm.model_name': 'gpt-4o' });
|
||||
expect(span.resource).toStrictEqual({ 'service.name': 'llm-gateway' });
|
||||
});
|
||||
|
||||
it('reads a clean envelope', () => {
|
||||
const span = parseSpanInput(`{
|
||||
"attributes": { "llm.model_name": "gpt-4o" },
|
||||
"resource": { "service.name": "llm-gateway" }
|
||||
}`);
|
||||
|
||||
expect(span.attributes).toStrictEqual({ 'llm.model_name': 'gpt-4o' });
|
||||
expect(span.resource).toStrictEqual({ 'service.name': 'llm-gateway' });
|
||||
});
|
||||
|
||||
it('treats an envelope-less object as a bare attribute map', () => {
|
||||
const span = parseSpanInput('{ "llm.model_name": "gpt-4o", "demo": "x" }');
|
||||
|
||||
expect(span.attributes).toStrictEqual({
|
||||
'llm.model_name': 'gpt-4o',
|
||||
demo: 'x',
|
||||
});
|
||||
expect(span.resource).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('drops an envelope key that is not an object', () => {
|
||||
const span = parseSpanInput(
|
||||
'{ "attributes": { "llm.provider": "openai" }, "resource": "oops" }',
|
||||
);
|
||||
|
||||
expect(span.attributes).toStrictEqual({ 'llm.provider': 'openai' });
|
||||
expect(span.resource).toStrictEqual({});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[' ', 'Paste a JSON span object to run the test.'],
|
||||
['{ "a": }', 'Invalid JSON — check for trailing commas or missing quotes.'],
|
||||
['[1, 2]', 'Span must be a JSON object of attribute key-value pairs.'],
|
||||
])('rejects %p', (input, message) => {
|
||||
expect(() => parseSpanInput(input)).toThrow(message);
|
||||
});
|
||||
});
|
||||
@@ -51,13 +51,9 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
// Any other top-level key (a real span carries name, spanId, kind...) is trimmed.
|
||||
function isSpanEnvelope(parsed: Record<string, unknown>): boolean {
|
||||
const keys = Object.keys(parsed);
|
||||
return (
|
||||
keys.length > 0 &&
|
||||
keys.every((key) => key === 'attributes' || key === 'resource') &&
|
||||
(isPlainObject(parsed.attributes) || isPlainObject(parsed.resource))
|
||||
);
|
||||
return isPlainObject(parsed.attributes) || isPlainObject(parsed.resource);
|
||||
}
|
||||
|
||||
export function parseSpanInput(input: string): SpantypesSpanMapperTestSpanDTO {
|
||||
|
||||
@@ -72,20 +72,15 @@ describe('LLMObservabilityAttributeMapping', () => {
|
||||
const attributeMappingsTab = screen.getByRole('tab', {
|
||||
name: 'Attribute Mappings',
|
||||
});
|
||||
expect(attributeMappingsTab).toHaveAttribute('data-state', 'active');
|
||||
expect(attributeMappingsTab).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(
|
||||
screen.findByTestId('attribute-mappings-tab'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the header with its description and no Save/Discard while pristine', () => {
|
||||
it('renders no Save/Discard while pristine', () => {
|
||||
render(<LLMObservabilityAttributeMapping />);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Configure source-to-target attribute remapping for LLM traces',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
// The actions only appear once there are staged changes.
|
||||
expect(screen.queryByTestId('save-changes-btn')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('discard-changes-btn')).not.toBeInTheDocument();
|
||||
@@ -124,7 +119,11 @@ describe('LLMObservabilityAttributeMapping', () => {
|
||||
|
||||
await user.click(screen.getByRole('tab', { name: 'Attribute Mappings' }));
|
||||
await screen.findByTestId('attribute-mappings-tab');
|
||||
expect(screen.queryByTestId('span-json-editor')).not.toBeInTheDocument();
|
||||
// antd keeps a visited pane mounted and marks it aria-hidden, rather than
|
||||
// unmounting it the way the previous tabs did.
|
||||
expect(
|
||||
screen.getByTestId('span-json-editor').closest('[role="tabpanel"]'),
|
||||
).toHaveAttribute('aria-hidden', 'true');
|
||||
|
||||
await user.click(screen.getByRole('tab', { name: 'Test' }));
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-6);
|
||||
}
|
||||
|
||||
.unsavedChanges {
|
||||
font-size: var(--periscope-font-size-base);
|
||||
color: var(--accent-amber);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
|
||||
import { useCanManageAttributeMapping } from '../../hooks/useCanManageAttributeMapping';
|
||||
import styles from './AttributeMappingActions.module.scss';
|
||||
|
||||
interface AttributeMappingActionsProps {
|
||||
isDirty: boolean;
|
||||
isSaving: boolean;
|
||||
onDiscard: () => void;
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
function AttributeMappingActions({
|
||||
isDirty,
|
||||
isSaving,
|
||||
onDiscard,
|
||||
onSave,
|
||||
}: AttributeMappingActionsProps): JSX.Element | null {
|
||||
const canManage = useCanManageAttributeMapping();
|
||||
|
||||
if (!canManage || !isDirty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.actions}>
|
||||
<span className={styles.unsavedChanges} data-testid="unsaved-changes">
|
||||
Unsaved changes
|
||||
</span>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={onDiscard}
|
||||
disabled={isSaving}
|
||||
testId="discard-changes-btn"
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={onSave}
|
||||
loading={isSaving}
|
||||
disabled={isSaving}
|
||||
testId="save-changes-btn"
|
||||
>
|
||||
{isSaving ? 'Saving…' : 'Save changes'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AttributeMappingActions;
|
||||
@@ -1,18 +0,0 @@
|
||||
.pageHeader {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-left: var(--spacing-2);
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
.pageHeaderActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-6);
|
||||
}
|
||||
|
||||
.unsavedChanges {
|
||||
font-size: var(--periscope-font-size-base);
|
||||
color: var(--accent-amber);
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import { useCanManageAttributeMapping } from '../../hooks/useCanManageAttributeMapping';
|
||||
import styles from './AttributeMappingHeader.module.scss';
|
||||
|
||||
interface AttributeMappingHeaderProps {
|
||||
isDirty: boolean;
|
||||
isSaving: boolean;
|
||||
onDiscard: () => void;
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
function AttributeMappingHeader({
|
||||
isDirty,
|
||||
isSaving,
|
||||
onDiscard,
|
||||
onSave,
|
||||
}: AttributeMappingHeaderProps): JSX.Element {
|
||||
const canManage = useCanManageAttributeMapping();
|
||||
return (
|
||||
<header className={styles.pageHeader}>
|
||||
<Typography.Text as="p" size="base" color="muted">
|
||||
Configure source-to-target attribute remapping for LLM traces
|
||||
</Typography.Text>
|
||||
{canManage && isDirty && (
|
||||
<div className={styles.pageHeaderActions}>
|
||||
<span className={styles.unsavedChanges} data-testid="unsaved-changes">
|
||||
Unsaved changes
|
||||
</span>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={onDiscard}
|
||||
disabled={isSaving}
|
||||
testId="discard-changes-btn"
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={onSave}
|
||||
loading={isSaving}
|
||||
disabled={isSaving}
|
||||
testId="save-changes-btn"
|
||||
>
|
||||
{isSaving ? 'Saving…' : 'Save changes'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default AttributeMappingHeader;
|
||||
@@ -1,4 +1,7 @@
|
||||
.groupForm {
|
||||
--input-foreground: var(--l1-foreground);
|
||||
--input-placeholder-color: var(--l3-foreground);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-10);
|
||||
@@ -18,11 +21,8 @@
|
||||
}
|
||||
|
||||
.groupFormLabel {
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--l3-foreground);
|
||||
font-size: var(--periscope-font-size-base);
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.groupFormHint {
|
||||
|
||||
@@ -5,17 +5,12 @@
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--l3-foreground);
|
||||
font-size: var(--periscope-font-size-base);
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.labelHint {
|
||||
font-weight: var(--font-weight-normal);
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
.keys {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
.form {
|
||||
--input-foreground: var(--l1-foreground);
|
||||
--input-placeholder-color: var(--l3-foreground);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-10);
|
||||
@@ -12,17 +14,12 @@
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--l3-foreground);
|
||||
font-size: var(--periscope-font-size-base);
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.labelHint {
|
||||
font-weight: var(--font-weight-normal);
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
.hint {
|
||||
|
||||
@@ -11,10 +11,12 @@ const PERSISTED_KEY = `@signoz/table-columns/${STORAGE_KEY}`;
|
||||
|
||||
const ROWS = [{ id: 't1', trace_id: 'abc', 'service.name': 'checkout' }];
|
||||
|
||||
// An aggregate outside the default order starts hidden, so the persisted
|
||||
// defaults are observable.
|
||||
const COLUMNS = buildTraceViewColumns([
|
||||
{ name: 'trace_id' },
|
||||
{ name: 'service.name', fieldContext: 'resource' },
|
||||
{ name: 'start_time' },
|
||||
{ name: 'unlisted_aggregate' },
|
||||
]);
|
||||
|
||||
function RaceHarness(): JSX.Element {
|
||||
@@ -66,7 +68,9 @@ describe('TracesTable column-init race', () => {
|
||||
|
||||
await expect(screen.findByRole('table')).resolves.toBeInTheDocument();
|
||||
expect(screen.getByText('trace_id')).toBeInTheDocument();
|
||||
expect(screen.queryByText('start_time')).not.toBeInTheDocument();
|
||||
expect(persistedState()?.hiddenColumnIds).toStrictEqual(['start_time']);
|
||||
expect(screen.queryByText('unlisted_aggregate')).not.toBeInTheDocument();
|
||||
expect(persistedState()?.hiddenColumnIds).toStrictEqual([
|
||||
'unlisted_aggregate',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -128,14 +128,7 @@ describe('TracesView column persistence', () => {
|
||||
await findTable();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(persistedState()?.hiddenColumnIds).toStrictEqual([
|
||||
'start_time',
|
||||
'end_time',
|
||||
'error_count',
|
||||
'input',
|
||||
'output',
|
||||
'trace:tool_call_count:float64',
|
||||
]);
|
||||
expect(persistedState()?.hiddenColumnIds).toStrictEqual([]);
|
||||
});
|
||||
expect(screen.getByText(OPTIONS_TRIGGER)).toBeInTheDocument();
|
||||
expect(screen.getByText('llm_call_count')).toBeInTheDocument();
|
||||
@@ -160,7 +153,7 @@ describe('TracesView column persistence', () => {
|
||||
expect(screen.queryByText(OPTIONS_TRIGGER)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders only the default-visible columns when the field keys fail', async () => {
|
||||
it('renders the display-only columns when the field keys fail', async () => {
|
||||
mockFieldKeysFailure();
|
||||
renderTracesView();
|
||||
|
||||
@@ -168,8 +161,8 @@ describe('TracesView column persistence', () => {
|
||||
|
||||
expect(screen.getByText('root_span_name')).toBeInTheDocument();
|
||||
expect(screen.getByText('trace_id')).toBeInTheDocument();
|
||||
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('output')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('input')).toBeInTheDocument();
|
||||
expect(screen.queryByText('llm_call_count')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('leaves an existing selection untouched while the field keys fail', async () => {
|
||||
|
||||
@@ -109,17 +109,24 @@ describe('useTraceViewColumns', () => {
|
||||
const { result } = await renderColumns();
|
||||
|
||||
expect(columnNames(result.current.columns)).toStrictEqual([
|
||||
'trace_id',
|
||||
'service.name',
|
||||
'root_span_name',
|
||||
'estimated_total_cost',
|
||||
'trace_duration_nano',
|
||||
'span_count',
|
||||
'trace_id',
|
||||
'total_tokens',
|
||||
'input_tokens',
|
||||
'output_tokens',
|
||||
'distinct_tool_count',
|
||||
'llm_call_count',
|
||||
'tool_call_count',
|
||||
'start_time',
|
||||
'end_time',
|
||||
'error_count',
|
||||
'input',
|
||||
'output',
|
||||
...AGGREGATE_KEYS,
|
||||
'max_llm_duration_nano',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -127,14 +134,24 @@ describe('useTraceViewColumns', () => {
|
||||
const { result } = await renderColumns();
|
||||
|
||||
expect(fieldNames(result.current.selectedFields)).toStrictEqual([
|
||||
'trace_id',
|
||||
'service.name',
|
||||
'root_span_name',
|
||||
'estimated_total_cost',
|
||||
'trace_duration_nano',
|
||||
'span_count',
|
||||
'trace_id',
|
||||
'llm_call_count',
|
||||
'total_tokens',
|
||||
'estimated_total_cost',
|
||||
'input_tokens',
|
||||
'output_tokens',
|
||||
'distinct_tool_count',
|
||||
'llm_call_count',
|
||||
'tool_call_count',
|
||||
'start_time',
|
||||
'end_time',
|
||||
'error_count',
|
||||
'input',
|
||||
'output',
|
||||
'max_llm_duration_nano',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -193,14 +210,24 @@ describe('useTraceViewColumns', () => {
|
||||
|
||||
expect(result.current.canPersistColumns).toBe(true);
|
||||
expect(fieldNames(result.current.selectedFields)).toStrictEqual([
|
||||
'trace_id',
|
||||
'service.name',
|
||||
'root_span_name',
|
||||
'estimated_total_cost',
|
||||
'trace_duration_nano',
|
||||
'span_count',
|
||||
'trace_id',
|
||||
'llm_call_count',
|
||||
'total_tokens',
|
||||
'estimated_total_cost',
|
||||
'input_tokens',
|
||||
'output_tokens',
|
||||
'distinct_tool_count',
|
||||
'llm_call_count',
|
||||
'tool_call_count',
|
||||
'start_time',
|
||||
'end_time',
|
||||
'error_count',
|
||||
'input',
|
||||
'output',
|
||||
'max_llm_duration_nano',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,20 +5,43 @@ import { DEFAULT_PER_PAGE_OPTIONS } from 'hooks/queryPagination';
|
||||
|
||||
export const PER_PAGE_OPTIONS: number[] = [10, ...DEFAULT_PER_PAGE_OPTIONS];
|
||||
|
||||
/** Always visible: it is the row's link to the trace. */
|
||||
/** Always present: it is the row's link to the trace, but it can be reordered. */
|
||||
export const TRACE_ID_COLUMN_ID = 'trace_id';
|
||||
|
||||
/** Everything else starts hidden; only applied at first init, since the store persists hidden ids. */
|
||||
const DEFAULT_VISIBLE_FIELDS = new Set([
|
||||
/** Fallback order, until the user drags a column; unlisted fields keep the order the keys endpoint returns them in. */
|
||||
const DEFAULT_COLUMN_ORDER = [
|
||||
TRACE_ID_COLUMN_ID,
|
||||
'service.name',
|
||||
'root_span_name',
|
||||
'estimated_total_cost',
|
||||
'trace_duration_nano',
|
||||
'span_count',
|
||||
'llm_call_count',
|
||||
'total_tokens',
|
||||
'estimated_total_cost',
|
||||
TRACE_ID_COLUMN_ID,
|
||||
]);
|
||||
'input_tokens',
|
||||
'output_tokens',
|
||||
'distinct_tool_count',
|
||||
'llm_call_count',
|
||||
'tool_call_count',
|
||||
'start_time',
|
||||
'end_time',
|
||||
'error_count',
|
||||
'input',
|
||||
'output',
|
||||
'max_llm_duration_nano',
|
||||
];
|
||||
|
||||
const orderRank = (field: TelemetryFieldKey): number => {
|
||||
const index = DEFAULT_COLUMN_ORDER.indexOf(field.name);
|
||||
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
||||
};
|
||||
|
||||
export const sortByDefaultOrder = (
|
||||
fields: TelemetryFieldKey[],
|
||||
): TelemetryFieldKey[] =>
|
||||
[...fields].sort((a, b) => orderRank(a) - orderRank(b));
|
||||
|
||||
/** Anything the keys endpoint adds beyond the ordered set starts hidden; only applied at first init, since the store persists hidden ids. */
|
||||
const DEFAULT_VISIBLE_FIELDS = new Set(DEFAULT_COLUMN_ORDER);
|
||||
|
||||
export const buildTraceViewColumns = (
|
||||
fields: TelemetryFieldKey[],
|
||||
@@ -27,7 +50,7 @@ export const buildTraceViewColumns = (
|
||||
...getFieldColumn(field),
|
||||
defaultVisibility: DEFAULT_VISIBLE_FIELDS.has(field.name),
|
||||
// The shared column builder pins anything in TIMESTAMP_FIELD_NAMES; these stay movable.
|
||||
enableMove: field.name !== TRACE_ID_COLUMN_ID,
|
||||
enableMove: true,
|
||||
enableRemove: field.name !== TRACE_ID_COLUMN_ID,
|
||||
canBeHidden: field.name !== TRACE_ID_COLUMN_ID,
|
||||
}));
|
||||
|
||||
@@ -21,7 +21,11 @@ import {
|
||||
TRACE_VIEW_COLUMN_EXTRA_FIELDS,
|
||||
TRACE_VIEW_FIELD_KEYS,
|
||||
} from '../constants';
|
||||
import { buildTraceViewColumns, TRACE_ID_COLUMN_ID } from './configs';
|
||||
import {
|
||||
buildTraceViewColumns,
|
||||
sortByDefaultOrder,
|
||||
TRACE_ID_COLUMN_ID,
|
||||
} from './configs';
|
||||
|
||||
const STORAGE_KEY = LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_COLUMNS;
|
||||
|
||||
@@ -55,7 +59,10 @@ export function useTraceViewColumns(): UseTraceViewColumns {
|
||||
);
|
||||
|
||||
const availableFields = useMemo(
|
||||
() => mergeExtraFields(TRACE_VIEW_COLUMN_EXTRA_FIELDS, fetchedFields),
|
||||
() =>
|
||||
sortByDefaultOrder(
|
||||
mergeExtraFields(TRACE_VIEW_COLUMN_EXTRA_FIELDS, fetchedFields),
|
||||
),
|
||||
[fetchedFields],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
.llmObservability {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
margin-top: var(--spacing-2);
|
||||
margin-left: var(--spacing-2);
|
||||
--tabs-text-color: var(--l1-foreground);
|
||||
--tabs-active-text-color: var(--l1-foreground);
|
||||
[role='tabpanel'] {
|
||||
margin: 0;
|
||||
padding: var(--spacing-0) var(--spacing-4);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Tabs } from '@signozhq/ui/tabs';
|
||||
|
||||
import { useLLMObservabilityTabs } from './hooks/useLLMObservabilityTabs';
|
||||
import styles from './LLMObservability.module.scss';
|
||||
|
||||
// Shell for the LLM Observability page: renders the top-level tab bar
|
||||
// (Overview / Configuration) using the SigNoz design-system Tabs, with
|
||||
// route-driven active state from useLLMObservabilityTabs.
|
||||
function LLMObservability(): JSX.Element {
|
||||
const { items, activeTab, onTabChange } = useLLMObservabilityTabs();
|
||||
|
||||
return (
|
||||
<div className={styles.llmObservability} data-testid="llm-observability-page">
|
||||
<Tabs
|
||||
items={items}
|
||||
value={activeTab}
|
||||
onChange={onTabChange}
|
||||
testId="llm-observability-tabs"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LLMObservability;
|
||||
@@ -6,7 +6,13 @@
|
||||
//TODO: remove this once we have a proper dashboard page.
|
||||
// The embedded V2 DashboardContainer renders its own page header (dashboard
|
||||
// title + share/feedback chrome) removed it for now
|
||||
:global([class*='dashboardPageHeader']) {
|
||||
:global([class*='dashboardPageHeader']),
|
||||
:global([class*='dashboardInfoWithActions']) {
|
||||
display: none;
|
||||
}
|
||||
// Embedded read-only: the add-variable and panel-action triggers are dead here.
|
||||
:global([class*='addSlot']),
|
||||
:global([data-testid^='panel-actions-']) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
--tabs-content-padding: 0;
|
||||
margin-top: var(--spacing-3);
|
||||
--tabs-text-color: var(--l1-foreground);
|
||||
--tabs-active-text-color: var(--l1-foreground);
|
||||
|
||||
:global(.ant-tabs-tabpane) {
|
||||
padding: var(--spacing-0) var(--spacing-8);
|
||||
}
|
||||
}
|
||||
|
||||
.tabLabel {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Tabs } from '@signozhq/ui/tabs';
|
||||
import { Tabs } from 'antd';
|
||||
import { useListUnmappedLLMModels } from 'api/generated/services/llmpricingrules';
|
||||
import { parseAsStringEnum, useQueryState } from 'nuqs';
|
||||
|
||||
@@ -26,7 +26,7 @@ function LLMObservabilityModelPricing(): JSX.Element {
|
||||
data-testid="llm-observability-model-pricing-page"
|
||||
>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
activeKey={activeTab}
|
||||
onChange={(key): void => {
|
||||
void setActiveTab(key as typeof activeTab);
|
||||
}}
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
.filtersBarSource {
|
||||
width: 160px;
|
||||
/* Trigger defaults to 2.25rem; the search input and Add button are both 2rem. */
|
||||
--select-trigger-height: 32px;
|
||||
}
|
||||
|
||||
.pageError {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.fieldLabel {
|
||||
composes: fieldLabel from './shared.module.scss';
|
||||
}
|
||||
|
||||
.drawerSection {
|
||||
composes: drawerSection from './shared.module.scss';
|
||||
}
|
||||
@@ -17,6 +21,9 @@
|
||||
--dialog-header-padding: var(--spacing-10) var(--spacing-12);
|
||||
--dialog-footer-padding: var(--spacing-8) var(--spacing-12);
|
||||
|
||||
--input-foreground: var(--l1-foreground);
|
||||
--input-placeholder-color: var(--l3-foreground);
|
||||
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ function ModelCostDrawer({
|
||||
drawerHeaderProps={{ className: styles.title }}
|
||||
>
|
||||
<div className={styles.drawerSection}>
|
||||
<label htmlFor="billing-model-id">
|
||||
<label htmlFor="billing-model-id" className={styles.fieldLabel}>
|
||||
Billing Model ID{' '}
|
||||
<span className={styles.required} aria-hidden="true">
|
||||
*
|
||||
@@ -144,7 +144,9 @@ function ModelCostDrawer({
|
||||
</div>
|
||||
|
||||
<div className={styles.drawerSection}>
|
||||
<label htmlFor="provider-select">Provider</label>
|
||||
<label htmlFor="provider-select" className={styles.fieldLabel}>
|
||||
Provider
|
||||
</label>
|
||||
<Controller
|
||||
name="provider"
|
||||
control={control}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.fieldLabel {
|
||||
composes: fieldLabel from '../../shared.module.scss';
|
||||
}
|
||||
|
||||
.drawerSection {
|
||||
composes: drawerSection from '../../shared.module.scss';
|
||||
}
|
||||
|
||||
@@ -67,9 +67,7 @@ function ExtraPricingBuckets({
|
||||
return (
|
||||
<div className={cx(styles.extraBucketsSection, styles.drawerSection)}>
|
||||
<div className={styles.extraBucketsSectionHead}>
|
||||
<Typography.Text as="span" size="small" color="muted">
|
||||
Extra Pricing Buckets
|
||||
</Typography.Text>
|
||||
<span className={styles.fieldLabel}>Extra Pricing Buckets</span>
|
||||
<Typography.Text as="span" size="small" color="muted">
|
||||
Optional
|
||||
</Typography.Text>
|
||||
@@ -116,7 +114,9 @@ function ExtraPricingBuckets({
|
||||
|
||||
{addedBuckets.length > 0 && (
|
||||
<div className={cx(styles.pricingField, styles.cacheModeField)}>
|
||||
<label htmlFor="cache-mode">Cache mode</label>
|
||||
<label htmlFor="cache-mode" className={styles.fieldLabel}>
|
||||
Cache mode
|
||||
</label>
|
||||
<SelectSimple
|
||||
id="cache-mode"
|
||||
value={pricing.cacheMode}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.fieldLabel {
|
||||
composes: fieldLabel from '../../shared.module.scss';
|
||||
}
|
||||
|
||||
.drawerSection {
|
||||
composes: drawerSection from '../../shared.module.scss';
|
||||
}
|
||||
|
||||
@@ -37,12 +37,12 @@ function PatternEditor({
|
||||
|
||||
return (
|
||||
<div className={styles.drawerSection}>
|
||||
<Typography.Text as="span">
|
||||
<span className={styles.fieldLabel}>
|
||||
Model name patterns{' '}
|
||||
<Typography.Text as="span" color="muted">
|
||||
(prefix match)
|
||||
</Typography.Text>
|
||||
</Typography.Text>
|
||||
</span>
|
||||
<div className={styles.patternBox}>
|
||||
<div className={styles.patternChips}>
|
||||
{patterns.map((pattern) => (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.fieldLabel {
|
||||
composes: fieldLabel from '../../shared.module.scss';
|
||||
}
|
||||
|
||||
.drawerSection {
|
||||
composes: drawerSection from '../../shared.module.scss';
|
||||
}
|
||||
|
||||
@@ -24,9 +24,7 @@ function PricingFields({
|
||||
return (
|
||||
<div className={cx(styles.drawerSection, styles.drawerSurface)}>
|
||||
<div className={styles.drawerSurfaceHead}>
|
||||
<Typography.Text size="base" weight="bold">
|
||||
Pricing (per 1M tokens, USD)
|
||||
</Typography.Text>
|
||||
<span className={styles.fieldLabel}>Pricing (per 1M tokens, USD)</span>
|
||||
|
||||
{isReadOnly && (
|
||||
<span className={styles.managedLabel} data-testid="drawer-readonly-label">
|
||||
@@ -38,7 +36,7 @@ function PricingFields({
|
||||
</div>
|
||||
<div className={styles.pricingGrid}>
|
||||
<div className={styles.pricingField}>
|
||||
<label htmlFor="input-cost">
|
||||
<label htmlFor="input-cost" className={styles.fieldLabel}>
|
||||
Input Cost{' '}
|
||||
<span className={styles.required} aria-hidden="true">
|
||||
*
|
||||
@@ -58,7 +56,7 @@ function PricingFields({
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.pricingField}>
|
||||
<label htmlFor="output-cost">
|
||||
<label htmlFor="output-cost" className={styles.fieldLabel}>
|
||||
Output Cost{' '}
|
||||
<span className={styles.required} aria-hidden="true">
|
||||
*
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.fieldLabel {
|
||||
composes: fieldLabel from '../../shared.module.scss';
|
||||
}
|
||||
|
||||
.drawerSection {
|
||||
composes: drawerSection from '../../shared.module.scss';
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useState } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
|
||||
import { Lock } from '@signozhq/icons';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
|
||||
import styles from './SourceSelector.module.scss';
|
||||
@@ -42,9 +41,7 @@ function SourceSelector({
|
||||
return (
|
||||
<div className={cx(styles.drawerSection, styles.drawerSurface)}>
|
||||
<div className={styles.drawerSurfaceHead}>
|
||||
<Typography.Text weight="bold" size="base">
|
||||
Source
|
||||
</Typography.Text>
|
||||
<span className={styles.fieldLabel}>Source</span>
|
||||
|
||||
{isReadOnly && (
|
||||
<span className={styles.managedLabel} data-testid="drawer-managed-label">
|
||||
|
||||
@@ -47,6 +47,14 @@
|
||||
color: var(--accent-cherry);
|
||||
}
|
||||
|
||||
/* Single treatment for every label in the drawer, so field labels and the */
|
||||
.fieldLabel {
|
||||
font-size: var(--periscope-font-size-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--spacing-10);
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.pricingField {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -167,7 +167,7 @@ describe('UnpricedModelsTab (integration)', () => {
|
||||
|
||||
await screen.findByTestId(`unpriced-model-name-${MODEL}`);
|
||||
|
||||
// Open the row's dropdown and take the "Create pricing for …" escape hatch
|
||||
// Open the row's dropdown and take the "Create a new pricing model" escape hatch
|
||||
// instead of mapping onto an existing billing model.
|
||||
await user.click(screen.getByTestId(`map-to-select-${MODEL}`));
|
||||
await user.click(await screen.findByTestId(`map-to-create-${MODEL}`));
|
||||
|
||||
@@ -14,6 +14,24 @@
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: var(--spacing-2);
|
||||
background-color: var(--l2-background);
|
||||
}
|
||||
|
||||
.createItem {
|
||||
gap: var(--spacing-4);
|
||||
font-style: normal;
|
||||
color: var(--accent-primary);
|
||||
--command-item-cursor: pointer;
|
||||
--command-item-svg-size: var(--spacing-7);
|
||||
|
||||
&[data-selected='true'] {
|
||||
background-color: var(--callout-primary-background);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.skeletonList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -119,15 +119,18 @@ function MapToBillingModelSelect({
|
||||
options scroll. Escape hatch when no existing billing model fits:
|
||||
define this model's own pricing rather than mapping onto another. */}
|
||||
<ComboboxSeparator alwaysRender />
|
||||
<ComboboxCreateItem
|
||||
inputValue={modelName}
|
||||
value={`create-pricing-${modelName}`}
|
||||
prefix={<Plus size={14} />}
|
||||
onSelect={handleCreateNew}
|
||||
testId={`map-to-create-${modelName}`}
|
||||
>
|
||||
Create pricing for "{modelName}"
|
||||
</ComboboxCreateItem>
|
||||
<div className={styles.footer}>
|
||||
<ComboboxCreateItem
|
||||
className={styles.createItem}
|
||||
inputValue={modelName}
|
||||
value={`create-pricing-${modelName}`}
|
||||
prefix={<Plus size={14} />}
|
||||
onSelect={handleCreateNew}
|
||||
testId={`map-to-create-${modelName}`}
|
||||
>
|
||||
Create a new pricing model
|
||||
</ComboboxCreateItem>
|
||||
</div>
|
||||
</ComboboxCommand>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { type TabItemProps } from '@signozhq/ui/tabs';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
|
||||
import LLMObservabilityAttributeMapping from '../AttributeMapping/LLMObservabilityAttributeMapping';
|
||||
import Explorer from '../Explorer/Explorer';
|
||||
import Overview from '../Overview/Overview';
|
||||
import LLMObservabilityModelPricing from '../Settings/ModelPricing/LLMObservabilityModelPricing';
|
||||
|
||||
const OVERVIEW_KEY = ROUTES.AI_OBSERVABILITY_OVERVIEW;
|
||||
const EXPLORER_KEY = ROUTES.AI_OBSERVABILITY_EXPLORER;
|
||||
const CONFIGURATION_KEY = ROUTES.AI_OBSERVABILITY_CONFIGURATION;
|
||||
const ATTRIBUTE_MAPPING_KEY = ROUTES.AI_OBSERVABILITY_ATTRIBUTE_MAPPING;
|
||||
|
||||
interface UseLLMObservabilityTabsResult {
|
||||
items: TabItemProps[];
|
||||
activeTab: string;
|
||||
onTabChange: (key: string) => void;
|
||||
}
|
||||
|
||||
// Drives the top-level LLM Observability tabs. Route-driven: the active tab is
|
||||
// derived from the pathname (each tab owns a URL) and changing tabs navigates,
|
||||
// so tabs stay shareable/back-button friendly while rendering with the SigNoz
|
||||
// design-system Tabs.
|
||||
export function useLLMObservabilityTabs(): UseLLMObservabilityTabsResult {
|
||||
const { pathname } = useLocation();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
let activeTab: string = OVERVIEW_KEY;
|
||||
if (pathname.startsWith(CONFIGURATION_KEY)) {
|
||||
activeTab = CONFIGURATION_KEY;
|
||||
} else if (pathname.startsWith(ATTRIBUTE_MAPPING_KEY)) {
|
||||
activeTab = ATTRIBUTE_MAPPING_KEY;
|
||||
} else if (pathname.startsWith(EXPLORER_KEY)) {
|
||||
activeTab = EXPLORER_KEY;
|
||||
}
|
||||
|
||||
const onTabChange = useCallback(
|
||||
(key: string): void => {
|
||||
safeNavigate(key);
|
||||
},
|
||||
[safeNavigate],
|
||||
);
|
||||
|
||||
const items: TabItemProps[] = [
|
||||
{
|
||||
key: OVERVIEW_KEY,
|
||||
label: 'Overview',
|
||||
children: <Overview />,
|
||||
},
|
||||
{
|
||||
key: EXPLORER_KEY,
|
||||
label: 'Explorer',
|
||||
children: <Explorer />,
|
||||
},
|
||||
{
|
||||
key: CONFIGURATION_KEY,
|
||||
label: 'Model pricing',
|
||||
children: <LLMObservabilityModelPricing />,
|
||||
},
|
||||
{
|
||||
key: ATTRIBUTE_MAPPING_KEY,
|
||||
label: 'Attribute Mapping',
|
||||
children: <LLMObservabilityAttributeMapping />,
|
||||
},
|
||||
];
|
||||
|
||||
return { items, activeTab, onTabChange };
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import { memo, useMemo } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Button, Flex } from 'antd';
|
||||
import { Divider } from '@signozhq/ui/divider';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import Controls from 'container/Controls';
|
||||
import Download from 'container/Download/Download';
|
||||
import { getGlobalTime } from 'container/LogsSearchFilter/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { OrderPreferenceItems } from 'pages/Logs/config';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Dispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
GET_NEXT_LOG_LINES,
|
||||
GET_PREVIOUS_LOG_LINES,
|
||||
RESET_ID_START_AND_END,
|
||||
SET_LOG_LINES_PER_PAGE,
|
||||
} from 'types/actions/logs';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import { Container } from './styles';
|
||||
import { SkipBack } from '@signozhq/icons';
|
||||
|
||||
function LogControls(): JSX.Element | null {
|
||||
const {
|
||||
logLinesPerPage,
|
||||
liveTail,
|
||||
isLoading: isLogsLoading,
|
||||
isLoadingAggregate,
|
||||
logs,
|
||||
order,
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const handleLogLinesPerPageChange = (e: Pagination['limit']): void => {
|
||||
dispatch({
|
||||
type: SET_LOG_LINES_PER_PAGE,
|
||||
payload: {
|
||||
logsLinesPerPage: e,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleGoToLatest = (): void => {
|
||||
const { maxTime, minTime } = getMinMaxForSelectedTime(
|
||||
globalTime.selectedTime,
|
||||
globalTime.minTime,
|
||||
globalTime.maxTime,
|
||||
);
|
||||
|
||||
const updatedGlobalTime = getGlobalTime(globalTime.selectedTime, {
|
||||
maxTime,
|
||||
minTime,
|
||||
});
|
||||
|
||||
if (updatedGlobalTime) {
|
||||
dispatch({
|
||||
type: RESET_ID_START_AND_END,
|
||||
payload: updatedGlobalTime,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleNavigatePrevious = (): void => {
|
||||
dispatch({
|
||||
type: GET_PREVIOUS_LOG_LINES,
|
||||
});
|
||||
};
|
||||
|
||||
const handleNavigateNext = (): void => {
|
||||
dispatch({
|
||||
type: GET_NEXT_LOG_LINES,
|
||||
});
|
||||
};
|
||||
|
||||
const flattenLogData = useMemo(
|
||||
() =>
|
||||
logs.map((log) => {
|
||||
const timestamp =
|
||||
typeof log.timestamp === 'string'
|
||||
? dayjs(log.timestamp).format(DATE_TIME_FORMATS.ISO_DATETIME_MS)
|
||||
: dayjs(log.timestamp / 1e6).format(DATE_TIME_FORMATS.ISO_DATETIME_MS);
|
||||
|
||||
return FlatLogData({
|
||||
...log,
|
||||
timestamp,
|
||||
});
|
||||
}),
|
||||
[logs],
|
||||
);
|
||||
|
||||
const isLoading = isLogsLoading || isLoadingAggregate;
|
||||
|
||||
if (liveTail !== 'STOPPED') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Download data={flattenLogData} isLoading={isLoading} fileName="log_data" />
|
||||
<Button
|
||||
loading={isLoading}
|
||||
size="small"
|
||||
type="link"
|
||||
disabled={order === OrderPreferenceItems.ASC}
|
||||
onClick={handleGoToLatest}
|
||||
>
|
||||
<Flex align="center" gap="4px">
|
||||
<SkipBack size="md" /> Go to latest
|
||||
</Flex>
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
<Controls
|
||||
isLoading={isLoading}
|
||||
totalCount={logs.length}
|
||||
countPerPage={logLinesPerPage}
|
||||
handleNavigatePrevious={handleNavigatePrevious}
|
||||
handleNavigateNext={handleNavigateNext}
|
||||
handleCountItemsPerPageChange={handleLogLinesPerPageChange}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LogControls);
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Button } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
`;
|
||||
|
||||
export const DownloadLogButton = styled(Button)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
@@ -1,6 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { Link, Pin } from '@signozhq/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
@@ -14,23 +12,19 @@ import { ResizeTable } from 'components/ResizeTable';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import { FontSize, OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import history from 'lib/history';
|
||||
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
|
||||
import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Dispatch } from 'redux';
|
||||
import AppActions from 'types/actions';
|
||||
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { ActionItemProps } from './ActionItem';
|
||||
import { RESTRICTED_SELECTED_FIELDS } from './config';
|
||||
import FieldRenderer from './FieldRenderer';
|
||||
import TableViewActions from './TableView/TableViewActions';
|
||||
import {
|
||||
@@ -65,7 +59,6 @@ function TableView({
|
||||
listViewPanelSelectedFields,
|
||||
handleChangeSelectedView,
|
||||
}: Props): JSX.Element | null {
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const [isfilterInLoading, setIsFilterInLoading] = useState<boolean>(false);
|
||||
const [isfilterOutLoading, setIsFilterOutLoading] = useState<boolean>(false);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
@@ -185,11 +178,6 @@ function TableView({
|
||||
const spanId = flattenLogData?.span_id;
|
||||
|
||||
if (traceId) {
|
||||
dispatch({
|
||||
type: SET_DETAILED_LOG_DATA,
|
||||
payload: null,
|
||||
});
|
||||
|
||||
const basePath = generatePath(ROUTES.TRACE_DETAIL, {
|
||||
id: traceId,
|
||||
});
|
||||
|
||||
@@ -10,7 +10,6 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { RESTRICTED_SELECTED_FIELDS } from '../config';
|
||||
import { DataType } from '../TableView';
|
||||
import {
|
||||
filterKeyForField,
|
||||
@@ -141,10 +141,9 @@ export default function TableViewActions(
|
||||
const { stagedQuery, updateQueriesData } = useQueryBuilder();
|
||||
const { dataType, logType: fieldType } = getFieldAttributes(record.field);
|
||||
|
||||
// there is no option for where clause in old logs explorer and live logs page or infra monitoring
|
||||
const isOldLogsExplorerOrLiveLogsPage = useMemo(
|
||||
// there is no option for where clause in live logs page or infra monitoring
|
||||
const isLiveLogsOrInfraPage = useMemo(
|
||||
() =>
|
||||
pathname === ROUTES.OLD_LOGS_EXPLORER ||
|
||||
pathname === ROUTES.LIVE_LOGS ||
|
||||
pathname === ROUTES.INFRASTRUCTURE_MONITORING_HOSTS ||
|
||||
pathname === ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES,
|
||||
@@ -400,7 +399,7 @@ export default function TableViewActions(
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
{!isOldLogsExplorerOrLiveLogsPage && (
|
||||
{!isLiveLogsOrInfraPage && (
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
@@ -487,7 +486,7 @@ export default function TableViewActions(
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
{!isOldLogsExplorerOrLiveLogsPage && (
|
||||
{!isLiveLogsOrInfraPage && (
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
|
||||
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
|
||||
import { RESTRICTED_SELECTED_FIELDS } from '../../config';
|
||||
import TableViewActions from '../TableViewActions';
|
||||
import useAsyncJSONProcessing from '../useAsyncJSONProcessing';
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
export const RESTRICTED_SELECTED_FIELDS = ['timestamp', 'id'];
|
||||
|
||||
// Fields that can be filtered on but not grouped by in the log details view.
|
||||
export const RESTRICTED_GROUP_BY_FIELDS = ['body', 'trace_id'];
|
||||
|
||||
export const typeToArrayTypeMapper: { [key in DataTypes]: DataTypes } = {
|
||||
[DataTypes.String]: DataTypes.ArrayString,
|
||||
[DataTypes.Float64]: DataTypes.ArrayFloat64,
|
||||
|
||||
@@ -61,8 +61,7 @@ export function useLogAttributeActions({
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_JSON_BODY)
|
||||
?.active || false;
|
||||
|
||||
const isOldExplorerOrLive =
|
||||
pathname === ROUTES.OLD_LOGS_EXPLORER || pathname === ROUTES.LIVE_LOGS;
|
||||
const isLiveLogs = pathname === ROUTES.LIVE_LOGS;
|
||||
|
||||
const filterFor = useCallback(
|
||||
(context: FieldContext, isFilterIn: boolean): void => {
|
||||
@@ -221,7 +220,7 @@ export function useLogAttributeActions({
|
||||
!handleChangeSelectedView ||
|
||||
!buildLogFilterTarget(fieldKeyPath, undefined, isBodyJsonQueryEnabled)
|
||||
.groupBySupported ||
|
||||
isOldExplorerOrLive,
|
||||
isLiveLogs,
|
||||
},
|
||||
{
|
||||
key: LogDetailsAction.REPLACE_FILTER,
|
||||
@@ -229,9 +228,7 @@ export function useLogAttributeActions({
|
||||
icon: <RefreshCw size={12} />,
|
||||
onClick: replaceFilter,
|
||||
shouldHide: (_key, fieldKeyPath): boolean =>
|
||||
!handleChangeSelectedView ||
|
||||
isRestricted(fieldKeyPath) ||
|
||||
isOldExplorerOrLive,
|
||||
!handleChangeSelectedView || isRestricted(fieldKeyPath) || isLiveLogs,
|
||||
},
|
||||
];
|
||||
}, [
|
||||
@@ -239,7 +236,7 @@ export function useLogAttributeActions({
|
||||
groupBy,
|
||||
replaceFilter,
|
||||
isBodyJsonQueryEnabled,
|
||||
isOldExplorerOrLive,
|
||||
isLiveLogs,
|
||||
handleChangeSelectedView,
|
||||
onApplyLogFilter,
|
||||
]);
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
import { memo, useCallback } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { getOldLogsOperatorFromNew } from 'hooks/logs/useActiveLog';
|
||||
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
|
||||
import getStep from 'lib/getStep';
|
||||
import { getIdConditions } from 'pages/Logs/utils';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { getLogs } from 'store/actions/logs/getLogs';
|
||||
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
SET_DETAILED_LOG_DATA,
|
||||
SET_SEARCH_QUERY_STRING,
|
||||
TOGGLE_LIVE_TAIL,
|
||||
} from 'types/actions/logs';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
type LogDetailedViewProps = {
|
||||
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
|
||||
getLogsAggregate: (
|
||||
props: Parameters<typeof getLogsAggregate>[0],
|
||||
) => ReturnType<typeof getLogsAggregate>;
|
||||
};
|
||||
|
||||
function LogDetailedView({
|
||||
getLogs,
|
||||
getLogsAggregate,
|
||||
}: LogDetailedViewProps): JSX.Element {
|
||||
const history = useHistory();
|
||||
const {
|
||||
detailedLog,
|
||||
searchFilter: { queryString },
|
||||
logLinesPerPage,
|
||||
idStart,
|
||||
liveTail,
|
||||
idEnd,
|
||||
order,
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const onDrawerClose = (): void => {
|
||||
dispatch({
|
||||
type: SET_DETAILED_LOG_DATA,
|
||||
payload: null,
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddToQuery = useCallback(
|
||||
(fieldKey: string, fieldValue: string, operator: string) => {
|
||||
const newOperator = getOldLogsOperatorFromNew(operator);
|
||||
const updatedQueryString = getGeneratedFilterQueryString(
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
newOperator,
|
||||
queryString,
|
||||
);
|
||||
|
||||
history.replace(`${ROUTES.OLD_LOGS_EXPLORER}?q=${updatedQueryString}`);
|
||||
},
|
||||
[history, queryString],
|
||||
);
|
||||
|
||||
const handleClickActionItem = useCallback(
|
||||
(fieldKey: string, fieldValue: string, operator: string): void => {
|
||||
const newOperator = getOldLogsOperatorFromNew(operator);
|
||||
const updatedQueryString = getGeneratedFilterQueryString(
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
newOperator,
|
||||
queryString,
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: SET_SEARCH_QUERY_STRING,
|
||||
payload: {
|
||||
searchQueryString: updatedQueryString,
|
||||
},
|
||||
});
|
||||
|
||||
if (liveTail === 'STOPPED') {
|
||||
getLogs({
|
||||
q: updatedQueryString,
|
||||
limit: logLinesPerPage,
|
||||
orderBy: 'timestamp',
|
||||
order,
|
||||
timestampStart: minTime,
|
||||
timestampEnd: maxTime,
|
||||
...getIdConditions(idStart, idEnd, order),
|
||||
});
|
||||
getLogsAggregate({
|
||||
timestampStart: minTime,
|
||||
timestampEnd: maxTime,
|
||||
step: getStep({
|
||||
start: minTime,
|
||||
end: maxTime,
|
||||
inputFormat: 'ns',
|
||||
}),
|
||||
q: updatedQueryString,
|
||||
});
|
||||
} else if (liveTail === 'PLAYING') {
|
||||
dispatch({
|
||||
type: TOGGLE_LIVE_TAIL,
|
||||
payload: 'PAUSED',
|
||||
});
|
||||
setTimeout(
|
||||
() =>
|
||||
dispatch({
|
||||
type: TOGGLE_LIVE_TAIL,
|
||||
payload: liveTail,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
dispatch,
|
||||
getLogs,
|
||||
getLogsAggregate,
|
||||
idEnd,
|
||||
idStart,
|
||||
liveTail,
|
||||
logLinesPerPage,
|
||||
maxTime,
|
||||
minTime,
|
||||
order,
|
||||
queryString,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
log={detailedLog}
|
||||
onClose={onDrawerClose}
|
||||
onAddToQuery={handleAddToQuery}
|
||||
onClickActionItem={handleClickActionItem}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
getLogs: (props: Parameters<typeof getLogs>[0]) => (dispatch: never) => void;
|
||||
getLogsAggregate: (
|
||||
props: Parameters<typeof getLogsAggregate>[0],
|
||||
) => (dispatch: never) => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
getLogs: bindActionCreators(getLogs, dispatch),
|
||||
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(memo(LogDetailedView as any));
|
||||
@@ -5,10 +5,6 @@ import {
|
||||
QUERY_BUILDER_FUNCTIONS,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { OPERATORS as QUERY_BUILDER_OPERATORS } from 'constants/queryBuilder';
|
||||
import {
|
||||
RESTRICTED_GROUP_BY_FIELDS,
|
||||
RESTRICTED_SELECTED_FIELDS,
|
||||
} from 'container/LogsFilters/config';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
|
||||
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
|
||||
@@ -18,6 +14,10 @@ import {
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
RESTRICTED_GROUP_BY_FIELDS,
|
||||
RESTRICTED_SELECTED_FIELDS,
|
||||
} from './config';
|
||||
import { LogAttributeBucket } from './constants';
|
||||
import { generateFieldKeyForArray, getDataTypes } from './utils';
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
export const TIME_PICKER_OPTIONS = [
|
||||
{
|
||||
value: 5,
|
||||
label: '5m',
|
||||
},
|
||||
{
|
||||
value: 15,
|
||||
label: '15m',
|
||||
},
|
||||
{
|
||||
value: 30,
|
||||
label: '30m',
|
||||
},
|
||||
{
|
||||
value: 60,
|
||||
label: '1hr',
|
||||
},
|
||||
{
|
||||
value: 360,
|
||||
label: '6hrs',
|
||||
},
|
||||
{
|
||||
value: 720,
|
||||
label: '12hrs',
|
||||
},
|
||||
];
|
||||
@@ -1,270 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
import { green } from '@ant-design/colors';
|
||||
import { Pause, Play, EllipsisVertical } from '@signozhq/icons';
|
||||
import { Button, Flex, Popover, Select, Space } from 'antd';
|
||||
import { LiveTail } from 'api/logs/livetail';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import getStep from 'lib/getStep';
|
||||
import { throttle } from 'lodash-es';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { UPDATE_AUTO_REFRESH_DISABLED } from 'types/actions/globalTime';
|
||||
import {
|
||||
FLUSH_LOGS,
|
||||
PUSH_LIVE_TAIL_EVENT,
|
||||
SET_LIVE_TAIL_START_TIME,
|
||||
SET_LOADING,
|
||||
TOGGLE_LIVE_TAIL,
|
||||
} from 'types/actions/logs';
|
||||
import { TLogsLiveTailState } from 'types/api/logs/liveTail';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { TIME_PICKER_OPTIONS } from './config';
|
||||
import { StopContainer, TimePickerCard, TimePickerSelect } from './styles';
|
||||
|
||||
function LogLiveTail({ getLogsAggregate }: Props): JSX.Element {
|
||||
const {
|
||||
liveTail,
|
||||
searchFilter: { queryString },
|
||||
liveTailStartRange,
|
||||
logs,
|
||||
idEnd,
|
||||
idStart,
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const { selectedAutoRefreshInterval } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const handleLiveTail = (toggleState: TLogsLiveTailState): void => {
|
||||
dispatch({
|
||||
type: TOGGLE_LIVE_TAIL,
|
||||
payload: toggleState,
|
||||
});
|
||||
dispatch({
|
||||
type: UPDATE_AUTO_REFRESH_DISABLED,
|
||||
payload: toggleState === 'PLAYING',
|
||||
});
|
||||
};
|
||||
|
||||
const batchedEventsRef = useRef<ILog[]>([]);
|
||||
|
||||
const pushLiveLog = useCallback(() => {
|
||||
dispatch({
|
||||
type: PUSH_LIVE_TAIL_EVENT,
|
||||
payload: batchedEventsRef.current.reverse(),
|
||||
});
|
||||
batchedEventsRef.current = [];
|
||||
}, [dispatch]);
|
||||
|
||||
const pushLiveLogThrottled = useMemo(
|
||||
() => throttle(pushLiveLog, 1000),
|
||||
[pushLiveLog],
|
||||
);
|
||||
|
||||
const batchLiveLog = useCallback(
|
||||
(e: { data: string }): void => {
|
||||
batchedEventsRef.current.push(JSON.parse(e.data as string) as never);
|
||||
pushLiveLogThrottled();
|
||||
},
|
||||
[pushLiveLogThrottled],
|
||||
);
|
||||
|
||||
const firstLogsId = useMemo(() => logs[0]?.id, [logs]);
|
||||
|
||||
// This ref depicts thats whether the live tail is played from paused state or not.
|
||||
const liveTailSourceRef = useRef<EventSource>();
|
||||
|
||||
useEffect(() => {
|
||||
if (liveTail === 'PLAYING') {
|
||||
const timeStamp = dayjs().subtract(liveTailStartRange, 'minute').valueOf();
|
||||
const queryParams = new URLSearchParams({
|
||||
...(queryString ? { q: queryString } : {}),
|
||||
timestampStart: (timeStamp * 1e6) as never,
|
||||
...(liveTailSourceRef.current && firstLogsId
|
||||
? {
|
||||
idGt: firstLogsId,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
if (liveTailSourceRef.current) {
|
||||
liveTailSourceRef.current.close();
|
||||
}
|
||||
|
||||
const source = LiveTail(queryParams.toString());
|
||||
liveTailSourceRef.current = source;
|
||||
source.onmessage = function connectionMessage(e): void {
|
||||
batchLiveLog(e);
|
||||
};
|
||||
source.onerror = function connectionError(event: unknown): void {
|
||||
console.error(event);
|
||||
source.close();
|
||||
dispatch({
|
||||
type: TOGGLE_LIVE_TAIL,
|
||||
payload: 'STOPPED',
|
||||
});
|
||||
dispatch({
|
||||
type: SET_LOADING,
|
||||
payload: false,
|
||||
});
|
||||
notifications.error({
|
||||
message: 'Live tail stopped due to some error.',
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
if (liveTail === 'STOPPED') {
|
||||
liveTailSourceRef.current = undefined;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [liveTail, queryString, notifications, dispatch]);
|
||||
|
||||
const handleLiveTailStart = (): void => {
|
||||
handleLiveTail('PLAYING');
|
||||
const startTime =
|
||||
dayjs().subtract(liveTailStartRange, 'minute').valueOf() * 1e6;
|
||||
|
||||
const endTime = dayjs().valueOf() * 1e6;
|
||||
|
||||
getLogsAggregate({
|
||||
timestampStart: startTime,
|
||||
timestampEnd: endTime,
|
||||
step: getStep({
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
inputFormat: 'ns',
|
||||
}),
|
||||
q: queryString,
|
||||
...(idStart ? { idGt: idStart } : {}),
|
||||
...(idEnd ? { idLt: idEnd } : {}),
|
||||
});
|
||||
|
||||
if (!liveTailSourceRef.current) {
|
||||
dispatch({
|
||||
type: FLUSH_LOGS,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const OptionsPopOverContent = useMemo(
|
||||
() => (
|
||||
<TimePickerSelect
|
||||
getPopupContainer={popupContainer}
|
||||
disabled={liveTail === 'PLAYING'}
|
||||
value={liveTailStartRange}
|
||||
onChange={(value): void => {
|
||||
if (typeof value === 'number') {
|
||||
dispatch({
|
||||
type: SET_LIVE_TAIL_START_TIME,
|
||||
payload: value,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{TIME_PICKER_OPTIONS.map((optionData) => (
|
||||
<Select.Option key={optionData.label} value={optionData.value}>
|
||||
Last {optionData.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</TimePickerSelect>
|
||||
),
|
||||
[dispatch, liveTail, liveTailStartRange],
|
||||
);
|
||||
|
||||
const isDisabled = useMemo(
|
||||
() => selectedAutoRefreshInterval?.length > 0,
|
||||
[selectedAutoRefreshInterval],
|
||||
);
|
||||
|
||||
const onLiveTailStop = (): void => {
|
||||
handleLiveTail('STOPPED');
|
||||
dispatch({
|
||||
type: UPDATE_AUTO_REFRESH_DISABLED,
|
||||
payload: false,
|
||||
});
|
||||
dispatch({
|
||||
type: SET_LOADING,
|
||||
payload: false,
|
||||
});
|
||||
if (liveTailSourceRef.current) {
|
||||
liveTailSourceRef.current.close();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TimePickerCard>
|
||||
<Space size={0} align="center">
|
||||
{liveTail === 'PLAYING' ? (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={onLiveTailStop}
|
||||
title="Pause live tail"
|
||||
style={{ background: green[6] }}
|
||||
>
|
||||
<Flex align="center" gap={4}>
|
||||
<span>Pause</span>
|
||||
<Pause size="md" />
|
||||
</Flex>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleLiveTailStart}
|
||||
title="Start live tail"
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<Flex align="center" gap={4}>
|
||||
Go Live <Play size="md" />
|
||||
</Flex>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{liveTail !== 'STOPPED' && (
|
||||
<Button type="dashed" onClick={onLiveTailStop} title="Exit live tail">
|
||||
<StopContainer isDarkMode={isDarkMode} />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Popover
|
||||
getPopupContainer={popupContainer}
|
||||
placement="bottomRight"
|
||||
title="Select Live Tail Timing"
|
||||
trigger="click"
|
||||
content={OptionsPopOverContent}
|
||||
>
|
||||
<EllipsisVertical size="lg" />
|
||||
</Popover>
|
||||
</Space>
|
||||
</TimePickerCard>
|
||||
);
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
getLogsAggregate: typeof getLogsAggregate;
|
||||
}
|
||||
|
||||
type Props = DispatchProps;
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(LogLiveTail);
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Card, Select } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const TimePickerCard = styled(Card)`
|
||||
.ant-card-body {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TimePickerSelect = styled(Select)`
|
||||
min-width: 100px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const StopContainer = styled.div<Props>`
|
||||
height: 0.8rem;
|
||||
width: 0.8rem;
|
||||
border-radius: 0.1rem;
|
||||
background-color: ${({ isDarkMode }): string =>
|
||||
isDarkMode ? '#fff' : '#000'};
|
||||
`;
|
||||
@@ -1,95 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { blue } from '@ant-design/colors';
|
||||
import Graph from 'components/Graph';
|
||||
import Spinner from 'components/Spinner';
|
||||
import dayjs from 'dayjs';
|
||||
import useInterval from 'hooks/useInterval';
|
||||
import getStep from 'lib/getStep';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import { Container } from './styles';
|
||||
|
||||
function LogsAggregate({ getLogsAggregate }: DispatchProps): JSX.Element {
|
||||
const {
|
||||
searchFilter: { queryString },
|
||||
idEnd,
|
||||
idStart,
|
||||
isLoadingAggregate,
|
||||
logsAggregate,
|
||||
liveTail,
|
||||
liveTailStartRange,
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
const startTime =
|
||||
dayjs().subtract(liveTailStartRange, 'minute').valueOf() * 1e6;
|
||||
|
||||
const endTime = dayjs().valueOf() * 1e6;
|
||||
|
||||
getLogsAggregate({
|
||||
timestampStart: startTime,
|
||||
timestampEnd: endTime,
|
||||
step: getStep({
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
inputFormat: 'ns',
|
||||
}),
|
||||
q: queryString,
|
||||
...(idStart ? { idGt: idStart } : {}),
|
||||
...(idEnd ? { idLt: idEnd } : {}),
|
||||
});
|
||||
},
|
||||
60000,
|
||||
liveTail === 'PLAYING',
|
||||
);
|
||||
|
||||
const graphData = useMemo(
|
||||
() => ({
|
||||
labels: logsAggregate.map((s) => new Date(s.timestamp / 1000000)),
|
||||
datasets: [
|
||||
{
|
||||
data: logsAggregate.map((s) => s.value),
|
||||
backgroundColor: blue[4],
|
||||
},
|
||||
],
|
||||
}),
|
||||
[logsAggregate],
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{isLoadingAggregate ? (
|
||||
<Spinner size="default" height="100%" />
|
||||
) : (
|
||||
<Graph
|
||||
name="usage"
|
||||
data={graphData}
|
||||
type="bar"
|
||||
containerHeight="100%"
|
||||
animate
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
getLogsAggregate: typeof getLogsAggregate;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(LogsAggregate);
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Card } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled(Card)`
|
||||
position: relative;
|
||||
margin: 0.5rem 0;
|
||||
.ant-card-body {
|
||||
height: 20vh;
|
||||
min-height: 200px;
|
||||
}
|
||||
`;
|
||||
@@ -1,93 +0,0 @@
|
||||
import { ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
import { Loader } from '@signozhq/icons';
|
||||
import { Button, Popover, Spin } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import {
|
||||
IField,
|
||||
IInterestingFields,
|
||||
ISelectedFields,
|
||||
} from 'types/api/logs/fields';
|
||||
|
||||
import { ICON_STYLE } from './config';
|
||||
import { Field } from './styles';
|
||||
|
||||
function FieldItem({
|
||||
name,
|
||||
buttonIcon,
|
||||
buttonOnClick,
|
||||
fieldData,
|
||||
fieldIndex,
|
||||
isLoading,
|
||||
iconHoverText,
|
||||
}: FieldItemProps): JSX.Element {
|
||||
const [isHovered, setIsHovered] = useState<boolean>(false);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const onClickHandler = useCallback(() => {
|
||||
if (!isLoading && buttonOnClick) {
|
||||
buttonOnClick({ fieldData, fieldIndex });
|
||||
}
|
||||
}, [buttonOnClick, fieldData, fieldIndex, isLoading]);
|
||||
|
||||
const renderContent = useMemo(() => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Spin
|
||||
spinning
|
||||
size="small"
|
||||
indicator={<Loader className="animate-spin" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isHovered) {
|
||||
return (
|
||||
<Popover content={<Typography>{iconHoverText}</Typography>}>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
icon={buttonIcon}
|
||||
onClick={onClickHandler}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [buttonIcon, iconHoverText, isHovered, isLoading, onClickHandler]);
|
||||
|
||||
const onMouseHoverHandler = useCallback(
|
||||
(value: boolean) => (): void => {
|
||||
setIsHovered(value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Field
|
||||
onMouseEnter={onMouseHoverHandler(true)}
|
||||
onMouseLeave={onMouseHoverHandler(false)}
|
||||
isDarkMode={isDarkMode}
|
||||
>
|
||||
<Typography style={ICON_STYLE.PLUS}>{name}</Typography>
|
||||
|
||||
{renderContent}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
interface FieldItemProps {
|
||||
name: string;
|
||||
buttonIcon: ReactNode;
|
||||
buttonOnClick: (props: {
|
||||
fieldData: IInterestingFields | ISelectedFields;
|
||||
fieldIndex: number;
|
||||
}) => void;
|
||||
fieldData: IField;
|
||||
fieldIndex: number;
|
||||
isLoading: boolean;
|
||||
iconHoverText: string;
|
||||
}
|
||||
|
||||
export default FieldItem;
|
||||
@@ -1,11 +0,0 @@
|
||||
import { blue, red } from '@ant-design/colors';
|
||||
|
||||
export const RESTRICTED_SELECTED_FIELDS = ['timestamp', 'id'];
|
||||
|
||||
// Fields that can be filtered on but not grouped by in the log details view.
|
||||
export const RESTRICTED_GROUP_BY_FIELDS = ['body', 'trace_id'];
|
||||
|
||||
export const ICON_STYLE = {
|
||||
PLUS: { color: blue[5] },
|
||||
CLOSE: { color: red[5] },
|
||||
};
|
||||
@@ -1,119 +0,0 @@
|
||||
import { ChangeEvent, useCallback, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { CirclePlus, X } from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Col } from 'antd';
|
||||
import CategoryHeading from 'components/Logs/CategoryHeading';
|
||||
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import { ICON_STYLE, RESTRICTED_SELECTED_FIELDS } from './config';
|
||||
import FieldItem from './FieldItem';
|
||||
import { CategoryContainer, FieldContainer } from './styles';
|
||||
import { IHandleInterestProps, IHandleRemoveInterestProps } from './types';
|
||||
import { onHandleAddInterest, onHandleRemoveInterest } from './utils';
|
||||
|
||||
function LogsFilters(): JSX.Element {
|
||||
const {
|
||||
fields: { interesting, selected },
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
|
||||
const [selectedFieldLoading, setSelectedFieldLoading] = useState<number[]>([]);
|
||||
const [interestingFieldLoading, setInterestingFieldLoading] = useState<
|
||||
number[]
|
||||
>([]);
|
||||
|
||||
const [filterValuesInput, setFilterValuesInput] = useState('');
|
||||
const handleSearch = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
setFilterValuesInput((e.target as HTMLInputElement).value);
|
||||
};
|
||||
|
||||
const onHandleAddSelectedToInteresting = useCallback(
|
||||
({ fieldData, fieldIndex }: IHandleInterestProps) =>
|
||||
(): Promise<void> =>
|
||||
onHandleAddInterest({
|
||||
fieldData,
|
||||
fieldIndex,
|
||||
interesting,
|
||||
interestingFieldLoading,
|
||||
setInterestingFieldLoading,
|
||||
selected,
|
||||
}),
|
||||
[interesting, interestingFieldLoading, selected],
|
||||
);
|
||||
|
||||
const onHandleRemoveSelected = useCallback(
|
||||
({ fieldData, fieldIndex }: IHandleRemoveInterestProps) =>
|
||||
(): Promise<void> =>
|
||||
onHandleRemoveInterest({
|
||||
fieldData,
|
||||
fieldIndex,
|
||||
interesting,
|
||||
interestingFieldLoading,
|
||||
selected,
|
||||
setSelectedFieldLoading,
|
||||
}),
|
||||
[interesting, interestingFieldLoading, selected, setSelectedFieldLoading],
|
||||
);
|
||||
|
||||
return (
|
||||
<Col flex="250px">
|
||||
<Input
|
||||
placeholder="Filter Values"
|
||||
onInput={handleSearch}
|
||||
value={filterValuesInput}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
|
||||
<CategoryContainer>
|
||||
<CategoryHeading>SELECTED FIELDS</CategoryHeading>
|
||||
<FieldContainer>
|
||||
{selected
|
||||
.filter((field) => fieldSearchFilter(field.name, filterValuesInput))
|
||||
.filter((field) => RESTRICTED_SELECTED_FIELDS.indexOf(field.name) === -1)
|
||||
.map((field, idx) => (
|
||||
<FieldItem
|
||||
key={`${JSON.stringify(field)}`}
|
||||
name={field.name}
|
||||
fieldData={field}
|
||||
fieldIndex={idx}
|
||||
buttonIcon={<X style={ICON_STYLE.CLOSE} size="md" />}
|
||||
buttonOnClick={onHandleRemoveSelected({
|
||||
fieldData: field,
|
||||
fieldIndex: idx,
|
||||
})}
|
||||
isLoading={selectedFieldLoading.includes(idx)}
|
||||
iconHoverText="Remove from Selected Fields"
|
||||
/>
|
||||
))}
|
||||
</FieldContainer>
|
||||
</CategoryContainer>
|
||||
<CategoryContainer>
|
||||
<CategoryHeading>INTERESTING FIELDS</CategoryHeading>
|
||||
<FieldContainer>
|
||||
{interesting
|
||||
.filter((field) => fieldSearchFilter(field.name, filterValuesInput))
|
||||
.map((field, idx) => (
|
||||
<FieldItem
|
||||
key={`${JSON.stringify(field)}`}
|
||||
name={field.name}
|
||||
fieldData={field}
|
||||
fieldIndex={idx}
|
||||
buttonIcon={<CirclePlus style={ICON_STYLE.PLUS} size="md" />}
|
||||
buttonOnClick={onHandleAddSelectedToInteresting({
|
||||
fieldData: field,
|
||||
fieldIndex: idx,
|
||||
})}
|
||||
isLoading={interestingFieldLoading.includes(idx)}
|
||||
iconHoverText="Add to Selected Fields"
|
||||
/>
|
||||
))}
|
||||
</FieldContainer>
|
||||
</CategoryContainer>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogsFilters;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { blue, grey } from '@ant-design/colors';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const CategoryContainer = styled.div`
|
||||
margin: 1rem 0;
|
||||
padding-left: 0.2rem;
|
||||
`;
|
||||
|
||||
export const FieldContainer = styled(Typography.Text)`
|
||||
margin: 0.2rem 0;
|
||||
color: ${blue[4]};
|
||||
`;
|
||||
|
||||
export const Field = styled.div<{ isDarkMode: boolean }>`
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.3rem 0.5rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
&:hover {
|
||||
background: ${({ isDarkMode }): string => (isDarkMode ? grey[7] : '#ddd')};
|
||||
}
|
||||
`;
|
||||
|
||||
export const ExtractField = styled(Typography.Text)`
|
||||
color: ${blue[4]};
|
||||
`;
|
||||
@@ -1,36 +0,0 @@
|
||||
import { SetStateAction } from 'react';
|
||||
import {
|
||||
IField,
|
||||
IInterestingFields,
|
||||
ISelectedFields,
|
||||
} from 'types/api/logs/fields';
|
||||
|
||||
type SetLoading = (value: SetStateAction<number[]>) => void;
|
||||
|
||||
export type IHandleInterestProps = {
|
||||
fieldData: IInterestingFields;
|
||||
fieldIndex: number;
|
||||
};
|
||||
|
||||
export type IHandleRemoveInterestProps = {
|
||||
fieldData: ISelectedFields;
|
||||
fieldIndex: number;
|
||||
};
|
||||
|
||||
export interface OnHandleAddInterestProps {
|
||||
setInterestingFieldLoading: SetLoading;
|
||||
fieldIndex: number;
|
||||
fieldData: ISelectedFields;
|
||||
interesting: IField[];
|
||||
interestingFieldLoading: number[];
|
||||
selected: IField[];
|
||||
}
|
||||
|
||||
export interface OnHandleRemoveInterestProps {
|
||||
setSelectedFieldLoading: SetLoading;
|
||||
selected: IField[];
|
||||
interesting: IField[];
|
||||
interestingFieldLoading: number[];
|
||||
fieldData: IInterestingFields;
|
||||
fieldIndex: number;
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import { message } from 'antd';
|
||||
import addToSelectedFields from 'api/logs/AddToSelectedField';
|
||||
import removeSelectedField from 'api/logs/RemoveFromSelectedField';
|
||||
import store from 'store';
|
||||
import {
|
||||
UPDATE_INTERESTING_FIELDS,
|
||||
UPDATE_SELECTED_FIELDS,
|
||||
} from 'types/actions/logs';
|
||||
import { ErrorResponse } from 'types/api';
|
||||
|
||||
import { RESTRICTED_SELECTED_FIELDS } from './config';
|
||||
import { OnHandleAddInterestProps, OnHandleRemoveInterestProps } from './types';
|
||||
|
||||
export const onHandleAddInterest = async ({
|
||||
setInterestingFieldLoading,
|
||||
fieldIndex,
|
||||
fieldData,
|
||||
interesting,
|
||||
interestingFieldLoading,
|
||||
selected,
|
||||
}: OnHandleAddInterestProps): Promise<void> => {
|
||||
const { dispatch } = store;
|
||||
|
||||
setInterestingFieldLoading((prevState: number[]) => {
|
||||
prevState.push(fieldIndex);
|
||||
return [...prevState];
|
||||
});
|
||||
|
||||
try {
|
||||
await addToSelectedFields({
|
||||
...fieldData,
|
||||
selected: true,
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_INTERESTING_FIELDS,
|
||||
payload: {
|
||||
field: interesting.filter((e) => e.name !== fieldData.name),
|
||||
type: 'selected',
|
||||
},
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_SELECTED_FIELDS,
|
||||
payload: {
|
||||
field: [...selected, fieldData],
|
||||
type: 'selected',
|
||||
},
|
||||
});
|
||||
} catch (errRes) {
|
||||
message.error((errRes as ErrorResponse)?.error);
|
||||
} finally {
|
||||
setInterestingFieldLoading(
|
||||
interestingFieldLoading.filter((e) => e !== fieldIndex),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const onHandleRemoveInterest = async ({
|
||||
setSelectedFieldLoading,
|
||||
selected,
|
||||
interesting,
|
||||
interestingFieldLoading,
|
||||
fieldData,
|
||||
fieldIndex,
|
||||
}: OnHandleRemoveInterestProps): Promise<void> => {
|
||||
if (RESTRICTED_SELECTED_FIELDS.includes(fieldData.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { dispatch } = store;
|
||||
|
||||
setSelectedFieldLoading((prevState) => {
|
||||
prevState.push(fieldIndex);
|
||||
return [...prevState];
|
||||
});
|
||||
|
||||
try {
|
||||
await removeSelectedField({
|
||||
...fieldData,
|
||||
selected: false,
|
||||
});
|
||||
dispatch({
|
||||
type: UPDATE_SELECTED_FIELDS,
|
||||
payload: {
|
||||
field: selected.filter((e) => e.name !== fieldData.name),
|
||||
type: 'selected',
|
||||
},
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_INTERESTING_FIELDS,
|
||||
payload: {
|
||||
field: [...interesting, fieldData],
|
||||
type: 'interesting',
|
||||
},
|
||||
});
|
||||
} catch (errRes) {
|
||||
message.error((errRes as ErrorResponse)?.error);
|
||||
} finally {
|
||||
setSelectedFieldLoading(
|
||||
interestingFieldLoading.filter((e) => e !== fieldIndex),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Button, Row } from 'antd';
|
||||
|
||||
interface SearchFieldsActionBarProps {
|
||||
applyUpdate: VoidFunction;
|
||||
clearFilters: VoidFunction;
|
||||
}
|
||||
|
||||
export function SearchFieldsActionBar({
|
||||
applyUpdate,
|
||||
clearFilters,
|
||||
}: SearchFieldsActionBarProps): JSX.Element | null {
|
||||
return (
|
||||
<Row style={{ justifyContent: 'flex-end', paddingRight: '2.4rem' }}>
|
||||
<Button
|
||||
type="default"
|
||||
onClick={clearFilters}
|
||||
style={{ marginRight: '1rem' }}
|
||||
>
|
||||
Clear Filter
|
||||
</Button>
|
||||
<Button type="primary" onClick={applyUpdate}>
|
||||
Apply
|
||||
</Button>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
export default SearchFieldsActionBar;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
interface FieldKeyProps {
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
function FieldKey({ name, type }: FieldKeyProps): JSX.Element {
|
||||
return (
|
||||
<span style={{ margin: '0.25rem 0', display: 'flex', gap: '0.5rem' }}>
|
||||
<Typography.Text>{name}</Typography.Text>
|
||||
<Typography.Text color="muted" italic>
|
||||
{type}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default FieldKey;
|
||||
@@ -1,259 +0,0 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { SquareX, X } from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Select } from 'antd';
|
||||
import CategoryHeading from 'components/Logs/CategoryHeading';
|
||||
import {
|
||||
ConditionalOperators,
|
||||
QueryOperatorsMultiVal,
|
||||
QueryOperatorsSingleVal,
|
||||
} from 'lib/logql/tokens';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import FieldKey from '../FieldKey';
|
||||
import { QueryFieldContainer } from '../styles';
|
||||
import { QueryFields } from '../utils';
|
||||
import { Container, QueryWrapper } from './styles';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
function QueryConditionField({
|
||||
query,
|
||||
queryIndex,
|
||||
onUpdate,
|
||||
}: QueryConditionFieldProps): JSX.Element {
|
||||
const allOptions = Object.values(ConditionalOperators);
|
||||
return (
|
||||
<Select
|
||||
defaultValue={
|
||||
(query as QueryFields).value &&
|
||||
(
|
||||
(query as QueryFields)
|
||||
?.value as unknown as QueryFields as unknown as string
|
||||
).toUpperCase()
|
||||
}
|
||||
onChange={(e): void => {
|
||||
onUpdate({ ...query, value: e }, queryIndex);
|
||||
}}
|
||||
>
|
||||
{allOptions.map((cond) => (
|
||||
<Option key={cond} value={cond} label={cond}>
|
||||
{cond}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
interface QueryFieldProps {
|
||||
query: Query;
|
||||
queryIndex: number;
|
||||
onUpdate: (query: Query, queryIndex: number) => void;
|
||||
onDelete: (queryIndex: number) => void;
|
||||
}
|
||||
function QueryField({
|
||||
query,
|
||||
queryIndex,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
}: QueryFieldProps): JSX.Element | null {
|
||||
const [isDropDownOpen, setIsDropDownOpen] = useState(false);
|
||||
|
||||
const {
|
||||
fields: { selected },
|
||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
const getFieldType = useCallback(
|
||||
(inputKey: string): string => {
|
||||
const selectedField = selected.find((field) => inputKey === field.name);
|
||||
if (selectedField) {
|
||||
return selectedField.type;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
[selected],
|
||||
);
|
||||
|
||||
const fieldType = useMemo(
|
||||
() => getFieldType(query[0].value as string),
|
||||
[getFieldType, query],
|
||||
);
|
||||
|
||||
const handleChange = (qIdx: number, value: string): void => {
|
||||
const updatedQuery = [...query];
|
||||
updatedQuery[qIdx].value = value || '';
|
||||
|
||||
if (qIdx === 1) {
|
||||
if (Object.values(QueryOperatorsMultiVal).includes(value)) {
|
||||
if (!Array.isArray(updatedQuery[2].value)) {
|
||||
updatedQuery[2].value = [];
|
||||
}
|
||||
} else if (
|
||||
Object.values(QueryOperatorsSingleVal).includes(value) &&
|
||||
Array.isArray(updatedQuery[2].value)
|
||||
) {
|
||||
updatedQuery[2].value = '';
|
||||
}
|
||||
}
|
||||
onUpdate(updatedQuery, queryIndex);
|
||||
};
|
||||
|
||||
const handleClear = (): void => {
|
||||
onDelete(queryIndex);
|
||||
};
|
||||
if (!Array.isArray(query)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryFieldContainer
|
||||
style={{ ...(queryIndex === 0 && { gridColumnStart: 2 }) }}
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: 100 }}>
|
||||
<FieldKey name={(query[0] && query[0].value) as string} type={fieldType} />
|
||||
</div>
|
||||
<Select
|
||||
defaultActiveFirstOption={false}
|
||||
placeholder="Select Operator"
|
||||
defaultValue={
|
||||
query[1] && query[1].value
|
||||
? (query[1].value as string).toUpperCase()
|
||||
: null
|
||||
}
|
||||
onChange={(e): void => handleChange(1, e)}
|
||||
style={{ minWidth: 150 }}
|
||||
>
|
||||
{Object.values({
|
||||
...QueryOperatorsMultiVal,
|
||||
...QueryOperatorsSingleVal,
|
||||
}).map((cond) => (
|
||||
<Option key={cond} value={cond} label={cond}>
|
||||
{cond}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<div style={{ flex: 2 }}>
|
||||
{Array.isArray(query[2].value) ||
|
||||
Object.values(QueryOperatorsMultiVal).some(
|
||||
(op) => op.toUpperCase() === (query[1].value as string)?.toUpperCase(),
|
||||
) ? (
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
open={isDropDownOpen}
|
||||
onChange={(e): void => handleChange(2, e as never)}
|
||||
defaultValue={(query[2] && query[2].value) || []}
|
||||
notFoundContent={null}
|
||||
onInputKeyDown={(): void => setIsDropDownOpen(true)}
|
||||
onSelect={(): void => setIsDropDownOpen(false)}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
onChange={(e): void => {
|
||||
handleChange(2, e.target.value);
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
defaultValue={query[2] && query[2].value}
|
||||
value={query[2] && query[2].value}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
icon={<X size="md" />}
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={handleClear}
|
||||
/>
|
||||
</QueryFieldContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface QueryConditionFieldProps {
|
||||
query: QueryFields;
|
||||
queryIndex: number;
|
||||
onUpdate: (arg0: unknown, arg1: number) => void;
|
||||
}
|
||||
|
||||
export type Query = { value: string | string[]; type: string }[];
|
||||
|
||||
export interface QueryBuilderProps {
|
||||
keyPrefix: string;
|
||||
onDropDownToggleHandler: (value: boolean) => VoidFunction;
|
||||
fieldsQuery: QueryFields[][];
|
||||
setFieldsQuery: (q: QueryFields[][]) => void;
|
||||
syncKeyPrefix: () => void;
|
||||
}
|
||||
|
||||
function QueryBuilder({
|
||||
keyPrefix,
|
||||
fieldsQuery,
|
||||
setFieldsQuery,
|
||||
onDropDownToggleHandler,
|
||||
syncKeyPrefix,
|
||||
}: QueryBuilderProps): JSX.Element {
|
||||
const handleUpdate = (query: Query, queryIndex: number): void => {
|
||||
const updated = [...fieldsQuery];
|
||||
updated[queryIndex] = query as never; // parseQuery(query) as never;
|
||||
setFieldsQuery(updated);
|
||||
};
|
||||
|
||||
const handleDelete = (queryIndex: number): void => {
|
||||
const updated = [...fieldsQuery];
|
||||
if (queryIndex !== 0) {
|
||||
updated.splice(queryIndex - 1, 2);
|
||||
} else {
|
||||
updated.splice(queryIndex, 2);
|
||||
}
|
||||
|
||||
setFieldsQuery(updated);
|
||||
|
||||
// initiate re-render query panel
|
||||
syncKeyPrefix();
|
||||
};
|
||||
|
||||
const QueryUI = (
|
||||
fieldsQuery: QueryFields[][],
|
||||
): JSX.Element | JSX.Element[] => {
|
||||
const result: JSX.Element[] = [];
|
||||
fieldsQuery.forEach((query, idx) => {
|
||||
if (Array.isArray(query) && query.length > 1) {
|
||||
result.push(
|
||||
<QueryField
|
||||
key={keyPrefix}
|
||||
query={query}
|
||||
queryIndex={idx}
|
||||
onUpdate={handleUpdate}
|
||||
onDelete={handleDelete}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
result.push(
|
||||
<div key={keyPrefix}>
|
||||
<QueryConditionField
|
||||
query={Array.isArray(query) ? query[0] : query}
|
||||
queryIndex={idx}
|
||||
onUpdate={handleUpdate as never}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container isMargin={fieldsQuery.length === 0}>
|
||||
<CategoryHeading>LOG QUERY BUILDER</CategoryHeading>
|
||||
<SquareX onClick={onDropDownToggleHandler(false)} size="md" />
|
||||
</Container>
|
||||
|
||||
<QueryWrapper key={keyPrefix}>{QueryUI(fieldsQuery)}</QueryWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default QueryBuilder;
|
||||
@@ -1,17 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface Props {
|
||||
isMargin: boolean;
|
||||
}
|
||||
export const Container = styled.div<Props>`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-bottom: ${(props): string => (props.isMargin ? '2rem' : '0')};
|
||||
`;
|
||||
|
||||
export const QueryWrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 80px 1fr;
|
||||
margin: 0.5rem 0px;
|
||||
`;
|
||||
@@ -1,62 +0,0 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Button } from 'antd';
|
||||
import CategoryHeading from 'components/Logs/CategoryHeading';
|
||||
import map from 'lodash-es/map';
|
||||
import { AppState } from 'store/reducers';
|
||||
// import { ADD_SEARCH_FIELD_QUERY_STRING } from 'types/actions/logs';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import FieldKey from './FieldKey';
|
||||
|
||||
interface SuggestedItemProps {
|
||||
name: string;
|
||||
type: string;
|
||||
applySuggestion: (name: string) => void;
|
||||
}
|
||||
function SuggestedItem({
|
||||
name,
|
||||
type,
|
||||
applySuggestion,
|
||||
}: SuggestedItemProps): JSX.Element {
|
||||
const addSuggestedField = (): void => {
|
||||
applySuggestion(name);
|
||||
};
|
||||
return (
|
||||
<Button
|
||||
type="text"
|
||||
style={{ display: 'block', padding: '0.2rem' }}
|
||||
onClick={addSuggestedField}
|
||||
>
|
||||
<FieldKey name={name} type={type} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
interface SuggestionsProps {
|
||||
applySuggestion: (name: string) => void;
|
||||
}
|
||||
|
||||
function Suggestions({ applySuggestion }: SuggestionsProps): JSX.Element {
|
||||
const {
|
||||
fields: { selected },
|
||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CategoryHeading>SUGGESTIONS</CategoryHeading>
|
||||
<div>
|
||||
{map(selected, (field) => (
|
||||
<SuggestedItem
|
||||
key={JSON.stringify(field)}
|
||||
name={field.name}
|
||||
type={field.type}
|
||||
applySuggestion={applySuggestion}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Suggestions;
|
||||
@@ -1,123 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { reverseParser } from 'lib/logql';
|
||||
import { flatten } from 'lodash-es';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import { SearchFieldsActionBar } from './ActionBar';
|
||||
import QueryBuilder from './QueryBuilder/QueryBuilder';
|
||||
import Suggestions from './Suggestions';
|
||||
import {
|
||||
createParsedQueryStructure,
|
||||
fieldsQueryIsvalid,
|
||||
hashCode,
|
||||
initQueryKOVPair,
|
||||
prepareConditionOperator,
|
||||
QueryFields,
|
||||
} from './utils';
|
||||
|
||||
export interface SearchFieldsProps {
|
||||
onDropDownToggleHandler: (value: boolean) => VoidFunction;
|
||||
updateQueryString: (value: string) => void;
|
||||
}
|
||||
|
||||
function SearchFields({
|
||||
onDropDownToggleHandler,
|
||||
updateQueryString,
|
||||
}: SearchFieldsProps): JSX.Element {
|
||||
const {
|
||||
searchFilter: { parsedQuery },
|
||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
|
||||
const [fieldsQuery, setFieldsQuery] = useState(
|
||||
createParsedQueryStructure([...parsedQuery] as never[]),
|
||||
);
|
||||
|
||||
const keyPrefixRef = useRef(hashCode(JSON.stringify(fieldsQuery)));
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
useEffect(() => {
|
||||
const updatedFieldsQuery = createParsedQueryStructure([
|
||||
...parsedQuery,
|
||||
] as never[]);
|
||||
setFieldsQuery(updatedFieldsQuery);
|
||||
const incomingHashCode = hashCode(JSON.stringify(updatedFieldsQuery));
|
||||
if (incomingHashCode !== keyPrefixRef.current) {
|
||||
keyPrefixRef.current = incomingHashCode;
|
||||
}
|
||||
}, [parsedQuery]);
|
||||
|
||||
// syncKeyPrefix initiates re-render. useful in situations like
|
||||
// delete field (in search panel). this method allows condiitonally
|
||||
// setting keyPrefix as doing it on every update of query initiates
|
||||
// a re-render. this is a problem for text fields where input focus goes away.
|
||||
const syncKeyPrefix = (): void => {
|
||||
keyPrefixRef.current = hashCode(JSON.stringify(fieldsQuery));
|
||||
};
|
||||
|
||||
const addSuggestedField = useCallback(
|
||||
(name: string): void => {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = [...fieldsQuery];
|
||||
|
||||
if (fieldsQuery.length > 0) {
|
||||
query.push([prepareConditionOperator()]);
|
||||
}
|
||||
|
||||
const newField: QueryFields[] = [];
|
||||
initQueryKOVPair(name).forEach((q) => newField.push(q));
|
||||
|
||||
query.push(newField);
|
||||
keyPrefixRef.current = hashCode(JSON.stringify(query));
|
||||
setFieldsQuery(query);
|
||||
},
|
||||
[fieldsQuery, setFieldsQuery],
|
||||
);
|
||||
|
||||
const applyUpdate = useCallback((): void => {
|
||||
const flatParsedQuery = flatten(fieldsQuery);
|
||||
|
||||
if (!fieldsQueryIsvalid(flatParsedQuery)) {
|
||||
notifications.error({
|
||||
message: 'Please enter a valid criteria for each of the selected fields',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
keyPrefixRef.current = hashCode(JSON.stringify(flatParsedQuery));
|
||||
updateQueryString(reverseParser(flatParsedQuery));
|
||||
onDropDownToggleHandler(false)();
|
||||
}, [fieldsQuery, notifications, onDropDownToggleHandler, updateQueryString]);
|
||||
|
||||
const clearFilters = useCallback((): void => {
|
||||
keyPrefixRef.current = hashCode(JSON.stringify([]));
|
||||
setFieldsQuery([]);
|
||||
updateQueryString('');
|
||||
}, [updateQueryString]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<QueryBuilder
|
||||
key={keyPrefixRef.current}
|
||||
keyPrefix={keyPrefixRef.current}
|
||||
onDropDownToggleHandler={onDropDownToggleHandler}
|
||||
fieldsQuery={fieldsQuery}
|
||||
setFieldsQuery={setFieldsQuery}
|
||||
syncKeyPrefix={syncKeyPrefix}
|
||||
/>
|
||||
<SearchFieldsActionBar
|
||||
applyUpdate={applyUpdate}
|
||||
clearFilters={clearFilters}
|
||||
/>
|
||||
<Suggestions applySuggestion={addSuggestedField} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default SearchFields;
|
||||
@@ -1,16 +0,0 @@
|
||||
import { blue } from '@ant-design/colors';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const QueryFieldContainer = styled.div`
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0.1rem 0.5rem 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-radius: 0.25rem;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
&:hover {
|
||||
background: ${blue[6]};
|
||||
}
|
||||
`;
|
||||
@@ -1,137 +0,0 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import {
|
||||
ConditionalOperators,
|
||||
QueryTypes,
|
||||
ValidTypeSequence,
|
||||
ValidTypeValue,
|
||||
} from 'lib/logql/tokens';
|
||||
|
||||
export interface QueryFields {
|
||||
type: keyof typeof QueryTypes;
|
||||
value: string | string[];
|
||||
}
|
||||
|
||||
export function fieldsQueryIsvalid(queryFields: QueryFields[]): boolean {
|
||||
let lastOp: string;
|
||||
let result = true;
|
||||
queryFields.forEach((q, idx) => {
|
||||
if (!q.value || q.value === null || q.value === '') {
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (Array.isArray(q.value) && q.value.length === 0) {
|
||||
result = false;
|
||||
}
|
||||
|
||||
const nextOp = idx < queryFields.length ? queryFields[idx + 1] : undefined;
|
||||
if (!ValidTypeSequence(lastOp?.type, q?.type, nextOp?.type)) {
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (!ValidTypeValue(lastOp?.value, q.value)) {
|
||||
result = false;
|
||||
}
|
||||
lastOp = q;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export const queryKOVPair = (): QueryFields[] => [
|
||||
{
|
||||
type: QueryTypes.QUERY_KEY,
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
type: QueryTypes.QUERY_OPERATOR,
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
type: QueryTypes.QUERY_VALUE,
|
||||
value: null,
|
||||
},
|
||||
];
|
||||
|
||||
export const initQueryKOVPair = (
|
||||
name: string = null,
|
||||
op: string = null,
|
||||
value: string | string[] = null,
|
||||
): QueryFields[] => [
|
||||
{
|
||||
type: QueryTypes.QUERY_KEY,
|
||||
value: name,
|
||||
},
|
||||
{
|
||||
type: QueryTypes.QUERY_OPERATOR,
|
||||
value: op,
|
||||
},
|
||||
{
|
||||
type: QueryTypes.QUERY_VALUE,
|
||||
value: value,
|
||||
},
|
||||
];
|
||||
|
||||
export const prepareConditionOperator = (
|
||||
op: string = ConditionalOperators.AND,
|
||||
): QueryFields => {
|
||||
return {
|
||||
type: QueryTypes.CONDITIONAL_OPERATOR,
|
||||
value: op,
|
||||
};
|
||||
};
|
||||
|
||||
export const createParsedQueryStructure = (
|
||||
parsedQuery = [],
|
||||
): QueryFields[][] => {
|
||||
if (parsedQuery.length === 0) {
|
||||
return parsedQuery;
|
||||
}
|
||||
|
||||
const structuredArray = [queryKOVPair()];
|
||||
|
||||
let cond;
|
||||
let qCtr = -1;
|
||||
parsedQuery.forEach((query) => {
|
||||
if (cond) {
|
||||
structuredArray.push(cond);
|
||||
structuredArray.push(queryKOVPair());
|
||||
cond = null;
|
||||
qCtr = -1;
|
||||
}
|
||||
const stagingArr = structuredArray.at(-1);
|
||||
const prevQuery =
|
||||
Array.isArray(stagingArr) && qCtr >= 0 ? stagingArr[qCtr] : null;
|
||||
|
||||
if (query.type === QueryTypes.QUERY_KEY) {
|
||||
stagingArr[qCtr + 1] = query;
|
||||
} else if (
|
||||
query.type === QueryTypes.QUERY_OPERATOR &&
|
||||
prevQuery &&
|
||||
prevQuery.type === QueryTypes.QUERY_KEY
|
||||
) {
|
||||
stagingArr[qCtr + 1] = query;
|
||||
} else if (
|
||||
query.type === QueryTypes.QUERY_VALUE &&
|
||||
prevQuery &&
|
||||
prevQuery.type === QueryTypes.QUERY_OPERATOR
|
||||
) {
|
||||
stagingArr[qCtr + 1] = query;
|
||||
} else if (query.type === QueryTypes.CONDITIONAL_OPERATOR) {
|
||||
cond = query;
|
||||
}
|
||||
qCtr++;
|
||||
});
|
||||
return structuredArray;
|
||||
};
|
||||
|
||||
export const hashCode = (s: string): string => {
|
||||
if (!s) {
|
||||
return '0';
|
||||
}
|
||||
return `${Math.abs(
|
||||
[...s].reduce((a, b) => {
|
||||
a = (a << 5) - a + b.codePointAt(0);
|
||||
return a & a;
|
||||
}, 0),
|
||||
)}`;
|
||||
};
|
||||
@@ -1,230 +0,0 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
import { Input, InputRef, Popover } from 'antd';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import getStep from 'lib/getStep';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import { getIdConditions } from 'pages/Logs/utils';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { GetLogsFields } from 'store/actions/logs/getFields';
|
||||
import { getLogs } from 'store/actions/logs/getLogs';
|
||||
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
FLUSH_LOGS,
|
||||
SET_LOADING,
|
||||
SET_LOADING_AGGREGATE,
|
||||
TOGGLE_LIVE_TAIL,
|
||||
} from 'types/actions/logs';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import SearchFields from './SearchFields';
|
||||
import { Container, DropDownContainer } from './styles';
|
||||
import { useSearchParser } from './useSearchParser';
|
||||
|
||||
function SearchFilter({
|
||||
getLogs,
|
||||
getLogsAggregate,
|
||||
getLogsFields,
|
||||
}: SearchFilterProps): JSX.Element {
|
||||
const { updateQueryString, queryString } = useSearchParser();
|
||||
const [searchText, setSearchText] = useState(queryString);
|
||||
const [showDropDown, setShowDropDown] = useState(false);
|
||||
const searchRef = useRef<InputRef>(null);
|
||||
const { logLinesPerPage, idEnd, idStart, liveTail, order } = useSelector<
|
||||
AppState,
|
||||
ILogsReducer
|
||||
>((state) => state.logs);
|
||||
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
// keep sync with url queryString
|
||||
useEffect(() => {
|
||||
setSearchText(queryString);
|
||||
}, [queryString]);
|
||||
|
||||
const debouncedupdateQueryString = useMemo(
|
||||
() => debounce(updateQueryString, 300),
|
||||
[updateQueryString],
|
||||
);
|
||||
|
||||
const onDropDownToggleHandler = useCallback(
|
||||
(value: boolean) => (): void => {
|
||||
setShowDropDown(value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(customQuery: string) => {
|
||||
getLogsFields();
|
||||
const { maxTime, minTime } = globalTime;
|
||||
|
||||
if (liveTail === 'PLAYING') {
|
||||
dispatch({
|
||||
type: TOGGLE_LIVE_TAIL,
|
||||
payload: 'PAUSED',
|
||||
});
|
||||
dispatch({
|
||||
type: FLUSH_LOGS,
|
||||
});
|
||||
dispatch({
|
||||
type: TOGGLE_LIVE_TAIL,
|
||||
payload: liveTail,
|
||||
});
|
||||
dispatch({
|
||||
type: SET_LOADING,
|
||||
payload: false,
|
||||
});
|
||||
|
||||
getLogsAggregate({
|
||||
timestampStart: minTime,
|
||||
timestampEnd: maxTime,
|
||||
step: getStep({
|
||||
start: minTime,
|
||||
end: maxTime,
|
||||
inputFormat: 'ns',
|
||||
}),
|
||||
q: customQuery,
|
||||
...(idStart ? { idGt: idStart } : {}),
|
||||
...(idEnd ? { idLt: idEnd } : {}),
|
||||
});
|
||||
} else {
|
||||
getLogs({
|
||||
q: customQuery,
|
||||
limit: logLinesPerPage,
|
||||
orderBy: 'timestamp',
|
||||
order,
|
||||
timestampStart: minTime,
|
||||
timestampEnd: maxTime,
|
||||
...getIdConditions(idStart, idEnd, order),
|
||||
});
|
||||
|
||||
getLogsAggregate({
|
||||
timestampStart: minTime,
|
||||
timestampEnd: maxTime,
|
||||
step: getStep({
|
||||
start: minTime,
|
||||
end: maxTime,
|
||||
inputFormat: 'ns',
|
||||
}),
|
||||
q: customQuery,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
dispatch,
|
||||
getLogs,
|
||||
getLogsAggregate,
|
||||
idEnd,
|
||||
idStart,
|
||||
liveTail,
|
||||
logLinesPerPage,
|
||||
globalTime,
|
||||
getLogsFields,
|
||||
order,
|
||||
],
|
||||
);
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
const urlQueryString = urlQuery.get('q');
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({
|
||||
type: SET_LOADING,
|
||||
payload: true,
|
||||
});
|
||||
dispatch({
|
||||
type: SET_LOADING_AGGREGATE,
|
||||
payload: true,
|
||||
});
|
||||
|
||||
const debouncedHandleSearch = debounce(handleSearch, 600);
|
||||
|
||||
debouncedHandleSearch(urlQueryString || '');
|
||||
|
||||
return (): void => {
|
||||
debouncedHandleSearch.cancel();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
urlQueryString,
|
||||
idEnd,
|
||||
idStart,
|
||||
logLinesPerPage,
|
||||
dispatch,
|
||||
globalTime.maxTime,
|
||||
globalTime.minTime,
|
||||
order,
|
||||
]);
|
||||
|
||||
const onPopOverChange = useCallback(
|
||||
(isVisible: boolean) => {
|
||||
onDropDownToggleHandler(isVisible)();
|
||||
},
|
||||
[onDropDownToggleHandler],
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Popover
|
||||
getPopupContainer={popupContainer}
|
||||
placement="bottom"
|
||||
content={
|
||||
<DropDownContainer>
|
||||
<SearchFields
|
||||
updateQueryString={updateQueryString}
|
||||
onDropDownToggleHandler={onDropDownToggleHandler}
|
||||
/>
|
||||
</DropDownContainer>
|
||||
}
|
||||
trigger="click"
|
||||
overlayInnerStyle={{
|
||||
width: `${searchRef?.current?.input?.offsetWidth || 0}px`,
|
||||
}}
|
||||
open={showDropDown}
|
||||
destroyTooltipOnHide
|
||||
onOpenChange={onPopOverChange}
|
||||
>
|
||||
<Input.Search
|
||||
ref={searchRef}
|
||||
placeholder="Search Filter"
|
||||
value={searchText}
|
||||
onChange={(e): void => {
|
||||
const { value } = e.target;
|
||||
setSearchText(value);
|
||||
}}
|
||||
onSearch={debouncedupdateQueryString}
|
||||
allowClear
|
||||
/>
|
||||
</Popover>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
getLogs: typeof getLogs;
|
||||
getLogsAggregate: typeof getLogsAggregate;
|
||||
getLogsFields: typeof GetLogsFields;
|
||||
}
|
||||
|
||||
type SearchFilterProps = DispatchProps;
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
getLogs: bindActionCreators(getLogs, dispatch),
|
||||
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
|
||||
getLogsFields: bindActionCreators(GetLogsFields, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(memo(SearchFilter));
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Card } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const DropDownContainer = styled(Card)`
|
||||
.ant-card-body {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Container = styled.div`
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
`;
|
||||
@@ -1,81 +0,0 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
||||
import history from 'lib/history';
|
||||
import { parseQuery } from 'lib/logql';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Dispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
SET_SEARCH_QUERY_PARSED_PAYLOAD,
|
||||
SET_SEARCH_QUERY_STRING,
|
||||
} from 'types/actions/logs';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import { getGlobalTime } from './utils';
|
||||
|
||||
export function useSearchParser(): {
|
||||
queryString: string;
|
||||
parsedQuery: unknown;
|
||||
updateQueryString: (arg0: string) => void;
|
||||
} {
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const {
|
||||
searchFilter: { parsedQuery, queryString },
|
||||
order,
|
||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
const parsedFilters = urlQuery.get('q');
|
||||
|
||||
const { minTime, maxTime, selectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((store) => store.globalTime);
|
||||
|
||||
const updateQueryString = useCallback(
|
||||
(updatedQueryString: string) => {
|
||||
history.replace({
|
||||
pathname: history.location.pathname,
|
||||
search: `?${QueryParams.q}=${updatedQueryString}&${QueryParams.order}=${order}`,
|
||||
});
|
||||
|
||||
const globalTime = getMinMaxForSelectedTime(selectedTime, minTime, maxTime);
|
||||
|
||||
dispatch({
|
||||
type: SET_SEARCH_QUERY_STRING,
|
||||
payload: {
|
||||
searchQueryString: updatedQueryString,
|
||||
globalTime: getGlobalTime(selectedTime, globalTime),
|
||||
},
|
||||
});
|
||||
|
||||
const parsedQueryFromString = parseQuery(updatedQueryString);
|
||||
if (!isEqual(parsedQuery, parsedQueryFromString)) {
|
||||
dispatch({
|
||||
type: SET_SEARCH_QUERY_PARSED_PAYLOAD,
|
||||
payload: parsedQueryFromString,
|
||||
});
|
||||
}
|
||||
},
|
||||
// need to hide this warning as we don't want to update the query string on every change
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[dispatch, parsedQuery, selectedTime, queryString],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updateQueryString(parsedFilters || '');
|
||||
}, [parsedFilters, updateQueryString]);
|
||||
|
||||
return {
|
||||
queryString,
|
||||
parsedQuery,
|
||||
updateQueryString,
|
||||
};
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { GetMinMaxPayload } from 'lib/getMinMax';
|
||||
|
||||
export const getGlobalTime = (
|
||||
selectedTime: Time | CustomTimeType,
|
||||
globalTime: GetMinMaxPayload,
|
||||
): GetMinMaxPayload | undefined => {
|
||||
if (selectedTime === 'custom') {
|
||||
return undefined;
|
||||
}
|
||||
return globalTime;
|
||||
};
|
||||
@@ -1,164 +0,0 @@
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { Card } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
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';
|
||||
import LogsTableView from 'components/Logs/TableView';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { CARD_BODY_STYLE } from 'constants/card';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
// interfaces
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
// styles
|
||||
import { Container, Heading } from './styles';
|
||||
|
||||
import './logsTable.styles.scss';
|
||||
|
||||
export type LogViewMode = 'raw' | 'table' | 'list';
|
||||
|
||||
type LogsTableProps = {
|
||||
viewMode: LogViewMode;
|
||||
linesPerRow: number;
|
||||
};
|
||||
|
||||
function LogsTable(props: LogsTableProps): JSX.Element {
|
||||
const { viewMode, linesPerRow } = props;
|
||||
|
||||
const { activeLog, onClearActiveLog, onAddToQuery, onSetActiveLog } =
|
||||
useActiveLog();
|
||||
|
||||
const {
|
||||
logs,
|
||||
fields: { selected },
|
||||
isLoading,
|
||||
liveTail,
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
|
||||
const isLiveTail = useMemo(
|
||||
() => logs.length === 0 && liveTail === 'PLAYING',
|
||||
[logs?.length, liveTail],
|
||||
);
|
||||
|
||||
const isNoLogs = useMemo(
|
||||
() => logs.length === 0 && liveTail === 'STOPPED',
|
||||
[logs?.length, liveTail],
|
||||
);
|
||||
|
||||
const { options } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
// this component will alwyays be called on old logs explorer page itself!
|
||||
dataSource: DataSource.LOGS,
|
||||
// and we do not have table / timeseries aggregated views in the old logs explorer!
|
||||
aggregateOperator: StringOperators.NOOP,
|
||||
});
|
||||
|
||||
const getItemContent = useCallback(
|
||||
(index: number): JSX.Element => {
|
||||
const log = logs[index];
|
||||
|
||||
if (viewMode === 'raw') {
|
||||
return (
|
||||
<RawLogView
|
||||
key={log.id}
|
||||
data={log}
|
||||
linesPerRow={linesPerRow}
|
||||
selectedFields={selected}
|
||||
fontSize={options.fontSize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ListLogView
|
||||
key={log.id}
|
||||
logData={log}
|
||||
selectedFields={selected}
|
||||
linesPerRow={linesPerRow}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onSetActiveLog={onSetActiveLog}
|
||||
fontSize={options.fontSize}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[
|
||||
logs,
|
||||
viewMode,
|
||||
selected,
|
||||
linesPerRow,
|
||||
onAddToQuery,
|
||||
onSetActiveLog,
|
||||
options.fontSize,
|
||||
],
|
||||
);
|
||||
|
||||
const renderContent = useMemo(() => {
|
||||
if (viewMode === 'table') {
|
||||
return (
|
||||
<LogsTableView
|
||||
onClickExpand={onSetActiveLog}
|
||||
logs={logs}
|
||||
fields={selected}
|
||||
linesPerRow={linesPerRow}
|
||||
fontSize={options.fontSize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="logs-card" bodyStyle={CARD_BODY_STYLE}>
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso totalCount={logs.length} itemContent={getItemContent} />
|
||||
</OverlayScrollbar>
|
||||
</Card>
|
||||
);
|
||||
}, [
|
||||
getItemContent,
|
||||
linesPerRow,
|
||||
logs,
|
||||
onSetActiveLog,
|
||||
options.fontSize,
|
||||
selected,
|
||||
viewMode,
|
||||
]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner height={20} tip="Getting Logs" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{viewMode !== 'table' && (
|
||||
<Heading>
|
||||
<Typography.Text>Event</Typography.Text>
|
||||
</Heading>
|
||||
)}
|
||||
|
||||
{isLiveTail && <Typography>Getting live logs...</Typography>}
|
||||
|
||||
{isNoLogs && <Typography>No logs lines found</Typography>}
|
||||
|
||||
{renderContent}
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
log={activeLog}
|
||||
onClose={onClearActiveLog}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LogsTable);
|
||||
@@ -1,3 +0,0 @@
|
||||
.logs-card {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Card } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
export const Heading = styled(Card)`
|
||||
margin-bottom: 0.1rem;
|
||||
height: 32px;
|
||||
.ant-card-body {
|
||||
padding: 0.3rem 0.5rem;
|
||||
}
|
||||
`;
|
||||
@@ -3,10 +3,9 @@ import './FormatField.styles.scss';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
|
||||
import { FieldTitle } from '../styles';
|
||||
import { OptionsMenuConfig } from '../types';
|
||||
import { LogViewMode, OptionsMenuConfig } from '../types';
|
||||
import { FormatFieldWrapper } from './styles';
|
||||
|
||||
function FormatField({ config }: FormatFieldProps): JSX.Element | null {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { InputNumberProps, RadioProps, SelectProps } from 'antd';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
|
||||
export type LogViewMode = 'raw' | 'table' | 'list';
|
||||
|
||||
export enum FontSize {
|
||||
SMALL = 'small',
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useQueries } from 'react-query';
|
||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@@ -33,6 +32,7 @@ import {
|
||||
import {
|
||||
FontSize,
|
||||
InitialOptions,
|
||||
LogViewMode,
|
||||
OptionsMenuConfig,
|
||||
OptionsQuery,
|
||||
} from './types';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user