@genspectrum/dashboard-components 0.1.5 → 0.3.0

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.
Files changed (69) hide show
  1. package/custom-elements.json +1161 -928
  2. package/dist/dashboard-components.js +663 -237
  3. package/dist/dashboard-components.js.map +1 -1
  4. package/dist/genspectrum-components.d.ts +177 -140
  5. package/dist/style.css +247 -50
  6. package/package.json +2 -3
  7. package/src/constants.ts +1 -1
  8. package/src/lapisApi/lapisApi.ts +46 -2
  9. package/src/lapisApi/lapisTypes.ts +14 -0
  10. package/src/preact/aggregatedData/aggregate.stories.tsx +4 -2
  11. package/src/preact/aggregatedData/aggregate.tsx +31 -29
  12. package/src/preact/components/error-boundary.stories.tsx +54 -0
  13. package/src/preact/components/error-boundary.tsx +22 -0
  14. package/src/preact/components/error-display.stories.tsx +32 -4
  15. package/src/preact/components/error-display.tsx +48 -1
  16. package/src/preact/components/loading-display.stories.tsx +6 -6
  17. package/src/preact/components/loading-display.tsx +1 -1
  18. package/src/preact/components/no-data-display.tsx +5 -1
  19. package/src/preact/components/resize-container.tsx +5 -14
  20. package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +19 -0
  21. package/src/preact/dateRangeSelector/date-range-selector.tsx +38 -7
  22. package/src/preact/locationFilter/fetchAutocompletionList.ts +15 -1
  23. package/src/preact/locationFilter/location-filter.stories.tsx +23 -6
  24. package/src/preact/locationFilter/location-filter.tsx +28 -18
  25. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +6 -3
  26. package/src/preact/mutationComparison/mutation-comparison.tsx +33 -32
  27. package/src/preact/mutationComparison/queryMutationData.ts +2 -3
  28. package/src/preact/mutationFilter/mutation-filter.stories.tsx +18 -3
  29. package/src/preact/mutationFilter/mutation-filter.tsx +26 -7
  30. package/src/preact/mutations/mutations.stories.tsx +6 -3
  31. package/src/preact/mutations/mutations.tsx +28 -26
  32. package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +14 -7
  33. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +50 -32
  34. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +6 -3
  35. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +46 -32
  36. package/src/preact/textInput/text-input.stories.tsx +26 -0
  37. package/src/preact/textInput/text-input.tsx +25 -3
  38. package/src/query/queryPrevalenceOverTime.ts +4 -10
  39. package/src/types.ts +4 -1
  40. package/src/web-components/ResizeContainer.mdx +13 -0
  41. package/src/web-components/app.stories.ts +1 -2
  42. package/src/web-components/app.ts +7 -3
  43. package/src/web-components/index.ts +1 -1
  44. package/src/web-components/input/{date-range-selector-component.stories.ts → gs-date-range-selector.stories.ts} +29 -4
  45. package/src/web-components/input/{date-range-selector-component.tsx → gs-date-range-selector.tsx} +32 -10
  46. package/src/web-components/input/{location-filter-component.stories.ts → gs-location-filter.stories.ts} +32 -5
  47. package/src/web-components/input/{location-filter-component.tsx → gs-location-filter.tsx} +11 -1
  48. package/src/web-components/input/{mutation-filter-component.stories.ts → gs-mutation-filter.stories.ts} +23 -4
  49. package/src/web-components/input/gs-mutation-filter.tsx +126 -0
  50. package/src/web-components/input/{text-input-component.stories.ts → gs-text-input.stories.ts} +34 -6
  51. package/src/web-components/input/{text-input-component.tsx → gs-text-input.tsx} +16 -4
  52. package/src/web-components/input/index.ts +4 -4
  53. package/src/web-components/input/introduction.mdx +11 -0
  54. package/src/web-components/introduction.mdx +15 -0
  55. package/src/web-components/visualization/data_visualization_statistical_analysis.mdx +26 -0
  56. package/src/web-components/{display/aggregate-component.stories.ts → visualization/gs-aggregate.stories.ts} +23 -11
  57. package/src/web-components/visualization/gs-aggregate.tsx +88 -0
  58. package/src/web-components/{display/mutation-comparison-component.stories.ts → visualization/gs-mutation-comparison.stories.ts} +21 -16
  59. package/src/web-components/{display/mutation-comparison-component.tsx → visualization/gs-mutation-comparison.tsx} +27 -18
  60. package/src/web-components/{display/mutations-component.stories.ts → visualization/gs-mutations.stories.ts} +20 -15
  61. package/src/web-components/{display/mutations-component.tsx → visualization/gs-mutations.tsx} +20 -10
  62. package/src/web-components/{display/prevalence-over-time-component.stories.ts → visualization/gs-prevalence-over-time.stories.ts} +29 -20
  63. package/src/web-components/{display/prevalence-over-time-component.tsx → visualization/gs-prevalence-over-time.tsx} +47 -22
  64. package/src/web-components/{display/relative-growth-advantage-component.stories.ts → visualization/gs-relative-growth-advantage.stories.ts} +12 -7
  65. package/src/web-components/{display/relative-growth-advantage-component.tsx → visualization/gs-relative-growth-advantage.tsx} +21 -9
  66. package/src/web-components/visualization/index.ts +5 -0
  67. package/src/web-components/display/aggregate-component.tsx +0 -72
  68. package/src/web-components/display/index.ts +0 -5
  69. package/src/web-components/input/mutation-filter-component.tsx +0 -83
package/dist/style.css CHANGED
@@ -975,37 +975,33 @@ html {
975
975
  --tw-contain-paint: ;
976
976
  --tw-contain-style: ;
977
977
  }
978
- .container {
978
+ .alert {
979
+ display: grid;
979
980
  width: 100%;
981
+ grid-auto-flow: row;
982
+ align-content: flex-start;
983
+ align-items: center;
984
+ justify-items: center;
985
+ gap: 1rem;
986
+ text-align: center;
987
+ border-radius: var(--rounded-box, 1rem);
988
+ border-width: 1px;
989
+ --tw-border-opacity: 1;
990
+ border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));
991
+ padding: 1rem;
992
+ --tw-text-opacity: 1;
993
+ color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
994
+ --alert-bg: var(--fallback-b2,oklch(var(--b2)/1));
995
+ --alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1));
996
+ background-color: var(--alert-bg);
980
997
  }
981
998
  @media (min-width: 640px) {
982
999
 
983
- .container {
984
- max-width: 640px;
985
- }
986
- }
987
- @media (min-width: 768px) {
988
-
989
- .container {
990
- max-width: 768px;
991
- }
992
- }
993
- @media (min-width: 1024px) {
994
-
995
- .container {
996
- max-width: 1024px;
997
- }
998
- }
999
- @media (min-width: 1280px) {
1000
-
1001
- .container {
1002
- max-width: 1280px;
1003
- }
1004
- }
1005
- @media (min-width: 1536px) {
1006
-
1007
- .container {
1008
- max-width: 1536px;
1000
+ .alert {
1001
+ grid-auto-flow: column;
1002
+ grid-template-columns: auto minmax(auto,1fr);
1003
+ justify-items: start;
1004
+ text-align: start;
1009
1005
  }
1010
1006
  }
1011
1007
  .avatar.placeholder > div {
@@ -1081,6 +1077,12 @@ html {
1081
1077
  .btn:disabled {
1082
1078
  pointer-events: none;
1083
1079
  }
1080
+ .btn-circle {
1081
+ height: 3rem;
1082
+ width: 3rem;
1083
+ border-radius: 9999px;
1084
+ padding: 0px;
1085
+ }
1084
1086
  :where(.btn:is(input[type="checkbox"])),
1085
1087
  :where(.btn:is(input[type="radio"])) {
1086
1088
  width: auto;
@@ -1233,6 +1235,17 @@ html {
1233
1235
  --glass-border-opacity: 15%;
1234
1236
  }
1235
1237
 
1238
+ .btn-ghost:hover {
1239
+ border-color: transparent;
1240
+ }
1241
+
1242
+ @supports (color: oklch(0% 0 0)) {
1243
+
1244
+ .btn-ghost:hover {
1245
+ background-color: var(--fallback-bc,oklch(var(--bc)/0.2));
1246
+ }
1247
+ }
1248
+
1236
1249
  .btn-outline.btn-primary:hover {
1237
1250
  --tw-text-opacity: 1;
1238
1251
  color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));
@@ -1455,6 +1468,69 @@ html {
1455
1468
  :where(.menu li) .badge {
1456
1469
  justify-self: end;
1457
1470
  }
1471
+ .modal {
1472
+ pointer-events: none;
1473
+ position: fixed;
1474
+ inset: 0px;
1475
+ margin: 0px;
1476
+ display: grid;
1477
+ height: 100%;
1478
+ max-height: none;
1479
+ width: 100%;
1480
+ max-width: none;
1481
+ justify-items: center;
1482
+ padding: 0px;
1483
+ opacity: 0;
1484
+ overscroll-behavior: contain;
1485
+ z-index: 999;
1486
+ background-color: transparent;
1487
+ color: inherit;
1488
+ transition-duration: 200ms;
1489
+ transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
1490
+ transition-property: transform, opacity, visibility;
1491
+ overflow-y: hidden;
1492
+ }
1493
+ :where(.modal) {
1494
+ align-items: center;
1495
+ }
1496
+ .modal-box {
1497
+ max-height: calc(100vh - 5em);
1498
+ grid-column-start: 1;
1499
+ grid-row-start: 1;
1500
+ width: 91.666667%;
1501
+ max-width: 32rem;
1502
+ --tw-scale-x: .9;
1503
+ --tw-scale-y: .9;
1504
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1505
+ border-bottom-right-radius: var(--rounded-box, 1rem);
1506
+ border-bottom-left-radius: var(--rounded-box, 1rem);
1507
+ border-top-left-radius: var(--rounded-box, 1rem);
1508
+ border-top-right-radius: var(--rounded-box, 1rem);
1509
+ --tw-bg-opacity: 1;
1510
+ background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
1511
+ padding: 1.5rem;
1512
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
1513
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
1514
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
1515
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1516
+ transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
1517
+ transition-duration: 200ms;
1518
+ box-shadow: rgba(0, 0, 0, 0.25) 0px 25px 50px -12px;
1519
+ overflow-y: auto;
1520
+ overscroll-behavior: contain;
1521
+ }
1522
+ .modal-open,
1523
+ .modal:target,
1524
+ .modal-toggle:checked + .modal,
1525
+ .modal[open] {
1526
+ pointer-events: auto;
1527
+ visibility: visible;
1528
+ opacity: 1;
1529
+ }
1530
+ :root:has(:is(.modal-open, .modal:target, .modal-toggle:checked + .modal, .modal[open])) {
1531
+ overflow: hidden;
1532
+ scrollbar-gutter: stable;
1533
+ }
1458
1534
  .radio {
1459
1535
  flex-shrink: 0;
1460
1536
  --chkbg: var(--bc);
@@ -1516,29 +1592,6 @@ html {
1516
1592
  .select[multiple] {
1517
1593
  height: auto;
1518
1594
  }
1519
- .stack {
1520
- display: inline-grid;
1521
- place-items: center;
1522
- align-items: flex-end;
1523
- }
1524
- .stack > * {
1525
- grid-column-start: 1;
1526
- grid-row-start: 1;
1527
- transform: translateY(10%) scale(0.9);
1528
- z-index: 1;
1529
- width: 100%;
1530
- opacity: 0.6;
1531
- }
1532
- .stack > *:nth-child(2) {
1533
- transform: translateY(5%) scale(0.95);
1534
- z-index: 2;
1535
- opacity: 0.8;
1536
- }
1537
- .stack > *:nth-child(1) {
1538
- transform: translateY(0) scale(1);
1539
- z-index: 3;
1540
- opacity: 1;
1541
- }
1542
1595
  .steps {
1543
1596
  display: inline-grid;
1544
1597
  grid-auto-flow: column;
@@ -1638,6 +1691,13 @@ input.tab:checked + .tab-content,
1638
1691
  --tw-bg-opacity: 1;
1639
1692
  background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
1640
1693
  }
1694
+ .alert-error {
1695
+ border-color: var(--fallback-er,oklch(var(--er)/0.2));
1696
+ --tw-text-opacity: 1;
1697
+ color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)));
1698
+ --alert-bg: var(--fallback-er,oklch(var(--er)/1));
1699
+ --alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1));
1700
+ }
1641
1701
  .btm-nav > *.disabled,
1642
1702
  .btm-nav > *[disabled] {
1643
1703
  pointer-events: none;
@@ -1706,6 +1766,20 @@ input.tab:checked + .tab-content,
1706
1766
  --glass-opacity: 25%;
1707
1767
  --glass-border-opacity: 15%;
1708
1768
  }
1769
+ .btn-ghost {
1770
+ border-width: 1px;
1771
+ border-color: transparent;
1772
+ background-color: transparent;
1773
+ color: currentColor;
1774
+ --tw-shadow: 0 0 #0000;
1775
+ --tw-shadow-colored: 0 0 #0000;
1776
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1777
+ outline-color: currentColor;
1778
+ }
1779
+ .btn-ghost.btn-active {
1780
+ border-color: transparent;
1781
+ background-color: var(--fallback-bc,oklch(var(--bc)/0.2));
1782
+ }
1709
1783
  .btn-outline.btn-primary {
1710
1784
  --tw-text-opacity: 1;
1711
1785
  color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)));
@@ -2027,6 +2101,29 @@ input.tab:checked + .tab-content,
2027
2101
  border-color: currentColor;
2028
2102
  opacity: 0.6;
2029
2103
  }
2104
+ .modal:not(dialog:not(.modal-open)),
2105
+ .modal::backdrop {
2106
+ background-color: #0006;
2107
+ animation: modal-pop 0.2s ease-out;
2108
+ }
2109
+ .modal-backdrop {
2110
+ z-index: -1;
2111
+ grid-column-start: 1;
2112
+ grid-row-start: 1;
2113
+ display: grid;
2114
+ align-self: stretch;
2115
+ justify-self: stretch;
2116
+ color: transparent;
2117
+ }
2118
+ .modal-open .modal-box,
2119
+ .modal-toggle:checked + .modal .modal-box,
2120
+ .modal:target .modal-box,
2121
+ .modal[open] .modal-box {
2122
+ --tw-translate-y: 0px;
2123
+ --tw-scale-x: 1;
2124
+ --tw-scale-y: 1;
2125
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
2126
+ }
2030
2127
  @keyframes modal-pop {
2031
2128
 
2032
2129
  0% {
@@ -2186,6 +2283,30 @@ input.tab:checked + .tab-content,
2186
2283
  background-position: calc(0% + 12px) calc(1px + 50%),
2187
2284
  calc(0% + 16px) calc(1px + 50%);
2188
2285
  }
2286
+ .skeleton {
2287
+ border-radius: var(--rounded-box, 1rem);
2288
+ --tw-bg-opacity: 1;
2289
+ background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));
2290
+ will-change: background-position;
2291
+ animation: skeleton 1.8s ease-in-out infinite;
2292
+ background-image: linear-gradient(
2293
+ 105deg,
2294
+ transparent 0%,
2295
+ transparent 40%,
2296
+ var(--fallback-b1,oklch(var(--b1)/1)) 50%,
2297
+ transparent 60%,
2298
+ transparent 100%
2299
+ );
2300
+ background-size: 200% auto;
2301
+ background-repeat: no-repeat;
2302
+ background-position-x: -50%;
2303
+ }
2304
+ @media (prefers-reduced-motion) {
2305
+
2306
+ .skeleton {
2307
+ animation-duration: 15s;
2308
+ }
2309
+ }
2189
2310
  @keyframes skeleton {
2190
2311
 
2191
2312
  from {
@@ -2488,6 +2609,18 @@ input.tab:checked + .tab-content,
2488
2609
  border-radius: 9999px;
2489
2610
  padding: 0px;
2490
2611
  }
2612
+ .btn-circle:where(.btn-md) {
2613
+ height: 3rem;
2614
+ width: 3rem;
2615
+ border-radius: 9999px;
2616
+ padding: 0px;
2617
+ }
2618
+ .btn-circle:where(.btn-lg) {
2619
+ height: 4rem;
2620
+ width: 4rem;
2621
+ border-radius: 9999px;
2622
+ padding: 0px;
2623
+ }
2491
2624
  .join.join-vertical {
2492
2625
  flex-direction: column;
2493
2626
  }
@@ -2602,6 +2735,42 @@ input.tab:checked + .tab-content,
2602
2735
  margin-bottom: 0px;
2603
2736
  margin-inline-start: -1px;
2604
2737
  }
2738
+ .modal-top :where(.modal-box) {
2739
+ width: 100%;
2740
+ max-width: none;
2741
+ --tw-translate-y: -2.5rem;
2742
+ --tw-scale-x: 1;
2743
+ --tw-scale-y: 1;
2744
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
2745
+ border-bottom-right-radius: var(--rounded-box, 1rem);
2746
+ border-bottom-left-radius: var(--rounded-box, 1rem);
2747
+ border-top-left-radius: 0px;
2748
+ border-top-right-radius: 0px;
2749
+ }
2750
+ .modal-middle :where(.modal-box) {
2751
+ width: 91.666667%;
2752
+ max-width: 32rem;
2753
+ --tw-translate-y: 0px;
2754
+ --tw-scale-x: .9;
2755
+ --tw-scale-y: .9;
2756
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
2757
+ border-top-left-radius: var(--rounded-box, 1rem);
2758
+ border-top-right-radius: var(--rounded-box, 1rem);
2759
+ border-bottom-right-radius: var(--rounded-box, 1rem);
2760
+ border-bottom-left-radius: var(--rounded-box, 1rem);
2761
+ }
2762
+ .modal-bottom :where(.modal-box) {
2763
+ width: 100%;
2764
+ max-width: none;
2765
+ --tw-translate-y: 2.5rem;
2766
+ --tw-scale-x: 1;
2767
+ --tw-scale-y: 1;
2768
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
2769
+ border-top-left-radius: var(--rounded-box, 1rem);
2770
+ border-top-right-radius: var(--rounded-box, 1rem);
2771
+ border-bottom-right-radius: 0px;
2772
+ border-bottom-left-radius: 0px;
2773
+ }
2605
2774
  .steps-horizontal .step {
2606
2775
  grid-template-rows: 40px 1fr;
2607
2776
  grid-template-columns: auto;
@@ -2727,9 +2896,15 @@ input.tab:checked + .tab-content,
2727
2896
  .relative {
2728
2897
  position: relative;
2729
2898
  }
2899
+ .right-2 {
2900
+ right: 0.5rem;
2901
+ }
2730
2902
  .right-6 {
2731
2903
  right: 1.5rem;
2732
2904
  }
2905
+ .top-2 {
2906
+ top: 0.5rem;
2907
+ }
2733
2908
  .top-8 {
2734
2909
  top: 2rem;
2735
2910
  }
@@ -2811,6 +2986,10 @@ input.tab:checked + .tab-content,
2811
2986
  .min-w-0 {
2812
2987
  min-width: 0px;
2813
2988
  }
2989
+ .min-w-max {
2990
+ min-width: -moz-max-content;
2991
+ min-width: max-content;
2992
+ }
2814
2993
  .max-w-screen-lg {
2815
2994
  max-width: 1024px;
2816
2995
  }
@@ -2853,6 +3032,9 @@ input.tab:checked + .tab-content,
2853
3032
  .overflow-auto {
2854
3033
  overflow: auto;
2855
3034
  }
3035
+ .overflow-scroll {
3036
+ overflow: scroll;
3037
+ }
2856
3038
  .whitespace-nowrap {
2857
3039
  white-space: nowrap;
2858
3040
  }
@@ -2871,6 +3053,9 @@ input.tab:checked + .tab-content,
2871
3053
  .rounded-lg {
2872
3054
  border-radius: 0.5rem;
2873
3055
  }
3056
+ .rounded-md {
3057
+ border-radius: 0.375rem;
3058
+ }
2874
3059
  .rounded-none {
2875
3060
  border-radius: 0px;
2876
3061
  }
@@ -2956,6 +3141,10 @@ input.tab:checked + .tab-content,
2956
3141
  padding-top: 0.5rem;
2957
3142
  padding-bottom: 0.5rem;
2958
3143
  }
3144
+ .py-4 {
3145
+ padding-top: 1rem;
3146
+ padding-bottom: 1rem;
3147
+ }
2959
3148
  .text-justify {
2960
3149
  text-align: justify;
2961
3150
  }
@@ -2996,6 +3185,10 @@ input.tab:checked + .tab-content,
2996
3185
  --tw-text-opacity: 1;
2997
3186
  color: rgb(75 85 99 / var(--tw-text-opacity));
2998
3187
  }
3188
+ .text-red-700 {
3189
+ --tw-text-opacity: 1;
3190
+ color: rgb(185 28 28 / var(--tw-text-opacity));
3191
+ }
2999
3192
  .underline {
3000
3193
  text-decoration-line: underline;
3001
3194
  }
@@ -3032,6 +3225,10 @@ input.tab:checked + .tab-content,
3032
3225
  --tw-text-opacity: 1;
3033
3226
  color: rgb(30 64 175 / var(--tw-text-opacity));
3034
3227
  }
3228
+ .hover\:text-gray-300:hover {
3229
+ --tw-text-opacity: 1;
3230
+ color: rgb(209 213 219 / var(--tw-text-opacity));
3231
+ }
3035
3232
  .hover\:text-gray-700:hover {
3036
3233
  --tw-text-opacity: 1;
3037
3234
  color: rgb(55 65 81 / var(--tw-text-opacity));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.1.5",
3
+ "version": "0.3.0",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -35,7 +35,7 @@
35
35
  "lint:lit-analyzer": "lit-analyzer",
36
36
  "generate-manifest": "npx custom-elements-manifest analyze --litelement --globs src/web-components/**",
37
37
  "generate-manifest:watch": "npm run generate-manifest -- --watch",
38
- "format": "prettier \"**/*.{cjs,html,js,json,md,ts,tsx}\" --write",
38
+ "format": "prettier \"**/*.{ts,tsx,json,md,mdx,mjs,cjs}\" --write",
39
39
  "check-format": "prettier --check \"**/*.{ts,tsx,json,md,mdx,mjs,cjs}\"",
40
40
  "check-types": "tsc --noEmit",
41
41
  "check-dependencies": "depcheck",
@@ -101,7 +101,6 @@
101
101
  "postcss": "^8.4.38",
102
102
  "prettier": "^3.2.5",
103
103
  "react": "^18.3.1",
104
- "release-please": "^16.10.2",
105
104
  "storybook": "^8.0.9",
106
105
  "storybook-addon-fetch-mock": "^2.0.0",
107
106
  "tailwindcss": "^3.4.3",
package/src/constants.ts CHANGED
@@ -1,4 +1,4 @@
1
- export const LAPIS_URL = 'https://s1.int.genspectrum.org/open';
1
+ export const LAPIS_URL = 'https://lapis.cov-spectrum.org/open/v2/';
2
2
 
3
3
  export const AGGREGATED_ENDPOINT = `${LAPIS_URL}/sample/aggregated`;
4
4
  export const NUCLEOTIDE_MUTATIONS_ENDPOINT = `${LAPIS_URL}/sample/nucleotideMutations`;
@@ -3,11 +3,35 @@ import {
3
3
  aggregatedResponse,
4
4
  insertionsResponse,
5
5
  type LapisBaseRequest,
6
+ lapisError,
6
7
  type MutationsRequest,
7
8
  mutationsResponse,
9
+ problemDetail,
10
+ type ProblemDetail,
8
11
  } from './lapisTypes';
9
12
  import { type SequenceType } from '../types';
10
13
 
14
+ export class UnknownLapisError extends Error {
15
+ constructor(
16
+ message: string,
17
+ public readonly status: number,
18
+ ) {
19
+ super(message);
20
+ this.name = 'UnknownLapisError';
21
+ }
22
+ }
23
+
24
+ export class LapisError extends Error {
25
+ constructor(
26
+ message: string,
27
+ public readonly status: number,
28
+ public readonly problemDetail: ProblemDetail,
29
+ ) {
30
+ super(message);
31
+ this.name = 'LapisError';
32
+ }
33
+ }
34
+
11
35
  export async function fetchAggregated(lapisUrl: string, body: LapisBaseRequest, signal?: AbortSignal) {
12
36
  const response = await fetch(aggregatedEndpoint(lapisUrl), {
13
37
  method: 'POST',
@@ -79,9 +103,29 @@ export async function fetchReferenceGenome(lapisUrl: string, signal?: AbortSigna
79
103
  const handleErrors = async (response: Response) => {
80
104
  if (!response.ok) {
81
105
  if (response.status >= 400 && response.status < 500) {
82
- throw new Error(`${response.statusText}: ${JSON.stringify(await response.json())}`);
106
+ const json = await response.json();
107
+
108
+ const lapisErrorResult = lapisError.safeParse(json);
109
+ if (lapisErrorResult.success) {
110
+ throw new LapisError(
111
+ response.statusText + lapisErrorResult.data.error.detail,
112
+ response.status,
113
+ lapisErrorResult.data.error,
114
+ );
115
+ }
116
+
117
+ const problemDetailResult = problemDetail.safeParse(json);
118
+ if (problemDetailResult.success) {
119
+ throw new LapisError(
120
+ response.statusText + problemDetailResult.data.detail,
121
+ response.status,
122
+ problemDetailResult.data,
123
+ );
124
+ }
125
+
126
+ throw new UnknownLapisError(`${response.statusText}: ${JSON.stringify(json)}`, response.status);
83
127
  }
84
- throw new Error(`${response.statusText}: ${response.status}`);
128
+ throw new UnknownLapisError(`${response.statusText}: ${response.status}`, response.status);
85
129
  }
86
130
  };
87
131
 
@@ -49,3 +49,17 @@ function makeLapisResponse<T extends ZodTypeAny>(data: T) {
49
49
  data,
50
50
  });
51
51
  }
52
+
53
+ export const problemDetail = z.object({
54
+ title: z.string().optional(),
55
+ status: z.number(),
56
+ detail: z.string().optional(),
57
+ type: z.string(),
58
+ instance: z.string().optional(),
59
+ });
60
+
61
+ export type ProblemDetail = z.infer<typeof problemDetail>;
62
+
63
+ export const lapisError = z.object({
64
+ error: problemDetail,
65
+ });
@@ -10,7 +10,8 @@ const meta: Meta<AggregateProps> = {
10
10
  component: Aggregate,
11
11
  argTypes: {
12
12
  fields: [{ control: 'object' }],
13
- size: [{ control: 'object' }],
13
+ width: { control: 'text' },
14
+ height: { control: 'text' },
14
15
  headline: { control: 'text' },
15
16
  },
16
17
  parameters: {
@@ -49,7 +50,8 @@ export const Default: StoryObj<AggregateProps> = {
49
50
  filter: {
50
51
  country: 'USA',
51
52
  },
52
- size: { width: '100%', height: '70vh' },
53
+ width: '100%',
54
+ height: '700px',
53
55
  headline: 'Aggregate',
54
56
  },
55
57
  };
@@ -6,32 +6,52 @@ import { type AggregateData, queryAggregateData } from '../../query/queryAggrega
6
6
  import { type LapisFilter } from '../../types';
7
7
  import { LapisUrlContext } from '../LapisUrlContext';
8
8
  import { CsvDownloadButton } from '../components/csv-download-button';
9
+ import { ErrorBoundary } from '../components/error-boundary';
9
10
  import { ErrorDisplay } from '../components/error-display';
10
11
  import Headline from '../components/headline';
11
12
  import Info from '../components/info';
12
13
  import { LoadingDisplay } from '../components/loading-display';
13
14
  import { NoDataDisplay } from '../components/no-data-display';
14
- import { ResizeContainer, type Size } from '../components/resize-container';
15
+ import { ResizeContainer } from '../components/resize-container';
15
16
  import Tabs from '../components/tabs';
16
17
  import { useQuery } from '../useQuery';
17
18
 
18
19
  export type View = 'table';
19
20
 
20
- export interface AggregateProps {
21
+ export type AggregateProps = {
22
+ width: string;
23
+ height: string;
24
+ headline?: string;
25
+ } & AggregateInnerProps;
26
+
27
+ export interface AggregateInnerProps {
21
28
  filter: LapisFilter;
22
29
  fields: string[];
23
30
  views: View[];
24
- size?: Size;
25
- headline?: string;
26
31
  }
27
32
 
28
33
  export const Aggregate: FunctionComponent<AggregateProps> = ({
29
- fields,
30
34
  views,
35
+ width,
36
+ height,
37
+ headline = 'Mutations',
31
38
  filter,
32
- size,
33
- headline = 'Aggregate',
39
+ fields,
34
40
  }) => {
41
+ const size = { height, width };
42
+
43
+ return (
44
+ <ErrorBoundary size={size} headline={headline}>
45
+ <ResizeContainer size={size}>
46
+ <Headline heading={headline}>
47
+ <AggregateInner fields={fields} filter={filter} views={views} />
48
+ </Headline>
49
+ </ResizeContainer>
50
+ </ErrorBoundary>
51
+ );
52
+ };
53
+
54
+ export const AggregateInner: FunctionComponent<AggregateInnerProps> = ({ fields, views, filter }) => {
35
55
  const lapis = useContext(LapisUrlContext);
36
56
 
37
57
  const { data, error, isLoading } = useQuery(async () => {
@@ -39,36 +59,18 @@ export const Aggregate: FunctionComponent<AggregateProps> = ({
39
59
  }, [filter, fields, lapis]);
40
60
 
41
61
  if (isLoading) {
42
- return (
43
- <Headline heading={headline}>
44
- <LoadingDisplay />
45
- </Headline>
46
- );
62
+ return <LoadingDisplay />;
47
63
  }
48
64
 
49
65
  if (error !== null) {
50
- return (
51
- <Headline heading={headline}>
52
- <ErrorDisplay error={error} />
53
- </Headline>
54
- );
66
+ return <ErrorDisplay error={error} />;
55
67
  }
56
68
 
57
69
  if (data === null) {
58
- return (
59
- <Headline heading={headline}>
60
- <NoDataDisplay />
61
- </Headline>
62
- );
70
+ return <NoDataDisplay />;
63
71
  }
64
72
 
65
- return (
66
- <ResizeContainer size={size} defaultSize={{ height: '700px', width: '100%' }}>
67
- <Headline heading={headline}>
68
- <AggregatedDataTabs data={data} views={views} fields={fields} />
69
- </Headline>
70
- </ResizeContainer>
71
- );
73
+ return <AggregatedDataTabs data={data} views={views} fields={fields} />;
72
74
  };
73
75
 
74
76
  type AggregatedDataTabsProps = {