mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
fix(ui): find logs by request id across pages and dates (LIT-3981) (#31743)
* fix(spend): resolve spend logs by request_id across all dates (LIT-3981) The /spend/logs/ui search only filtered the page already loaded, so a log id copied from another page or from outside the active date window could not be found. request_id is the primary key of LiteLLM_SpendLogs, so when it is supplied on the internal UI route the mandatory date window is dropped and the lookup resolves across all time. The date window stays required when no request_id is given, and the public /spend/logs/v2 contract is unchanged. A non-admin id lookup is gated by the same ownership check the detail endpoint uses, so the relaxed window cannot be used to read another tenant's log by id * fix(ui): send the logs request_id search to the server (LIT-3981) The "Search by Request ID" box filtered only the rows already on the current page, so an id from another page never matched. It now feeds the existing server-side request_id filter via handleFilterChange, which debounces, resets to page one, and rides the existing react-query key. The dead client-side filter and its searchTerm state are removed; the session composition and dedup logic is unchanged. The box is now an exact request_id lookup, matching its label; the incidental client-side model and user substring matching it used to do is dropped in favor of the dedicated filters * refactor(spend): model the request_id spend-log lookup as an explicit point lookup (LIT-3981) The date-window relaxation for request_id lookups rode an apply_date_window flag threaded through the date validation and parsing. Model the two intents directly instead. A UI request_id query is a point lookup on the @id primary key that drops the time window and authorizes by row ownership; every other query, including the public /spend/logs/v2 route, takes the range-scan path that still requires a window Because the ownership check fully authorizes the single row, the general user/team scoping is now skipped for id lookups rather than layered on top redundantly. The confusing `is_v2 or request_id is None` guard is gone, and moving the date requirement into the range-scan branch lets the type checker narrow the dates it parses Behavior is preserved: the v2 contract still requires dates even when a request_id is supplied, and a non-owner is still rejected with 403. A regression test covers the non-admin owner id lookup, which resolves across all time and filters by the primary key alone
This commit is contained in:
parent
7aaaa055b7
commit
a507394841
4 changed files with 312 additions and 68 deletions
|
|
@ -1695,13 +1695,10 @@ async def ui_view_spend_logs(
|
|||
code=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
|
||||
if start_date is None or end_date is None:
|
||||
raise ProxyException(
|
||||
message="Start date and end date are required",
|
||||
type="bad_request",
|
||||
param="None",
|
||||
code=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
# Inline import — auth_utils participates in a proxy import cycle.
|
||||
from litellm.proxy.auth.auth_utils import get_request_route # noqa: PLC0415
|
||||
|
||||
is_v2 = "/spend/logs/v2" in get_request_route(request)
|
||||
|
||||
# Validate sort_by and sort_order
|
||||
valid_sort_fields = {
|
||||
|
|
@ -1729,36 +1726,50 @@ async def ui_view_spend_logs(
|
|||
)
|
||||
|
||||
try:
|
||||
# Inline import — auth_utils participates in a proxy import cycle.
|
||||
from litellm.proxy.auth.auth_utils import get_request_route # noqa: PLC0415
|
||||
is_admin_view = _is_admin_view_safe(user_api_key_dict=user_api_key_dict)
|
||||
is_request_id_lookup = request_id is not None and not is_v2
|
||||
|
||||
is_v2 = "/spend/logs/v2" in get_request_route(request)
|
||||
formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%d"] if is_v2 else ["%Y-%m-%d %H:%M:%S"]
|
||||
if is_request_id_lookup:
|
||||
# request_id is the @id primary key: it identifies a single row, so a
|
||||
# time window is meaningless. The dashboard always sends a default 24h
|
||||
# window, which hid ids copied from an older page (LIT-3981). Drop the
|
||||
# window for the id lookup so it resolves across all time; every other
|
||||
# query, including the public v2 route, still requires one (below).
|
||||
start_date_obj: datetime | None = None
|
||||
end_date_obj: datetime | None = None
|
||||
else:
|
||||
if start_date is None or end_date is None:
|
||||
raise ProxyException(
|
||||
message="Start date and end date are required",
|
||||
type="bad_request",
|
||||
param="None",
|
||||
code=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%d"] if is_v2 else ["%Y-%m-%d %H:%M:%S"]
|
||||
|
||||
def parse_date(date_str: str) -> datetime:
|
||||
date_str = date_str.strip()
|
||||
for fmt in formats:
|
||||
try:
|
||||
return datetime.strptime(date_str, fmt).replace(tzinfo=timezone.utc)
|
||||
except ValueError:
|
||||
continue
|
||||
expected = "'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'" if is_v2 else "'YYYY-MM-DD HH:MM:SS'"
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid date format: {date_str}. Expected: {expected}",
|
||||
)
|
||||
def parse_date(date_str: str) -> datetime:
|
||||
date_str = date_str.strip()
|
||||
for fmt in formats:
|
||||
try:
|
||||
return datetime.strptime(date_str, fmt).replace(tzinfo=timezone.utc)
|
||||
except ValueError:
|
||||
continue
|
||||
expected = "'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'" if is_v2 else "'YYYY-MM-DD HH:MM:SS'"
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid date format: {date_str}. Expected: {expected}",
|
||||
)
|
||||
|
||||
start_date_obj = parse_date(start_date)
|
||||
end_date_obj = parse_date(end_date)
|
||||
|
||||
# Convert to ISO format strings for Prisma
|
||||
start_date_iso = start_date_obj.isoformat() # Already in UTC, no need to add Z
|
||||
end_date_iso = end_date_obj.isoformat() # Already in UTC, no need to add Z
|
||||
start_date_obj = parse_date(start_date)
|
||||
end_date_obj = parse_date(end_date)
|
||||
|
||||
# Build where conditions
|
||||
where_conditions: dict[str, Any] = {
|
||||
"startTime": {"gte": start_date_iso, "lte": end_date_iso},
|
||||
}
|
||||
where_conditions: dict[str, Any] = {}
|
||||
if start_date_obj is not None and end_date_obj is not None:
|
||||
where_conditions["startTime"] = {
|
||||
"gte": start_date_obj.isoformat(), # Already in UTC, no need to add Z
|
||||
"lte": end_date_obj.isoformat(),
|
||||
}
|
||||
|
||||
if team_id is not None:
|
||||
where_conditions["team_id"] = team_id
|
||||
|
|
@ -1827,9 +1838,19 @@ async def ui_view_spend_logs(
|
|||
where_conditions["spend"]["gte"] = min_spend
|
||||
if max_spend is not None:
|
||||
where_conditions["spend"]["lte"] = max_spend
|
||||
is_admin_view = _is_admin_view_safe(user_api_key_dict=user_api_key_dict)
|
||||
# A request_id lookup drops the date window, so a non-admin could otherwise
|
||||
# reach any single row by id; require they own it, mirroring the detail
|
||||
# endpoint. That ownership check fully authorizes the one row, so the
|
||||
# general scoping below is skipped for id lookups. Scoped to the UI route
|
||||
# so the public v2 contract is unchanged.
|
||||
if request_id is not None and not is_v2 and not is_admin_view:
|
||||
await _assert_user_can_view_request_id(
|
||||
prisma_client=prisma_client,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
request_id=request_id,
|
||||
)
|
||||
permitted_team_ids: List[str] | None = None
|
||||
if not is_admin_view:
|
||||
if not is_request_id_lookup and not is_admin_view:
|
||||
if team_id is not None:
|
||||
can_view_team = await _can_team_member_view_log(
|
||||
prisma_client=prisma_client,
|
||||
|
|
@ -1875,15 +1896,16 @@ async def ui_view_spend_logs(
|
|||
sql_params: List[Any] = []
|
||||
p = 1 # parameter index counter
|
||||
|
||||
# Date range (always present). Wrap the param side with
|
||||
# `AT TIME ZONE 'UTC'` so comparison against the plain `timestamp`
|
||||
# column does not depend on the DB session timezone (see #22529).
|
||||
sql_conditions.append(f"\"startTime\" >= (${p}::timestamptz AT TIME ZONE 'UTC')")
|
||||
sql_params.append(start_date_obj)
|
||||
p += 1
|
||||
sql_conditions.append(f"\"startTime\" <= (${p}::timestamptz AT TIME ZONE 'UTC')")
|
||||
sql_params.append(end_date_obj)
|
||||
p += 1
|
||||
# Date range. Wrap the param side with `AT TIME ZONE 'UTC'` so comparison
|
||||
# against the plain `timestamp` column does not depend on the DB session
|
||||
# timezone (see #22529). Absent for a request_id-only lookup (see above).
|
||||
if start_date_obj is not None and end_date_obj is not None:
|
||||
sql_conditions.append(f"\"startTime\" >= (${p}::timestamptz AT TIME ZONE 'UTC')")
|
||||
sql_params.append(start_date_obj)
|
||||
p += 1
|
||||
sql_conditions.append(f"\"startTime\" <= (${p}::timestamptz AT TIME ZONE 'UTC')")
|
||||
sql_params.append(end_date_obj)
|
||||
p += 1
|
||||
|
||||
# Equality filters - read effective values from where_conditions (post-authorization)
|
||||
for sql_col, wc_key in [
|
||||
|
|
|
|||
|
|
@ -1628,6 +1628,226 @@ async def test_ui_view_spend_logs_date_range_filter(client, monkeypatch):
|
|||
assert data["data"][0]["id"] == "log2"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_view_spend_logs_request_id_lookup_ignores_date_window(
|
||||
client, monkeypatch
|
||||
):
|
||||
"""
|
||||
LIT-3981: a request_id lookup on the UI route resolves across all time even
|
||||
when the caller sends a date window that excludes the log (the dashboard
|
||||
always sends a window). The window is dropped and request_id alone scopes
|
||||
the query. Pre-fix the window was always applied, so an id from an older
|
||||
page returned nothing.
|
||||
"""
|
||||
today = datetime.datetime.now(timezone.utc)
|
||||
mock_spend_logs = [
|
||||
{
|
||||
"id": "log_old",
|
||||
"request_id": "req-old",
|
||||
"api_key": "sk-test-key",
|
||||
"user": "test_user_1",
|
||||
"team_id": "team1",
|
||||
"spend": 0.05,
|
||||
"startTime": (today - datetime.timedelta(days=90)).isoformat(),
|
||||
"model": "gpt-4",
|
||||
},
|
||||
]
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
def filter_fn(where):
|
||||
captured["where"] = where
|
||||
rows = _filter_logs_by_date_range(mock_spend_logs, where)
|
||||
if where.get("request_id"):
|
||||
rows = [r for r in rows if r["request_id"] == where["request_id"]]
|
||||
return rows
|
||||
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.proxy_server.prisma_client",
|
||||
make_ui_spend_logs_mock_prisma(mock_spend_logs, filter_fn),
|
||||
)
|
||||
|
||||
# A 5-day window that EXCLUDES the 90-day-old log, as the dashboard sends.
|
||||
start_date = (today - datetime.timedelta(days=5)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
end_date = today.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN
|
||||
)
|
||||
try:
|
||||
response = client.get(
|
||||
"/spend/logs/ui",
|
||||
params={
|
||||
"request_id": "req-old",
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
},
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 1
|
||||
assert data["data"][0]["request_id"] == "req-old"
|
||||
# Query dropped the time window and scoped solely by the primary key.
|
||||
assert "startTime" not in captured["where"]
|
||||
assert captured["where"]["request_id"] == "req-old"
|
||||
finally:
|
||||
app.dependency_overrides.pop(ps.user_api_key_auth, None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_view_spend_logs_requires_dates_without_request_id(
|
||||
client, monkeypatch
|
||||
):
|
||||
"""The date window stays mandatory on the UI route when no request_id is set."""
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.proxy_server.prisma_client",
|
||||
make_ui_spend_logs_mock_prisma([], lambda where: []),
|
||||
)
|
||||
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN
|
||||
)
|
||||
try:
|
||||
response = client.get(
|
||||
"/spend/logs/ui", headers={"Authorization": "Bearer sk-test"}
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "date" in response.text.lower()
|
||||
finally:
|
||||
app.dependency_overrides.pop(ps.user_api_key_auth, None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_spend_logs_v2_still_requires_dates_with_request_id(client, monkeypatch):
|
||||
"""The public /spend/logs/v2 contract is unchanged: dates remain required even
|
||||
when request_id is supplied. Only the internal UI route relaxes the window."""
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.proxy_server.prisma_client",
|
||||
make_ui_spend_logs_mock_prisma([], lambda where: []),
|
||||
)
|
||||
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN
|
||||
)
|
||||
try:
|
||||
response = client.get(
|
||||
"/spend/logs/v2",
|
||||
params={"request_id": "req-old"},
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "date" in response.text.lower()
|
||||
finally:
|
||||
app.dependency_overrides.pop(ps.user_api_key_auth, None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_view_spend_logs_request_id_blocks_non_owner(client, monkeypatch):
|
||||
"""A non-admin looking up a request_id they do not own is rejected (403), so
|
||||
the relaxed date window cannot read another tenant's log by id."""
|
||||
|
||||
class _ForeignRow:
|
||||
user = "other_user"
|
||||
team_id = None
|
||||
|
||||
class _SpendLogs:
|
||||
async def find_unique(self, where, include=None):
|
||||
return _ForeignRow()
|
||||
|
||||
class _DB:
|
||||
def __init__(self):
|
||||
self.litellm_spendlogs = _SpendLogs()
|
||||
|
||||
class _Prisma:
|
||||
def __init__(self):
|
||||
self.db = _DB()
|
||||
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", _Prisma())
|
||||
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.INTERNAL_USER, user_id="user_1"
|
||||
)
|
||||
try:
|
||||
response = client.get(
|
||||
"/spend/logs/ui",
|
||||
params={"request_id": "foreign-req"},
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
)
|
||||
assert response.status_code == 403
|
||||
finally:
|
||||
app.dependency_overrides.pop(ps.user_api_key_auth, None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_view_spend_logs_request_id_owner_scoped_by_id_only(
|
||||
client, monkeypatch
|
||||
):
|
||||
"""A non-admin owner looking up their own request_id resolves across all time.
|
||||
The ownership check authorizes the single row, so the query drops both the date
|
||||
window and the general user/team scoping and filters by the primary key alone;
|
||||
without that skip an internal user would have a `user`/`OR` clause added."""
|
||||
today = datetime.datetime.now(timezone.utc)
|
||||
mock_spend_logs = [
|
||||
{
|
||||
"id": "log_old",
|
||||
"request_id": "req-old",
|
||||
"api_key": "sk-test-key",
|
||||
"user": "user_1",
|
||||
"team_id": "team1",
|
||||
"spend": 0.05,
|
||||
"startTime": (today - datetime.timedelta(days=90)).isoformat(),
|
||||
"model": "gpt-4",
|
||||
},
|
||||
]
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
def filter_fn(where):
|
||||
captured["where"] = where
|
||||
rows = _filter_logs_by_date_range(mock_spend_logs, where)
|
||||
if where.get("request_id"):
|
||||
rows = [r for r in rows if r["request_id"] == where["request_id"]]
|
||||
return rows
|
||||
|
||||
mock_prisma = make_ui_spend_logs_mock_prisma(mock_spend_logs, filter_fn)
|
||||
|
||||
class _OwnedRow:
|
||||
user = "user_1"
|
||||
team_id = "team1"
|
||||
|
||||
async def _find_unique(where, include=None):
|
||||
return _OwnedRow()
|
||||
|
||||
mock_prisma.db.find_unique = _find_unique
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma)
|
||||
|
||||
# A 5-day window that EXCLUDES the 90-day-old log, as the dashboard sends.
|
||||
start_date = (today - datetime.timedelta(days=5)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
end_date = today.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.INTERNAL_USER, user_id="user_1"
|
||||
)
|
||||
try:
|
||||
response = client.get(
|
||||
"/spend/logs/ui",
|
||||
params={
|
||||
"request_id": "req-old",
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
},
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] == 1
|
||||
assert data["data"][0]["request_id"] == "req-old"
|
||||
assert "startTime" not in captured["where"]
|
||||
assert captured["where"]["request_id"] == "req-old"
|
||||
assert "user" not in captured["where"]
|
||||
assert "OR" not in captured["where"]
|
||||
finally:
|
||||
app.dependency_overrides.pop(ps.user_api_key_auth, None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_view_spend_logs_unauthorized(client):
|
||||
# Test without authorization header
|
||||
|
|
|
|||
|
|
@ -121,23 +121,21 @@ describe("RequestLogsPanel", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("client-side search", () => {
|
||||
it("narrows the visible rows without refetching", async () => {
|
||||
describe("search by request id (LIT-3981)", () => {
|
||||
it("sends the typed request id to the server on the first page instead of filtering the loaded rows", async () => {
|
||||
const user = userEvent.setup();
|
||||
respondWith([
|
||||
logEntry({ request_id: "req-alpha", model: "gpt-4o" }),
|
||||
logEntry({ request_id: "req-beta", model: "claude-opus" }),
|
||||
]);
|
||||
renderWithProviders(<RequestLogsPanel {...defaultProps} />);
|
||||
|
||||
await waitFor(() => expect(row("req-alpha")).not.toBeNull());
|
||||
const callsBefore = vi.mocked(uiSpendLogsCall).mock.calls.length;
|
||||
await waitFor(() => expect(uiSpendLogsCall).toHaveBeenCalled());
|
||||
|
||||
await user.type(screen.getByTestId("datatable-search"), "alpha");
|
||||
await user.type(screen.getByTestId("datatable-search"), "req-on-another-page");
|
||||
|
||||
await waitFor(() => expect(row("req-beta")).toBeNull());
|
||||
expect(row("req-alpha")).not.toBeNull();
|
||||
expect(vi.mocked(uiSpendLogsCall).mock.calls.length).toBe(callsBefore);
|
||||
await waitFor(() => {
|
||||
const call = lastCall();
|
||||
if (!call) throw new Error("uiSpendLogsCall was not called");
|
||||
expect(call.params?.request_id).toBe("req-on-another-page");
|
||||
expect(call.page).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { keyInfoV1Call } from "../networking";
|
|||
import KeyInfoView from "../templates/key_info_view";
|
||||
import type { LogEntry } from "./columns";
|
||||
import { AGENT_CALL_TYPES, MCP_CALL_TYPES } from "./constants";
|
||||
import { DEFAULT_LOGS_SORTING, useLogFilterLogic } from "./log_filter_logic";
|
||||
import { DEFAULT_LOGS_SORTING, LOG_FILTER_IDS, useLogFilterLogic } from "./log_filter_logic";
|
||||
import { LogDetailsDrawer } from "./LogDetailsDrawer";
|
||||
import { LiveTailBanner, LogsTableToolbar } from "./LogsTableToolbar";
|
||||
import { RequestLogsTable } from "./RequestLogsTable";
|
||||
|
|
@ -34,7 +34,6 @@ interface SessionComposition {
|
|||
}
|
||||
|
||||
export default function RequestLogsPanel({ accessToken, token, userRole, userID, isActive }: RequestLogsPanelProps) {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: PAGE_SIZE });
|
||||
const [sorting, setSorting] = useState<SortingState>(DEFAULT_LOGS_SORTING);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
|
|
@ -93,14 +92,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
|
|||
const { data: selectedKeyInfo } = useQuery(keyInfoQueryOptions);
|
||||
|
||||
const rows = useMemo<LogEntry[]>(() => {
|
||||
const searchedLogs = filteredLogs.data.filter((log) => {
|
||||
if (!searchTerm) return true;
|
||||
return (
|
||||
log.request_id.includes(searchTerm) ||
|
||||
log.model.includes(searchTerm) ||
|
||||
(log.user !== undefined && log.user.includes(searchTerm))
|
||||
);
|
||||
});
|
||||
const searchedLogs = filteredLogs.data;
|
||||
|
||||
const sessionCompositionById = searchedLogs.reduce<Record<string, SessionComposition>>((acc, log) => {
|
||||
if (!log.session_id) return acc;
|
||||
|
|
@ -141,7 +133,20 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
|
|||
if (!log.session_id || (log.session_total_count || 1) <= 1) return true;
|
||||
return sessionRepresentativeMap.get(log.session_id)?.requestId === log.request_id;
|
||||
});
|
||||
}, [filteredLogs.data, searchTerm]);
|
||||
}, [filteredLogs.data]);
|
||||
|
||||
const searchTerm = useMemo(() => {
|
||||
const entry = columnFilters.find((filter) => filter.id === LOG_FILTER_IDS.REQUEST_ID);
|
||||
return typeof entry?.value === "string" ? entry.value : "";
|
||||
}, [columnFilters]);
|
||||
|
||||
const handleSearchChange = useCallback((value: string) => {
|
||||
setColumnFilters((previous) => {
|
||||
const others = previous.filter((filter) => filter.id !== LOG_FILTER_IDS.REQUEST_ID);
|
||||
return value === "" ? others : [...others, { id: LOG_FILTER_IDS.REQUEST_ID, value }];
|
||||
});
|
||||
setPagination((previous) => ({ ...previous, pageIndex: 0 }));
|
||||
}, []);
|
||||
|
||||
const handleSortingChange = useCallback<OnChangeFn<SortingState>>((updaterOrValue) => {
|
||||
setSorting(updaterOrValue);
|
||||
|
|
@ -159,7 +164,6 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
|
|||
|
||||
const handleResetFilters = useCallback(() => {
|
||||
setColumnFilters([]);
|
||||
setSearchTerm("");
|
||||
setStartTime(moment().subtract(24, "hours").format("YYYY-MM-DDTHH:mm"));
|
||||
setEndTime(moment().format("YYYY-MM-DDTHH:mm"));
|
||||
setIsCustomDate(false);
|
||||
|
|
@ -221,7 +225,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
|
|||
columnFilters={columnFilters}
|
||||
onColumnFiltersChange={handleColumnFiltersChange}
|
||||
searchValue={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
onSearchChange={handleSearchChange}
|
||||
onRefresh={() => void logsQuery.refetch()}
|
||||
onRowClick={handleRowClick}
|
||||
onKeyHashClick={handleKeyHashClick}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue