Oysiyl commited on
Commit
9ade7f9
·
1 Parent(s): e434cff

feat: add pagination to My QR Codes dashboard

Browse files
Files changed (3) hide show
  1. app.py +186 -6
  2. hf_dashboard.py +23 -6
  3. tests-unit/hf_dashboard_test.py +29 -2
app.py CHANGED
@@ -340,6 +340,26 @@ def _coerce_pil_image(image: Any) -> Image.Image:
340
  return Image.fromarray(np.asarray(image))
341
 
342
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  def _dashboard_empty_payload(message: str):
344
  return (
345
  message,
@@ -347,6 +367,10 @@ def _dashboard_empty_payload(message: str):
347
  [],
348
  [],
349
  [],
 
 
 
 
350
  None,
351
  "Click a row in the table to inspect analytics and reload settings.",
352
  "",
@@ -397,6 +421,10 @@ def _load_my_qr_dashboard(
397
  [],
398
  records,
399
  [],
 
 
 
 
400
  None,
401
  "No saved QR codes yet. Generate one while signed in, then come back here.",
402
  "",
@@ -410,13 +438,17 @@ def _load_my_qr_dashboard(
410
  gr.update(visible=False),
411
  )
412
 
413
- table_rows, row_map, summary = build_generation_table_rows(records)
414
  return (
415
  str(payload.get("status") or "Loaded."),
416
  summary,
417
  table_rows,
418
  records,
419
  row_map,
 
 
 
 
420
  None,
421
  "Click a row in the table to inspect a saved QR and reload settings.",
422
  "",
@@ -530,21 +562,81 @@ def _select_my_qr_generation_from_table(
530
  return _select_my_qr_generation(records, selected_index, request)
531
 
532
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
  def _update_my_qr_destination(
534
  records: list[dict[str, Any]],
 
535
  selected_index: int | None,
536
  new_destination: str,
537
  request: Union[gr.Request, None] = None,
538
  ):
539
  if not records or selected_index is None:
540
  detail = _select_my_qr_generation(records, None, request)
541
- table_rows, row_map, summary = build_generation_table_rows(records)
 
 
 
542
  return (
543
  "Select a saved QR first.",
544
  summary,
545
  table_rows,
546
  records,
547
  row_map,
 
 
 
 
548
  *detail,
549
  )
550
 
@@ -557,30 +649,47 @@ def _update_my_qr_destination(
557
  )
558
  except DashboardError as exc:
559
  detail = _select_my_qr_generation(records, selected_index, request)
560
- table_rows, row_map, summary = build_generation_table_rows(records)
 
 
 
561
  return (
562
  f"Could not update QR destination: {exc}",
563
  summary,
564
  table_rows,
565
  records,
566
  row_map,
 
 
 
 
567
  *detail,
568
  )
569
  except Exception as exc:
570
  print("[dashboard-update-destination]", traceback.format_exc())
571
  detail = _select_my_qr_generation(records, selected_index, request)
572
- table_rows, row_map, summary = build_generation_table_rows(records)
 
 
 
573
  return (
574
  f"Could not update QR destination: {_format_exception_message(exc)}",
575
  summary,
576
  table_rows,
577
  records,
578
  row_map,
 
 
 
 
579
  *detail,
580
  )
581
 
582
  updated_records = result["records"]
583
- table_rows, row_map, summary = build_generation_table_rows(updated_records)
 
 
 
584
  status = "Updated QR destination."
585
  short_url = str(result.get("short_url") or "").strip()
586
  normalized_url = str(result.get("normalized_url") or "").strip()
@@ -594,6 +703,10 @@ def _update_my_qr_destination(
594
  table_rows,
595
  updated_records,
596
  row_map,
 
 
 
 
597
  *detail,
598
  )
599
 
@@ -6814,6 +6927,7 @@ with gr.Blocks(delete_cache=(3600, 3600), css=DASHBOARD_CSS) as demo:
6814
  )
6815
  my_qr_records_state = gr.State([])
6816
  my_qr_row_map_state = gr.State([])
 
6817
  my_qr_selected_index_state = gr.State(value=None)
6818
  my_qr_table_summary = gr.Markdown("No saved QR codes loaded yet.")
6819
  my_qr_table = gr.Dataframe(
@@ -6826,6 +6940,10 @@ with gr.Blocks(delete_cache=(3600, 3600), css=DASHBOARD_CSS) as demo:
6826
  value=[],
6827
  wrap=True,
6828
  )
 
 
 
 
6829
  with gr.Row():
6830
  with gr.Column(scale=1):
6831
  my_qr_detail_image = gr.HTML(
@@ -6892,6 +7010,10 @@ with gr.Blocks(delete_cache=(3600, 3600), css=DASHBOARD_CSS) as demo:
6892
  my_qr_table,
6893
  my_qr_records_state,
6894
  my_qr_row_map_state,
 
 
 
 
6895
  my_qr_selected_index_state,
6896
  my_qr_detail_markdown,
6897
  my_qr_detail_image,
@@ -6915,6 +7037,10 @@ with gr.Blocks(delete_cache=(3600, 3600), css=DASHBOARD_CSS) as demo:
6915
  my_qr_table,
6916
  my_qr_records_state,
6917
  my_qr_row_map_state,
 
 
 
 
6918
  my_qr_selected_index_state,
6919
  my_qr_detail_markdown,
6920
  my_qr_detail_image,
@@ -6947,15 +7073,69 @@ with gr.Blocks(delete_cache=(3600, 3600), css=DASHBOARD_CSS) as demo:
6947
  ],
6948
  )
6949
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6950
  update_my_qr_destination_btn.click(
6951
  fn=_update_my_qr_destination,
6952
- inputs=[my_qr_records_state, my_qr_selected_index_state, my_qr_new_destination],
6953
  outputs=[
6954
  dashboard_status,
6955
  my_qr_table_summary,
6956
  my_qr_table,
6957
  my_qr_records_state,
6958
  my_qr_row_map_state,
 
 
 
 
6959
  my_qr_selected_index_state,
6960
  my_qr_detail_markdown,
6961
  my_qr_detail_image,
 
340
  return Image.fromarray(np.asarray(image))
341
 
342
 
343
+ def _build_dashboard_page_view(
344
+ records: list[dict[str, Any]],
345
+ current_page: int = 1,
346
+ ):
347
+ table_rows, row_map, summary, safe_page, total_pages = build_generation_table_rows(
348
+ records,
349
+ page=current_page,
350
+ )
351
+ page_label = f"Page {safe_page} of {total_pages}"
352
+ return (
353
+ summary,
354
+ table_rows,
355
+ row_map,
356
+ safe_page,
357
+ page_label,
358
+ gr.update(interactive=safe_page > 1),
359
+ gr.update(interactive=safe_page < total_pages),
360
+ )
361
+
362
+
363
  def _dashboard_empty_payload(message: str):
364
  return (
365
  message,
 
367
  [],
368
  [],
369
  [],
370
+ 1,
371
+ "",
372
+ gr.update(interactive=False),
373
+ gr.update(interactive=False),
374
  None,
375
  "Click a row in the table to inspect analytics and reload settings.",
376
  "",
 
421
  [],
422
  records,
423
  [],
424
+ 1,
425
+ "",
426
+ gr.update(interactive=False),
427
+ gr.update(interactive=False),
428
  None,
429
  "No saved QR codes yet. Generate one while signed in, then come back here.",
430
  "",
 
438
  gr.update(visible=False),
439
  )
440
 
441
+ summary, table_rows, row_map, safe_page, page_label, prev_update, next_update = _build_dashboard_page_view(records, 1)
442
  return (
443
  str(payload.get("status") or "Loaded."),
444
  summary,
445
  table_rows,
446
  records,
447
  row_map,
448
+ safe_page,
449
+ page_label,
450
+ prev_update,
451
+ next_update,
452
  None,
453
  "Click a row in the table to inspect a saved QR and reload settings.",
454
  "",
 
562
  return _select_my_qr_generation(records, selected_index, request)
563
 
564
 
565
+ def _change_my_qr_page(
566
+ records: list[dict[str, Any]],
567
+ current_page: int,
568
+ delta: int,
569
+ ):
570
+ if not records:
571
+ return (
572
+ "",
573
+ [],
574
+ [],
575
+ 1,
576
+ "",
577
+ gr.update(interactive=False),
578
+ gr.update(interactive=False),
579
+ None,
580
+ "No saved QR codes are loaded yet.",
581
+ "",
582
+ [],
583
+ "",
584
+ "",
585
+ "{}",
586
+ gr.update(visible=False),
587
+ gr.update(visible=False),
588
+ gr.update(visible=False),
589
+ gr.update(visible=False),
590
+ )
591
+ summary, table_rows, row_map, safe_page, page_label, prev_update, next_update = _build_dashboard_page_view(
592
+ records,
593
+ current_page + delta,
594
+ )
595
+ return (
596
+ summary,
597
+ table_rows,
598
+ row_map,
599
+ safe_page,
600
+ page_label,
601
+ prev_update,
602
+ next_update,
603
+ None,
604
+ "Click a row in the table to inspect a saved QR and reload settings.",
605
+ "",
606
+ [],
607
+ "",
608
+ "",
609
+ "{}",
610
+ gr.update(visible=False),
611
+ gr.update(visible=False),
612
+ gr.update(visible=False),
613
+ gr.update(visible=False),
614
+ )
615
+
616
+
617
  def _update_my_qr_destination(
618
  records: list[dict[str, Any]],
619
+ current_page: int,
620
  selected_index: int | None,
621
  new_destination: str,
622
  request: Union[gr.Request, None] = None,
623
  ):
624
  if not records or selected_index is None:
625
  detail = _select_my_qr_generation(records, None, request)
626
+ summary, table_rows, row_map, safe_page, page_label, prev_update, next_update = _build_dashboard_page_view(
627
+ records,
628
+ current_page,
629
+ )
630
  return (
631
  "Select a saved QR first.",
632
  summary,
633
  table_rows,
634
  records,
635
  row_map,
636
+ safe_page,
637
+ page_label,
638
+ prev_update,
639
+ next_update,
640
  *detail,
641
  )
642
 
 
649
  )
650
  except DashboardError as exc:
651
  detail = _select_my_qr_generation(records, selected_index, request)
652
+ summary, table_rows, row_map, safe_page, page_label, prev_update, next_update = _build_dashboard_page_view(
653
+ records,
654
+ current_page,
655
+ )
656
  return (
657
  f"Could not update QR destination: {exc}",
658
  summary,
659
  table_rows,
660
  records,
661
  row_map,
662
+ safe_page,
663
+ page_label,
664
+ prev_update,
665
+ next_update,
666
  *detail,
667
  )
668
  except Exception as exc:
669
  print("[dashboard-update-destination]", traceback.format_exc())
670
  detail = _select_my_qr_generation(records, selected_index, request)
671
+ summary, table_rows, row_map, safe_page, page_label, prev_update, next_update = _build_dashboard_page_view(
672
+ records,
673
+ current_page,
674
+ )
675
  return (
676
  f"Could not update QR destination: {_format_exception_message(exc)}",
677
  summary,
678
  table_rows,
679
  records,
680
  row_map,
681
+ safe_page,
682
+ page_label,
683
+ prev_update,
684
+ next_update,
685
  *detail,
686
  )
687
 
688
  updated_records = result["records"]
689
+ summary, table_rows, row_map, safe_page, page_label, prev_update, next_update = _build_dashboard_page_view(
690
+ updated_records,
691
+ current_page,
692
+ )
693
  status = "Updated QR destination."
694
  short_url = str(result.get("short_url") or "").strip()
695
  normalized_url = str(result.get("normalized_url") or "").strip()
 
703
  table_rows,
704
  updated_records,
705
  row_map,
706
+ safe_page,
707
+ page_label,
708
+ prev_update,
709
+ next_update,
710
  *detail,
711
  )
712
 
 
6927
  )
6928
  my_qr_records_state = gr.State([])
6929
  my_qr_row_map_state = gr.State([])
6930
+ my_qr_page_state = gr.State(value=1)
6931
  my_qr_selected_index_state = gr.State(value=None)
6932
  my_qr_table_summary = gr.Markdown("No saved QR codes loaded yet.")
6933
  my_qr_table = gr.Dataframe(
 
6940
  value=[],
6941
  wrap=True,
6942
  )
6943
+ with gr.Row():
6944
+ my_qr_prev_page_btn = gr.Button("← Previous", interactive=False)
6945
+ my_qr_page_label = gr.Markdown("")
6946
+ my_qr_next_page_btn = gr.Button("Next →", interactive=False)
6947
  with gr.Row():
6948
  with gr.Column(scale=1):
6949
  my_qr_detail_image = gr.HTML(
 
7010
  my_qr_table,
7011
  my_qr_records_state,
7012
  my_qr_row_map_state,
7013
+ my_qr_page_state,
7014
+ my_qr_page_label,
7015
+ my_qr_prev_page_btn,
7016
+ my_qr_next_page_btn,
7017
  my_qr_selected_index_state,
7018
  my_qr_detail_markdown,
7019
  my_qr_detail_image,
 
7037
  my_qr_table,
7038
  my_qr_records_state,
7039
  my_qr_row_map_state,
7040
+ my_qr_page_state,
7041
+ my_qr_page_label,
7042
+ my_qr_prev_page_btn,
7043
+ my_qr_next_page_btn,
7044
  my_qr_selected_index_state,
7045
  my_qr_detail_markdown,
7046
  my_qr_detail_image,
 
7073
  ],
7074
  )
7075
 
7076
+ my_qr_prev_page_btn.click(
7077
+ fn=lambda records, current_page: _change_my_qr_page(records, current_page, -1),
7078
+ inputs=[my_qr_records_state, my_qr_page_state],
7079
+ outputs=[
7080
+ my_qr_table_summary,
7081
+ my_qr_table,
7082
+ my_qr_row_map_state,
7083
+ my_qr_page_state,
7084
+ my_qr_page_label,
7085
+ my_qr_prev_page_btn,
7086
+ my_qr_next_page_btn,
7087
+ my_qr_selected_index_state,
7088
+ my_qr_detail_markdown,
7089
+ my_qr_detail_image,
7090
+ my_qr_scan_breakdown,
7091
+ my_qr_current_destination,
7092
+ my_qr_new_destination,
7093
+ my_qr_settings_json,
7094
+ my_qr_settings_accordion,
7095
+ load_standard_from_dashboard_btn,
7096
+ load_artistic_from_dashboard_btn,
7097
+ my_qr_retarget_group,
7098
+ ],
7099
+ )
7100
+
7101
+ my_qr_next_page_btn.click(
7102
+ fn=lambda records, current_page: _change_my_qr_page(records, current_page, 1),
7103
+ inputs=[my_qr_records_state, my_qr_page_state],
7104
+ outputs=[
7105
+ my_qr_table_summary,
7106
+ my_qr_table,
7107
+ my_qr_row_map_state,
7108
+ my_qr_page_state,
7109
+ my_qr_page_label,
7110
+ my_qr_prev_page_btn,
7111
+ my_qr_next_page_btn,
7112
+ my_qr_selected_index_state,
7113
+ my_qr_detail_markdown,
7114
+ my_qr_detail_image,
7115
+ my_qr_scan_breakdown,
7116
+ my_qr_current_destination,
7117
+ my_qr_new_destination,
7118
+ my_qr_settings_json,
7119
+ my_qr_settings_accordion,
7120
+ load_standard_from_dashboard_btn,
7121
+ load_artistic_from_dashboard_btn,
7122
+ my_qr_retarget_group,
7123
+ ],
7124
+ )
7125
+
7126
  update_my_qr_destination_btn.click(
7127
  fn=_update_my_qr_destination,
7128
+ inputs=[my_qr_records_state, my_qr_page_state, my_qr_selected_index_state, my_qr_new_destination],
7129
  outputs=[
7130
  dashboard_status,
7131
  my_qr_table_summary,
7132
  my_qr_table,
7133
  my_qr_records_state,
7134
  my_qr_row_map_state,
7135
+ my_qr_page_state,
7136
+ my_qr_page_label,
7137
+ my_qr_prev_page_btn,
7138
+ my_qr_next_page_btn,
7139
  my_qr_selected_index_state,
7140
  my_qr_detail_markdown,
7141
  my_qr_detail_image,
hf_dashboard.py CHANGED
@@ -4,6 +4,7 @@ import copy
4
  import hashlib
5
  import io
6
  import json
 
7
  import os
8
  import re
9
  from collections import Counter
@@ -1012,20 +1013,36 @@ def update_generation_destination(
1012
  def build_generation_table_rows(
1013
  records: list[dict[str, Any]],
1014
  *,
 
1015
  limit: int = 10,
1016
- ) -> tuple[list[list[Any]], list[int], str]:
1017
- visible_records = list(records[: max(0, limit)])
1018
- row_map = list(range(len(visible_records)))
 
 
 
 
 
 
1019
  table_rows: list[list[Any]] = []
1020
- for position, record in enumerate(visible_records, start=1):
1021
  created = str(record.get("created_at") or "")[:16].replace("T", " ")
1022
  qr_mode = str(record.get("qr_mode") or "qr").title()
1023
  destination = str(record.get("destination_url_original") or "—")
1024
  short_url = str(record.get("short_url") or "—")
1025
  status = str(record.get("status") or "unknown")
1026
  table_rows.append([position, created, qr_mode, destination, short_url, status])
1027
- summary = f"Showing {len(visible_records)} of {len(records)} saved QR code(s). Click a row to inspect it."
1028
- return table_rows, row_map, summary
 
 
 
 
 
 
 
 
 
1029
 
1030
 
1031
  def resolve_generation_index_from_table_event(
 
4
  import hashlib
5
  import io
6
  import json
7
+ import math
8
  import os
9
  import re
10
  from collections import Counter
 
1013
  def build_generation_table_rows(
1014
  records: list[dict[str, Any]],
1015
  *,
1016
+ page: int = 1,
1017
  limit: int = 10,
1018
+ ) -> tuple[list[list[Any]], list[int], str, int, int]:
1019
+ safe_limit = max(1, int(limit))
1020
+ total_records = len(records)
1021
+ total_pages = max(1, math.ceil(total_records / safe_limit))
1022
+ safe_page = min(max(1, int(page)), total_pages)
1023
+ start_idx = (safe_page - 1) * safe_limit
1024
+ end_idx = start_idx + safe_limit
1025
+ visible_records = list(records[start_idx:end_idx])
1026
+ row_map = list(range(start_idx, start_idx + len(visible_records)))
1027
  table_rows: list[list[Any]] = []
1028
+ for position, record in enumerate(visible_records, start=start_idx + 1):
1029
  created = str(record.get("created_at") or "")[:16].replace("T", " ")
1030
  qr_mode = str(record.get("qr_mode") or "qr").title()
1031
  destination = str(record.get("destination_url_original") or "—")
1032
  short_url = str(record.get("short_url") or "—")
1033
  status = str(record.get("status") or "unknown")
1034
  table_rows.append([position, created, qr_mode, destination, short_url, status])
1035
+ if total_records:
1036
+ shown_start = start_idx + 1
1037
+ shown_end = start_idx + len(visible_records)
1038
+ range_label = f"Showing {shown_start}-{shown_end} of {total_records}"
1039
+ else:
1040
+ range_label = "Showing 0-0 of 0"
1041
+ summary = (
1042
+ f"{range_label} saved QR code(s). "
1043
+ f"Page {safe_page} of {total_pages}. Click a row to inspect it."
1044
+ )
1045
+ return table_rows, row_map, summary, safe_page, total_pages
1046
 
1047
 
1048
  def resolve_generation_index_from_table_event(
tests-unit/hf_dashboard_test.py CHANGED
@@ -466,14 +466,41 @@ def test_build_generation_table_rows_limits_to_10_and_returns_row_map() -> None:
466
  }
467
  )
468
 
469
- table_rows, row_map, summary = hf_dashboard.build_generation_table_rows(rows)
470
 
471
  assert len(table_rows) == 10
472
  assert len(row_map) == 10
473
  assert row_map == list(range(10))
474
  assert table_rows[0][0] == 1
475
  assert table_rows[0][2] in {"Standard", "Artistic"}
476
- assert "Showing 10 of 12" in summary
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
 
478
 
479
  def test_resolve_generation_index_from_table_event_handles_tuple_index() -> None:
 
466
  }
467
  )
468
 
469
+ table_rows, row_map, summary, page, total_pages = hf_dashboard.build_generation_table_rows(rows)
470
 
471
  assert len(table_rows) == 10
472
  assert len(row_map) == 10
473
  assert row_map == list(range(10))
474
  assert table_rows[0][0] == 1
475
  assert table_rows[0][2] in {"Standard", "Artistic"}
476
+ assert "Showing 1-10 of 12" in summary
477
+ assert page == 1
478
+ assert total_pages == 2
479
+
480
+
481
+ def test_build_generation_table_rows_returns_second_page_slice() -> None:
482
+ rows = []
483
+ for idx in range(12):
484
+ rows.append(
485
+ {
486
+ "id": f"gen-{idx}",
487
+ "created_at": f"2026-05-31T1{idx % 10}:00:00+00:00",
488
+ "qr_mode": "standard",
489
+ "destination_url_original": f"https://example.com/{idx}",
490
+ "short_url": f"https://qrcut.co/{idx}",
491
+ "status": "completed",
492
+ }
493
+ )
494
+
495
+ table_rows, row_map, summary, page, total_pages = hf_dashboard.build_generation_table_rows(rows, page=2)
496
+
497
+ assert len(table_rows) == 2
498
+ assert row_map == [10, 11]
499
+ assert table_rows[0][0] == 11
500
+ assert table_rows[1][0] == 12
501
+ assert "Showing 11-12 of 12" in summary
502
+ assert page == 2
503
+ assert total_pages == 2
504
 
505
 
506
  def test_resolve_generation_index_from_table_event_handles_tuple_index() -> None: