vedana-backoffice 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,483 @@
1
+ import reflex as rx
2
+
3
+ from vedana_backoffice.states.main_dashboard import DashboardState
4
+ from vedana_backoffice.ui import app_header
5
+
6
+
7
+ def _stat_tile(title: str, value: rx.Var | str, subtitle: str = "", color: str = "indigo") -> rx.Component:
8
+ return rx.card(
9
+ rx.vstack(
10
+ rx.text(title, size="1", color="gray"),
11
+ rx.heading(value, size="6"),
12
+ rx.cond(subtitle != "", rx.text(subtitle, size="1", color="gray"), rx.box()),
13
+ spacing="1",
14
+ width="100%",
15
+ ),
16
+ variant="surface",
17
+ width="100%",
18
+ padding="1em",
19
+ style={"borderTop": f"3px solid var(--{color}-9)"},
20
+ )
21
+
22
+
23
+ def _graph_stats_card() -> rx.Component:
24
+ return rx.card(
25
+ rx.vstack(
26
+ rx.hstack(
27
+ rx.heading("Graph stats", size="3"),
28
+ rx.spacer(),
29
+ rx.badge(
30
+ rx.cond(
31
+ (DashboardState.nodes_total_diff == 0) & (DashboardState.edges_total_diff == 0), # type: ignore[operator]
32
+ "OK",
33
+ "Warning!",
34
+ ),
35
+ color_scheme=rx.cond( # type: ignore[operator]
36
+ (DashboardState.nodes_total_diff == 0) & (DashboardState.edges_total_diff == 0),
37
+ "green",
38
+ "red",
39
+ ),
40
+ variant="soft",
41
+ ),
42
+ align="center",
43
+ width="100%",
44
+ ),
45
+ rx.grid(
46
+ _stat_tile(
47
+ "Total Nodes",
48
+ DashboardState.graph_total_nodes, # type: ignore[arg-type]
49
+ subtitle=rx.cond( # type: ignore[arg-type]
50
+ DashboardState.nodes_total_diff == 0, # type: ignore[operator]
51
+ "Matches pipeline",
52
+ rx.cond( # type: ignore[arg-type]
53
+ DashboardState.nodes_total_diff > 0,
54
+ rx.text(f"+{DashboardState.nodes_total_diff} vs ETL", color_scheme="red", weight="bold"),
55
+ rx.text(f"{DashboardState.nodes_total_diff} vs ETL", color_scheme="red", weight="bold"),
56
+ ),
57
+ ),
58
+ color="green",
59
+ ),
60
+ _stat_tile(
61
+ "Total Edges",
62
+ DashboardState.graph_total_edges, # type: ignore[arg-type]
63
+ subtitle=rx.cond( # type: ignore[arg-type]
64
+ DashboardState.edges_total_diff == 0, # type: ignore[operator]
65
+ "Matches pipeline",
66
+ rx.cond( # type: ignore[operator]
67
+ DashboardState.edges_total_diff > 0,
68
+ rx.text(f"+{DashboardState.edges_total_diff} vs ETL", color_scheme="red", weight="bold"),
69
+ rx.text(f"{DashboardState.edges_total_diff} vs ETL", color_scheme="red", weight="bold"),
70
+ ),
71
+ ),
72
+ color="green",
73
+ ),
74
+ columns="2",
75
+ spacing="4",
76
+ width="100%",
77
+ ),
78
+ rx.grid(
79
+ _stat_tile("Nodes Added", DashboardState.new_nodes, color="indigo"), # type: ignore[arg-type]
80
+ _stat_tile("Edges Added", DashboardState.new_edges, color="indigo"), # type: ignore[arg-type]
81
+ _stat_tile("Nodes Updated", DashboardState.updated_nodes, color="amber"), # type: ignore[arg-type]
82
+ _stat_tile("Edges Updated", DashboardState.updated_edges, color="amber"), # type: ignore[arg-type]
83
+ _stat_tile("Nodes Deleted", DashboardState.deleted_nodes, color="red"), # type: ignore[arg-type]
84
+ _stat_tile("Edges Deleted", DashboardState.deleted_edges, color="red"), # type: ignore[arg-type]
85
+ columns="2",
86
+ spacing="4",
87
+ width="100%",
88
+ ),
89
+ spacing="3",
90
+ width="100%",
91
+ ),
92
+ padding="1em",
93
+ width="100%",
94
+ style={"height": "100%", "display": "flex", "flexDirection": "column"},
95
+ )
96
+
97
+
98
+ def _changes_preview_table() -> rx.Component:
99
+ """Table with expandable cells for changes preview."""
100
+
101
+ def _expandable_cell(row: dict[str, rx.Var], col: rx.Var) -> rx.Component:
102
+ """Create an expandable/collapsible cell for long text content."""
103
+ row_id = row.get("row_id", "")
104
+ return rx.table.cell(
105
+ rx.box(
106
+ rx.cond(
107
+ row.get("expanded", False),
108
+ rx.text(
109
+ row.get(col, "—"), # type: ignore[call-overload]
110
+ size="1",
111
+ white_space="pre-wrap",
112
+ style={"wordBreak": "break-word"},
113
+ ),
114
+ rx.text(
115
+ row.get(col, "—"), # type: ignore[call-overload]
116
+ size="1",
117
+ style={
118
+ "display": "-webkit-box",
119
+ "WebkitLineClamp": "2",
120
+ "WebkitBoxOrient": "vertical",
121
+ "overflow": "hidden",
122
+ "textOverflow": "ellipsis",
123
+ "maxWidth": "400px",
124
+ "wordBreak": "break-word",
125
+ },
126
+ ),
127
+ ),
128
+ cursor="pointer",
129
+ on_click=DashboardState.toggle_changes_preview_row_expand(row_id=row_id), # type: ignore[arg-type,call-arg,func-returns-value]
130
+ style={"minWidth": "0", "width": "100%"},
131
+ ),
132
+ style={"minWidth": "0"},
133
+ )
134
+
135
+ def _make_row_renderer(row: dict[str, rx.Var]):
136
+ """Create a column renderer that captures the row context."""
137
+ return lambda col: _expandable_cell(row, col)
138
+
139
+ def _row(r: dict[str, rx.Var]) -> rx.Component:
140
+ return rx.table.row(
141
+ rx.foreach(DashboardState.changes_preview_columns, _make_row_renderer(r)), # type: ignore[arg-type]
142
+ style=r.get("row_style", {}),
143
+ )
144
+
145
+ return rx.table.root(
146
+ rx.table.header(
147
+ rx.table.row(
148
+ rx.foreach(
149
+ DashboardState.changes_preview_columns, # type: ignore[arg-type]
150
+ lambda c: rx.table.column_header_cell(c),
151
+ )
152
+ )
153
+ ),
154
+ rx.table.body(rx.foreach(DashboardState.changes_preview_rows, _row)), # type: ignore[arg-type]
155
+ variant="surface",
156
+ style={"width": "100%", "tableLayout": "auto"},
157
+ )
158
+
159
+
160
+ def _changes_preview_dialog() -> rx.Component:
161
+ return rx.dialog.root(
162
+ rx.dialog.content(
163
+ rx.vstack(
164
+ rx.hstack(
165
+ rx.dialog.title(
166
+ rx.cond(
167
+ DashboardState.changes_preview_table_name, # type: ignore[operator]
168
+ DashboardState.changes_preview_table_name, # type: ignore[arg-type]
169
+ "",
170
+ ),
171
+ size="4",
172
+ ),
173
+ rx.spacer(),
174
+ rx.dialog.close(
175
+ rx.button("Close", variant="ghost", color_scheme="gray", size="1"),
176
+ ),
177
+ align="center",
178
+ width="100%",
179
+ ),
180
+ rx.cond(
181
+ DashboardState.changes_has_preview, # type: ignore[operator]
182
+ rx.vstack(
183
+ rx.scroll_area(
184
+ _changes_preview_table(),
185
+ type="always",
186
+ scrollbars="both",
187
+ style={"maxHeight": "68vh", "maxWidth": "calc(90vw - 3em)"},
188
+ ),
189
+ # Server-side pagination controls
190
+ rx.hstack(
191
+ rx.text(DashboardState.changes_preview_rows_display, size="2", color="gray"), # type: ignore[arg-type]
192
+ # Color legend
193
+ rx.hstack(
194
+ rx.hstack(
195
+ rx.box(
196
+ style={
197
+ "width": "12px",
198
+ "height": "12px",
199
+ "backgroundColor": "rgba(34,197,94,0.08)",
200
+ "borderRadius": "2px",
201
+ }
202
+ ),
203
+ rx.text("Added", size="1", color="gray"),
204
+ spacing="1",
205
+ align="center",
206
+ ),
207
+ rx.hstack(
208
+ rx.box(
209
+ style={
210
+ "width": "12px",
211
+ "height": "12px",
212
+ "backgroundColor": "rgba(245,158,11,0.08)",
213
+ "borderRadius": "2px",
214
+ }
215
+ ),
216
+ rx.text("Updated", size="1", color="gray"),
217
+ spacing="1",
218
+ align="center",
219
+ ),
220
+ rx.hstack(
221
+ rx.box(
222
+ style={
223
+ "width": "12px",
224
+ "height": "12px",
225
+ "backgroundColor": "rgba(239,68,68,0.08)",
226
+ "borderRadius": "2px",
227
+ }
228
+ ),
229
+ rx.text("Deleted", size="1", color="gray"),
230
+ spacing="1",
231
+ align="center",
232
+ ),
233
+ spacing="3",
234
+ align="center",
235
+ ),
236
+ rx.spacer(),
237
+ rx.hstack(
238
+ rx.button(
239
+ "⏮",
240
+ variant="soft",
241
+ size="1",
242
+ on_click=DashboardState.changes_preview_first_page,
243
+ disabled=~DashboardState.changes_preview_has_prev, # type: ignore[operator]
244
+ ),
245
+ rx.button(
246
+ "← Prev",
247
+ variant="soft",
248
+ size="1",
249
+ on_click=DashboardState.changes_preview_prev_page,
250
+ disabled=~DashboardState.changes_preview_has_prev, # type: ignore[operator]
251
+ ),
252
+ rx.text(
253
+ DashboardState.changes_preview_page_display, # type: ignore[arg-type]
254
+ size="2",
255
+ style={"minWidth": "100px", "textAlign": "center"},
256
+ ),
257
+ rx.button(
258
+ "Next →",
259
+ variant="soft",
260
+ size="1",
261
+ on_click=DashboardState.changes_preview_next_page,
262
+ disabled=~DashboardState.changes_preview_has_next, # type: ignore[operator]
263
+ ),
264
+ rx.button(
265
+ "⏭",
266
+ variant="soft",
267
+ size="1",
268
+ on_click=DashboardState.changes_preview_last_page,
269
+ disabled=~DashboardState.changes_preview_has_next, # type: ignore[operator]
270
+ ),
271
+ spacing="2",
272
+ align="center",
273
+ ),
274
+ width="100%",
275
+ align="center",
276
+ padding_top="0.5em",
277
+ ),
278
+ width="100%",
279
+ spacing="2",
280
+ ),
281
+ rx.box(rx.text("No updates in selected span")),
282
+ ),
283
+ spacing="3",
284
+ width="100%",
285
+ ),
286
+ style={
287
+ "maxWidth": "90vw",
288
+ "maxHeight": "85vh",
289
+ "width": "fit-content",
290
+ "minWidth": "400px",
291
+ },
292
+ ),
293
+ open=DashboardState.changes_preview_open, # type: ignore[arg-type]
294
+ on_open_change=DashboardState.set_changes_preview_open, # type: ignore[arg-type]
295
+ )
296
+
297
+
298
+ def _graph_stats_expanded_card() -> rx.Component:
299
+ return rx.card(
300
+ rx.grid(
301
+ _per_label_stats_table("Graph Nodes by Label", DashboardState.graph_nodes_by_label, "nodes"), # type: ignore[arg-type]
302
+ _per_label_stats_table("Graph Edges by Type", DashboardState.graph_edges_by_type, "edges"), # type: ignore[arg-type]
303
+ columns="1", # type: ignore[arg-type]
304
+ spacing="4", # type: ignore[arg-type]
305
+ width="100%",
306
+ style={"height": "100%"},
307
+ ),
308
+ padding="1em",
309
+ style={"height": "100%", "display": "flex", "flexDirection": "column"},
310
+ )
311
+
312
+
313
+ def _ingest_card() -> rx.Component:
314
+ return rx.card(
315
+ rx.vstack(
316
+ rx.hstack(
317
+ rx.heading("Ingest Activity", size="3"),
318
+ rx.spacer(),
319
+ # rx.text("Generator tables", size="1", color="gray"),
320
+ align="center",
321
+ width="100%",
322
+ ),
323
+ rx.grid(
324
+ _stat_tile("New Entries", DashboardState.ingest_new_total, color="indigo"), # type: ignore[arg-type]
325
+ _stat_tile("Updated Entries", DashboardState.ingest_updated_total, color="amber"), # type: ignore[arg-type]
326
+ _stat_tile("Deleted Entries", DashboardState.ingest_deleted_total, color="red"), # type: ignore[arg-type]
327
+ columns="3",
328
+ spacing="4",
329
+ width="100%",
330
+ ),
331
+ rx.table.root(
332
+ rx.table.header(
333
+ rx.table.row(
334
+ rx.table.column_header_cell("Table"),
335
+ rx.table.column_header_cell("Total"),
336
+ rx.table.column_header_cell("Added"),
337
+ rx.table.column_header_cell("Updated"),
338
+ rx.table.column_header_cell("Deleted"),
339
+ )
340
+ ),
341
+ rx.table.body(
342
+ rx.foreach(
343
+ DashboardState.ingest_breakdown, # type: ignore[arg-type]
344
+ lambda r: rx.table.row(
345
+ rx.table.cell(rx.text(r.get("table", ""))),
346
+ # todo order by ?
347
+ rx.table.cell(rx.text(r.get("total", 0))), # type: ignore[arg-type]
348
+ rx.table.cell(rx.text(r.get("added", 0))), # type: ignore[arg-type]
349
+ rx.table.cell(rx.text(r.get("updated", 0))), # type: ignore[arg-type]
350
+ rx.table.cell(rx.text(r.get("deleted", 0))), # type: ignore[arg-type]
351
+ on_click=DashboardState.open_changes_preview(table_name=r.get("table", "")), # type: ignore
352
+ style={"cursor": "pointer"},
353
+ ),
354
+ )
355
+ ),
356
+ variant="surface",
357
+ style={"width": "100%", "tableLayout": "fixed"},
358
+ ),
359
+ spacing="3",
360
+ width="100%",
361
+ ),
362
+ padding="1em",
363
+ width="100%",
364
+ style={"height": "100%", "display": "flex", "flexDirection": "column"},
365
+ )
366
+
367
+
368
+ def _per_label_stats_table(title: str, rows: rx.Var | list[dict], kind: str) -> rx.Component:
369
+ def _row(r: dict) -> rx.Component:
370
+ label = r.get("label", "")
371
+ return rx.table.row(
372
+ rx.table.cell(rx.text(label)),
373
+ rx.table.cell(rx.text(r.get("graph_count", 0))),
374
+ rx.table.cell(rx.text(r.get("etl_count", 0))),
375
+ rx.table.cell(rx.text(r.get("added", 0))),
376
+ rx.table.cell(rx.text(r.get("updated", 0))),
377
+ rx.table.cell(rx.text(r.get("deleted", 0))),
378
+ on_click=rx.cond(
379
+ kind == "nodes",
380
+ DashboardState.open_graph_per_label_changes_preview(kind="nodes", label=label), # type: ignore
381
+ DashboardState.open_graph_per_label_changes_preview(kind="edges", label=label), # type: ignore
382
+ ),
383
+ style={"cursor": "pointer"},
384
+ )
385
+
386
+ return rx.vstack(
387
+ rx.hstack(rx.heading(title, size="3"), align="center", justify="between", width="100%"),
388
+ rx.box(
389
+ rx.scroll_area(
390
+ rx.table.root(
391
+ rx.table.header(
392
+ rx.table.row(
393
+ rx.table.column_header_cell("Label"),
394
+ rx.table.column_header_cell("Graph"),
395
+ rx.table.column_header_cell("ETL"),
396
+ rx.table.column_header_cell("Added"),
397
+ rx.table.column_header_cell("Updated"),
398
+ rx.table.column_header_cell("Deleted"),
399
+ )
400
+ ),
401
+ rx.table.body(rx.foreach(rows, _row)),
402
+ variant="surface",
403
+ style={"width": "100%", "tableLayout": "fixed"},
404
+ ),
405
+ type="always",
406
+ scrollbars="vertical",
407
+ style={
408
+ "position": "absolute",
409
+ "top": 0,
410
+ "bottom": 0,
411
+ "left": 0,
412
+ "right": 0,
413
+ },
414
+ ),
415
+ style={
416
+ "position": "relative",
417
+ "flex": "1 1 0",
418
+ "minHeight": 0,
419
+ "width": "100%",
420
+ },
421
+ ),
422
+ spacing="2",
423
+ style={"height": "100%", "display": "flex", "flexDirection": "column"},
424
+ )
425
+
426
+
427
+ def page() -> rx.Component:
428
+ return rx.vstack(
429
+ app_header(),
430
+ rx.card(
431
+ rx.vstack(
432
+ rx.hstack(
433
+ rx.hstack(
434
+ rx.text("ETL Overview", weight="medium"),
435
+ align="center",
436
+ spacing="3",
437
+ ),
438
+ rx.spacer(),
439
+ rx.hstack(
440
+ rx.text("Timeframe (days):", size="1", color="gray"),
441
+ rx.select(
442
+ items=DashboardState.time_window_options, # type: ignore[arg-type]
443
+ value="1",
444
+ on_change=DashboardState.set_time_window_days, # type: ignore[arg-type]
445
+ width="8em",
446
+ ),
447
+ rx.button(
448
+ rx.cond(DashboardState.loading, "Refreshing…", "Refresh"), # type: ignore[operator]
449
+ on_click=DashboardState.load_dashboard, # type: ignore[arg-type]
450
+ loading=DashboardState.loading,
451
+ size="2",
452
+ ),
453
+ spacing="3",
454
+ align="center",
455
+ ),
456
+ align="center",
457
+ width="100%",
458
+ ),
459
+ rx.grid(
460
+ _ingest_card(),
461
+ _graph_stats_card(),
462
+ _graph_stats_expanded_card(),
463
+ columns="3",
464
+ spacing="4",
465
+ width="100%",
466
+ align="start",
467
+ style={"gridTemplateColumns": "3fr 2fr 5fr", "height": "90vh", "align-items": "stretch"},
468
+ ),
469
+ ),
470
+ padding="1em",
471
+ width="100%",
472
+ ),
473
+ rx.cond(
474
+ DashboardState.error_message != "", # type: ignore[operator]
475
+ rx.callout(DashboardState.error_message, color_scheme="red", variant="soft"), # type: ignore[arg-type]
476
+ rx.box(),
477
+ ),
478
+ _changes_preview_dialog(),
479
+ spacing="4",
480
+ align="start",
481
+ padding="1em",
482
+ width="100%",
483
+ )
File without changes
@@ -0,0 +1,39 @@
1
+ import signal
2
+ import subprocess
3
+ import sys
4
+ from pathlib import Path
5
+
6
+
7
+ def shutdown(signum, frame):
8
+ """Handle shutdown gracefully"""
9
+ print("Shutting down services...")
10
+ sys.exit(0)
11
+
12
+
13
+ def main():
14
+ """Start vedana backoffice services"""
15
+ # Set up signal handlers
16
+ signal.signal(signal.SIGTERM, shutdown)
17
+ signal.signal(signal.SIGINT, shutdown)
18
+
19
+ # Get Caddyfile path from package
20
+ caddyfile_path = Path(__file__).parent / "Caddyfile"
21
+
22
+ print("Starting Caddy...")
23
+ caddy = subprocess.Popen(["caddy", "run", "--config", str(caddyfile_path)])
24
+
25
+ print("Starting backend application...")
26
+ backend = subprocess.Popen(["reflex", "run", "--env", "prod", "--backend-only"])
27
+
28
+ print(f"Services started. PIDs: Caddy={caddy.pid}, Backend={backend.pid}")
29
+ print("Waiting for services...")
30
+
31
+ # Wait for either process to exit
32
+ try:
33
+ backend.wait()
34
+ except KeyboardInterrupt:
35
+ pass
36
+
37
+
38
+ if __name__ == "__main__":
39
+ main()
File without changes
File without changes