Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,30 @@ describe('useTraceItemSearchQueryBuilderProps', () => {
});
});

it('uses a custom placeholder when provided', () => {
const {result} = renderHookWithProviders(useTraceItemSearchQueryBuilderProps, {
initialProps: {
...defaultInitialProps,
placeholder: 'Custom placeholder text',
},
organization,
});

expect(result.current.placeholder).toBe('Custom placeholder text');
});

it('falls back to the default placeholder for logs when no placeholder is provided', () => {
const {result} = renderHookWithProviders(useTraceItemSearchQueryBuilderProps, {
initialProps: {
...defaultInitialProps,
itemType: TraceItemDataset.LOGS,
},
organization,
});

expect(result.current.placeholder).toBe('Search for logs, users, tags, and more');
});

it('calls validateQuery when a new filter key is added', async () => {
const validateMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/trace-items/attributes/validate/',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,9 @@ export function useTraceItemSearchQueryBuilderProps({
attributeQuery,
hiddenAttributeKeys,
allowedAttributeKeys,
placeholder,
}: TraceItemSearchQueryBuilderProps) {
const placeholderText = itemTypeToDefaultPlaceholder(itemType);
const placeholderText = placeholder ?? itemTypeToDefaultPlaceholder(itemType);

const {selection} = usePageFilters();
const effectiveProjects = projects ?? selection.projects;
Expand Down Expand Up @@ -259,6 +260,7 @@ export function TraceItemSearchQueryBuilder({
attributeQuery,
hiddenAttributeKeys,
allowedAttributeKeys,
placeholder,
}: TraceItemSearchQueryBuilderProps) {
const searchQueryBuilderProps = useTraceItemSearchQueryBuilderProps({
itemType,
Expand Down Expand Up @@ -289,6 +291,7 @@ export function TraceItemSearchQueryBuilder({
attributeQuery,
hiddenAttributeKeys,
allowedAttributeKeys,
placeholder,
});

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {TraceItemDataset} from 'sentry/views/explore/types';
import {findSuggestedColumns} from 'sentry/views/explore/utils';

export function useLogsSearchQueryBuilderProps({
attributeQuery,
booleanAttributes,
booleanSecondaryAliases,
numberAttributes,
Expand All @@ -31,6 +32,7 @@ export function useLogsSearchQueryBuilderProps({
numberSecondaryAliases: TagCollection;
stringAttributes: TagCollection;
stringSecondaryAliases: TagCollection;
attributeQuery?: string;
}) {
const logsSearch = useQueryParamsSearch();
const oldLogsSearch = usePrevious(logsSearch);
Expand Down Expand Up @@ -83,8 +85,10 @@ export function useLogsSearchQueryBuilderProps({
replaceRawSearchKeys: ['message'],
matchKeySuggestions: [{key: 'trace', valuePattern: /^[0-9a-fA-F]{32}$/}],
hiddenAttributeKeys: HiddenLogSearchFields,
attributeQuery,
}),
[
attributeQuery,
booleanAttributes,
booleanSecondaryAliases,
caseInsensitive,
Expand Down
69 changes: 69 additions & 0 deletions static/app/views/explore/replays/detail/noRowRenderer.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';

import {NoRowRenderer} from 'sentry/views/explore/replays/detail/noRowRenderer';

const children = 'No logs yet';

describe('NoRowRenderer', () => {
it('renders children when unfilteredItems is empty', () => {
render(
<NoRowRenderer unfilteredItems={[]} clearSearchTerm={jest.fn()}>
{children}
</NoRowRenderer>
);

expect(screen.getByText('No logs yet')).toBeInTheDocument();
expect(screen.queryByText('No results found')).not.toBeInTheDocument();
});

it('renders no results state when unfilteredItems has items', () => {
render(
<NoRowRenderer unfilteredItems={[{id: 1}]} clearSearchTerm={jest.fn()}>
{children}
</NoRowRenderer>
);

expect(screen.getByText('No results found')).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Clear filters'})).toBeInTheDocument();
expect(screen.queryByText('No logs yet')).not.toBeInTheDocument();
});

it('calls clearSearchTerm when Clear filters is clicked', async () => {
const clearSearchTerm = jest.fn();
render(
<NoRowRenderer unfilteredItems={[{id: 1}]} clearSearchTerm={clearSearchTerm}>
{children}
</NoRowRenderer>
);

await userEvent.click(screen.getByRole('button', {name: 'Clear filters'}));

expect(clearSearchTerm).toHaveBeenCalledTimes(1);
});

it('uses hasUnfilteredItems prop over unfilteredItems array length', () => {
render(
<NoRowRenderer unfilteredItems={[]} hasUnfilteredItems clearSearchTerm={jest.fn()}>
{children}
</NoRowRenderer>
);

expect(screen.getByText('No results found')).toBeInTheDocument();
expect(screen.queryByText('No logs yet')).not.toBeInTheDocument();
});

it('renders children when hasUnfilteredItems is false even with items in array', () => {
render(
<NoRowRenderer
unfilteredItems={[{id: 1}]}
hasUnfilteredItems={false}
clearSearchTerm={jest.fn()}
>
{children}
</NoRowRenderer>
);

expect(screen.getByText('No logs yet')).toBeInTheDocument();
expect(screen.queryByText('No results found')).not.toBeInTheDocument();
});
});
20 changes: 14 additions & 6 deletions static/app/views/explore/replays/detail/noRowRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,27 @@ type Props = {
children: ReactNode;
clearSearchTerm: () => void;
unfilteredItems: unknown[];
hasUnfilteredItems?: boolean;
};

export function NoRowRenderer({children, unfilteredItems, clearSearchTerm}: Props) {
return unfilteredItems.length === 0 ? (
<EmptyState>
<p>{children}</p>
</EmptyState>
) : (
export function NoRowRenderer({
children,
unfilteredItems,
hasUnfilteredItems,
clearSearchTerm,
}: Props) {
const itemsExist = hasUnfilteredItems ?? unfilteredItems.length > 0;

return itemsExist ? (
<EmptyState>
<p>{t('No results found')}</p>
<Button icon={<IconClose variant="primary" />} onClick={clearSearchTerm}>
{t('Clear filters')}
</Button>
</EmptyState>
) : (
<EmptyState>
<p>{children}</p>
</EmptyState>
);
}
71 changes: 59 additions & 12 deletions static/app/views/explore/replays/detail/ourlogs/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {useCallback, useMemo} from 'react';
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import styled from '@emotion/styled';

import {Placeholder} from 'sentry/components/placeholder';
Expand All @@ -9,6 +9,7 @@ import {defined} from 'sentry/utils';
import {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalyticsEvent';
import {useReplayReader} from 'sentry/utils/replays/playback/providers/replayReaderProvider';
import {useCurrentHoverTime} from 'sentry/utils/replays/playback/providers/useCurrentHoverTime';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {defaultLogFields} from 'sentry/views/explore/contexts/logs/fields';
import {
LogsPageDataProvider,
Expand All @@ -23,11 +24,15 @@ import {
LogsInfiniteTable,
} from 'sentry/views/explore/logs/tables/logsInfiniteTable';
import {rearrangedLogsReplayFields} from 'sentry/views/explore/logs/tables/logsTableUtils';
import {useLogsSearchQueryBuilderProps} from 'sentry/views/explore/logs/useLogsSearchQueryBuilderProps';
import {
useQueryParamsSearch,
useSetQueryParamsSearch,
} from 'sentry/views/explore/queryParams/context';
import {FluidHeight} from 'sentry/views/explore/replays/detail/layout/fluidHeight';
import {NoRowRenderer} from 'sentry/views/explore/replays/detail/noRowRenderer';
import {OurLogFilters} from 'sentry/views/explore/replays/detail/ourlogs/ourlogFilters';
import {ourlogsAsFrames} from 'sentry/views/explore/replays/detail/ourlogs/ourlogsAsFrames';
import {useOurLogFilters} from 'sentry/views/explore/replays/detail/ourlogs/useOurLogFilters';

export function OurLogs() {
const replay = useReplayReader();
Expand Down Expand Up @@ -71,9 +76,15 @@ interface OurLogsContentProps {
}

function OurLogsContent({replayId, startTimestampMs}: OurLogsContentProps) {
const {attributes: stringAttributes} = useLogItemAttributes({}, 'string');
const {attributes: numberAttributes} = useLogItemAttributes({}, 'number');
const {attributes: booleanAttributes} = useLogItemAttributes({}, 'boolean');
const replayAttributeFilter = MutableSearch.fromQueryObject({
[`sentry._internal.cooccuring.replay_id.${replayId}`]: ['true'],
}).formatString();
const {attributes: stringAttributes, secondaryAliases: stringSecondaryAliases} =
useLogItemAttributes({query: replayAttributeFilter}, 'string');
const {attributes: numberAttributes, secondaryAliases: numberSecondaryAliases} =
useLogItemAttributes({query: replayAttributeFilter}, 'number');
const {attributes: booleanAttributes, secondaryAliases: booleanSecondaryAliases} =
useLogItemAttributes({query: replayAttributeFilter}, 'boolean');

const {currentTime, setCurrentTime} = useReplayContext();
const [currentHoverTime] = useCurrentHoverTime();
Expand All @@ -82,9 +93,37 @@ function OurLogsContent({replayId, startTimestampMs}: OurLogsContentProps) {
const {infiniteLogsQueryResult} = useLogsPageData();
const {data: logItems, isPending} = infiniteLogsQueryResult;

const filterProps = useOurLogFilters({logItems});
const {items: filteredLogItems, setSearchTerm} = filterProps;
const clearSearchTerm = () => setSearchTerm('');
const logsSearch = useQueryParamsSearch();
const setLogsSearch = useSetQueryParamsSearch();
const filterText = logsSearch.freeText.join(' ');
const clearSearch = useCallback(
() => setLogsSearch(new MutableSearch('')),
[setLogsSearch]
);

const [hasAnyLogs, setHasAnyLogs] = useState(!!logItems?.length);
const previousReplayId = useRef(replayId);
useEffect(() => {
if (previousReplayId.current !== replayId) {
previousReplayId.current = replayId;
setHasAnyLogs(!!logItems?.length);
Comment thread
sentry[bot] marked this conversation as resolved.
return;
}
if (logItems?.length) {
setHasAnyLogs(true);
}
}, [logItems, replayId]);

const {tracesItemSearchQueryBuilderProps, searchQueryBuilderProviderProps} =
useLogsSearchQueryBuilderProps({
attributeQuery: replayAttributeFilter,
stringAttributes,
numberAttributes,
booleanAttributes,
stringSecondaryAliases,
numberSecondaryAliases,
booleanSecondaryAliases,
});

const handleReplayTimeClick = useCallback(
(offsetMs: string) => {
Expand Down Expand Up @@ -120,7 +159,11 @@ function OurLogsContent({replayId, startTimestampMs}: OurLogsContentProps) {

return (
<OurLogsContentWrapper>
<OurLogFilters logItems={logItems} replayId={replayId} {...filterProps} />
<OurLogFilters
replayId={replayId}
searchQueryBuilderProps={tracesItemSearchQueryBuilderProps}
searchQueryBuilderProviderProps={searchQueryBuilderProviderProps}
/>
<LogsItemContainer border="primary" radius="md" flex="1 1 auto">
{isPending ? (
<Placeholder height="100%" />
Expand All @@ -133,12 +176,16 @@ function OurLogsContent({replayId, startTimestampMs}: OurLogsContentProps) {
embedded
embeddedOptions={embeddedOptions}
localOnlyItemFilters={{
filteredItems: filteredLogItems,
filterText: filterProps.searchTerm,
filteredItems: logItems,
filterText,
}}
embeddedStyling={{disableBodyPadding: true, showVerticalScrollbar: false}}
emptyRenderer={() => (
<NoRowRenderer unfilteredItems={logItems} clearSearchTerm={clearSearchTerm}>
<NoRowRenderer
unfilteredItems={logItems}
hasUnfilteredItems={hasAnyLogs}
clearSearchTerm={clearSearch}
>
{t('No logs recorded')}
</NoRowRenderer>
)}
Expand Down
Loading
Loading