Skip to content

Commit 01e0b36

Browse files
fix: overall improvements to span logs drawer empty state (i.e. trace logs empty state vs. span logs empty state + UI improvements) (#9252)
* chore: remove the applied filters in related signals drawer * chore: make the span logs highlight color more prominent * fix: add label to open trace logs in logs explorer button * feat: improve the span logs empty state i.e. add support for no logs for trace_id * refactor: refactor the span logs content and make it readable * test: add tests for span logs * chore: improve tests * refactor: simplify condition * chore: remove redundant test * fix: make trace_id logs request only if drawer is open * chore: fix failing tests + overall improvements * Update frontend/src/container/SpanDetailsDrawer/__tests__/SpanDetailsDrawer.test.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * chore: fix the failing test * fix: fix the light mode styles for empty logs component * chore: update the empty state copy * chore: fix the failing tests by updating the assertions with correct empty state copy --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
1 parent e90bb01 commit 01e0b36

File tree

12 files changed

+421
-120
lines changed

12 files changed

+421
-120
lines changed

frontend/src/components/Logs/RawLogView/styles.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ export const RawLogViewContainer = styled(Row)<{
5757
transition: background-color 2s ease-in;`
5858
: ''}
5959
60-
${({ $isCustomHighlighted, $isDarkMode, $logType }): string =>
61-
getCustomHighlightBackground($isCustomHighlighted, $isDarkMode, $logType)}
60+
${({ $isCustomHighlighted }): string =>
61+
getCustomHighlightBackground($isCustomHighlighted)}
6262
`;
6363

6464
export const InfoIconWrapper = styled(Info)`

frontend/src/constants/reactQueryKeys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export const REACT_QUERY_KEY = {
8686
SPAN_LOGS: 'SPAN_LOGS',
8787
SPAN_BEFORE_LOGS: 'SPAN_BEFORE_LOGS',
8888
SPAN_AFTER_LOGS: 'SPAN_AFTER_LOGS',
89+
TRACE_ONLY_LOGS: 'TRACE_ONLY_LOGS',
8990

9091
// Routing Policies Query Keys
9192
GET_ROUTING_POLICIES: 'GET_ROUTING_POLICIES',

frontend/src/container/EmptyLogsSearch/EmptyLogsSearch.styles.scss

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,30 @@
171171
}
172172
}
173173
}
174+
.lightMode {
175+
.empty-logs-search {
176+
&__resources-card {
177+
background: var(--bg-vanilla-100);
178+
border: 1px solid var(--bg-vanilla-300);
179+
}
180+
181+
&__resources-title {
182+
color: var(--bg-ink-400);
183+
}
184+
185+
&__resources-description,
186+
&__description-list,
187+
&__subtitle {
188+
color: var(--bg-ink-300);
189+
}
190+
191+
&__title {
192+
color: var(--bg-ink-500);
193+
}
194+
195+
&__clear-filters-btn {
196+
border: 1px dashed var(--bg-vanilla-300);
197+
color: var(--bg-ink-400);
198+
}
199+
}
200+
}

frontend/src/container/SpanDetailsDrawer/SpanLogs/SpanLogs.tsx

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import {
1212
PANEL_TYPES,
1313
} from 'constants/queryBuilder';
1414
import ROUTES from 'constants/routes';
15+
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
1516
import LogsError from 'container/LogsError/LogsError';
17+
import { EmptyLogsListConfig } from 'container/LogsExplorerList/utils';
1618
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
1719
import { FontSize } from 'container/OptionsMenu/types';
1820
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
@@ -30,38 +32,36 @@ import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
3032
import { DataSource } from 'types/common/queryBuilder';
3133
import { v4 as uuid } from 'uuid';
3234

33-
import { useSpanContextLogs } from './useSpanContextLogs';
34-
3535
interface SpanLogsProps {
3636
traceId: string;
3737
spanId: string;
3838
timeRange: {
3939
startTime: number;
4040
endTime: number;
4141
};
42+
logs: ILog[];
43+
isLoading: boolean;
44+
isError: boolean;
45+
isFetching: boolean;
46+
isLogSpanRelated: (logId: string) => boolean;
4247
handleExplorerPageRedirect: () => void;
48+
emptyStateConfig?: EmptyLogsListConfig;
4349
}
4450

4551
function SpanLogs({
4652
traceId,
4753
spanId,
4854
timeRange,
55+
logs,
56+
isLoading,
57+
isError,
58+
isFetching,
59+
isLogSpanRelated,
4960
handleExplorerPageRedirect,
61+
emptyStateConfig,
5062
}: SpanLogsProps): JSX.Element {
5163
const { updateAllQueriesOperators } = useQueryBuilder();
5264

53-
const {
54-
logs,
55-
isLoading,
56-
isError,
57-
isFetching,
58-
isLogSpanRelated,
59-
} = useSpanContextLogs({
60-
traceId,
61-
spanId,
62-
timeRange,
63-
});
64-
6565
// Create trace_id and span_id filters for logs explorer navigation
6666
const createLogsFilter = useCallback(
6767
(targetSpanId: string): TagFilter => {
@@ -236,9 +236,7 @@ function SpanLogs({
236236
<img src="/Icons/no-data.svg" alt="no-data" className="no-data-img" />
237237
<Typography.Text className="no-data-text-1">
238238
No logs found for selected span.
239-
<span className="no-data-text-2">
240-
Try viewing logs for the current trace.
241-
</span>
239+
<span className="no-data-text-2">View logs for the current trace.</span>
242240
</Typography.Text>
243241
</section>
244242
<section className="action-section">
@@ -249,24 +247,45 @@ function SpanLogs({
249247
onClick={handleExplorerPageRedirect}
250248
size="md"
251249
>
252-
Log Explorer
250+
View Logs
253251
</Button>
254252
</section>
255253
</div>
256254
);
257255

256+
const renderSpanLogsContent = (): JSX.Element | null => {
257+
if (isLoading || isFetching) {
258+
return <LogsLoading />;
259+
}
260+
261+
if (isError) {
262+
return <LogsError />;
263+
}
264+
265+
if (logs.length === 0) {
266+
if (emptyStateConfig) {
267+
return (
268+
<EmptyLogsSearch
269+
dataSource={DataSource.LOGS}
270+
panelType="LIST"
271+
customMessage={emptyStateConfig}
272+
/>
273+
);
274+
}
275+
return renderNoLogsFound();
276+
}
277+
278+
return renderContent;
279+
};
280+
258281
return (
259282
<div className={cx('span-logs', { 'span-logs-empty': logs.length === 0 })}>
260-
{(isLoading || isFetching) && <LogsLoading />}
261-
{!isLoading &&
262-
!isFetching &&
263-
!isError &&
264-
logs.length === 0 &&
265-
renderNoLogsFound()}
266-
{isError && !isLoading && !isFetching && <LogsError />}
267-
{!isLoading && !isFetching && !isError && logs.length > 0 && renderContent}
283+
{renderSpanLogsContent()}
268284
</div>
269285
);
270286
}
287+
SpanLogs.defaultProps = {
288+
emptyStateConfig: undefined,
289+
};
271290

272291
export default SpanLogs;
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import { getEmptyLogsListConfig } from 'container/LogsExplorerList/utils';
2+
import { server } from 'mocks-server/server';
3+
import { render, screen, userEvent } from 'tests/test-utils';
4+
5+
import SpanLogs from '../SpanLogs';
6+
7+
// Mock external dependencies
8+
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
9+
useQueryBuilder: (): any => ({
10+
updateAllQueriesOperators: jest.fn().mockReturnValue({
11+
builder: {
12+
queryData: [
13+
{
14+
dataSource: 'logs',
15+
queryName: 'A',
16+
aggregateOperator: 'noop',
17+
filter: { expression: "trace_id = 'test-trace-id'" },
18+
expression: 'A',
19+
disabled: false,
20+
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
21+
groupBy: [],
22+
limit: null,
23+
having: [],
24+
},
25+
],
26+
queryFormulas: [],
27+
},
28+
queryType: 'builder',
29+
}),
30+
}),
31+
}));
32+
33+
// Mock window.open
34+
const mockWindowOpen = jest.fn();
35+
Object.defineProperty(window, 'open', {
36+
writable: true,
37+
value: mockWindowOpen,
38+
});
39+
40+
// Mock Virtuoso to avoid complex virtualization
41+
jest.mock('react-virtuoso', () => ({
42+
Virtuoso: jest.fn(({ data, itemContent }: any) => (
43+
<div data-testid="virtuoso">
44+
{data?.map((item: any, index: number) => (
45+
<div key={item.id || index} data-testid={`log-item-${item.id}`}>
46+
{itemContent(index, item)}
47+
</div>
48+
))}
49+
</div>
50+
)),
51+
}));
52+
53+
// Mock RawLogView component
54+
jest.mock(
55+
'components/Logs/RawLogView',
56+
() =>
57+
function MockRawLogView({
58+
data,
59+
onLogClick,
60+
isHighlighted,
61+
helpTooltip,
62+
}: any): JSX.Element {
63+
return (
64+
<button
65+
type="button"
66+
data-testid={`raw-log-${data.id}`}
67+
className={isHighlighted ? 'log-highlighted' : 'log-context'}
68+
title={helpTooltip}
69+
onClick={(e): void => onLogClick?.(data, e)}
70+
>
71+
<div>{data.body}</div>
72+
<div>{data.timestamp}</div>
73+
</button>
74+
);
75+
},
76+
);
77+
78+
// Mock PreferenceContextProvider
79+
jest.mock('providers/preferences/context/PreferenceContextProvider', () => ({
80+
PreferenceContextProvider: ({ children }: any): JSX.Element => (
81+
<div>{children}</div>
82+
),
83+
}));
84+
85+
// Mock OverlayScrollbar
86+
jest.mock('components/OverlayScrollbar/OverlayScrollbar', () => ({
87+
default: ({ children }: any): JSX.Element => (
88+
<div data-testid="overlay-scrollbar">{children}</div>
89+
),
90+
}));
91+
92+
// Mock LogsLoading component
93+
jest.mock('container/LogsLoading/LogsLoading', () => ({
94+
LogsLoading: function MockLogsLoading(): JSX.Element {
95+
return <div data-testid="logs-loading">Loading logs...</div>;
96+
},
97+
}));
98+
99+
// Mock LogsError component
100+
jest.mock(
101+
'container/LogsError/LogsError',
102+
() =>
103+
function MockLogsError(): JSX.Element {
104+
return <div data-testid="logs-error">Error loading logs</div>;
105+
},
106+
);
107+
108+
// Don't mock EmptyLogsSearch - test the actual component behavior
109+
110+
const TEST_TRACE_ID = 'test-trace-id';
111+
const TEST_SPAN_ID = 'test-span-id';
112+
113+
const defaultProps = {
114+
traceId: TEST_TRACE_ID,
115+
spanId: TEST_SPAN_ID,
116+
timeRange: {
117+
startTime: 1640995200000,
118+
endTime: 1640995260000,
119+
},
120+
logs: [],
121+
isLoading: false,
122+
isError: false,
123+
isFetching: false,
124+
isLogSpanRelated: jest.fn().mockReturnValue(false),
125+
handleExplorerPageRedirect: jest.fn(),
126+
};
127+
128+
describe('SpanLogs', () => {
129+
beforeEach(() => {
130+
jest.clearAllMocks();
131+
mockWindowOpen.mockClear();
132+
});
133+
134+
afterEach(() => {
135+
server.resetHandlers();
136+
});
137+
138+
it('should show simple empty state when emptyStateConfig is not provided', () => {
139+
// eslint-disable-next-line react/jsx-props-no-spreading
140+
render(<SpanLogs {...defaultProps} />);
141+
142+
// Should show simple empty state (no emptyStateConfig provided)
143+
expect(
144+
screen.getByText('No logs found for selected span.'),
145+
).toBeInTheDocument();
146+
expect(
147+
screen.getByText('View logs for the current trace.'),
148+
).toBeInTheDocument();
149+
expect(
150+
screen.getByRole('button', {
151+
name: /view logs/i,
152+
}),
153+
).toBeInTheDocument();
154+
155+
// Should NOT show enhanced empty state
156+
expect(screen.queryByTestId('empty-logs-search')).not.toBeInTheDocument();
157+
expect(screen.queryByTestId('documentation-links')).not.toBeInTheDocument();
158+
});
159+
160+
it('should show enhanced empty state when entire trace has no logs', () => {
161+
render(
162+
<SpanLogs
163+
// eslint-disable-next-line react/jsx-props-no-spreading
164+
{...defaultProps}
165+
emptyStateConfig={getEmptyLogsListConfig(jest.fn())}
166+
/>,
167+
);
168+
169+
// Should show enhanced empty state with custom message
170+
expect(screen.getByText('No logs found for this trace.')).toBeInTheDocument();
171+
expect(screen.getByText('This could be because :')).toBeInTheDocument();
172+
173+
// Should show description list
174+
expect(
175+
screen.getByText('Logs are not linked to Traces.'),
176+
).toBeInTheDocument();
177+
expect(
178+
screen.getByText('Logs are not being sent to SigNoz.'),
179+
).toBeInTheDocument();
180+
expect(
181+
screen.getByText('No logs are associated with this particular trace/span.'),
182+
).toBeInTheDocument();
183+
184+
// Should show documentation links
185+
expect(screen.getByText('RESOURCES')).toBeInTheDocument();
186+
expect(screen.getByText('Sending logs to SigNoz')).toBeInTheDocument();
187+
expect(screen.getByText('Correlate traces and logs')).toBeInTheDocument();
188+
189+
// Should NOT show simple empty state
190+
expect(
191+
screen.queryByText('No logs found for selected span.'),
192+
).not.toBeInTheDocument();
193+
});
194+
195+
it('should call handleExplorerPageRedirect when Log Explorer button is clicked', async () => {
196+
const user = userEvent.setup({ pointerEventsCheck: 0 });
197+
const mockHandleExplorerPageRedirect = jest.fn();
198+
199+
render(
200+
<SpanLogs
201+
// eslint-disable-next-line react/jsx-props-no-spreading
202+
{...defaultProps}
203+
handleExplorerPageRedirect={mockHandleExplorerPageRedirect}
204+
/>,
205+
);
206+
207+
const logExplorerButton = screen.getByRole('button', {
208+
name: /view logs/i,
209+
});
210+
await user.click(logExplorerButton);
211+
212+
expect(mockHandleExplorerPageRedirect).toHaveBeenCalledTimes(1);
213+
});
214+
});

0 commit comments

Comments
 (0)