diff --git a/static/app/views/explore/components/traceItemSearchQueryBuilder.spec.tsx b/static/app/views/explore/components/traceItemSearchQueryBuilder.spec.tsx
index 42ac973ced99ff..e2e6024bff7b59 100644
--- a/static/app/views/explore/components/traceItemSearchQueryBuilder.spec.tsx
+++ b/static/app/views/explore/components/traceItemSearchQueryBuilder.spec.tsx
@@ -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/',
diff --git a/static/app/views/explore/components/traceItemSearchQueryBuilder.tsx b/static/app/views/explore/components/traceItemSearchQueryBuilder.tsx
index 8cddfe407f7d35..97acf2447a45a3 100644
--- a/static/app/views/explore/components/traceItemSearchQueryBuilder.tsx
+++ b/static/app/views/explore/components/traceItemSearchQueryBuilder.tsx
@@ -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;
@@ -259,6 +260,7 @@ export function TraceItemSearchQueryBuilder({
attributeQuery,
hiddenAttributeKeys,
allowedAttributeKeys,
+ placeholder,
}: TraceItemSearchQueryBuilderProps) {
const searchQueryBuilderProps = useTraceItemSearchQueryBuilderProps({
itemType,
@@ -289,6 +291,7 @@ export function TraceItemSearchQueryBuilder({
attributeQuery,
hiddenAttributeKeys,
allowedAttributeKeys,
+ placeholder,
});
return (
diff --git a/static/app/views/explore/logs/useLogsSearchQueryBuilderProps.tsx b/static/app/views/explore/logs/useLogsSearchQueryBuilderProps.tsx
index cc74fe76ca8a90..98fc324a10d62a 100644
--- a/static/app/views/explore/logs/useLogsSearchQueryBuilderProps.tsx
+++ b/static/app/views/explore/logs/useLogsSearchQueryBuilderProps.tsx
@@ -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,
@@ -31,6 +32,7 @@ export function useLogsSearchQueryBuilderProps({
numberSecondaryAliases: TagCollection;
stringAttributes: TagCollection;
stringSecondaryAliases: TagCollection;
+ attributeQuery?: string;
}) {
const logsSearch = useQueryParamsSearch();
const oldLogsSearch = usePrevious(logsSearch);
@@ -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,
diff --git a/static/app/views/explore/replays/detail/noRowRenderer.spec.tsx b/static/app/views/explore/replays/detail/noRowRenderer.spec.tsx
new file mode 100644
index 00000000000000..6f59aa31908c36
--- /dev/null
+++ b/static/app/views/explore/replays/detail/noRowRenderer.spec.tsx
@@ -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(
+
+ {children}
+
+ );
+
+ 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(
+
+ {children}
+
+ );
+
+ 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(
+
+ {children}
+
+ );
+
+ await userEvent.click(screen.getByRole('button', {name: 'Clear filters'}));
+
+ expect(clearSearchTerm).toHaveBeenCalledTimes(1);
+ });
+
+ it('uses hasUnfilteredItems prop over unfilteredItems array length', () => {
+ render(
+
+ {children}
+
+ );
+
+ 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(
+
+ {children}
+
+ );
+
+ expect(screen.getByText('No logs yet')).toBeInTheDocument();
+ expect(screen.queryByText('No results found')).not.toBeInTheDocument();
+ });
+});
diff --git a/static/app/views/explore/replays/detail/noRowRenderer.tsx b/static/app/views/explore/replays/detail/noRowRenderer.tsx
index 826426ff14b2c2..e1fdb367233836 100644
--- a/static/app/views/explore/replays/detail/noRowRenderer.tsx
+++ b/static/app/views/explore/replays/detail/noRowRenderer.tsx
@@ -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 ? (
-
- {children}
-
- ) : (
+export function NoRowRenderer({
+ children,
+ unfilteredItems,
+ hasUnfilteredItems,
+ clearSearchTerm,
+}: Props) {
+ const itemsExist = hasUnfilteredItems ?? unfilteredItems.length > 0;
+
+ return itemsExist ? (
{t('No results found')}
} onClick={clearSearchTerm}>
{t('Clear filters')}
+ ) : (
+
+ {children}
+
);
}
diff --git a/static/app/views/explore/replays/detail/ourlogs/index.tsx b/static/app/views/explore/replays/detail/ourlogs/index.tsx
index 4c465a43a26af5..533c0e35831c37 100644
--- a/static/app/views/explore/replays/detail/ourlogs/index.tsx
+++ b/static/app/views/explore/replays/detail/ourlogs/index.tsx
@@ -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';
@@ -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,
@@ -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();
@@ -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();
@@ -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);
+ 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) => {
@@ -120,7 +159,11 @@ function OurLogsContent({replayId, startTimestampMs}: OurLogsContentProps) {
return (
-
+
{isPending ? (
@@ -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={() => (
-
+
{t('No logs recorded')}
)}
diff --git a/static/app/views/explore/replays/detail/ourlogs/openInLogsButton.spec.tsx b/static/app/views/explore/replays/detail/ourlogs/openInLogsButton.spec.tsx
new file mode 100644
index 00000000000000..dea3f230b74a45
--- /dev/null
+++ b/static/app/views/explore/replays/detail/ourlogs/openInLogsButton.spec.tsx
@@ -0,0 +1,93 @@
+import {OrganizationFixture} from 'sentry-fixture/organization';
+
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import {PageFiltersStore} from 'sentry/components/pageFilters/store';
+import {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalyticsEvent';
+import {LogsQueryParamsProvider} from 'sentry/views/explore/logs/logsQueryParamsProvider';
+import {OpenInLogsButton} from 'sentry/views/explore/replays/detail/ourlogs/openInLogsButton';
+
+function Wrapper({children}: {children: React.ReactNode}) {
+ return (
+
+ {children}
+
+ );
+}
+
+describe('OpenInLogsButton', () => {
+ beforeEach(() => {
+ PageFiltersStore.init();
+ PageFiltersStore.onInitializeUrlState({
+ projects: [1],
+ environments: [],
+ datetime: {period: '14d', start: null, end: null, utc: false},
+ });
+ });
+
+ it('renders nothing when the explore feature flag is disabled', () => {
+ const organization = OrganizationFixture({features: []});
+
+ const {container} = render(
+
+
+ ,
+ {organization}
+ );
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('renders the button when the explore feature flag is enabled', () => {
+ const organization = OrganizationFixture({
+ features: ['visibility-explore-view'],
+ });
+
+ render(
+
+
+ ,
+ {organization}
+ );
+
+ expect(screen.getByRole('button', {name: 'Open in Logs'})).toBeInTheDocument();
+ });
+
+ it('appends replay_id to the URL when replayId is provided', () => {
+ const organization = OrganizationFixture({
+ features: ['visibility-explore-view'],
+ });
+
+ render(
+
+
+ ,
+ {organization}
+ );
+
+ const link = screen.getByRole('button', {name: 'Open in Logs'});
+ expect(link).toHaveAttribute('href', expect.stringContaining('replay_id%3Adeadbeef'));
+ });
+
+ it('includes the existing search query before replay_id in the URL', () => {
+ const organization = OrganizationFixture({
+ features: ['visibility-explore-view'],
+ });
+
+ render(
+
+
+ ,
+ {organization}
+ );
+
+ const link = screen.getByRole('button', {name: 'Open in Logs'});
+ expect(link).toHaveAttribute(
+ 'href',
+ expect.stringMatching(/logsQuery=.*replay_id%3Adeadbeef/)
+ );
+ });
+});
diff --git a/static/app/views/explore/replays/detail/ourlogs/openInLogsButton.tsx b/static/app/views/explore/replays/detail/ourlogs/openInLogsButton.tsx
index 387e838895a2ba..abfecc8e791a36 100644
--- a/static/app/views/explore/replays/detail/ourlogs/openInLogsButton.tsx
+++ b/static/app/views/explore/replays/detail/ourlogs/openInLogsButton.tsx
@@ -4,22 +4,23 @@ import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {t} from 'sentry/locale';
import {useOrganization} from 'sentry/utils/useOrganization';
import {getLogsUrl} from 'sentry/views/explore/logs/utils';
+import {useQueryParamsSearch} from 'sentry/views/explore/queryParams/context';
type Props = {
- searchTerm: string;
replayId?: string;
};
-export function OpenInLogsButton({searchTerm, replayId}: Props) {
+export function OpenInLogsButton({replayId}: Props) {
const organization = useOrganization();
const hasExploreEnabled = organization.features.includes('visibility-explore-view');
const {selection} = usePageFilters();
+ const logsSearch = useQueryParamsSearch();
if (!hasExploreEnabled) {
return null;
}
- let query = searchTerm || '';
+ let query = logsSearch.formatString();
if (replayId) {
const existingQuery = query ? `${query} ` : '';
query = `${existingQuery}replay_id:${replayId}`;
@@ -32,7 +33,7 @@ export function OpenInLogsButton({searchTerm, replayId}: Props) {
});
return (
-
+
{t('Open in Logs')}
);
diff --git a/static/app/views/explore/replays/detail/ourlogs/ourlogFilters.spec.tsx b/static/app/views/explore/replays/detail/ourlogs/ourlogFilters.spec.tsx
new file mode 100644
index 00000000000000..72f740003a60c7
--- /dev/null
+++ b/static/app/views/explore/replays/detail/ourlogs/ourlogFilters.spec.tsx
@@ -0,0 +1,117 @@
+import {OrganizationFixture} from 'sentry-fixture/organization';
+
+import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
+
+import {PageFiltersStore} from 'sentry/components/pageFilters/store';
+import {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalyticsEvent';
+import {FieldKind} from 'sentry/utils/fields';
+import {useTraceItemSearchQueryBuilderProps} from 'sentry/views/explore/components/traceItemSearchQueryBuilder';
+import {LogsQueryParamsProvider} from 'sentry/views/explore/logs/logsQueryParamsProvider';
+import {OurLogFilters} from 'sentry/views/explore/replays/detail/ourlogs/ourlogFilters';
+import {TraceItemDataset} from 'sentry/views/explore/types';
+
+const baseSearchQueryBuilderProps = {
+ itemType: TraceItemDataset.LOGS as TraceItemDataset.LOGS,
+ booleanAttributes: {},
+ booleanSecondaryAliases: {},
+ numberAttributes: {},
+ numberSecondaryAliases: {},
+ stringAttributes: {
+ 'log.message': {key: 'log.message', name: 'log.message', kind: FieldKind.TAG},
+ },
+ stringSecondaryAliases: {},
+ initialQuery: '',
+ searchSource: 'ourlogs' as const,
+ onSearch: jest.fn(),
+};
+
+function Wrapper({children}: {children: React.ReactNode}) {
+ return (
+
+ {children}
+
+ );
+}
+
+function TestOurLogFilters() {
+ const searchQueryBuilderProviderProps = useTraceItemSearchQueryBuilderProps(
+ baseSearchQueryBuilderProps
+ );
+ return (
+
+ );
+}
+
+describe('OurLogFilters', () => {
+ beforeEach(() => {
+ PageFiltersStore.init();
+ PageFiltersStore.onInitializeUrlState({
+ projects: [1],
+ environments: [],
+ datetime: {period: '14d', start: null, end: null, utc: false},
+ });
+ MockApiClient.clearMockResponses();
+ MockApiClient.addMockResponse({
+ url: '/organizations/org-slug/trace-items/attributes/',
+ body: [],
+ });
+ MockApiClient.addMockResponse({
+ url: '/organizations/org-slug/trace-items/attributes/validate/',
+ method: 'POST',
+ body: {attributes: {}},
+ });
+ MockApiClient.addMockResponse({
+ url: '/organizations/org-slug/recent-searches/',
+ body: [],
+ });
+ });
+
+ it('renders with replay-specific placeholder text', async () => {
+ const organization = OrganizationFixture({features: ['visibility-explore-view']});
+
+ render(
+
+
+ ,
+ {organization}
+ );
+
+ expect(
+ await screen.findByPlaceholderText('Search on log levels, messages, and more')
+ ).toBeInTheDocument();
+ });
+
+ it('renders the Open in Logs button when explore feature is enabled', async () => {
+ const organization = OrganizationFixture({features: ['visibility-explore-view']});
+
+ render(
+
+
+ ,
+ {organization}
+ );
+
+ expect(await screen.findByRole('button', {name: 'Open in Logs'})).toBeInTheDocument();
+ });
+
+ it('does not render the Open in Logs button when explore feature is disabled', async () => {
+ const organization = OrganizationFixture({features: []});
+
+ render(
+
+
+ ,
+ {organization}
+ );
+
+ await waitFor(() =>
+ expect(screen.queryByRole('button', {name: 'Open in Logs'})).not.toBeInTheDocument()
+ );
+ });
+});
diff --git a/static/app/views/explore/replays/detail/ourlogs/ourlogFilters.tsx b/static/app/views/explore/replays/detail/ourlogs/ourlogFilters.tsx
index f74fef7ef9dcaf..a72cb61a397e44 100644
--- a/static/app/views/explore/replays/detail/ourlogs/ourlogFilters.tsx
+++ b/static/app/views/explore/replays/detail/ourlogs/ourlogFilters.tsx
@@ -1,58 +1,41 @@
import styled from '@emotion/styled';
-import {CompactSelect} from '@sentry/scraps/compactSelect';
-import {OverlayTrigger} from '@sentry/scraps/overlayTrigger';
-
-import {SearchBar} from 'sentry/components/searchBar';
+import {SearchQueryBuilderProvider} from 'sentry/components/searchQueryBuilder/context';
import {t} from 'sentry/locale';
-import type {OurLogsResponseItem} from 'sentry/views/explore/logs/types';
+import {
+ TraceItemSearchQueryBuilder,
+ type TraceItemSearchQueryBuilderProps,
+ type useTraceItemSearchQueryBuilderProps,
+} from 'sentry/views/explore/components/traceItemSearchQueryBuilder';
import {FiltersGrid} from 'sentry/views/explore/replays/detail/filtersGrid';
import {OpenInLogsButton} from 'sentry/views/explore/replays/detail/ourlogs/openInLogsButton';
-import {type useOurLogFilters} from 'sentry/views/explore/replays/detail/ourlogs/useOurLogFilters';
type Props = {
- logItems: OurLogsResponseItem[];
+ searchQueryBuilderProps: TraceItemSearchQueryBuilderProps;
+ searchQueryBuilderProviderProps: ReturnType;
replayId?: string;
-} & ReturnType;
+};
+
+const REPLAY_LOGS_PLACEHOLDER = t('Search on log levels, messages, and more');
export function OurLogFilters({
- logItems,
replayId,
- getSeverityLevels,
- searchTerm,
- selectValues,
- setSeverityLevel,
- setSearchTerm,
+ searchQueryBuilderProps,
+ searchQueryBuilderProviderProps,
}: Props) {
- const severityLevels = getSeverityLevels();
-
return (
-
- (
-
- {selectValues.length === 0 ? t('Any') : triggerProps.children}
-
- )}
- multiple
- options={severityLevels}
- onChange={setSeverityLevel}
- size="sm"
- value={selectValues.map(v => v.value)}
- disabled={!severityLevels.length}
- />
-
-
-
+
+
+
+
+
+
);
}
const StyledFiltersGrid = styled(FiltersGrid)`
- grid-template-columns: max-content 1fr min-content;
+ grid-template-columns: 1fr min-content;
`;
diff --git a/static/app/views/explore/replays/detail/ourlogs/useOurLogFilters.tsx b/static/app/views/explore/replays/detail/ourlogs/useOurLogFilters.tsx
deleted file mode 100644
index 6d5f73e7d2f2aa..00000000000000
--- a/static/app/views/explore/replays/detail/ourlogs/useOurLogFilters.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-import {useCallback, useMemo} from 'react';
-import debounce from 'lodash/debounce';
-
-import type {SelectOption} from '@sentry/scraps/compactSelect';
-
-import {DEFAULT_DEBOUNCE_DURATION} from 'sentry/constants';
-import {decodeList, decodeScalar} from 'sentry/utils/queryString';
-import {useFiltersInLocationQuery} from 'sentry/utils/replays/hooks/useFiltersInLocationQuery';
-import {capitalize} from 'sentry/utils/string/capitalize';
-import type {OurLogsResponseItem} from 'sentry/views/explore/logs/types';
-import {OurLogKnownFieldKey} from 'sentry/views/explore/logs/types';
-import {getLogSeverityLevel} from 'sentry/views/explore/logs/utils';
-
-type Options = {
- logItems: OurLogsResponseItem[];
-};
-
-type Return = {
- getSeverityLevels: () => Array>;
- items: OurLogsResponseItem[];
- searchTerm: string;
- selectValues: Array>;
- setSearchTerm: (searchTerm: string) => void;
- setSeverityLevel: (val: Array>) => void;
-};
-
-const FILTERS = {
- severity: (item: OurLogsResponseItem, severities: string[]) => {
- if (severities.length === 0) {
- return true;
- }
-
- const severityText = item[OurLogKnownFieldKey.SEVERITY] as string | null;
- const severityNumber = item[OurLogKnownFieldKey.SEVERITY_NUMBER] as number | null;
- const level = getLogSeverityLevel(severityNumber, severityText);
-
- return severities.includes(level);
- },
-
- searchTerm: (item: OurLogsResponseItem, searchTerm: string) => {
- const message = item[OurLogKnownFieldKey.MESSAGE];
- return message?.toLowerCase().includes(searchTerm.toLowerCase()) ?? false;
- },
-};
-
-function filterItems(options: {
- filterFns: Record boolean>;
- filterVals: Record;
- items: T[];
-}): T[] {
- const {items, filterFns, filterVals} = options;
-
- return items.filter(item => {
- return Object.entries(filterFns).every(([key, filterFn]) => {
- const filterValue = filterVals[key];
- return filterFn(item, filterValue);
- });
- });
-}
-
-export function useOurLogFilters({logItems}: Options): Return {
- const {setFilter, query} = useFiltersInLocationQuery();
-
- const severityValues = useMemo(
- () => decodeList(query.f_ol_severity),
- [query.f_ol_severity]
- );
- const searchTerm = decodeScalar(query.f_ol_search, '').toLowerCase();
-
- const items = useMemo(
- () =>
- filterItems({
- items: logItems,
- filterFns: FILTERS,
- filterVals: {severity: severityValues, searchTerm},
- }),
- [logItems, severityValues, searchTerm]
- );
-
- const getSeverityLevels = useCallback(() => {
- const severityLevels = Array.from(
- new Set(
- logItems.map(item => {
- const severityText = item[OurLogKnownFieldKey.SEVERITY] as string | null;
- const severityNumber = item[OurLogKnownFieldKey.SEVERITY_NUMBER] as
- | number
- | null;
- return getLogSeverityLevel(severityNumber, severityText);
- })
- )
- ).sort();
-
- return severityLevels.map(
- (value): SelectOption => ({
- value,
- label: capitalize(value),
- })
- );
- }, [logItems]);
-
- const debouncedSetFilter = useMemo(
- () =>
- debounce((f_ol_search: string) => {
- setFilter({f_ol_search: f_ol_search || undefined});
- }, DEFAULT_DEBOUNCE_DURATION),
- [setFilter]
- );
-
- const setSearchTerm = useCallback(
- (f_ol_search: string) => debouncedSetFilter(f_ol_search),
- [debouncedSetFilter]
- );
-
- const setSeverityLevel = useCallback(
- (value: Array>) => {
- setFilter({f_ol_severity: value.map(v => v.value)});
- },
- [setFilter]
- );
-
- return {
- getSeverityLevels,
- items,
- searchTerm,
- selectValues: severityValues.map(s => ({value: s, label: s, qs: 'f_ol_severity'})),
- setSeverityLevel,
- setSearchTerm,
- };
-}