diff --git a/CHANGELOG.md b/CHANGELOG.md index fc275e6..a9c0ca7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Include full query options when generating download/share/preview links, [PR-160](https://github.com/reductstore/web-console/pull/160) +- Fix the inconsistency in the time range selection with an empty end, [PR-162](https://github.com/reductstore/web-console/pull/162) ## 1.12.1 - 2025-11-17 diff --git a/src/Views/BucketPanel/EntryDetail.test.tsx b/src/Views/BucketPanel/EntryDetail.test.tsx index 5da9bee..b1c974f 100644 --- a/src/Views/BucketPanel/EntryDetail.test.tsx +++ b/src/Views/BucketPanel/EntryDetail.test.tsx @@ -782,4 +782,81 @@ describe("EntryDetail", () => { ); }); }); + + describe("Time Range Input", () => { + beforeEach(async () => { + await act(async () => { + jest.runOnlyPendingTimers(); + await waitUntil( + () => wrapper.update().find(".ant-table-row").length > 0, + ); + }); + }); + + it("should query with undefined end time when stop field is empty", async () => { + (bucket.query as jest.Mock).mockClear(); + + const timeInputs = wrapper.find(".timeInputs Input"); + const stopInput = timeInputs.at(1); + + await act(async () => { + const onChange = stopInput.prop("onChange") as any; + if (onChange) { + onChange({ target: { value: "" } }); + } + }); + wrapper.update(); + + const fetchButton = wrapper.find(".fetchButton button").at(0); + await act(async () => { + fetchButton.simulate("click"); + jest.runAllTimers(); + }); + + expect(bucket.query).toHaveBeenCalledWith( + "testEntry", + 0n, + undefined, + expect.objectContaining({ + head: true, + strict: true, + }), + ); + }); + + it("should query with specific end time when stop field has value", async () => { + (bucket.query as jest.Mock).mockClear(); + + const timeInputs = wrapper.find(".timeInputs Input"); + const stopInput = timeInputs.at(1); + + const specificTime = "1970-01-01T00:00:01.000Z"; + await act(async () => { + const onChange = stopInput.prop("onChange") as any; + if (onChange) { + onChange({ target: { value: specificTime } }); + } + }); + wrapper.update(); + + const fetchButton = wrapper.find(".fetchButton button").at(0); + await act(async () => { + fetchButton.simulate("click"); + jest.runAllTimers(); + }); + + expect(bucket.query).toHaveBeenCalled(); + const [[entry, startTime, endTime, options]] = (bucket.query as jest.Mock) + .mock.calls; + + expect(entry).toBe("testEntry"); + expect(startTime).toBe(0n); + expect(endTime).toBe(1000000n); + + expect(options).toMatchObject({ + head: true, + strict: true, + }); + }); + }); }); diff --git a/src/Views/BucketPanel/EntryDetail.tsx b/src/Views/BucketPanel/EntryDetail.tsx index 6a46a84..623129b 100644 --- a/src/Views/BucketPanel/EntryDetail.tsx +++ b/src/Views/BucketPanel/EntryDetail.tsx @@ -69,8 +69,8 @@ interface Props { } interface RecordQueryContext { - rangeStart?: bigint; - rangeEnd?: bigint; + start?: bigint; + end?: bigint; options?: QueryOptions; } @@ -226,14 +226,13 @@ export default function EntryDetail(props: Readonly) { const bucketInstance = await props.client.getBucket(bucketName); setBucket(bucketInstance); - const rangeStart = start ?? entryInfo?.oldestRecord; - const rangeEnd = end ?? entryInfo?.latestRecord; - const options = new QueryOptions(); options.head = true; options.strict = true; if (whenCondition.trim()) { + const rangeStart = start ?? entryInfo?.oldestRecord; + const rangeEnd = end ?? entryInfo?.latestRecord; const macroValue = pickEachTInterval(rangeStart, rangeEnd); const conditionResult = processConditionWithMacros( whenCondition, @@ -258,8 +257,8 @@ export default function EntryDetail(props: Readonly) { } setQueryContext({ - rangeStart, - rangeEnd, + start, + end, options, }); @@ -268,8 +267,8 @@ export default function EntryDetail(props: Readonly) { for await (const record of bucketInstance.query( entryName, - rangeStart, - rangeEnd, + start, + end, options, )) { if (abortSignal.aborted) return; @@ -315,8 +314,8 @@ export default function EntryDetail(props: Readonly) { const expireAt = new Date(Date.now() + 60 * 60 * 1000); const shareLink = await bucket.createQueryLink( entryName, - queryContext?.rangeStart, - queryContext?.rangeEnd, + queryContext?.start, + queryContext?.end, buildLinkQueryOptions(queryContext?.options), row.tableIndex, expireAt, @@ -353,8 +352,8 @@ export default function EntryDetail(props: Readonly) { const bucket = await props.client.getBucket(bucketName); return bucket.createQueryLink( entryName, - queryContext?.rangeStart, - queryContext?.rangeEnd, + queryContext?.start, + queryContext?.end, buildLinkQueryOptions(queryContext?.options), recordToShare?.tableIndex, expireAt, @@ -871,8 +870,8 @@ export default function EntryDetail(props: Readonly) { timestamp={row.timestamp} bucket={bucket} apiUrl={props.apiUrl} - queryStart={queryContext?.rangeStart} - queryEnd={queryContext?.rangeEnd} + queryStart={queryContext?.start} + queryEnd={queryContext?.end} queryOptions={buildLinkQueryOptions(queryContext?.options)} recordIndex={queryContext ? row.tableIndex : 0} />