Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
af486b6
feat: date-range filter for knowledge table file list
Oct 7, 2026
dba1a05
style: ruff autofix (auto)
autofix-ci[bot] Oct 7, 2026
1b7a222
coderabbit + react suggestions
Oct 7, 2026
d154315
other coderabbit suggestion
Oct 7, 2026
4168335
Merge remote-tracking branch 'origin/main' into feat/date-range-filter
Oct 8, 2026
66bcf84
coderabbit suggestions
Oct 8, 2026
fb367cc
fix frontend testing
Oct 8, 2026
ac6daba
calendar range fixing
Oct 8, 2026
6edeee2
pin date in calendar range test
Oct 8, 2026
a3a24c2
Merge remote-tracking branch 'origin/main' into feat/date-range-filter
Oct 8, 2026
5a4f991
pass date range to search API server-side instead of filtering on cli…
Oct 8, 2026
0e0479c
style: ruff autofix (auto)
autofix-ci[bot] Oct 8, 2026
7afb031
fix lint backend
Oct 8, 2026
37cc587
remove unused imports
Oct 8, 2026
ea3fe7e
remove duplicate type annotation
Oct 8, 2026
87eb787
Merge branch 'main' into feat/date-range-filter
VisveshJ Oct 9, 2026
844d05b
Merge branch 'main' into feat/date-range-filter
VisveshJ Oct 12, 2026
20256b8
chore: merge main into feat/date-range-filter
Oct 12, 2026
7ba998d
feat: validation, SDK parity, timezone docs, narrow-view fix
Oct 12, 2026
24dd740
Merge branch 'main' into feat/date-range-filter
VisveshJ Oct 13, 2026
70f8b5c
Merge remote-tracking branch 'origin/main' into feat/date-range-filter
Oct 13, 2026
2037d5d
feat: redesign date range filter with quick ranges + upload density f…
Oct 13, 2026
0f759b7
fix: inclusive date-bound docs and date filtering for file search
Oct 14, 2026
0df5d6b
Merge remote-tracking branch 'origin/main' into feat/date-range-filter
Oct 14, 2026
907f9b1
feat: show the knowledge date range filter on OSS only
Oct 14, 2026
5956e7c
Merge branch 'main' into feat/date-range-filter
VisveshJ Oct 15, 2026
4e4b437
Merge branch 'main' into feat/date-range-filter
VisveshJ Oct 19, 2026
708151d
Merge branch 'main' into feat/date-range-filter
VisveshJ Oct 20, 2026
1dd388f
test: replace useUploadHistogram module mock with MSW handler
Oct 20, 2026
8960291
feat: add custom last-N-days input to date range filter
Oct 20, 2026
6a1c3c5
coderabbit suggestions
Oct 20, 2026
95ccb34
Merge branch 'main' into feat/date-range-filter
VisveshJ Oct 20, 2026
7583a94
fix frontend testing
Oct 20, 2026
63de567
fix: apply date range to task rows and refresh today across midnight
Oct 20, 2026
671e050
coderabbit + github actions
Oct 20, 2026
525f9e1
fixing review comments
Oct 21, 2026
f148849
fix: make date range picker responsive with different widths/formats
Oct 21, 2026
0064907
fix potential bug with duplicates appearing
Oct 21, 2026
fe447be
Merge remote-tracking branch 'origin/main' into feat/date-range-filter
Oct 21, 2026
20a30a6
Merge branch 'main' into feat/date-range-filter
VisveshJ Oct 22, 2026
f58e38c
Merge remote-tracking branch 'origin/main' into feat/date-range-filter
Oct 22, 2026
a4e9572
Merge branch 'main' into feat/date-range-filter
VisveshJ Oct 26, 2026
5b5846f
feat: save date range in knowledge filters and enforce it in Langflow…
Oct 26, 2026
1aaed3c
Merge remote-tracking branch 'origin/main' into feat/date-range-filter
Oct 26, 2026
fd46e4a
Merge branch 'feat/date-range-filter' of https://github.com/langflow-…
Oct 26, 2026
a099dc9
github comments
Oct 26, 2026
8f9212b
fix: ASCII-only calendar days and longer timeout for pagination test
Oct 26, 2026
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
24 changes: 23 additions & 1 deletion custom_components/openrag/opensearch_multimodal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1777,6 +1777,21 @@ def _is_placeholder_term(self, term_obj: dict) -> bool:
# term_obj like {"filename": "__IMPOSSIBLE_VALUE__"}
return any(v == "__IMPOSSIBLE_VALUE__" for v in term_obj.values())

@staticmethod
def _is_valid_range_clause(clause: Any) -> bool:
"""Return True for a single-field range clause with at least one bound."""
if not isinstance(clause, dict) or not isinstance(clause.get("range"), dict):
return False
range_obj = clause["range"]
if len(range_obj) != 1:
return False
bounds = next(iter(range_obj.values()))
if not isinstance(bounds, dict):
return False
return any(
key in bounds and bounds[key] not in (None, "") for key in ("gte", "gt", "lte", "lt")
)

def _coerce_filter_clauses(self, filter_obj: dict | None) -> list[dict]:
"""Convert filter expressions into OpenSearch-compatible filter clauses.

Expand All @@ -1794,7 +1809,7 @@ def _coerce_filter_clauses(self, filter_obj: dict | None) -> list[dict]:
filter_obj: Filter configuration dictionary or None

Returns:
List of OpenSearch filter clauses (term/terms objects)
List of OpenSearch filter clauses (term/terms/range objects)
Placeholder values with "__IMPOSSIBLE_VALUE__" are ignored
"""
if not filter_obj:
Expand Down Expand Up @@ -1825,6 +1840,8 @@ def _coerce_filter_clauses(self, filter_obj: dict | None) -> list[dict]:
field, vals = next(iter(f["terms"].items()))
if isinstance(vals, list) and len(vals) > 0:
explicit_clauses.append(f)
elif self._is_valid_range_clause(f):
explicit_clauses.append(f)
return explicit_clauses

# Case B: convert context-style maps into clauses
Expand Down Expand Up @@ -2368,6 +2385,11 @@ def search(self, query: str | None = None) -> list[dict[str, Any]]:
}
}
}
# Filter inside each kNN clause so the nearest neighbours are chosen
# from matching documents; the outer bool filter alone would only
# post-filter the global top-k and miss narrow ranges.
if filter_clauses:
base_query["knn"][selected_field]["filter"] = {"bool": {"filter": filter_clauses}}

if use_num_candidates:
query_with_candidates = copy.deepcopy(base_query)
Expand Down
4 changes: 2 additions & 2 deletions flows/ingestion_flow.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions flows/openrag_agent.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions flows/openrag_nudges.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions flows/openrag_url_mcp.json

Large diffs are not rendered by default.

52 changes: 48 additions & 4 deletions frontend/app/api/queries/useGetSearchQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,17 @@ function serveSearch(body: Record<string, unknown>, status = 200) {
return seen;
}

async function runSearch(query: string, data?: ParsedQueryData | null) {
const rendered = renderHook(() => useGetSearchQuery(query, data), {
wrapper: createQueryWrapper(),
});
async function runSearch(
query: string,
data?: ParsedQueryData | null,
dateRange?: { createdAfter?: string; createdBefore?: string },
) {
const rendered = renderHook(
() => useGetSearchQuery(query, data, undefined, dateRange),
{
wrapper: createQueryWrapper(),
},
);
await waitFor(() =>
expect(
rendered.result.current.isSuccess || rendered.result.current.isError,
Expand Down Expand Up @@ -389,4 +396,41 @@ describe("useGetSearchQuery", () => {
expect(calls).toBe(1);
});
});

describe("date bounds", () => {
const AFTER = "2026-10-07T07:00:00.000Z";
const BEFORE = "2026-10-11T06:59:59.999999Z";
const saved = () =>
queryData({
filters: {
data_sources: [],
document_types: ["application/pdf"],
owners: [],
connector_types: [],
created_after: AFTER,
created_before: BEFORE,
},
});

it("sends the effective bounds beside the PDF filter", async () => {
const seen = serveSearch({ results: [] });
await runSearch("term", saved(), {
createdAfter: AFTER,
createdBefore: BEFORE,
});
expect(seen.payload?.filters).toEqual({
document_types: ["application/pdf"],
});
expect(seen.payload?.created_after).toBe(AFTER);
expect(seen.payload?.created_before).toBe(BEFORE);
});

it("does not fall back to saved dates when the range was cleared", async () => {
const seen = serveSearch({ results: [] });
await runSearch("term", saved(), {});
expect(seen.payload?.created_after).toBeUndefined();
expect(seen.payload?.created_before).toBeUndefined();
expect(JSON.stringify(seen.payload)).not.toContain("created_");
});
});
});
22 changes: 20 additions & 2 deletions frontend/app/api/queries/useGetSearchQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export interface SearchPayload {
owners?: string[];
connector_types?: string[];
};
created_after?: string;
created_before?: string;
}

export interface ChunkResult {
Expand Down Expand Up @@ -46,6 +48,7 @@ export interface ChunkResult {
index?: number;
allowed_users?: string[];
allowed_groups?: string[];
indexed_time?: string;
}

export interface File {
Expand Down Expand Up @@ -82,6 +85,7 @@ export interface File {
chunks?: ChunkResult[];
allowed_users?: string[];
allowed_groups?: string[];
indexed_time?: string;
}

// Non-fatal signal from the backend — e.g. an embedding provider was removed
Expand Down Expand Up @@ -124,6 +128,9 @@ export const useGetSearchQuery = (
UseQueryOptions<SearchResult, Error, SearchResult, unknown[]>,
"queryKey" | "queryFn"
>,
// Effective UTC bounds from the caller. Saved dates inside `queryData` are
// deliberately not sent so a cleared temporary range cannot fall back to them.
dateRange?: { createdAfter?: string; createdBefore?: string },
) => {
const queryClient = useQueryClient();

Expand Down Expand Up @@ -159,7 +166,15 @@ export const useGetSearchQuery = (
};
if (queryData?.filters) {
searchPayload.filters =
buildSearchPayloadFilters(queryData.filters) ?? undefined;
buildSearchPayloadFilters(queryData.filters, {
includeDates: false,
}) ?? undefined;
}
if (dateRange?.createdAfter) {
searchPayload.created_after = dateRange.createdAfter;
}
if (dateRange?.createdBefore) {
searchPayload.created_before = dateRange.createdBefore;
}

const response = await apiClient.post(`/search`, searchPayload);
Expand Down Expand Up @@ -193,6 +208,7 @@ export const useGetSearchQuery = (
embedding_dimensions?: number;
allowed_users?: string[];
allowed_groups?: string[];
indexed_time?: string;
}
>();

Expand Down Expand Up @@ -233,6 +249,7 @@ export const useGetSearchQuery = (
embedding_dimensions: chunk.embedding_dimensions,
allowed_users: chunk.allowed_users || [],
allowed_groups: chunk.allowed_groups || [],
indexed_time: chunk.indexed_time,
});
}
});
Expand All @@ -253,6 +270,7 @@ export const useGetSearchQuery = (
chunks: file.chunks,
allowed_users: file.allowed_users || [],
allowed_groups: file.allowed_groups || [],
indexed_time: file.indexed_time,
}));

const warnings: SearchWarning[] = Array.isArray(data.warnings)
Expand All @@ -272,7 +290,7 @@ export const useGetSearchQuery = (

return useQuery(
{
queryKey: ["search", queryData, query],
queryKey: ["search", queryData, query, dateRange],
placeholderData: (prev) => prev,
staleTime: 0,
queryFn: getFiles,
Expand Down
162 changes: 162 additions & 0 deletions frontend/app/api/queries/useListFiles.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* Tests for useListFiles.
*
* Verifies URL parameter serialization (including the new createdAfter /
* createdBefore date-range params), response parsing, and error handling.
*/
import { renderHook, waitFor } from "@testing-library/react";
import { HttpResponse, http } from "msw";
import { describe, expect, it } from "vitest";
import { server } from "@/test-utils/msw/server";
import { createQueryWrapper } from "@/test-utils/render";
import { useListFiles } from "./useListFiles";

function makeFileResponse(overrides: Record<string, unknown> = {}) {
return {
files: [],
total: 0,
is_approximate: false,
page: 1,
page_size: 25,
after_key: null,
...overrides,
};
}

describe("useListFiles", () => {
it("sends created_after param when createdAfter is provided", async () => {
let capturedUrl: string | undefined;
server.use(
http.get("/api/files", ({ request }) => {
capturedUrl = request.url;
return HttpResponse.json(makeFileResponse());
}),
);

const { result } = renderHook(
() =>
useListFiles({
createdAfter: "2024-01-01T00:00:00.000Z",
}),
{ wrapper: createQueryWrapper() },
);

await waitFor(() => expect(result.current.isSuccess).toBe(true));

const url = new URL(capturedUrl!);
expect(url.searchParams.get("created_after")).toBe(
"2024-01-01T00:00:00.000Z",
);
});

it("sends created_before param when createdBefore is provided", async () => {
let capturedUrl: string | undefined;
server.use(
http.get("/api/files", ({ request }) => {
capturedUrl = request.url;
return HttpResponse.json(makeFileResponse());
}),
);

const { result } = renderHook(
() =>
useListFiles({
createdBefore: "2024-01-31T23:59:59.999Z",
}),
{ wrapper: createQueryWrapper() },
);

await waitFor(() => expect(result.current.isSuccess).toBe(true));

const url = new URL(capturedUrl!);
expect(url.searchParams.get("created_before")).toBe(
"2024-01-31T23:59:59.999Z",
);
});

it("sends both date params together", async () => {
let capturedUrl: string | undefined;
server.use(
http.get("/api/files", ({ request }) => {
capturedUrl = request.url;
return HttpResponse.json(makeFileResponse());
}),
);

const { result } = renderHook(
() =>
useListFiles({
createdAfter: "2024-03-01T00:00:00.000Z",
createdBefore: "2024-03-31T23:59:59.999Z",
}),
{ wrapper: createQueryWrapper() },
);

await waitFor(() => expect(result.current.isSuccess).toBe(true));

const url = new URL(capturedUrl!);
expect(url.searchParams.get("created_after")).toBe(
"2024-03-01T00:00:00.000Z",
);
expect(url.searchParams.get("created_before")).toBe(
"2024-03-31T23:59:59.999Z",
);
});

it("omits date params when not provided", async () => {
let capturedUrl: string | undefined;
server.use(
http.get("/api/files", ({ request }) => {
capturedUrl = request.url;
return HttpResponse.json(makeFileResponse());
}),
);

const { result } = renderHook(() => useListFiles({}), {
wrapper: createQueryWrapper(),
});

await waitFor(() => expect(result.current.isSuccess).toBe(true));

const url = new URL(capturedUrl!);
expect(url.searchParams.has("created_after")).toBe(false);
expect(url.searchParams.has("created_before")).toBe(false);
});

it("maps indexed_time from the response", async () => {
server.use(
http.get("/api/files", () =>
HttpResponse.json(
makeFileResponse({
files: [
{
filename: "doc.pdf",
mimetype: "application/pdf",
chunk_count: 5,
source_url: "",
owner: "user1",
owner_name: "User One",
owner_email: "user@example.com",
file_size: 1024,
connector_type: "local",
allowed_users: [],
allowed_groups: [],
indexed_time: "2024-06-15T10:30:00.000Z",
},
],
}),
),
),
);

const { result } = renderHook(() => useListFiles({}), {
wrapper: createQueryWrapper(),
});

await waitFor(() => expect(result.current.isSuccess).toBe(true));

expect(result.current.data?.files[0]?.indexed_time).toBe(
"2024-06-15T10:30:00.000Z",
);
});
});
7 changes: 7 additions & 0 deletions frontend/app/api/queries/useListFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface ListFilesParams {
dataSources?: string[];
search?: string;
afterKey?: Record<string, unknown> | null; //composite pagination cursor, could be undefined in the beginning
createdAfter?: string;
createdBefore?: string;
}

export interface ListFilesResponse {
Expand Down Expand Up @@ -50,6 +52,10 @@ export const useListFiles = (
if (params.search) searchParams.set("search", params.search);
if (params.afterKey)
searchParams.set("after_key", JSON.stringify(params.afterKey));
if (params.createdAfter)
searchParams.set("created_after", params.createdAfter);
if (params.createdBefore)
searchParams.set("created_before", params.createdBefore);

const url = `/files?${searchParams.toString()}`; //internal (cookie auth)

Expand Down Expand Up @@ -79,6 +85,7 @@ export const useListFiles = (
embedding_dimensions: f.embedding_dimensions as number | undefined,
allowed_users: (f.allowed_users as string[]) || [],
allowed_groups: (f.allowed_groups as string[]) || [],
indexed_time: (f.indexed_time as string) || undefined,
status: "active" as const,
}),
);
Expand Down
Loading
Loading