Skip to content

Commit

Permalink
Feature/mx 1679 aux extractor wikidata search (#225)
Browse files Browse the repository at this point in the history
# 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
ZehraVictoria and cutoffthetop authored Feb 13, 2025
1 parent a7d03e6 commit 4585086
Show file tree
Hide file tree
Showing 12 changed files with 625 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- aux extractor search for wikidata

### Changes

- update mex-common to version 0.49.3
Expand Down
Empty file.
228 changes: 228 additions & 0 deletions mex/editor/aux_search/main.py
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",
)
)
)
13 changes: 13 additions & 0 deletions mex/editor/aux_search/models.py
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]
134 changes: 134 additions & 0 deletions mex/editor/aux_search/state.py
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()
Loading

0 comments on commit 4585086

Please sign in to comment.