Skip to content

Commit

Permalink
Merge pull request writer#581 from FabienArcellier/WF-74-prevent-work…
Browse files Browse the repository at this point in the history
…flows-tree-to-be-sent-during-run-mode

chore: prevent workflows tree to be sent during run mode - Wf 74
  • Loading branch information
ramedina86 authored Oct 10, 2024
2 parents 89ea122 + cc6efb9 commit eb547ee
Show file tree
Hide file tree
Showing 26 changed files with 235 additions and 75 deletions.
10 changes: 10 additions & 0 deletions alfred/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ def apps_update(app: str = None):
'tests/backend/testbasicauth',
'tests/backend/testmultiapp/app1',
'tests/backend/testmultiapp/app2',
'tests/e2e/presets/2columns',
'tests/e2e/presets/2pages',
'tests/e2e/presets/empty_page',
'tests/e2e/presets/jsonviewer',
'tests/e2e/presets/low_code',
'tests/e2e/presets/section',
'tests/e2e/presets/state',
]

for app in apps:
Expand All @@ -35,6 +42,9 @@ def apps_update(app: str = None):
print(f'{app} : migrate ui.json')
wf_project.migrate_obsolete_ui_json(abs_path)

if not os.path.isfile(os.path.join(abs_path, ".wf", 'components-workflows_root.jsonl')):
wf_project.create_default_workflows_root(abs_path)

metadata, components = wf_project.read_files(abs_path)
if metadata.get('writer_version') == writer.VERSION:
print("The app is already up to date")
Expand Down
1 change: 1 addition & 0 deletions apps/ai-starter/.wf/components-workflows_root.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"id": "workflows_root", "type": "workflows_root", "content": {}, "isCodeManaged": false, "position": 0, "handlers": {}, "visible": {"expression": true, "binding": "", "reversed": false}}
1 change: 1 addition & 0 deletions apps/hello/.wf/components-workflows_root.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"id": "workflows_root", "type": "workflows_root", "content": {}, "isCodeManaged": false, "position": 0, "handlers": {}, "visible": {"expression": true, "binding": "", "reversed": false}}
1 change: 1 addition & 0 deletions apps/pdg-tutorial/.wf/components-workflows_root.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"id": "workflows_root", "type": "workflows_root", "content": {}, "isCodeManaged": false, "position": 0, "handlers": {}, "visible": {"expression": true, "binding": "", "reversed": false}}
1 change: 1 addition & 0 deletions apps/quickstart/.wf/components-workflows_root.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"id": "workflows_root", "type": "workflows_root", "content": {}, "isCodeManaged": false, "position": 0, "handlers": {}, "visible": {"expression": true, "binding": "", "reversed": false}}
1 change: 1 addition & 0 deletions apps/text-demo/.wf/components-workflows_root.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"id": "workflows_root", "type": "workflows_root", "content": {}, "isCodeManaged": false, "position": 0, "handlers": {}, "visible": {"expression": true, "binding": "", "reversed": false}}
28 changes: 22 additions & 6 deletions src/writer/app_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,14 @@
from pydantic import ValidationError
from watchdog.observers.polling import PollingObserver

from writer import VERSION, audit_and_fix, wf_project
from writer.core import EventHandlerRegistry, MiddlewareRegistry, WriterSession, use_request_context
from writer import VERSION, audit_and_fix, core_ui, wf_project
from writer.core import (
Config,
EventHandlerRegistry,
MiddlewareRegistry,
WriterSession,
use_request_context,
)
from writer.core_ui import ingest_bmc_component_tree
from writer.ss_types import (
AppProcessServerRequest,
Expand All @@ -34,6 +40,7 @@
InitSessionRequest,
InitSessionRequestPayload,
InitSessionResponsePayload,
ServeMode,
StateContentRequest,
StateContentResponsePayload,
StateEnquiryRequest,
Expand Down Expand Up @@ -83,7 +90,7 @@ def __init__(self,
client_conn: multiprocessing.connection.Connection,
server_conn: multiprocessing.connection.Connection,
app_path: str,
mode: str,
mode: ServeMode,
run_code: str,
bmc_components: Dict,
is_app_process_server_ready: multiprocessing.synchronize.Event,
Expand Down Expand Up @@ -147,11 +154,14 @@ def _handle_session_init(self, payload: InitSessionRequestPayload) -> InitSessio
session.session_state.add_log_entry(
"error", "Serialisation error", tb.format_exc())

ui_component_tree = core_ui.export_component_tree(
session.session_component_tree, mode=writer.Config.mode)

res_payload = InitSessionResponsePayload(
userState=user_state,
sessionId=session.session_id,
mail=session.session_state.mail,
components=session.session_component_tree.to_dict(),
components=ui_component_tree,
userFunctions=self._get_user_functions(),
featureFlags=writer.Config.feature_flags
)
Expand All @@ -177,10 +187,13 @@ def _handle_event(self, session: WriterSession, event: WriterEvent) -> EventResp

mail = session.session_state.mail

ui_component_tree = core_ui.export_component_tree(
session.session_component_tree, mode=Config.mode, only_update=True)

res_payload = EventResponsePayload(
result=result,
mutations=mutations,
components=session.session_component_tree.fetch_updates(),
components=ui_component_tree,
mail=mail
)
session.session_state.clear_mail()
Expand Down Expand Up @@ -591,7 +604,7 @@ def __init__(self, app_path: str, mode: str):
if mode not in ("edit", "run"):
raise ValueError("Invalid mode.")

self.mode = mode
self.mode = cast(ServeMode, mode)
self._set_logger()

def hook_to_running_event_loop(self):
Expand Down Expand Up @@ -686,6 +699,9 @@ def _load_persisted_components(self) -> Dict[str, ComponentDefinition]:
if os.path.isfile(os.path.join(self.app_path, "ui.json")):
wf_project.migrate_obsolete_ui_json(self.app_path)

if not os.path.isfile(os.path.join(self.app_path, ".wf", 'components-workflows_root.jsonl')):
wf_project.create_default_workflows_root(self.app_path)

if not os.path.isdir(os.path.join(self.app_path, ".wf")):
logger.error("Couldn't find .wf in the path provided: %s.", self.app_path)
sys.exit(1)
Expand Down
3 changes: 2 additions & 1 deletion src/writer/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
InstancePath,
InstancePathItem,
Readable,
ServeMode,
WriterEvent,
WriterEventResult,
WriterFileItem,
Expand Down Expand Up @@ -129,7 +130,7 @@ def wrapper(*args, **kwargs):
class Config:

is_mail_enabled_for_log: bool = False
mode: str = "run"
mode: ServeMode = "run"
logger: Optional[logging.Logger] = None
feature_flags: list[str] = []

Expand Down
29 changes: 28 additions & 1 deletion src/writer/core_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from pydantic import BaseModel, Field
from typing_extensions import TypedDict

from writer.ss_types import ComponentDefinition
from writer.ss_types import ComponentDefinition, ServeMode

current_parent_container: ContextVar[Union["Component", None]] = \
ContextVar("current_parent_container")
Expand Down Expand Up @@ -432,6 +432,33 @@ def session_components_list(component_tree: ComponentTree) -> list:
"""
return list(component_tree.branch(Branch.session_cmc).components.values())

def export_component_tree(component_tree: ComponentTree, mode: ServeMode, only_update=False) -> Optional[Dict]:
"""
Exports the component tree to the ui.
>>> filtered_component_tree = core_ui.export_component_tree(session.session_component_tree, mode=writer.Config.mode)
This function filters artifacts that should be hidden from the user, for example workflows in run mode.
:param component_tree: the full component tree
:param mode: the mode of the application (edit, run)
:param updated: return something only if component tree has been updated
:return: a dictionary representing the component tree
"""
if only_update is True and component_tree.updated is False:
return None

roots = ['root']
if mode == "edit":
roots.append('workflows_root')

_components: List[Component] = []
for root in roots:
_root_component = cast(Component, component_tree.get_component(root))
_components.append(_root_component)
_components += component_tree.get_descendents(root)

return {c.id: c.to_dict() for c in _components}

class UIError(Exception):
...
Expand Down
6 changes: 6 additions & 0 deletions src/writer/wf_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,9 @@ def migrate_obsolete_ui_json(app_path: str) -> None:
os.remove(os.path.join(app_path, "ui.json"))
logger.warning('project format has changed and has been migrated with success. ui.json file has been removed.')


def create_default_workflows_root(abs_path: str) -> None:
with io.open(os.path.join(abs_path, '.wf', 'components-workflows_root.jsonl'), 'w') as f:
f.write('{"id": "workflows_root", "type": "workflows_root", "content": {}, "isCodeManaged": false, "position": 0, "handlers": {}, "visible": {"expression": true, "binding": "", "reversed": false}}')
logger = logging.getLogger('writer')
logger.warning('project format has changed and has been migrated with success. components-workflows_root.jsonl has been added.')
72 changes: 72 additions & 0 deletions tests/backend/test_app_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,78 @@ def test_init_wrong_mode(self) -> None:
with pytest.raises(ValueError):
AppRunner(test_app_dir, "virus")

@pytest.mark.asyncio
@pytest.mark.usefixtures("setup_app_runner")
async def test_init_should_not_load_workflow_component_in_run_mode(self, setup_app_runner) -> None:
ar: AppRunner
with setup_app_runner(test_app_dir, "run", load = True) as ar:
response = await ar.init_session(InitSessionRequestPayload(
cookies={},
headers={},
proposedSessionId=self.proposed_session_id
))

assert response.payload.components.get("workflows_root") is None

@pytest.mark.asyncio
@pytest.mark.usefixtures("setup_app_runner")
async def test_init_should_load_workflow_component_in_edit_mode(self, setup_app_runner) -> None:
ar: AppRunner
with setup_app_runner(test_app_dir, "edit", load = True) as ar:
response = await ar.init_session(InitSessionRequestPayload(
cookies={},
headers={},
proposedSessionId=self.proposed_session_id
))

assert response.payload.components.get("workflows_root") is not None

@pytest.mark.asyncio
@pytest.mark.usefixtures("setup_app_runner")
async def test_backend_ui_event_should_not_load_workflow_component_in_run_mode(self, setup_app_runner) -> None:
ar: AppRunner
with setup_app_runner(test_app_dir, "run", load = True) as ar:
await init_app_session(ar, session_id=self.proposed_session_id)

ev_req = EventRequest(type="event", payload=WriterEvent(
type="wf-click",
instancePath=[
{"componentId": "root", "instanceNumber": 0},
{"componentId": "bb4d0e86-619e-4367-a180-be28ab6059f4", "instanceNumber": 0},
{"componentId": "92a2c0c8-7ab4-4865-b7eb-ed437408c8f5", "instanceNumber": 0},
{"componentId": "d1e01ce1-fab1-4a6e-91a1-1f45f9e57aa5", "instanceNumber": 0},
{"componentId": "9c30af6d-4ee5-4782-9169-0f361d67fa76", "instanceNumber": 0},
{"componentId": "8ykyk5avd9ioyr6l", "instanceNumber": 0},
],
payload={}
))

rev = await ar.dispatch_message(self.proposed_session_id, ev_req)
assert rev.payload.components.get("workflows_root") is None

@pytest.mark.asyncio
@pytest.mark.usefixtures("setup_app_runner")
async def test_backend_ui_event_should_load_workflow_component_in_edit_mode(self, setup_app_runner) -> None:
ar: AppRunner
with setup_app_runner(test_app_dir, "edit", load = True) as ar:
await init_app_session(ar, session_id=self.proposed_session_id)

ev_req = EventRequest(type="event", payload=WriterEvent(
type="wf-click",
instancePath=[
{"componentId": "root", "instanceNumber": 0},
{"componentId": "bb4d0e86-619e-4367-a180-be28ab6059f4", "instanceNumber": 0},
{"componentId": "92a2c0c8-7ab4-4865-b7eb-ed437408c8f5", "instanceNumber": 0},
{"componentId": "d1e01ce1-fab1-4a6e-91a1-1f45f9e57aa5", "instanceNumber": 0},
{"componentId": "9c30af6d-4ee5-4782-9169-0f361d67fa76", "instanceNumber": 0},
{"componentId": "8ykyk5avd9ioyr6l", "instanceNumber": 0},
],
payload={}
))

rev = await ar.dispatch_message(self.proposed_session_id, ev_req)
assert rev.payload.components.get("workflows_root") is not None

@pytest.mark.asyncio
@pytest.mark.usefixtures("setup_app_runner")
async def test_pre_session(self, setup_app_runner) -> None:
Expand Down
6 changes: 3 additions & 3 deletions tests/backend/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ def cumulative_sum(state):
"total": 0
})

initial_state.subscribe_mutation('a\.b', cumulative_sum)
initial_state.subscribe_mutation(r'a\.b', cumulative_sum)

# Acts
initial_state['a.b'] = 1
Expand Down Expand Up @@ -1708,6 +1708,6 @@ def test_parse_state_variable_expression_should_process_expression():
# When
assert parse_state_variable_expression('features') == ['features']
assert parse_state_variable_expression('features.eyes') == ['features', 'eyes']
assert parse_state_variable_expression('features\.eyes') == ['features.eyes']
assert parse_state_variable_expression('features\.eyes.color') == ['features.eyes', 'color']
assert parse_state_variable_expression(r'features\.eyes') == ['features.eyes']
assert parse_state_variable_expression(r'features\.eyes.color') == ['features.eyes', 'color']

Loading

0 comments on commit eb547ee

Please sign in to comment.