-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature/mx 1679 aux extractor wikidata search (#225)
# PR Context - extractor menu has no functionality yet # added - Add iteration 1 of aux extractor search and import functionality to mex-editor --------- Signed-off-by: ZehraVictoria <[email protected]> Co-authored-by: Nicolas Drebenstedt <[email protected]>
- Loading branch information
1 parent
a7d03e6
commit 4585086
Showing
12 changed files
with
625 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
from typing import cast | ||
|
||
import reflex as rx | ||
|
||
from mex.editor.aux_search.models import AuxResult | ||
from mex.editor.aux_search.state import AuxState | ||
from mex.editor.components import render_value | ||
from mex.editor.layout import page | ||
|
||
|
||
def expand_properties_button(result: AuxResult, index: int) -> rx.Component: | ||
"""Render a button to expand all properties of an aux search result.""" | ||
return rx.button( | ||
rx.cond( | ||
result.show_properties, | ||
rx.icon("minimize-2", size=15), | ||
rx.icon("maximize-2", size=15), | ||
), | ||
on_click=lambda: AuxState.toggle_show_properties(index), | ||
align="end", | ||
custom_attrs={"data-testid": "expand-properties-button"}, | ||
) | ||
|
||
|
||
def import_button(index: int) -> rx.Component: | ||
"""Render a button to import the aux search result to the MEx backend.""" | ||
return rx.button( | ||
"Import", | ||
on_click=lambda: AuxState.import_result(index), | ||
align="end", | ||
disabled=True, | ||
) | ||
|
||
|
||
def render_preview(result: AuxResult) -> rx.Component: | ||
"""Render a preview of the aux search result.""" | ||
return rx.text( | ||
rx.hstack( | ||
rx.foreach( | ||
result.preview, | ||
render_value, | ||
) | ||
), | ||
style={ | ||
"fontWeight": "var(--font-weight-light)", | ||
"whiteSpace": "nowrap", | ||
"overflow": "hidden", | ||
"textOverflow": "ellipsis", | ||
"maxWidth": "100%", | ||
}, | ||
) | ||
|
||
|
||
def render_all_properties(result: AuxResult) -> rx.Component: | ||
"""Render all properties of the aux search result.""" | ||
return rx.text( | ||
rx.hstack( | ||
rx.foreach( | ||
result.all_properties, | ||
render_value, | ||
), | ||
style={ | ||
"fontWeight": "var(--font-weight-light)", | ||
"flexWrap": "wrap", | ||
"alignItems": "start", | ||
}, | ||
), | ||
custom_attrs={"data-testid": "all-properties-display"}, | ||
) | ||
|
||
|
||
def result_title_and_buttons(result: AuxResult, index: int) -> rx.Component: | ||
"""Render the title and buttons for an aux search result.""" | ||
return rx.hstack( | ||
rx.text( | ||
rx.hstack( | ||
rx.foreach( | ||
result.title, | ||
render_value, | ||
) | ||
), | ||
style={ | ||
"fontWeight": "var(--font-weight-bold)", | ||
"whiteSpace": "nowrap", | ||
"overflow": "hidden", | ||
"width": "95%", | ||
}, | ||
), | ||
expand_properties_button(result, index), | ||
import_button(index), | ||
style={"width": "100%"}, | ||
) | ||
|
||
|
||
def aux_search_result(result: AuxResult, index: int) -> rx.Component: | ||
"""Render an aux search result with title, buttons and preview or all properties.""" | ||
return rx.box( | ||
rx.card( | ||
rx.vstack( | ||
result_title_and_buttons(result, index), | ||
rx.cond( | ||
result.show_properties, | ||
render_all_properties(result), | ||
render_preview(result), | ||
), | ||
), | ||
style={ | ||
"width": "100%", | ||
"flexWrap": "wrap", | ||
}, | ||
), | ||
style={"width": "100%"}, | ||
) | ||
|
||
|
||
def search_input() -> rx.Component: | ||
"""Render a search input element that will trigger the results to refresh.""" | ||
return rx.debounce_input( | ||
rx.input( | ||
rx.input.slot(rx.icon("search"), padding_left="0"), | ||
placeholder="Search here...", | ||
value=AuxState.query_string, | ||
on_change=AuxState.set_query_string, | ||
max_length=100, | ||
style={ | ||
"--text-field-selection-color": "", | ||
"--text-field-focus-color": "transparent", | ||
"--text-field-border-width": "1px", | ||
"backgroundClip": "content-box", | ||
"backgroundColor": "transparent", | ||
"boxShadow": ("inset 0 0 0 var(--text-field-border-width) transparent"), | ||
"color": "", | ||
}, | ||
), | ||
style={"margin": "1em 0 1em"}, | ||
debounce_timeout=250, | ||
width="100%", | ||
) | ||
|
||
|
||
def search_results() -> rx.Component: | ||
"""Render the search results with a heading, result list, and pagination.""" | ||
return rx.vstack( | ||
rx.center( | ||
rx.text( | ||
f"Showing {AuxState.current_results_length} " | ||
f"of {AuxState.total} items found", | ||
style={ | ||
"color": "var(--gray-12)", | ||
"fontWeight": "var(--font-weight-bold)", | ||
"margin": "var(--space-4)", | ||
"userSelect": "none", | ||
}, | ||
custom_attrs={"data-testid": "search-results-heading"}, | ||
), | ||
style={"width": "100%"}, | ||
), | ||
rx.foreach( | ||
AuxState.results_transformed, | ||
aux_search_result, | ||
), | ||
pagination(), | ||
custom_attrs={"data-testid": "search-results-section"}, | ||
width="70vw", | ||
) | ||
|
||
|
||
def nav_bar() -> rx.Component: | ||
"""Render a bar with an extractor navigation menu.""" | ||
return rx.flex( | ||
rx.foreach( | ||
AuxState.aux_data_sources, | ||
lambda item: rx.text( | ||
item, | ||
cursor="pointer", | ||
size="5", | ||
), | ||
), | ||
direction="row", | ||
gap="50px", | ||
custom_attrs={"data-testid": "aux-nav-bar"}, | ||
) | ||
|
||
|
||
def pagination() -> rx.Component: | ||
"""Render pagination for navigating search results.""" | ||
return rx.center( | ||
rx.button( | ||
rx.text("Previous"), | ||
on_click=AuxState.go_to_previous_page, | ||
disabled=AuxState.disable_previous_page, | ||
variant="surface", | ||
custom_attrs={"data-testid": "pagination-previous-button"}, | ||
style={"minWidth": "10%"}, | ||
), | ||
rx.select( | ||
AuxState.total_pages, | ||
value=cast(rx.vars.NumberVar, AuxState.current_page).to_string(), | ||
on_change=AuxState.set_page, | ||
custom_attrs={"data-testid": "pagination-page-select"}, | ||
), | ||
rx.button( | ||
rx.text("Next", weight="bold"), | ||
on_click=AuxState.go_to_next_page, | ||
disabled=AuxState.disable_next_page, | ||
variant="surface", | ||
custom_attrs={"data-testid": "pagination-next-button"}, | ||
style={"minWidth": "10%"}, | ||
), | ||
spacing="4", | ||
style={"width": "100%"}, | ||
) | ||
|
||
|
||
def index() -> rx.Component: | ||
"""Return the index for the search component.""" | ||
return rx.center( | ||
page( | ||
rx.vstack( | ||
nav_bar(), | ||
search_input(), | ||
search_results(), | ||
spacing="5", | ||
justify="center", | ||
align="center", | ||
) | ||
) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import reflex as rx | ||
|
||
from mex.editor.models import EditorValue | ||
|
||
|
||
class AuxResult(rx.Base): | ||
"""Auxiliary search result.""" | ||
|
||
identifier: str | ||
title: list[EditorValue] | ||
preview: list[EditorValue] | ||
show_properties: bool | ||
all_properties: list[EditorValue] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import math | ||
from collections.abc import Generator | ||
|
||
import reflex as rx | ||
from reflex.event import EventSpec | ||
from requests import HTTPError | ||
|
||
from mex.common.backend_api.connector import BackendApiConnector | ||
from mex.common.backend_api.models import PaginatedItemsContainer | ||
from mex.common.models import AnyExtractedModel | ||
from mex.editor.aux_search.models import AuxResult | ||
from mex.editor.aux_search.transform import transform_models_to_results | ||
from mex.editor.exceptions import escalate_error | ||
from mex.editor.state import State | ||
|
||
|
||
class AuxState(State): | ||
"""State management for the aux extractor search page.""" | ||
|
||
results_transformed: list[AuxResult] = [] | ||
results_extracted: list[AnyExtractedModel] = [] | ||
total: int = 0 | ||
query_string: str = "" | ||
current_page: int = 1 | ||
limit: int = 50 | ||
aux_data_sources: list[str] = ["Wikidata", "LDAP"] | ||
|
||
@rx.var | ||
def total_pages(self) -> list[str]: | ||
"""Return a list of total pages based on the number of results.""" | ||
return [f"{i + 1}" for i in range(math.ceil(self.total / self.limit))] | ||
|
||
@rx.var | ||
def disable_previous_page(self) -> bool: | ||
"""Disable the 'Previous' button if on the first page.""" | ||
return self.current_page <= 1 | ||
|
||
@rx.var | ||
def disable_next_page(self) -> bool: | ||
"""Disable the 'Next' button if on the last page.""" | ||
max_page = math.ceil(self.total / self.limit) | ||
return self.current_page >= max_page | ||
|
||
@rx.var | ||
def current_results_length(self) -> int: | ||
"""Return the number of current search results.""" | ||
return len(self.results_transformed) | ||
|
||
@rx.event | ||
def toggle_show_properties(self, index: int) -> None: | ||
"""Toggle the show properties state.""" | ||
self.results_transformed[index].show_properties = not self.results_transformed[ | ||
index | ||
].show_properties | ||
|
||
@rx.event | ||
def set_query_string(self, value: str) -> Generator[EventSpec | None, None, None]: | ||
"""Set the query string and refresh the results.""" | ||
self.query_string = value | ||
return self.search() | ||
|
||
@rx.event | ||
def set_page( | ||
self, page_number: str | int | ||
) -> Generator[EventSpec | None, None, None]: | ||
"""Set the current page and refresh the results.""" | ||
self.current_page = int(page_number) | ||
return self.search() | ||
|
||
@rx.event | ||
def go_to_previous_page(self) -> Generator[EventSpec | None, None, None]: | ||
"""Navigate to the previous page.""" | ||
return self.set_page(self.current_page - 1) | ||
|
||
@rx.event | ||
def go_to_next_page(self) -> Generator[EventSpec | None, None, None]: | ||
"""Navigate to the next page.""" | ||
return self.set_page(self.current_page + 1) | ||
|
||
@rx.event | ||
def import_result(self, index: int) -> Generator[EventSpec | None, None, None]: | ||
"""Import the selected result to MEx backend.""" | ||
connector = BackendApiConnector.get() | ||
try: | ||
connector.post_extracted_items( | ||
extracted_items=[self.results_extracted[index].model_copy()], | ||
) | ||
except HTTPError as exc: | ||
yield from escalate_error( | ||
"backend", "error importing aux search result: %s", exc.response.text | ||
) | ||
else: | ||
yield rx.toast.success( | ||
"Aux search result imported successfully", | ||
duration=5000, | ||
close_button=True, | ||
dismissible=True, | ||
) | ||
|
||
@rx.event | ||
def search(self) -> Generator[EventSpec | None, None, None]: | ||
"""Search for wikidata organizations based on the query string.""" | ||
if self.query_string == "": | ||
return | ||
connector = BackendApiConnector.get() | ||
try: | ||
response = connector.request( | ||
method="GET", | ||
endpoint="wikidata", | ||
params={ | ||
"q": self.query_string, | ||
"offset": str((self.current_page - 1) * self.limit), | ||
"limit": str(self.limit), | ||
}, | ||
) | ||
except HTTPError as exc: | ||
self.reset() | ||
yield from escalate_error( | ||
"backend", "error fetching wikidata items: %s", exc.response.text | ||
) | ||
else: | ||
yield rx.call_script("window.scrollTo({top: 0, behavior: 'smooth'});") | ||
container = PaginatedItemsContainer[AnyExtractedModel].model_validate( | ||
response | ||
) | ||
self.results_extracted = container.items | ||
self.results_transformed = transform_models_to_results(container.items) | ||
self.total = max(container.total, len(self.results_transformed)) | ||
|
||
@rx.event | ||
def refresh(self) -> Generator[EventSpec | None, None, None]: | ||
"""Refresh the search page.""" | ||
self.reset() | ||
return self.search() |
Oops, something went wrong.