Compare commits

..

10 Commits

Author SHA1 Message Date
Gaurav Tewari
6d7c08172a feat: maketrace columns appear in a specific order 2026-09-22 15:48:20 +05:30
Gaurav Tewari
d879273e44 chore: allow users to move trace id 2026-09-22 15:48:20 +05:30
Gaurav Tewari
a4314382d1 chore: update colors for drawers inputs 2026-09-22 15:48:20 +05:30
Gaurav Tewari
9844e81c36 chore: removed unused padding in mapping tabel 2026-09-22 15:48:20 +05:30
Gaurav Tewari
fa6197ced1 chore: migrate tabs in model pricing & attribute mapping to antD tabs 2026-09-22 15:48:11 +05:30
Gaurav Tewari
89bb599cbe chore: make label of add model cost look same 2026-09-22 15:29:30 +05:30
Gaurav Tewari
e3bc7fa8e8 fix: create new mapping color 2026-09-22 15:29:09 +05:30
Gaurav Tewari
d61b558334 fix: test json being send 2026-09-22 10:35:54 +05:30
Aditya Singh
d39467f5ef chore(logs): delete the old logs explorer (#12930)
#### Description

- `/logs/old-logs-explorer` has been rendering nothing since #8299
gutted its two endpoints. deleted the page along with its containers,
the logs redux slice, the legacy `api/logs` clients and the logql
parser. `LogViewMode` and the restricted field constants move out first
since they have consumers elsewhere.
- deleted what the removal orphans.. the logql test fixture,
`CategoryHeading` and the antd table behind `components/Logs/TableView`.
that folder does not go entirely, `useLogsTableColumns` and
`ColumnTypeRender` still have consumers.
- `ROUTES.LOGS` and `ROUTES.LOGS_EXPLORER` are the same path and both
were registered exact. Switch takes the first so the second entry never
ran and its chunk just duplicated the first. dropped the entry, both
constants stay.

#### Issues closed by this PR

Part of https://github.com/SigNoz/engineering-pod/issues/6103

#### Additional Information

- frontend only. `GET /api/v1/logs` and `/api/v1/logs/aggregate` stay
registered and still return empty payloads, so this can go in without a
backend change.
- overlaps #12919 (old trace explorer) on the `NewExplorerCTA` cleanup,
and overlaps the log details v1 removal on a few files it deletes
outright. whichever merges second needs a rebase.
- cc. @therealpandey
2026-09-22 04:44:25 +00:00
Gaurav Tewari
d457ce6144 feat(llm-observability): move module tabs to RouteTab , add tab icons & other ui changes (#12762)
#### Description

- Replaces the bespoke LLM Observability tab shell
(`container/LLMObservability/LLMObservability.tsx` +
`useLLMObservabilityTabs`) with the shared `RouteTab` component that
every other module page already uses. Tabs are now declared as
`TabRoutes` in `pages/LLMObservability/constants.tsx`.
- Adds icons to the tab labels across LLM Observability, Infrastructure
Monitoring, All Errors, Messaging Queues, Meter Explorer and Logs
Settings, following the existing `.tab-item` pattern.
- Fixes tab-panel padding and aligns the source-filter select height
with the adjacent search input.
- Removed the dashboardInfoWithActions


#### Additional Information

`AllErrors/config.ts` is renamed to `.tsx` since the tab labels now
contain JSX.

Visual Diff - 
1. Look into the wrapper  - 
 a. Before 
<img width="1474" height="997" alt="image"
src="https://github.com/user-attachments/assets/39fe854b-fa79-4e24-9227-4da77825f726"
/>
b. Now 
<img width="1489" height="991" alt="image"
src="https://github.com/user-attachments/assets/4c02e991-3764-4f66-9efc-bef934ca5e2a"
/>



2. Removed the dashboardInfoWithActions 
a. Before 
<img width="1392" height="304" alt="image"
src="https://github.com/user-attachments/assets/bb2efef8-5126-4728-b221-b2cc2b3da18f"
/>
b. now
<img width="1420" height="290" alt="image"
src="https://github.com/user-attachments/assets/50412047-5eff-4035-ac6c-ed79393070ac"
/>


3. After feedback from nitya we have to hide add variable and the
actions button as well.
a. Before
<img width="1368" height="597" alt="image"
src="https://github.com/user-attachments/assets/669de39b-bbc2-4b29-a16e-139457fbb483"
/>

b. Now
<img width="1382" height="728" alt="image"
src="https://github.com/user-attachments/assets/7973dc3a-9914-42ed-a334-887ea73b1ba0"
/>

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-09-22 04:44:00 +00:00
163 changed files with 587 additions and 5002 deletions

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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,

View File

@@ -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'),
);

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
},
);

View File

@@ -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;

View File

@@ -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;
`;

View File

@@ -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,
};

View File

@@ -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;

View File

@@ -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;`}
`;

View File

@@ -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;
};

View File

@@ -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;
}
}

View File

@@ -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 };
};

View File

@@ -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,

View File

@@ -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',

View File

@@ -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 }}`,
};

View File

@@ -1,7 +1,6 @@
.tableWrapper {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
}
.toolbar {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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(

View File

@@ -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);
});
});

View File

@@ -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 {

View File

@@ -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' }));

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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',
]);
});
});

View File

@@ -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 () => {

View File

@@ -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',
]);
});
});

View File

@@ -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,
}));

View File

@@ -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],
);

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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);
}}

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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}

View File

@@ -1,3 +1,7 @@
.fieldLabel {
composes: fieldLabel from '../../shared.module.scss';
}
.drawerSection {
composes: drawerSection from '../../shared.module.scss';
}

View File

@@ -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}

View File

@@ -1,3 +1,7 @@
.fieldLabel {
composes: fieldLabel from '../../shared.module.scss';
}
.drawerSection {
composes: drawerSection from '../../shared.module.scss';
}

View File

@@ -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) => (

View File

@@ -1,3 +1,7 @@
.fieldLabel {
composes: fieldLabel from '../../shared.module.scss';
}
.drawerSection {
composes: drawerSection from '../../shared.module.scss';
}

View File

@@ -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">
*

View File

@@ -1,3 +1,7 @@
.fieldLabel {
composes: fieldLabel from '../../shared.module.scss';
}
.drawerSection {
composes: drawerSection from '../../shared.module.scss';
}

View File

@@ -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">

View File

@@ -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;

View File

@@ -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}`));

View File

@@ -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;

View File

@@ -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 &quot;{modelName}&quot;
</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>

View File

@@ -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 };
}

View File

@@ -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);

View File

@@ -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;
`;

View File

@@ -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,
});

View File

@@ -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}

View File

@@ -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';

View File

@@ -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,

View File

@@ -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,
]);

View File

@@ -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));

View File

@@ -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';

View File

@@ -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',
},
];

View File

@@ -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);

View File

@@ -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'};
`;

View File

@@ -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);

View File

@@ -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;
}
`;

View File

@@ -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;

View File

@@ -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] },
};

View File

@@ -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;

View File

@@ -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]};
`;

View File

@@ -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;
}

View File

@@ -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),
);
}
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
`;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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]};
}
`;

View File

@@ -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),
)}`;
};

View File

@@ -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));

View File

@@ -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;
`;

View File

@@ -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,
};
}

View File

@@ -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;
};

View File

@@ -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);

View File

@@ -1,3 +0,0 @@
.logs-card {
flex: 1;
}

View File

@@ -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;
}
`;

View File

@@ -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 {

View File

@@ -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',

View File

@@ -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