Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/mx-1702 resolve types in edit view #158

Merged
merged 44 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
8ca3090
first draft
cutoffthetop Sep 18, 2024
35601ab
Merge branch 'main' of https://github.com/robert-koch-institut/mex-ed…
cutoffthetop Sep 18, 2024
7256377
wip
cutoffthetop Sep 25, 2024
686f79e
rework, prepare, clean
cutoffthetop Sep 25, 2024
f49a9d4
add temporal case and up tests
cutoffthetop Sep 30, 2024
2555b67
Merge branch 'main' of https://github.com/robert-koch-institut/mex-ed…
cutoffthetop Sep 30, 2024
30b7fc4
lock
cutoffthetop Sep 30, 2024
a3790f1
Merge branch 'main' of https://github.com/robert-koch-institut/mex-ed…
cutoffthetop Oct 21, 2024
55aaa63
lock
cutoffthetop Oct 21, 2024
872108f
Merge branch 'main' of https://github.com/robert-koch-institut/mex-ed…
cutoffthetop Oct 24, 2024
49969ca
fix merge
cutoffthetop Oct 24, 2024
167cb37
lock
cutoffthetop Oct 25, 2024
c07ccde
lock and lint
cutoffthetop Oct 28, 2024
2493f98
fix handler
cutoffthetop Oct 29, 2024
f7c03fd
update common, pin requests
cutoffthetop Oct 29, 2024
6ef1c5a
Merge branch 'main' of https://github.com/robert-koch-institut/mex-ed…
cutoffthetop Oct 29, 2024
13e72cc
fix tests
cutoffthetop Oct 29, 2024
0724548
lint
cutoffthetop Oct 29, 2024
54aa8b1
fix test
cutoffthetop Oct 29, 2024
1199332
CL
cutoffthetop Oct 29, 2024
b55ea66
fix tests
cutoffthetop Oct 29, 2024
621efbb
include all pngs
cutoffthetop Oct 29, 2024
9acc637
feature/model update v3 (#137)
cutoffthetop Oct 29, 2024
4069837
fix yaml
cutoffthetop Oct 29, 2024
abed263
Merge branch 'feature/mx-1702-resolve-types' of https://github.com/ro…
cutoffthetop Oct 29, 2024
078887f
CL
cutoffthetop Oct 30, 2024
14df0c5
try multiprocessing.Process
cutoffthetop Oct 30, 2024
891d955
add ResettingGraphConnector to fix tests
cutoffthetop Oct 30, 2024
3ee21ba
enhance log msgs
cutoffthetop Oct 31, 2024
99fc450
make frontend_url a fixture for tests
cutoffthetop Oct 31, 2024
e05ba89
Update ghcr.io/robert-koch-institut/mex-backend Docker tag to v0.21.0…
RKIMetadataExchange Nov 4, 2024
f483a7e
Merge branch 'feature/mx-1702-resolve-types' of https://github.com/ro…
cutoffthetop Nov 4, 2024
6a615c7
Merge remote-tracking branch 'origin/main' into feature/mx-1703-resol…
cutoffthetop Nov 8, 2024
c10be8a
lint
cutoffthetop Nov 8, 2024
89e84ef
Update CHANGELOG.md
cutoffthetop Nov 14, 2024
c63dde7
fix docstr
cutoffthetop Nov 14, 2024
5c52a9d
Update tests/test_transform.py
cutoffthetop Nov 14, 2024
e8430d2
Update tests/test_transform.py
cutoffthetop Nov 14, 2024
7fbb1c5
bail on rendering null
cutoffthetop Nov 14, 2024
1e0977a
Merge branch 'feature/mx-1702-resolve-types' of https://github.com/ro…
cutoffthetop Nov 14, 2024
ea66218
Merge branch 'main' of https://github.com/robert-koch-institut/mex-ed…
cutoffthetop Nov 14, 2024
ec64850
add transform tests
cutoffthetop Nov 14, 2024
d6beb76
update tests
cutoffthetop Nov 14, 2024
550a942
add transform_models_to_preview empty test case
cutoffthetop Nov 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
ports:
- 7687:7687
backend:
image: ghcr.io/robert-koch-institut/mex-backend:0.20.0
image: ghcr.io/robert-koch-institut/mex-backend:0.22.0
env:
MEX_BACKEND_API_USER_DATABASE: ${{ secrets.MEX_BACKEND_API_USER_DATABASE }}
MEX_BACKEND_API_KEY_DATABASE: ${{ secrets.MEX_BACKEND_API_KEY_DATABASE }}
Expand Down Expand Up @@ -91,6 +91,6 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: playwright-screenshots-${{ github.run_id }}
path: tests_*.jpeg
path: test*.png
if-no-files-found: error
retention-days: 10
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

### Changes

- upgrade mex-common and model dependencies to v3
- overhaul and simplify margins and spaces
- move transform_models_to_fields from State to transform module
- use dedicated backend connector methods for edit and search
- use same rendering components for edit and search pages
- pop toasts when backend connector encounters errors
- scroll to top when pagination is triggered
- update dependency versions

### Deprecated
Expand All @@ -18,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- fix routing issues by moving the refresh handlers section.from on_mount to page.on_load
cutoffthetop marked this conversation as resolved.
Show resolved Hide resolved

### Security

## [0.5.0] - 2024-09-19
Expand Down
67 changes: 67 additions & 0 deletions mex/editor/components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import reflex as rx

from mex.editor.edit.models import FixedValue


def fixed_internal_link(value: FixedValue) -> rx.Component:
"""Render a fixed value as a clickable internal link that reloads the edit page."""
return rx.link(
value.text,
href=value.href,
high_contrast=True,
role="link",
)


def fixed_external_link(value: FixedValue) -> rx.Component:
"""Render a fixed value as a clickable external link that opens in a new window."""
return rx.link(
value.text,
href=value.href,
high_contrast=True,
is_external=True,
role="link",
)


def fixed_link(value: FixedValue) -> rx.Component:
"""Render a fixed value as a clickable link that reloads the edit page."""
return rx.cond(
value.external,
fixed_external_link(value),
fixed_internal_link(value),
)


def fixed_text(value: FixedValue) -> rx.Component:
"""Render a fixed value as a text span with language attribute."""
return rx.text(
rababerladuseladim marked this conversation as resolved.
Show resolved Hide resolved
value.text,
as_="span",
)


def postfix_badge(value: FixedValue) -> rx.Component:
"""Render a generic badge after the fixed value."""
return rx.badge(
value.badge,
radius="full",
variant="surface",
style={"margin": "auto 0"},
)


def fixed_value(value: FixedValue) -> rx.Component:
"""Return a single fixed value."""
return rx.hstack(
rx.cond(
value.href,
fixed_link(value),
fixed_text(value),
),
rx.cond(
value.badge,
postfix_badge(value),
),
spacing="1",
)
62 changes: 46 additions & 16 deletions mex/editor/edit/main.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,43 @@
import reflex as rx

from mex.editor.edit.state import EditableField, EditablePrimarySource, EditState
from mex.editor.components import fixed_value
from mex.editor.edit.models import EditableField, EditablePrimarySource, FixedValue
from mex.editor.edit.state import EditState
from mex.editor.layout import page


def editable_value(value: str) -> rx.Component:
"""Return a single card for editing one value."""
def fixed_value_card(
field_name: str, primary_source: str | None, index: int, value: FixedValue
) -> rx.Component:
"""Return a card containing a single fixed value."""
return rx.card(
value,
fixed_value(value),
style={"width": "30vw"},
custom_attrs={"data-testid": f"value-{field_name}_{primary_source}_{index}"},
)


def editable_primary_source(model: EditablePrimarySource) -> rx.Component:
def editable_primary_source(
field_name: str, model: EditablePrimarySource
) -> rx.Component:
"""Return a horizontal grid of cards for editing one primary source."""
return rx.hstack(
rx.card(
model.name,
style={"width": "15vw"},
fixed_value(model.name),
style={"width": "20vw"},
custom_attrs={
"data-testid": f"primary-source-{field_name}_{model.name.text}"
},
),
rx.vstack(
rx.foreach(
model.editable_values,
editable_value,
model.editor_values,
lambda value, index: fixed_value_card(
field_name,
model.name.text,
index,
value,
),
)
),
)
Expand All @@ -32,33 +47,48 @@ def editable_field(model: EditableField) -> rx.Component:
"""Return a horizontal grid of cards for editing one field."""
return rx.hstack(
rx.card(
model.name,
rx.text(model.name),
style={"width": "15vw"},
custom_attrs={"data-testid": f"field-{model.name}"},
),
rx.foreach(
model.primary_sources,
editable_primary_source,
lambda primary_source: editable_primary_source(
model.name,
primary_source,
),
),
role="row",
)


def index() -> rx.Component:
"""Return the index for the edit component."""
return page(
rx.section(
rx.box(
rx.heading(
EditState.item_title,
rx.hstack(
rx.foreach(
EditState.item_title,
fixed_value,
)
),
custom_attrs={"data-testid": "edit-heading"},
style={"margin": "1em 0"},
style={
"margin": "1em 0",
"whiteSpace": "nowrap",
"overflow": "hidden",
"textOverflow": "ellipsis",
"maxWidth": "80%",
},
),
rx.vstack(
rx.foreach(
EditState.fields,
editable_field,
),
),
on_mount=EditState.refresh,
style={"width": "100%"},
style={"width": "100%", "margin": "0 2em 1em"},
custom_attrs={"data-testid": "edit-section"},
),
)
6 changes: 4 additions & 2 deletions mex/editor/edit/models.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import reflex as rx

from mex.editor.models import FixedValue


class EditablePrimarySource(rx.Base):
"""Model for describing the editor state for one primary source."""

name: str
editable_values: list[str]
name: FixedValue
editor_values: list[FixedValue]


class EditableField(rx.Base):
Expand Down
81 changes: 36 additions & 45 deletions mex/editor/edit/state.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,48 @@
from pydantic import BaseModel
import reflex as rx
from reflex.event import EventSpec
from requests import HTTPError

from mex.common.backend_api.connector import BackendApiConnector
from mex.common.models import AnyExtractedModel
from mex.editor.edit.models import EditableField, EditablePrimarySource
from mex.common.logging import logger
from mex.editor.edit.models import EditableField
from mex.editor.edit.transform import transform_models_to_fields
from mex.editor.models import FixedValue
from mex.editor.state import State
from mex.editor.transform import render_any_value, render_model_title


class _BackendSearchResponse(BaseModel):
total: int
items: list[AnyExtractedModel]
from mex.editor.transform import transform_models_to_title


class EditState(State):
"""State for the edit component."""

fields: list[EditableField] = []
item_title: str = ""
item_title: list[FixedValue] = []

def refresh(self) -> None:
"""Refresh the search results."""
def refresh(self) -> EventSpec | None:
"""Refresh the edit page."""
self.reset()
# TODO(ND): use the user auth for backend requests (stop-gap MX-1616)
connector = BackendApiConnector.get()

# TODO(ND): use a specialized extracted-item search method
response = connector.request(
"GET", f"extracted-item?stableTargetId={self.item_id}"
)
items = _BackendSearchResponse.model_validate(response).items
self.fields = self.extracted_to_fields(items)

# TODO(ND): use title of merged item instead of title of first item
self.item_title = render_model_title(items[0])

@staticmethod
def extracted_to_fields(models: list[AnyExtractedModel]) -> list[EditableField]:
"""Convert a list of extracted models into editable field models."""
fields_by_name: dict[str, EditableField] = {}
for model in models:
model_dump = model.model_dump()
for field_name in model.model_fields:
editable_field = fields_by_name.setdefault(
field_name, EditableField(name=field_name, primary_sources=[])
)
value = model_dump[field_name]
if not value:
continue
if not isinstance(value, list):
value = [value]
editable_field.primary_sources.append(
EditablePrimarySource(
name=model.hadPrimarySource,
editable_values=[render_any_value(v) for v in value],
)
)
return list(fields_by_name.values())
try:
response = connector.fetch_extracted_items(
None,
self.item_id,
None,
0,
100,
)
except HTTPError as exc:
self.reset()
logger.error(
"backend error fetching extracted items: %s",
exc.response.text,
exc_info=False,
)
return rx.toast.error(
exc.response.text,
duration=5000,
close_button=True,
dismissible=True,
)
self.item_title = transform_models_to_title(response.items)
self.fields = transform_models_to_fields(response.items)
return None
39 changes: 39 additions & 0 deletions mex/editor/edit/transform.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from collections.abc import Iterable

from mex.common.exceptions import MExError
from mex.common.models import (
MEX_PRIMARY_SOURCE_STABLE_TARGET_ID,
AnyExtractedModel,
AnyRuleModel,
)
from mex.common.types import Identifier
from mex.editor.edit.models import EditableField, EditablePrimarySource
from mex.editor.transform import transform_value, transform_values


def transform_models_to_fields(
models: Iterable[AnyExtractedModel | AnyRuleModel],
) -> list[EditableField]:
"""Convert a list of extracted models into editable field models."""
fields_by_name: dict[str, EditableField] = {}
for model in models:
if isinstance(model, AnyExtractedModel):
primary_source_name = transform_value(Identifier(model.hadPrimarySource))
elif isinstance(model, AnyRuleModel):
primary_source_name = transform_value(MEX_PRIMARY_SOURCE_STABLE_TARGET_ID)
else:
msg = (
"cannot transform model, expected extracted ExtractedData or "
f"RuleItem, got {type(model).__name__}"
)
raise MExError(msg)
for field_name in model.model_fields:
editable_field = EditableField(name=field_name, primary_sources=[])
fields_by_name.setdefault(field_name, editable_field)
if values := transform_values(getattr(model, field_name)):
editable_field.primary_sources.append(
EditablePrimarySource(
name=primary_source_name, editor_values=values
)
)
return list(fields_by_name.values())
5 changes: 1 addition & 4 deletions mex/editor/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ def nav_bar() -> rx.Component:
user_menu(),
rx.spacer(),
rx.color_mode.button(),
id="navbar",
padding="1em",
position="fixed",
style={"background": "var(--accent-4)"},
Expand All @@ -91,11 +90,9 @@ def page(*children: str | rx.Component) -> rx.Component:
rx.hstack(
*children,
min_height="85vh",
margin="2em 1em 1em",
margin="4em 0 0",
spacing="5",
),
on_mount=State.load_page,
on_unmount=State.unload_page,
),
rx.center(
rx.spinner(size="3"),
Expand Down
Loading