Skip to content

Commit

Permalink
Newswires UI: fix filter search by date issues
Browse files Browse the repository at this point in the history
  • Loading branch information
sb-dev committed Feb 26, 2025
1 parent 4f009c0 commit 86822e5
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 42 deletions.
22 changes: 20 additions & 2 deletions newswires/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '@elastic/eui';
import { css } from '@emotion/react';
import { useSearch } from './context/SearchContext.tsx';
import { isRestricted } from './dateMathHelpers.ts';
import { Feed } from './Feed';
import { Item } from './Item';
import { SideNav } from './SideNav';
Expand All @@ -31,7 +32,7 @@ export function App() {
handlePreviousItem,
} = useSearch();

const { view, itemId: selectedItemId } = config;
const { view, itemId: selectedItemId, query } = config;
const { status } = state;

const isPoppedOut = !!window.opener;
Expand Down Expand Up @@ -74,9 +75,26 @@ export function App() {
</p>
</EuiToast>
)}
{isRestricted(query.end) && (
<EuiToast
title="Restricted"
iconType="warning"
css={css`
border-radius: 0;
background: #fdf6d8;
position: fixed;
`}
>
<p>
Your current filter settings exclude recent updates. Adjust the
filter to see the latest data.
</p>
</EuiToast>
)}
<div
css={css`
${status === 'offline' && 'padding-top: 84px;'}
${(status === 'offline' || isRestricted(query.end)) &&
'padding-top: 84px;'}
height: 100%;
${(status === 'loading' || status === 'error') &&
'display: flex; align-items: center;'}
Expand Down
7 changes: 4 additions & 3 deletions newswires/client/src/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,14 @@ export const DatePicker = () => {
<div style={{ paddingTop: 20, paddingBottom: 20 }}>
<EuiSuperDatePicker
width={'auto'}
start={config.query.start}
end={config.query.end}
start={config.query.start ?? 'now/d'}
end={config.query.end ?? 'now'}
minDate={minDate}
maxDate={maxDate}
onTimeChange={onTimeChange}
showUpdateButton={true}
updateButtonProps={{ showTooltip: true, iconOnly: true }}
customQuickSelectRender={customQuickSelectRender}
dateFormat={'MMM D • HH:mm'}
commonlyUsedRanges={[
timeRangeOption('30m'),
timeRangeOption('1h'),
Expand Down
48 changes: 30 additions & 18 deletions newswires/client/src/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,32 +24,44 @@ export const Feed = () => {
)}
{(status == 'success' || status == 'offline') &&
queryData.results.length === 0 && (
<EuiEmptyPrompt
body={
<>
<SearchSummary />
<p>Try another search or reset filters.</p>
</>
}
color="subdued"
layout="horizontal"
title={<h2>No results match your search criteria</h2>}
titleSize="s"
/>
)}
{(status == 'success' || status == 'offline') &&
queryData.results.length > 0 && (
<>
<EuiFlexGroup>
<EuiFlexItem
style={{ flex: 1, paddingTop: 20, paddingBottom: 20 }}
>
<SearchSummary />
</EuiFlexItem>
></EuiFlexItem>
<EuiFlexItem grow={false}>
<DatePicker />
</EuiFlexItem>
</EuiFlexGroup>
<EuiEmptyPrompt
body={
<>
<SearchSummary />
<p>Try another search or reset filters.</p>
</>
}
color="subdued"
layout="horizontal"
title={<h2>No results match your search criteria</h2>}
titleSize="s"
/>
</>
)}
{(status == 'success' || status == 'offline') &&
queryData.results.length > 0 && (
<>
<div>
<EuiFlexGroup>
<EuiFlexItem
style={{ flex: 1, paddingTop: 20, paddingBottom: 20 }}
>
<SearchSummary />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<DatePicker />
</EuiFlexItem>
</EuiFlexGroup>
</div>

<WireItemList
wires={queryData.results}
Expand Down
11 changes: 9 additions & 2 deletions newswires/client/src/SearchSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { EuiBadge, EuiText } from '@elastic/eui';
import { css } from '@emotion/react';
import { useEffect, useState } from 'react';
import { useSearch } from './context/SearchContext.tsx';
import { deriveDateMathRangeLabel } from './dateMathHelpers.ts';

const Summary = ({ searchSummary }: { searchSummary: string }) => {
const { config, handleEnterQuery } = useSearch();
const { q, bucket, subjects, supplier: suppliers } = config.query;
const { q, bucket, subjects, supplier: suppliers, start, end } = config.query;

const displaySubjects = (subjects ?? []).length > 0;
const displaySuppliers = (suppliers ?? []).length > 0;
Expand All @@ -20,6 +21,8 @@ const Summary = ({ searchSummary }: { searchSummary: string }) => {
handleEnterQuery({
...config.query,
q: label === 'Search term' ? '' : config.query.q,
start: label === 'Time range' ? undefined : config.query.start,
end: label === 'Time range' ? undefined : config.query.end,
bucket: label === 'Bucket' ? undefined : config.query.bucket,
supplier:
label === 'Supplier'
Expand All @@ -43,7 +46,8 @@ const Summary = ({ searchSummary }: { searchSummary: string }) => {
handleBadgeClick(label, value);
}}
>
<strong>{label}</strong>: {value}
<strong>{label}</strong>
{value !== '' ? `: ${value}` : ''}
</EuiBadge>
) : null;

Expand All @@ -53,6 +57,9 @@ const Summary = ({ searchSummary }: { searchSummary: string }) => {
{searchSummary}
{displayFilters && ' for: '}
</span>
{start &&
end &&
renderBadge('Time range', deriveDateMathRangeLabel(start, end))}
{q && renderBadge('Search term', q)}
{bucket && renderBadge('Bucket', bucket)}
{displaySuppliers &&
Expand Down
11 changes: 9 additions & 2 deletions newswires/client/src/context/SearchContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ const ActionSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('SELECT_ITEM'), item: z.string().optional() }),
z.object({
type: z.literal('UPDATE_RESULTS'),
query: QuerySchema,
data: WiresQueryResponseSchema,
}),
z.object({ type: z.literal('TOGGLE_AUTO_UPDATE') }),
Expand Down Expand Up @@ -193,7 +194,11 @@ export function SearchContextProvider({ children }: PropsWithChildren) {
fetchResults(currentConfig.query, { sinceId }, abortController)
.then((data) => {
if (!abortController.signal.aborted) {
dispatch({ type: 'UPDATE_RESULTS', data });
dispatch({
type: 'UPDATE_RESULTS',
data,
query: currentConfig.query,
});
}
})
.catch(handleFetchError);
Expand All @@ -215,7 +220,9 @@ export function SearchContextProvider({ children }: PropsWithChildren) {
]);

const handleEnterQuery = (query: Query) => {
dispatch({ type: 'ENTER_QUERY' });
dispatch({
type: 'ENTER_QUERY',
});

if (currentConfig.view === 'item') {
pushConfigState({
Expand Down
83 changes: 83 additions & 0 deletions newswires/client/src/context/SearchReducer.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import dateMath from '@elastic/datemath';
import moment from 'moment/moment';
import { sampleWireData } from '../tests/fixtures/wireData.ts';
import type { Action, State } from './SearchContext.tsx';
import { SearchReducer } from './SearchReducer';

jest.mock('@elastic/datemath', () => ({
__esModule: true,
default: {
parse: jest.fn(),
},
}));

describe('SearchReducer', () => {
const initialState: State = {
status: 'loading',
Expand Down Expand Up @@ -80,6 +89,7 @@ describe('SearchReducer', () => {
const action: Action = {
type: 'UPDATE_RESULTS',
data: { results: [{ ...sampleWireData, id: 2 }], totalCount: 1 },
query: { q: 'test' },
};

const newState = SearchReducer(state, action);
Expand All @@ -99,6 +109,79 @@ describe('SearchReducer', () => {
});
});

it(`should filter storing when handling UPDATE_RESULTS action in success state`, () => {
const state: State = {
...successState,
queryData: {
results: [
{ ...sampleWireData, id: 1, ingestedAt: '2025-01-01T02:00:00Z' },
{ ...sampleWireData, id: 2, ingestedAt: '2025-01-01T02:05:00Z' },
],
totalCount: 2,
},
};

(dateMath.parse as jest.Mock).mockImplementation(() =>
moment('2025-01-01T02:04:00Z'),
);

const action: Action = {
type: 'UPDATE_RESULTS',
data: {
results: [
{ ...sampleWireData, id: 4, ingestedAt: '2025-01-01T02:07:00Z' },
{ ...sampleWireData, id: 3, ingestedAt: '2025-01-01T02:06:00Z' },
],
totalCount: 2,
},
query: { q: 'test', start: 'now-30' },
};

expect(state.queryData.results).toContainEqual({
...sampleWireData,
id: 2,
ingestedAt: '2025-01-01T02:05:00Z',
});

expect(state.queryData.results).toContainEqual({
...sampleWireData,
id: 1,
ingestedAt: '2025-01-01T02:00:00Z',
});

const newState = SearchReducer(state, action);

expect(newState.status).toBe('success');
expect(newState.queryData?.results).toHaveLength(3);
expect(newState.queryData?.totalCount).toBe(3);

expect(newState.queryData?.results).toContainEqual({
...sampleWireData,
id: 4,
ingestedAt: '2025-01-01T02:07:00Z',
isFromRefresh: true,
});

expect(newState.queryData?.results).toContainEqual({
...sampleWireData,
id: 3,
ingestedAt: '2025-01-01T02:06:00Z',
isFromRefresh: true,
});

expect(newState.queryData?.results).toContainEqual({
...sampleWireData,
id: 2,
ingestedAt: '2025-01-01T02:05:00Z',
});

expect(newState.queryData?.results).not.toContainEqual({
...sampleWireData,
id: 1,
ingestedAt: '2025-01-01T02:00:00Z',
});
});

it(`should handle APPEND_RESULTS action in success state`, () => {
const state: State = {
...successState,
Expand Down
44 changes: 34 additions & 10 deletions newswires/client/src/context/SearchReducer.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,42 @@
import dateMath from '@elastic/datemath';
import { isEqual as deepIsEqual } from 'lodash';
import moment from 'moment';
import type { Query, WiresQueryResponse } from '../sharedTypes.ts';
import { defaultQuery } from '../urlState.ts';
import type { Action, SearchHistory, State } from './SearchContext.tsx';

function mergeQueryData(
existing: WiresQueryResponse | undefined,
newData: WiresQueryResponse,
{ start }: Query,
): WiresQueryResponse {
const parsePostgresTimestamp = (timestamp: string) =>
moment(timestamp.replace(/\[.*]$/, ''));

if (existing) {
const existingIds = new Set(existing.results.map((item) => item.id));

const mergedResults = [
...newData.results
.filter((newItem) => !existingIds.has(newItem.id))
.map((newItem) => ({ ...newItem, isFromRefresh: true })),
...existing.results,
];
const filteredExistingResults =
start !== undefined
? existing.results.filter((existingItem) => {
return parsePostgresTimestamp(
existingItem.ingestedAt,
).isSameOrAfter(dateMath.parse(start));
})
: existing.results;

const filteredOutCount =
existing.results.length - filteredExistingResults.length;

return {
...newData,
totalCount: existing.totalCount + newData.totalCount,
results: mergedResults,
totalCount: existing.totalCount + newData.totalCount - filteredOutCount,
results: [
...newData.results
.filter((newItem) => !existingIds.has(newItem.id))
.map((newItem) => ({ ...newItem, isFromRefresh: true })),
...filteredExistingResults,
],
};
} else {
return {
Expand Down Expand Up @@ -89,14 +105,22 @@ export const SearchReducer = (state: State, action: Action): State => {
case 'success':
return {
...state,
queryData: mergeQueryData(state.queryData, action.data),
queryData: mergeQueryData(
state.queryData,
action.data,
action.query,
),
};
case 'offline':
case 'error':
return {
...state,
status: 'success',
queryData: mergeQueryData(state.queryData, action.data),
queryData: mergeQueryData(
state.queryData,
action.data,
action.query,
),
};
default:
return state;
Expand Down
Loading

0 comments on commit 86822e5

Please sign in to comment.