mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-07-30 13:48:07 +02:00
Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
commit
4a9d21cc00
@ -250,6 +250,8 @@ def events(params: EventsQueryParams = Depends()):
|
|||||||
order_by = Event.start_time.desc()
|
order_by = Event.start_time.desc()
|
||||||
else:
|
else:
|
||||||
order_by = Event.start_time.desc()
|
order_by = Event.start_time.desc()
|
||||||
|
else:
|
||||||
|
order_by = Event.start_time.desc()
|
||||||
|
|
||||||
events = (
|
events = (
|
||||||
Event.select(*selected_columns)
|
Event.select(*selected_columns)
|
||||||
@ -582,13 +584,11 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends())
|
|||||||
|
|
||||||
processed_events.append(processed_event)
|
processed_events.append(processed_event)
|
||||||
|
|
||||||
# Sort by search distance if search_results are available, otherwise by start_time as default
|
if (sort is None or sort == "relevance") and search_results:
|
||||||
if search_results:
|
|
||||||
processed_events.sort(key=lambda x: x.get("search_distance", float("inf")))
|
processed_events.sort(key=lambda x: x.get("search_distance", float("inf")))
|
||||||
else:
|
elif min_score is not None and max_score is not None and sort == "score_asc":
|
||||||
if sort == "score_asc":
|
|
||||||
processed_events.sort(key=lambda x: x["score"])
|
processed_events.sort(key=lambda x: x["score"])
|
||||||
elif sort == "score_desc":
|
elif min_score is not None and max_score is not None and sort == "score_desc":
|
||||||
processed_events.sort(key=lambda x: x["score"], reverse=True)
|
processed_events.sort(key=lambda x: x["score"], reverse=True)
|
||||||
elif sort == "date_asc":
|
elif sort == "date_asc":
|
||||||
processed_events.sort(key=lambda x: x["start_time"])
|
processed_events.sort(key=lambda x: x["start_time"])
|
||||||
|
@ -26,6 +26,7 @@ from frigate.api.defs.review_responses import (
|
|||||||
)
|
)
|
||||||
from frigate.api.defs.tags import Tags
|
from frigate.api.defs.tags import Tags
|
||||||
from frigate.models import Recordings, ReviewSegment
|
from frigate.models import Recordings, ReviewSegment
|
||||||
|
from frigate.review.maintainer import SeverityEnum
|
||||||
from frigate.util.builtin import get_tz_modifiers
|
from frigate.util.builtin import get_tz_modifiers
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -161,7 +162,7 @@ def review_summary(params: ReviewSummaryQueryParams = Depends()):
|
|||||||
None,
|
None,
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
(ReviewSegment.severity == "alert"),
|
(ReviewSegment.severity == SeverityEnum.alert),
|
||||||
ReviewSegment.has_been_reviewed,
|
ReviewSegment.has_been_reviewed,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -173,7 +174,7 @@ def review_summary(params: ReviewSummaryQueryParams = Depends()):
|
|||||||
None,
|
None,
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
(ReviewSegment.severity == "detection"),
|
(ReviewSegment.severity == SeverityEnum.detection),
|
||||||
ReviewSegment.has_been_reviewed,
|
ReviewSegment.has_been_reviewed,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -185,7 +186,7 @@ def review_summary(params: ReviewSummaryQueryParams = Depends()):
|
|||||||
None,
|
None,
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
(ReviewSegment.severity == "alert"),
|
(ReviewSegment.severity == SeverityEnum.alert),
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -197,7 +198,7 @@ def review_summary(params: ReviewSummaryQueryParams = Depends()):
|
|||||||
None,
|
None,
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
(ReviewSegment.severity == "detection"),
|
(ReviewSegment.severity == SeverityEnum.detection),
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -230,6 +231,7 @@ def review_summary(params: ReviewSummaryQueryParams = Depends()):
|
|||||||
label_clause = reduce(operator.or_, label_clauses)
|
label_clause = reduce(operator.or_, label_clauses)
|
||||||
clauses.append((label_clause))
|
clauses.append((label_clause))
|
||||||
|
|
||||||
|
day_in_seconds = 60 * 60 * 24
|
||||||
last_month = (
|
last_month = (
|
||||||
ReviewSegment.select(
|
ReviewSegment.select(
|
||||||
fn.strftime(
|
fn.strftime(
|
||||||
@ -246,7 +248,7 @@ def review_summary(params: ReviewSummaryQueryParams = Depends()):
|
|||||||
None,
|
None,
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
(ReviewSegment.severity == "alert"),
|
(ReviewSegment.severity == SeverityEnum.alert),
|
||||||
ReviewSegment.has_been_reviewed,
|
ReviewSegment.has_been_reviewed,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -258,7 +260,7 @@ def review_summary(params: ReviewSummaryQueryParams = Depends()):
|
|||||||
None,
|
None,
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
(ReviewSegment.severity == "detection"),
|
(ReviewSegment.severity == SeverityEnum.detection),
|
||||||
ReviewSegment.has_been_reviewed,
|
ReviewSegment.has_been_reviewed,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -270,7 +272,7 @@ def review_summary(params: ReviewSummaryQueryParams = Depends()):
|
|||||||
None,
|
None,
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
(ReviewSegment.severity == "alert"),
|
(ReviewSegment.severity == SeverityEnum.alert),
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -282,7 +284,7 @@ def review_summary(params: ReviewSummaryQueryParams = Depends()):
|
|||||||
None,
|
None,
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
(ReviewSegment.severity == "detection"),
|
(ReviewSegment.severity == SeverityEnum.detection),
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -292,7 +294,7 @@ def review_summary(params: ReviewSummaryQueryParams = Depends()):
|
|||||||
)
|
)
|
||||||
.where(reduce(operator.and_, clauses))
|
.where(reduce(operator.and_, clauses))
|
||||||
.group_by(
|
.group_by(
|
||||||
(ReviewSegment.start_time + seconds_offset).cast("int") / (3600 * 24),
|
(ReviewSegment.start_time + seconds_offset).cast("int") / day_in_seconds,
|
||||||
)
|
)
|
||||||
.order_by(ReviewSegment.start_time.desc())
|
.order_by(ReviewSegment.start_time.desc())
|
||||||
)
|
)
|
||||||
|
@ -9,7 +9,7 @@ from playhouse.sqliteq import SqliteQueueDatabase
|
|||||||
|
|
||||||
from frigate.api.fastapi_app import create_fastapi_app
|
from frigate.api.fastapi_app import create_fastapi_app
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
from frigate.models import Event, ReviewSegment
|
from frigate.models import Event, Recordings, ReviewSegment
|
||||||
from frigate.review.maintainer import SeverityEnum
|
from frigate.review.maintainer import SeverityEnum
|
||||||
from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS
|
from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS
|
||||||
|
|
||||||
@ -146,17 +146,35 @@ class BaseTestHttp(unittest.TestCase):
|
|||||||
def insert_mock_review_segment(
|
def insert_mock_review_segment(
|
||||||
self,
|
self,
|
||||||
id: str,
|
id: str,
|
||||||
start_time: datetime.datetime = datetime.datetime.now().timestamp(),
|
start_time: float = datetime.datetime.now().timestamp(),
|
||||||
end_time: datetime.datetime = datetime.datetime.now().timestamp() + 20,
|
end_time: float = datetime.datetime.now().timestamp() + 20,
|
||||||
|
severity: SeverityEnum = SeverityEnum.alert,
|
||||||
|
has_been_reviewed: bool = False,
|
||||||
) -> Event:
|
) -> Event:
|
||||||
"""Inserts a basic event model with a given id."""
|
"""Inserts a review segment model with a given id."""
|
||||||
return ReviewSegment.insert(
|
return ReviewSegment.insert(
|
||||||
id=id,
|
id=id,
|
||||||
camera="front_door",
|
camera="front_door",
|
||||||
start_time=start_time,
|
start_time=start_time,
|
||||||
end_time=end_time,
|
end_time=end_time,
|
||||||
has_been_reviewed=False,
|
has_been_reviewed=has_been_reviewed,
|
||||||
severity=SeverityEnum.alert,
|
severity=severity,
|
||||||
thumb_path=False,
|
thumb_path=False,
|
||||||
data={},
|
data={},
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
|
def insert_mock_recording(
|
||||||
|
self,
|
||||||
|
id: str,
|
||||||
|
start_time: float = datetime.datetime.now().timestamp(),
|
||||||
|
end_time: float = datetime.datetime.now().timestamp() + 20,
|
||||||
|
) -> Event:
|
||||||
|
"""Inserts a recording model with a given id."""
|
||||||
|
return Recordings.insert(
|
||||||
|
id=id,
|
||||||
|
path=id,
|
||||||
|
camera="front_door",
|
||||||
|
start_time=start_time,
|
||||||
|
end_time=end_time,
|
||||||
|
duration=end_time - start_time,
|
||||||
|
).execute()
|
||||||
|
@ -1,76 +1,89 @@
|
|||||||
import datetime
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from frigate.models import Event, ReviewSegment
|
from frigate.models import Event, Recordings, ReviewSegment
|
||||||
|
from frigate.review.maintainer import SeverityEnum
|
||||||
from frigate.test.http_api.base_http_test import BaseTestHttp
|
from frigate.test.http_api.base_http_test import BaseTestHttp
|
||||||
|
|
||||||
|
|
||||||
class TestHttpReview(BaseTestHttp):
|
class TestHttpReview(BaseTestHttp):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp([Event, ReviewSegment])
|
super().setUp([Event, Recordings, ReviewSegment])
|
||||||
|
self.app = super().create_app()
|
||||||
|
|
||||||
|
def _get_reviews(self, ids: list[str]):
|
||||||
|
return list(
|
||||||
|
ReviewSegment.select(ReviewSegment.id)
|
||||||
|
.where(ReviewSegment.id.in_(ids))
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_recordings(self, ids: list[str]):
|
||||||
|
return list(
|
||||||
|
Recordings.select(Recordings.id).where(Recordings.id.in_(ids)).execute()
|
||||||
|
)
|
||||||
|
|
||||||
|
####################################################################################################################
|
||||||
|
################################### GET /review Endpoint ########################################################
|
||||||
|
####################################################################################################################
|
||||||
|
|
||||||
# Does not return any data point since the end time (before parameter) is not passed and the review segment end_time is 2 seconds from now
|
# Does not return any data point since the end time (before parameter) is not passed and the review segment end_time is 2 seconds from now
|
||||||
def test_get_review_no_filters_no_matches(self):
|
def test_get_review_no_filters_no_matches(self):
|
||||||
app = super().create_app()
|
now = datetime.now().timestamp()
|
||||||
now = datetime.datetime.now().timestamp()
|
|
||||||
|
|
||||||
with TestClient(app) as client:
|
with TestClient(self.app) as client:
|
||||||
super().insert_mock_review_segment("123456.random", now, now + 2)
|
super().insert_mock_review_segment("123456.random", now, now + 2)
|
||||||
reviews_response = client.get("/review")
|
response = client.get("/review")
|
||||||
assert reviews_response.status_code == 200
|
assert response.status_code == 200
|
||||||
reviews_in_response = reviews_response.json()
|
response_json = response.json()
|
||||||
assert len(reviews_in_response) == 0
|
assert len(response_json) == 0
|
||||||
|
|
||||||
def test_get_review_no_filters(self):
|
def test_get_review_no_filters(self):
|
||||||
app = super().create_app()
|
now = datetime.now().timestamp()
|
||||||
now = datetime.datetime.now().timestamp()
|
|
||||||
|
|
||||||
with TestClient(app) as client:
|
with TestClient(self.app) as client:
|
||||||
super().insert_mock_review_segment("123456.random", now - 2, now - 1)
|
super().insert_mock_review_segment("123456.random", now - 2, now - 1)
|
||||||
reviews_response = client.get("/review")
|
response = client.get("/review")
|
||||||
assert reviews_response.status_code == 200
|
assert response.status_code == 200
|
||||||
reviews_in_response = reviews_response.json()
|
response_json = response.json()
|
||||||
assert len(reviews_in_response) == 1
|
assert len(response_json) == 1
|
||||||
|
|
||||||
def test_get_review_with_time_filter_no_matches(self):
|
def test_get_review_with_time_filter_no_matches(self):
|
||||||
app = super().create_app()
|
now = datetime.now().timestamp()
|
||||||
now = datetime.datetime.now().timestamp()
|
|
||||||
|
|
||||||
with TestClient(app) as client:
|
with TestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
super().insert_mock_review_segment(id, now, now + 2)
|
super().insert_mock_review_segment(id, now, now + 2)
|
||||||
params = {
|
params = {
|
||||||
"after": now,
|
"after": now,
|
||||||
"before": now + 3,
|
"before": now + 3,
|
||||||
}
|
}
|
||||||
reviews_response = client.get("/review", params=params)
|
response = client.get("/review", params=params)
|
||||||
assert reviews_response.status_code == 200
|
assert response.status_code == 200
|
||||||
reviews_in_response = reviews_response.json()
|
response_json = response.json()
|
||||||
assert len(reviews_in_response) == 0
|
assert len(response_json) == 0
|
||||||
|
|
||||||
def test_get_review_with_time_filter(self):
|
def test_get_review_with_time_filter(self):
|
||||||
app = super().create_app()
|
now = datetime.now().timestamp()
|
||||||
now = datetime.datetime.now().timestamp()
|
|
||||||
|
|
||||||
with TestClient(app) as client:
|
with TestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
super().insert_mock_review_segment(id, now, now + 2)
|
super().insert_mock_review_segment(id, now, now + 2)
|
||||||
params = {
|
params = {
|
||||||
"after": now - 1,
|
"after": now - 1,
|
||||||
"before": now + 3,
|
"before": now + 3,
|
||||||
}
|
}
|
||||||
reviews_response = client.get("/review", params=params)
|
response = client.get("/review", params=params)
|
||||||
assert reviews_response.status_code == 200
|
assert response.status_code == 200
|
||||||
reviews_in_response = reviews_response.json()
|
response_json = response.json()
|
||||||
assert len(reviews_in_response) == 1
|
assert len(response_json) == 1
|
||||||
assert reviews_in_response[0]["id"] == id
|
assert response_json[0]["id"] == id
|
||||||
|
|
||||||
def test_get_review_with_limit_filter(self):
|
def test_get_review_with_limit_filter(self):
|
||||||
app = super().create_app()
|
now = datetime.now().timestamp()
|
||||||
now = datetime.datetime.now().timestamp()
|
|
||||||
|
|
||||||
with TestClient(app) as client:
|
with TestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
id2 = "654321.random"
|
id2 = "654321.random"
|
||||||
super().insert_mock_review_segment(id, now, now + 2)
|
super().insert_mock_review_segment(id, now, now + 2)
|
||||||
@ -80,17 +93,49 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
"after": now,
|
"after": now,
|
||||||
"before": now + 3,
|
"before": now + 3,
|
||||||
}
|
}
|
||||||
reviews_response = client.get("/review", params=params)
|
response = client.get("/review", params=params)
|
||||||
assert reviews_response.status_code == 200
|
assert response.status_code == 200
|
||||||
reviews_in_response = reviews_response.json()
|
response_json = response.json()
|
||||||
assert len(reviews_in_response) == 1
|
assert len(response_json) == 1
|
||||||
assert reviews_in_response[0]["id"] == id2
|
assert response_json[0]["id"] == id2
|
||||||
|
|
||||||
|
def test_get_review_with_severity_filters_no_matches(self):
|
||||||
|
now = datetime.now().timestamp()
|
||||||
|
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
id = "123456.random"
|
||||||
|
super().insert_mock_review_segment(id, now, now + 2, SeverityEnum.detection)
|
||||||
|
params = {
|
||||||
|
"severity": "detection",
|
||||||
|
"after": now - 1,
|
||||||
|
"before": now + 3,
|
||||||
|
}
|
||||||
|
response = client.get("/review", params=params)
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_json = response.json()
|
||||||
|
assert len(response_json) == 1
|
||||||
|
assert response_json[0]["id"] == id
|
||||||
|
|
||||||
|
def test_get_review_with_severity_filters(self):
|
||||||
|
now = datetime.now().timestamp()
|
||||||
|
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
id = "123456.random"
|
||||||
|
super().insert_mock_review_segment(id, now, now + 2, SeverityEnum.detection)
|
||||||
|
params = {
|
||||||
|
"severity": "alert",
|
||||||
|
"after": now - 1,
|
||||||
|
"before": now + 3,
|
||||||
|
}
|
||||||
|
response = client.get("/review", params=params)
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_json = response.json()
|
||||||
|
assert len(response_json) == 0
|
||||||
|
|
||||||
def test_get_review_with_all_filters(self):
|
def test_get_review_with_all_filters(self):
|
||||||
app = super().create_app()
|
now = datetime.now().timestamp()
|
||||||
now = datetime.datetime.now().timestamp()
|
|
||||||
|
|
||||||
with TestClient(app) as client:
|
with TestClient(self.app) as client:
|
||||||
id = "123456.random"
|
id = "123456.random"
|
||||||
super().insert_mock_review_segment(id, now, now + 2)
|
super().insert_mock_review_segment(id, now, now + 2)
|
||||||
params = {
|
params = {
|
||||||
@ -103,8 +148,424 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
"after": now - 1,
|
"after": now - 1,
|
||||||
"before": now + 3,
|
"before": now + 3,
|
||||||
}
|
}
|
||||||
reviews_response = client.get("/review", params=params)
|
response = client.get("/review", params=params)
|
||||||
assert reviews_response.status_code == 200
|
assert response.status_code == 200
|
||||||
reviews_in_response = reviews_response.json()
|
response_json = response.json()
|
||||||
assert len(reviews_in_response) == 1
|
assert len(response_json) == 1
|
||||||
assert reviews_in_response[0]["id"] == id
|
assert response_json[0]["id"] == id
|
||||||
|
|
||||||
|
####################################################################################################################
|
||||||
|
################################### GET /review/summary Endpoint #################################################
|
||||||
|
####################################################################################################################
|
||||||
|
def test_get_review_summary_all_filters(self):
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
super().insert_mock_review_segment("123456.random")
|
||||||
|
params = {
|
||||||
|
"cameras": "front_door",
|
||||||
|
"labels": "all",
|
||||||
|
"zones": "all",
|
||||||
|
"timezone": "utc",
|
||||||
|
}
|
||||||
|
response = client.get("/review/summary", params=params)
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_json = response.json()
|
||||||
|
# e.g. '2024-11-24'
|
||||||
|
today_formatted = datetime.today().strftime("%Y-%m-%d")
|
||||||
|
expected_response = {
|
||||||
|
"last24Hours": {
|
||||||
|
"reviewed_alert": 0,
|
||||||
|
"reviewed_detection": 0,
|
||||||
|
"total_alert": 1,
|
||||||
|
"total_detection": 0,
|
||||||
|
},
|
||||||
|
today_formatted: {
|
||||||
|
"day": today_formatted,
|
||||||
|
"reviewed_alert": 0,
|
||||||
|
"reviewed_detection": 0,
|
||||||
|
"total_alert": 1,
|
||||||
|
"total_detection": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
self.assertEqual(response_json, expected_response)
|
||||||
|
|
||||||
|
def test_get_review_summary_no_filters(self):
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
super().insert_mock_review_segment("123456.random")
|
||||||
|
response = client.get("/review/summary")
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_json = response.json()
|
||||||
|
# e.g. '2024-11-24'
|
||||||
|
today_formatted = datetime.today().strftime("%Y-%m-%d")
|
||||||
|
expected_response = {
|
||||||
|
"last24Hours": {
|
||||||
|
"reviewed_alert": 0,
|
||||||
|
"reviewed_detection": 0,
|
||||||
|
"total_alert": 1,
|
||||||
|
"total_detection": 0,
|
||||||
|
},
|
||||||
|
today_formatted: {
|
||||||
|
"day": today_formatted,
|
||||||
|
"reviewed_alert": 0,
|
||||||
|
"reviewed_detection": 0,
|
||||||
|
"total_alert": 1,
|
||||||
|
"total_detection": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
self.assertEqual(response_json, expected_response)
|
||||||
|
|
||||||
|
def test_get_review_summary_multiple_days(self):
|
||||||
|
now = datetime.now()
|
||||||
|
five_days_ago = datetime.today() - timedelta(days=5)
|
||||||
|
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
super().insert_mock_review_segment(
|
||||||
|
"123456.random", now.timestamp() - 2, now.timestamp() - 1
|
||||||
|
)
|
||||||
|
super().insert_mock_review_segment(
|
||||||
|
"654321.random",
|
||||||
|
five_days_ago.timestamp(),
|
||||||
|
five_days_ago.timestamp() + 1,
|
||||||
|
)
|
||||||
|
response = client.get("/review/summary")
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_json = response.json()
|
||||||
|
# e.g. '2024-11-24'
|
||||||
|
today_formatted = now.strftime("%Y-%m-%d")
|
||||||
|
# e.g. '2024-11-19'
|
||||||
|
five_days_ago_formatted = five_days_ago.strftime("%Y-%m-%d")
|
||||||
|
expected_response = {
|
||||||
|
"last24Hours": {
|
||||||
|
"reviewed_alert": 0,
|
||||||
|
"reviewed_detection": 0,
|
||||||
|
"total_alert": 1,
|
||||||
|
"total_detection": 0,
|
||||||
|
},
|
||||||
|
today_formatted: {
|
||||||
|
"day": today_formatted,
|
||||||
|
"reviewed_alert": 0,
|
||||||
|
"reviewed_detection": 0,
|
||||||
|
"total_alert": 1,
|
||||||
|
"total_detection": 0,
|
||||||
|
},
|
||||||
|
five_days_ago_formatted: {
|
||||||
|
"day": five_days_ago_formatted,
|
||||||
|
"reviewed_alert": 0,
|
||||||
|
"reviewed_detection": 0,
|
||||||
|
"total_alert": 1,
|
||||||
|
"total_detection": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
self.assertEqual(response_json, expected_response)
|
||||||
|
|
||||||
|
def test_get_review_summary_multiple_days_edge_cases(self):
|
||||||
|
now = datetime.now()
|
||||||
|
five_days_ago = datetime.today() - timedelta(days=5)
|
||||||
|
twenty_days_ago = datetime.today() - timedelta(days=20)
|
||||||
|
one_month_ago = datetime.today() - timedelta(days=30)
|
||||||
|
one_month_ago_ts = one_month_ago.timestamp()
|
||||||
|
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
super().insert_mock_review_segment("123456.random", now.timestamp())
|
||||||
|
super().insert_mock_review_segment(
|
||||||
|
"123457.random", five_days_ago.timestamp()
|
||||||
|
)
|
||||||
|
super().insert_mock_review_segment(
|
||||||
|
"123458.random",
|
||||||
|
twenty_days_ago.timestamp(),
|
||||||
|
None,
|
||||||
|
SeverityEnum.detection,
|
||||||
|
)
|
||||||
|
# One month ago plus 5 seconds fits within the condition (review.start_time > month_ago). Assuming that the endpoint does not take more than 5 seconds to be invoked
|
||||||
|
super().insert_mock_review_segment(
|
||||||
|
"123459.random",
|
||||||
|
one_month_ago_ts + 5,
|
||||||
|
None,
|
||||||
|
SeverityEnum.detection,
|
||||||
|
)
|
||||||
|
# This won't appear in the output since it's not within last month start_time clause (review.start_time > month_ago)
|
||||||
|
super().insert_mock_review_segment("123450.random", one_month_ago_ts)
|
||||||
|
response = client.get("/review/summary")
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_json = response.json()
|
||||||
|
# e.g. '2024-11-24'
|
||||||
|
today_formatted = now.strftime("%Y-%m-%d")
|
||||||
|
# e.g. '2024-11-19'
|
||||||
|
five_days_ago_formatted = five_days_ago.strftime("%Y-%m-%d")
|
||||||
|
# e.g. '2024-11-04'
|
||||||
|
twenty_days_ago_formatted = twenty_days_ago.strftime("%Y-%m-%d")
|
||||||
|
# e.g. '2024-10-24'
|
||||||
|
one_month_ago_formatted = one_month_ago.strftime("%Y-%m-%d")
|
||||||
|
expected_response = {
|
||||||
|
"last24Hours": {
|
||||||
|
"reviewed_alert": 0,
|
||||||
|
"reviewed_detection": 0,
|
||||||
|
"total_alert": 1,
|
||||||
|
"total_detection": 0,
|
||||||
|
},
|
||||||
|
today_formatted: {
|
||||||
|
"day": today_formatted,
|
||||||
|
"reviewed_alert": 0,
|
||||||
|
"reviewed_detection": 0,
|
||||||
|
"total_alert": 1,
|
||||||
|
"total_detection": 0,
|
||||||
|
},
|
||||||
|
five_days_ago_formatted: {
|
||||||
|
"day": five_days_ago_formatted,
|
||||||
|
"reviewed_alert": 0,
|
||||||
|
"reviewed_detection": 0,
|
||||||
|
"total_alert": 1,
|
||||||
|
"total_detection": 0,
|
||||||
|
},
|
||||||
|
twenty_days_ago_formatted: {
|
||||||
|
"day": twenty_days_ago_formatted,
|
||||||
|
"reviewed_alert": 0,
|
||||||
|
"reviewed_detection": 0,
|
||||||
|
"total_alert": 0,
|
||||||
|
"total_detection": 1,
|
||||||
|
},
|
||||||
|
one_month_ago_formatted: {
|
||||||
|
"day": one_month_ago_formatted,
|
||||||
|
"reviewed_alert": 0,
|
||||||
|
"reviewed_detection": 0,
|
||||||
|
"total_alert": 0,
|
||||||
|
"total_detection": 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
self.assertEqual(response_json, expected_response)
|
||||||
|
|
||||||
|
def test_get_review_summary_multiple_in_same_day(self):
|
||||||
|
now = datetime.now()
|
||||||
|
five_days_ago = datetime.today() - timedelta(days=5)
|
||||||
|
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
super().insert_mock_review_segment("123456.random", now.timestamp())
|
||||||
|
five_days_ago_ts = five_days_ago.timestamp()
|
||||||
|
for i in range(20):
|
||||||
|
super().insert_mock_review_segment(
|
||||||
|
f"123456_{i}.random_alert",
|
||||||
|
five_days_ago_ts,
|
||||||
|
five_days_ago_ts,
|
||||||
|
SeverityEnum.alert,
|
||||||
|
)
|
||||||
|
for i in range(15):
|
||||||
|
super().insert_mock_review_segment(
|
||||||
|
f"123456_{i}.random_detection",
|
||||||
|
five_days_ago_ts,
|
||||||
|
five_days_ago_ts,
|
||||||
|
SeverityEnum.detection,
|
||||||
|
)
|
||||||
|
response = client.get("/review/summary")
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_json = response.json()
|
||||||
|
# e.g. '2024-11-24'
|
||||||
|
today_formatted = now.strftime("%Y-%m-%d")
|
||||||
|
# e.g. '2024-11-19'
|
||||||
|
five_days_ago_formatted = five_days_ago.strftime("%Y-%m-%d")
|
||||||
|
expected_response = {
|
||||||
|
"last24Hours": {
|
||||||
|
"reviewed_alert": 0,
|
||||||
|
"reviewed_detection": 0,
|
||||||
|
"total_alert": 1,
|
||||||
|
"total_detection": 0,
|
||||||
|
},
|
||||||
|
today_formatted: {
|
||||||
|
"day": today_formatted,
|
||||||
|
"reviewed_alert": 0,
|
||||||
|
"reviewed_detection": 0,
|
||||||
|
"total_alert": 1,
|
||||||
|
"total_detection": 0,
|
||||||
|
},
|
||||||
|
five_days_ago_formatted: {
|
||||||
|
"day": five_days_ago_formatted,
|
||||||
|
"reviewed_alert": 0,
|
||||||
|
"reviewed_detection": 0,
|
||||||
|
"total_alert": 20,
|
||||||
|
"total_detection": 15,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
self.assertEqual(response_json, expected_response)
|
||||||
|
|
||||||
|
def test_get_review_summary_multiple_in_same_day_with_reviewed(self):
|
||||||
|
five_days_ago = datetime.today() - timedelta(days=5)
|
||||||
|
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
five_days_ago_ts = five_days_ago.timestamp()
|
||||||
|
for i in range(10):
|
||||||
|
super().insert_mock_review_segment(
|
||||||
|
f"123456_{i}.random_alert_not_reviewed",
|
||||||
|
five_days_ago_ts,
|
||||||
|
five_days_ago_ts,
|
||||||
|
SeverityEnum.alert,
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
for i in range(10):
|
||||||
|
super().insert_mock_review_segment(
|
||||||
|
f"123456_{i}.random_alert_reviewed",
|
||||||
|
five_days_ago_ts,
|
||||||
|
five_days_ago_ts,
|
||||||
|
SeverityEnum.alert,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
for i in range(10):
|
||||||
|
super().insert_mock_review_segment(
|
||||||
|
f"123456_{i}.random_detection_not_reviewed",
|
||||||
|
five_days_ago_ts,
|
||||||
|
five_days_ago_ts,
|
||||||
|
SeverityEnum.detection,
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
for i in range(5):
|
||||||
|
super().insert_mock_review_segment(
|
||||||
|
f"123456_{i}.random_detection_reviewed",
|
||||||
|
five_days_ago_ts,
|
||||||
|
five_days_ago_ts,
|
||||||
|
SeverityEnum.detection,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
response = client.get("/review/summary")
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_json = response.json()
|
||||||
|
# e.g. '2024-11-19'
|
||||||
|
five_days_ago_formatted = five_days_ago.strftime("%Y-%m-%d")
|
||||||
|
expected_response = {
|
||||||
|
"last24Hours": {
|
||||||
|
"reviewed_alert": None,
|
||||||
|
"reviewed_detection": None,
|
||||||
|
"total_alert": None,
|
||||||
|
"total_detection": None,
|
||||||
|
},
|
||||||
|
five_days_ago_formatted: {
|
||||||
|
"day": five_days_ago_formatted,
|
||||||
|
"reviewed_alert": 10,
|
||||||
|
"reviewed_detection": 5,
|
||||||
|
"total_alert": 20,
|
||||||
|
"total_detection": 15,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
self.assertEqual(response_json, expected_response)
|
||||||
|
|
||||||
|
####################################################################################################################
|
||||||
|
################################### POST reviews/viewed Endpoint ################################################
|
||||||
|
####################################################################################################################
|
||||||
|
def test_post_reviews_viewed_no_body(self):
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
super().insert_mock_review_segment("123456.random")
|
||||||
|
response = client.post("/reviews/viewed")
|
||||||
|
# Missing ids
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
def test_post_reviews_viewed_no_body_ids(self):
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
super().insert_mock_review_segment("123456.random")
|
||||||
|
body = {"ids": [""]}
|
||||||
|
response = client.post("/reviews/viewed", json=body)
|
||||||
|
# Missing ids
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
def test_post_reviews_viewed_non_existent_id(self):
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
id = "123456.random"
|
||||||
|
super().insert_mock_review_segment(id)
|
||||||
|
body = {"ids": ["1"]}
|
||||||
|
response = client.post("/reviews/viewed", json=body)
|
||||||
|
assert response.status_code == 200
|
||||||
|
response = response.json()
|
||||||
|
assert response["success"] == True
|
||||||
|
assert response["message"] == "Reviewed multiple items"
|
||||||
|
# Verify that in DB the review segment was not changed
|
||||||
|
review_segment_in_db = (
|
||||||
|
ReviewSegment.select(ReviewSegment.has_been_reviewed)
|
||||||
|
.where(ReviewSegment.id == id)
|
||||||
|
.get()
|
||||||
|
)
|
||||||
|
assert review_segment_in_db.has_been_reviewed == False
|
||||||
|
|
||||||
|
def test_post_reviews_viewed(self):
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
id = "123456.random"
|
||||||
|
super().insert_mock_review_segment(id)
|
||||||
|
body = {"ids": [id]}
|
||||||
|
response = client.post("/reviews/viewed", json=body)
|
||||||
|
assert response.status_code == 200
|
||||||
|
response = response.json()
|
||||||
|
assert response["success"] == True
|
||||||
|
assert response["message"] == "Reviewed multiple items"
|
||||||
|
# Verify that in DB the review segment was changed
|
||||||
|
review_segment_in_db = (
|
||||||
|
ReviewSegment.select(ReviewSegment.has_been_reviewed)
|
||||||
|
.where(ReviewSegment.id == id)
|
||||||
|
.get()
|
||||||
|
)
|
||||||
|
assert review_segment_in_db.has_been_reviewed == True
|
||||||
|
|
||||||
|
####################################################################################################################
|
||||||
|
################################### POST reviews/delete Endpoint ################################################
|
||||||
|
####################################################################################################################
|
||||||
|
def test_post_reviews_delete_no_body(self):
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
super().insert_mock_review_segment("123456.random")
|
||||||
|
response = client.post("/reviews/delete")
|
||||||
|
# Missing ids
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
def test_post_reviews_delete_no_body_ids(self):
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
super().insert_mock_review_segment("123456.random")
|
||||||
|
body = {"ids": [""]}
|
||||||
|
response = client.post("/reviews/delete", json=body)
|
||||||
|
# Missing ids
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
def test_post_reviews_delete_non_existent_id(self):
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
id = "123456.random"
|
||||||
|
super().insert_mock_review_segment(id)
|
||||||
|
body = {"ids": ["1"]}
|
||||||
|
response = client.post("/reviews/delete", json=body)
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_json = response.json()
|
||||||
|
assert response_json["success"] == True
|
||||||
|
assert response_json["message"] == "Delete reviews"
|
||||||
|
# Verify that in DB the review segment was not deleted
|
||||||
|
review_ids_in_db_after = self._get_reviews([id])
|
||||||
|
assert len(review_ids_in_db_after) == 1
|
||||||
|
assert review_ids_in_db_after[0].id == id
|
||||||
|
|
||||||
|
def test_post_reviews_delete(self):
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
id = "123456.random"
|
||||||
|
super().insert_mock_review_segment(id)
|
||||||
|
body = {"ids": [id]}
|
||||||
|
response = client.post("/reviews/delete", json=body)
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_json = response.json()
|
||||||
|
assert response_json["success"] == True
|
||||||
|
assert response_json["message"] == "Delete reviews"
|
||||||
|
# Verify that in DB the review segment was deleted
|
||||||
|
review_ids_in_db_after = self._get_reviews([id])
|
||||||
|
assert len(review_ids_in_db_after) == 0
|
||||||
|
|
||||||
|
def test_post_reviews_delete_many(self):
|
||||||
|
with TestClient(self.app) as client:
|
||||||
|
ids = ["123456.random", "654321.random"]
|
||||||
|
for id in ids:
|
||||||
|
super().insert_mock_review_segment(id)
|
||||||
|
super().insert_mock_recording(id)
|
||||||
|
|
||||||
|
review_ids_in_db_before = self._get_reviews(ids)
|
||||||
|
recordings_ids_in_db_before = self._get_recordings(ids)
|
||||||
|
assert len(review_ids_in_db_before) == 2
|
||||||
|
assert len(recordings_ids_in_db_before) == 2
|
||||||
|
|
||||||
|
body = {"ids": ids}
|
||||||
|
response = client.post("/reviews/delete", json=body)
|
||||||
|
assert response.status_code == 200
|
||||||
|
response_json = response.json()
|
||||||
|
assert response_json["success"] == True
|
||||||
|
assert response_json["message"] == "Delete reviews"
|
||||||
|
|
||||||
|
# Verify that in DB all review segments and recordings that were passed were deleted
|
||||||
|
review_ids_in_db_after = self._get_reviews(ids)
|
||||||
|
recording_ids_in_db_after = self._get_recordings(ids)
|
||||||
|
assert len(review_ids_in_db_after) == 0
|
||||||
|
assert len(recording_ids_in_db_after) == 0
|
||||||
|
@ -15,13 +15,15 @@ import {
|
|||||||
SearchFilter,
|
SearchFilter,
|
||||||
SearchFilters,
|
SearchFilters,
|
||||||
SearchSource,
|
SearchSource,
|
||||||
|
SearchSortType,
|
||||||
} from "@/types/search";
|
} from "@/types/search";
|
||||||
import { DateRange } from "react-day-picker";
|
import { DateRange } from "react-day-picker";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { MdLabel } from "react-icons/md";
|
import { MdLabel, MdSort } from "react-icons/md";
|
||||||
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||||
import SearchFilterDialog from "../overlay/dialog/SearchFilterDialog";
|
import SearchFilterDialog from "../overlay/dialog/SearchFilterDialog";
|
||||||
import { CalendarRangeFilterButton } from "./CalendarFilterButton";
|
import { CalendarRangeFilterButton } from "./CalendarFilterButton";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
|
||||||
type SearchFilterGroupProps = {
|
type SearchFilterGroupProps = {
|
||||||
className: string;
|
className: string;
|
||||||
@ -107,6 +109,25 @@ export default function SearchFilterGroup({
|
|||||||
[config, allLabels, allZones],
|
[config, allLabels, allZones],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const availableSortTypes = useMemo(() => {
|
||||||
|
const sortTypes = ["date_asc", "date_desc"];
|
||||||
|
if (filter?.min_score || filter?.max_score) {
|
||||||
|
sortTypes.push("score_desc", "score_asc");
|
||||||
|
}
|
||||||
|
if (filter?.event_id || filter?.query) {
|
||||||
|
sortTypes.push("relevance");
|
||||||
|
}
|
||||||
|
return sortTypes as SearchSortType[];
|
||||||
|
}, [filter]);
|
||||||
|
|
||||||
|
const defaultSortType = useMemo<SearchSortType>(() => {
|
||||||
|
if (filter?.query || filter?.event_id) {
|
||||||
|
return "relevance";
|
||||||
|
} else {
|
||||||
|
return "date_desc";
|
||||||
|
}
|
||||||
|
}, [filter]);
|
||||||
|
|
||||||
const groups = useMemo(() => {
|
const groups = useMemo(() => {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return [];
|
return [];
|
||||||
@ -179,6 +200,16 @@ export default function SearchFilterGroup({
|
|||||||
filterValues={filterValues}
|
filterValues={filterValues}
|
||||||
onUpdateFilter={onUpdateFilter}
|
onUpdateFilter={onUpdateFilter}
|
||||||
/>
|
/>
|
||||||
|
{filters.includes("sort") && Object.keys(filter ?? {}).length > 0 && (
|
||||||
|
<SortTypeButton
|
||||||
|
availableSortTypes={availableSortTypes ?? []}
|
||||||
|
defaultSortType={defaultSortType}
|
||||||
|
selectedSortType={filter?.sort}
|
||||||
|
updateSortType={(newSort) => {
|
||||||
|
onUpdateFilter({ ...filter, sort: newSort });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -362,3 +393,176 @@ export function GeneralFilterContent({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SortTypeButtonProps = {
|
||||||
|
availableSortTypes: SearchSortType[];
|
||||||
|
defaultSortType: SearchSortType;
|
||||||
|
selectedSortType: SearchSortType | undefined;
|
||||||
|
updateSortType: (sortType: SearchSortType | undefined) => void;
|
||||||
|
};
|
||||||
|
function SortTypeButton({
|
||||||
|
availableSortTypes,
|
||||||
|
defaultSortType,
|
||||||
|
selectedSortType,
|
||||||
|
updateSortType,
|
||||||
|
}: SortTypeButtonProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [currentSortType, setCurrentSortType] = useState<
|
||||||
|
SearchSortType | undefined
|
||||||
|
>(selectedSortType as SearchSortType);
|
||||||
|
|
||||||
|
// ui
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentSortType(selectedSortType);
|
||||||
|
// only refresh when state changes
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedSortType]);
|
||||||
|
|
||||||
|
const trigger = (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={
|
||||||
|
selectedSortType != defaultSortType && selectedSortType != undefined
|
||||||
|
? "select"
|
||||||
|
: "default"
|
||||||
|
}
|
||||||
|
className="flex items-center gap-2 capitalize"
|
||||||
|
aria-label="Labels"
|
||||||
|
>
|
||||||
|
<MdSort
|
||||||
|
className={`${selectedSortType != defaultSortType && selectedSortType != undefined ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`${selectedSortType != defaultSortType && selectedSortType != undefined ? "text-selected-foreground" : "text-primary"}`}
|
||||||
|
>
|
||||||
|
Sort
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
const content = (
|
||||||
|
<SortTypeContent
|
||||||
|
availableSortTypes={availableSortTypes ?? []}
|
||||||
|
defaultSortType={defaultSortType}
|
||||||
|
selectedSortType={selectedSortType}
|
||||||
|
currentSortType={currentSortType}
|
||||||
|
setCurrentSortType={setCurrentSortType}
|
||||||
|
updateSortType={updateSortType}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PlatformAwareDialog
|
||||||
|
trigger={trigger}
|
||||||
|
content={content}
|
||||||
|
contentClassName={
|
||||||
|
isDesktop
|
||||||
|
? "scrollbar-container h-auto max-h-[80dvh] overflow-y-auto"
|
||||||
|
: "max-h-[75dvh] overflow-hidden p-4"
|
||||||
|
}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setCurrentSortType(selectedSortType);
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(open);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type SortTypeContentProps = {
|
||||||
|
availableSortTypes: SearchSortType[];
|
||||||
|
defaultSortType: SearchSortType;
|
||||||
|
selectedSortType: SearchSortType | undefined;
|
||||||
|
currentSortType: SearchSortType | undefined;
|
||||||
|
updateSortType: (sort_type: SearchSortType | undefined) => void;
|
||||||
|
setCurrentSortType: (sort_type: SearchSortType | undefined) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
export function SortTypeContent({
|
||||||
|
availableSortTypes,
|
||||||
|
defaultSortType,
|
||||||
|
selectedSortType,
|
||||||
|
currentSortType,
|
||||||
|
updateSortType,
|
||||||
|
setCurrentSortType,
|
||||||
|
onClose,
|
||||||
|
}: SortTypeContentProps) {
|
||||||
|
const sortLabels = {
|
||||||
|
date_asc: "Date (Ascending)",
|
||||||
|
date_desc: "Date (Descending)",
|
||||||
|
score_asc: "Object Score (Ascending)",
|
||||||
|
score_desc: "Object Score (Descending)",
|
||||||
|
relevance: "Relevance",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="overflow-x-hidden">
|
||||||
|
<div className="my-2.5 flex flex-col gap-2.5">
|
||||||
|
<RadioGroup
|
||||||
|
value={
|
||||||
|
Array.isArray(currentSortType)
|
||||||
|
? currentSortType?.[0]
|
||||||
|
: (currentSortType ?? defaultSortType)
|
||||||
|
}
|
||||||
|
defaultValue={defaultSortType}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setCurrentSortType(value as SearchSortType)
|
||||||
|
}
|
||||||
|
className="w-full space-y-1"
|
||||||
|
>
|
||||||
|
{availableSortTypes.map((value) => (
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<RadioGroupItem
|
||||||
|
key={value}
|
||||||
|
value={value}
|
||||||
|
id={`sort-${value}`}
|
||||||
|
className={
|
||||||
|
value == (currentSortType ?? defaultSortType)
|
||||||
|
? "bg-selected from-selected/50 to-selected/90 text-selected"
|
||||||
|
: "bg-secondary from-secondary/50 to-secondary/90 text-secondary"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor={`sort-${value}`}
|
||||||
|
className="flex cursor-pointer items-center space-x-2"
|
||||||
|
>
|
||||||
|
<span>{sortLabels[value]}</span>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<div className="flex items-center justify-evenly p-2">
|
||||||
|
<Button
|
||||||
|
aria-label="Apply"
|
||||||
|
variant="select"
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedSortType != currentSortType) {
|
||||||
|
updateSortType(currentSortType);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
aria-label="Reset"
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentSortType(undefined);
|
||||||
|
updateSortType(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
FilterType,
|
FilterType,
|
||||||
SavedSearchQuery,
|
SavedSearchQuery,
|
||||||
SearchFilter,
|
SearchFilter,
|
||||||
|
SearchSortType,
|
||||||
SearchSource,
|
SearchSource,
|
||||||
} from "@/types/search";
|
} from "@/types/search";
|
||||||
import useSuggestions from "@/hooks/use-suggestions";
|
import useSuggestions from "@/hooks/use-suggestions";
|
||||||
@ -323,6 +324,9 @@ export default function InputWithTags({
|
|||||||
case "event_id":
|
case "event_id":
|
||||||
newFilters.event_id = value;
|
newFilters.event_id = value;
|
||||||
break;
|
break;
|
||||||
|
case "sort":
|
||||||
|
newFilters.sort = value as SearchSortType;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// Handle array types (cameras, labels, subLabels, zones)
|
// Handle array types (cameras, labels, subLabels, zones)
|
||||||
if (!newFilters[type]) newFilters[type] = [];
|
if (!newFilters[type]) newFilters[type] = [];
|
||||||
|
@ -175,7 +175,7 @@ export default function SearchFilterDialog({
|
|||||||
time_range: undefined,
|
time_range: undefined,
|
||||||
zones: undefined,
|
zones: undefined,
|
||||||
sub_labels: undefined,
|
sub_labels: undefined,
|
||||||
search_type: ["thumbnail", "description"],
|
search_type: undefined,
|
||||||
min_score: undefined,
|
min_score: undefined,
|
||||||
max_score: undefined,
|
max_score: undefined,
|
||||||
has_snapshot: undefined,
|
has_snapshot: undefined,
|
||||||
|
@ -15,7 +15,10 @@ export function useOverlayState<S>(
|
|||||||
(value: S, replace: boolean = false) => {
|
(value: S, replace: boolean = false) => {
|
||||||
const newLocationState = { ...currentLocationState };
|
const newLocationState = { ...currentLocationState };
|
||||||
newLocationState[key] = value;
|
newLocationState[key] = value;
|
||||||
navigate(location.pathname, { state: newLocationState, replace });
|
navigate(location.pathname + location.search, {
|
||||||
|
state: newLocationState,
|
||||||
|
replace,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
// we know that these deps are correct
|
// we know that these deps are correct
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
@ -116,6 +116,7 @@ export default function Explore() {
|
|||||||
is_submitted: searchSearchParams["is_submitted"],
|
is_submitted: searchSearchParams["is_submitted"],
|
||||||
has_clip: searchSearchParams["has_clip"],
|
has_clip: searchSearchParams["has_clip"],
|
||||||
event_id: searchSearchParams["event_id"],
|
event_id: searchSearchParams["event_id"],
|
||||||
|
sort: searchSearchParams["sort"],
|
||||||
limit:
|
limit:
|
||||||
Object.keys(searchSearchParams).length == 0 ? API_LIMIT : undefined,
|
Object.keys(searchSearchParams).length == 0 ? API_LIMIT : undefined,
|
||||||
timezone,
|
timezone,
|
||||||
@ -148,6 +149,7 @@ export default function Explore() {
|
|||||||
is_submitted: searchSearchParams["is_submitted"],
|
is_submitted: searchSearchParams["is_submitted"],
|
||||||
has_clip: searchSearchParams["has_clip"],
|
has_clip: searchSearchParams["has_clip"],
|
||||||
event_id: searchSearchParams["event_id"],
|
event_id: searchSearchParams["event_id"],
|
||||||
|
sort: searchSearchParams["sort"],
|
||||||
timezone,
|
timezone,
|
||||||
include_thumbnails: 0,
|
include_thumbnails: 0,
|
||||||
},
|
},
|
||||||
@ -165,12 +167,17 @@ export default function Explore() {
|
|||||||
|
|
||||||
const [url, params] = searchQuery;
|
const [url, params] = searchQuery;
|
||||||
|
|
||||||
// If it's not the first page, use the last item's start_time as the 'before' parameter
|
const isAscending = params.sort?.includes("date_asc");
|
||||||
|
|
||||||
if (pageIndex > 0 && previousPageData) {
|
if (pageIndex > 0 && previousPageData) {
|
||||||
const lastDate = previousPageData[previousPageData.length - 1].start_time;
|
const lastDate = previousPageData[previousPageData.length - 1].start_time;
|
||||||
return [
|
return [
|
||||||
url,
|
url,
|
||||||
{ ...params, before: lastDate.toString(), limit: API_LIMIT },
|
{
|
||||||
|
...params,
|
||||||
|
[isAscending ? "after" : "before"]: lastDate.toString(),
|
||||||
|
limit: API_LIMIT,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ const SEARCH_FILTERS = [
|
|||||||
"zone",
|
"zone",
|
||||||
"sub",
|
"sub",
|
||||||
"source",
|
"source",
|
||||||
|
"sort",
|
||||||
] as const;
|
] as const;
|
||||||
export type SearchFilters = (typeof SEARCH_FILTERS)[number];
|
export type SearchFilters = (typeof SEARCH_FILTERS)[number];
|
||||||
export const DEFAULT_SEARCH_FILTERS: SearchFilters[] = [
|
export const DEFAULT_SEARCH_FILTERS: SearchFilters[] = [
|
||||||
@ -16,10 +17,18 @@ export const DEFAULT_SEARCH_FILTERS: SearchFilters[] = [
|
|||||||
"zone",
|
"zone",
|
||||||
"sub",
|
"sub",
|
||||||
"source",
|
"source",
|
||||||
|
"sort",
|
||||||
];
|
];
|
||||||
|
|
||||||
export type SearchSource = "similarity" | "thumbnail" | "description";
|
export type SearchSource = "similarity" | "thumbnail" | "description";
|
||||||
|
|
||||||
|
export type SearchSortType =
|
||||||
|
| "date_asc"
|
||||||
|
| "date_desc"
|
||||||
|
| "score_asc"
|
||||||
|
| "score_desc"
|
||||||
|
| "relevance";
|
||||||
|
|
||||||
export type SearchResult = {
|
export type SearchResult = {
|
||||||
id: string;
|
id: string;
|
||||||
camera: string;
|
camera: string;
|
||||||
@ -65,6 +74,7 @@ export type SearchFilter = {
|
|||||||
time_range?: string;
|
time_range?: string;
|
||||||
search_type?: SearchSource[];
|
search_type?: SearchSource[];
|
||||||
event_id?: string;
|
event_id?: string;
|
||||||
|
sort?: SearchSortType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_TIME_RANGE_AFTER = "00:00";
|
export const DEFAULT_TIME_RANGE_AFTER = "00:00";
|
||||||
@ -86,6 +96,7 @@ export type SearchQueryParams = {
|
|||||||
query?: string;
|
query?: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
time_range?: string;
|
time_range?: string;
|
||||||
|
sort?: SearchSortType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SearchQuery = [string, SearchQueryParams] | null;
|
export type SearchQuery = [string, SearchQueryParams] | null;
|
||||||
|
Loading…
Reference in New Issue
Block a user