@genspectrum/dashboard-components 0.2.0 → 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.
- package/custom-elements.json +328 -177
- package/dist/dashboard-components.js +376 -129
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +156 -110
- package/dist/style.css +179 -33
- package/package.json +1 -2
- package/src/constants.ts +1 -1
- package/src/lapisApi/lapisApi.ts +46 -2
- package/src/lapisApi/lapisTypes.ts +14 -0
- package/src/preact/aggregatedData/aggregate.stories.tsx +4 -2
- package/src/preact/aggregatedData/aggregate.tsx +8 -6
- package/src/preact/components/error-boundary.stories.tsx +6 -14
- package/src/preact/components/error-boundary.tsx +2 -11
- package/src/preact/components/error-display.stories.tsx +12 -5
- package/src/preact/components/error-display.tsx +37 -3
- package/src/preact/components/loading-display.stories.tsx +1 -1
- package/src/preact/components/resize-container.tsx +5 -14
- package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +2 -0
- package/src/preact/dateRangeSelector/date-range-selector.tsx +11 -8
- package/src/preact/locationFilter/fetchAutocompletionList.ts +15 -1
- package/src/preact/locationFilter/location-filter.tsx +4 -5
- package/src/preact/mutationComparison/mutation-comparison.stories.tsx +6 -3
- package/src/preact/mutationComparison/mutation-comparison.tsx +10 -13
- package/src/preact/mutationComparison/queryMutationData.ts +2 -3
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +8 -8
- package/src/preact/mutationFilter/mutation-filter.tsx +7 -6
- package/src/preact/mutations/mutations.stories.tsx +6 -3
- package/src/preact/mutations/mutations.tsx +8 -6
- package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +14 -7
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +10 -8
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +6 -3
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +9 -7
- package/src/preact/textInput/text-input.stories.tsx +26 -0
- package/src/preact/textInput/text-input.tsx +4 -5
- package/src/query/queryPrevalenceOverTime.ts +4 -10
- package/src/types.ts +4 -1
- package/src/web-components/ResizeContainer.mdx +13 -0
- package/src/web-components/app.ts +3 -1
- package/src/web-components/input/gs-date-range-selector.stories.ts +10 -2
- package/src/web-components/input/gs-date-range-selector.tsx +26 -16
- package/src/web-components/input/gs-location-filter.stories.ts +5 -3
- package/src/web-components/input/gs-location-filter.tsx +5 -6
- package/src/web-components/input/gs-mutation-filter.stories.ts +11 -8
- package/src/web-components/input/gs-mutation-filter.tsx +38 -26
- package/src/web-components/input/gs-text-input.stories.ts +3 -3
- package/src/web-components/input/gs-text-input.tsx +10 -10
- package/src/web-components/input/introduction.mdx +11 -0
- package/src/web-components/introduction.mdx +15 -0
- package/src/web-components/visualization/gs-aggregate.stories.ts +19 -6
- package/src/web-components/visualization/gs-aggregate.tsx +31 -15
- package/src/web-components/visualization/gs-mutation-comparison.stories.ts +13 -7
- package/src/web-components/visualization/gs-mutation-comparison.tsx +26 -17
- package/src/web-components/visualization/gs-mutations.stories.ts +14 -8
- package/src/web-components/visualization/gs-mutations.tsx +18 -8
- package/src/web-components/visualization/gs-prevalence-over-time.stories.ts +28 -18
- package/src/web-components/visualization/gs-prevalence-over-time.tsx +45 -22
- package/src/web-components/visualization/gs-relative-growth-advantage.stories.ts +11 -5
- package/src/web-components/visualization/gs-relative-growth-advantage.tsx +21 -9
package/dist/style.css
CHANGED
|
@@ -975,39 +975,6 @@ html {
|
|
|
975
975
|
--tw-contain-paint: ;
|
|
976
976
|
--tw-contain-style: ;
|
|
977
977
|
}
|
|
978
|
-
.container {
|
|
979
|
-
width: 100%;
|
|
980
|
-
}
|
|
981
|
-
@media (min-width: 640px) {
|
|
982
|
-
|
|
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;
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
978
|
.alert {
|
|
1012
979
|
display: grid;
|
|
1013
980
|
width: 100%;
|
|
@@ -1110,6 +1077,12 @@ html {
|
|
|
1110
1077
|
.btn:disabled {
|
|
1111
1078
|
pointer-events: none;
|
|
1112
1079
|
}
|
|
1080
|
+
.btn-circle {
|
|
1081
|
+
height: 3rem;
|
|
1082
|
+
width: 3rem;
|
|
1083
|
+
border-radius: 9999px;
|
|
1084
|
+
padding: 0px;
|
|
1085
|
+
}
|
|
1113
1086
|
:where(.btn:is(input[type="checkbox"])),
|
|
1114
1087
|
:where(.btn:is(input[type="radio"])) {
|
|
1115
1088
|
width: auto;
|
|
@@ -1262,6 +1235,17 @@ html {
|
|
|
1262
1235
|
--glass-border-opacity: 15%;
|
|
1263
1236
|
}
|
|
1264
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
|
+
|
|
1265
1249
|
.btn-outline.btn-primary:hover {
|
|
1266
1250
|
--tw-text-opacity: 1;
|
|
1267
1251
|
color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));
|
|
@@ -1484,6 +1468,69 @@ html {
|
|
|
1484
1468
|
:where(.menu li) .badge {
|
|
1485
1469
|
justify-self: end;
|
|
1486
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
|
+
}
|
|
1487
1534
|
.radio {
|
|
1488
1535
|
flex-shrink: 0;
|
|
1489
1536
|
--chkbg: var(--bc);
|
|
@@ -1719,6 +1766,20 @@ input.tab:checked + .tab-content,
|
|
|
1719
1766
|
--glass-opacity: 25%;
|
|
1720
1767
|
--glass-border-opacity: 15%;
|
|
1721
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
|
+
}
|
|
1722
1783
|
.btn-outline.btn-primary {
|
|
1723
1784
|
--tw-text-opacity: 1;
|
|
1724
1785
|
color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)));
|
|
@@ -2040,6 +2101,29 @@ input.tab:checked + .tab-content,
|
|
|
2040
2101
|
border-color: currentColor;
|
|
2041
2102
|
opacity: 0.6;
|
|
2042
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
|
+
}
|
|
2043
2127
|
@keyframes modal-pop {
|
|
2044
2128
|
|
|
2045
2129
|
0% {
|
|
@@ -2525,6 +2609,18 @@ input.tab:checked + .tab-content,
|
|
|
2525
2609
|
border-radius: 9999px;
|
|
2526
2610
|
padding: 0px;
|
|
2527
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
|
+
}
|
|
2528
2624
|
.join.join-vertical {
|
|
2529
2625
|
flex-direction: column;
|
|
2530
2626
|
}
|
|
@@ -2639,6 +2735,42 @@ input.tab:checked + .tab-content,
|
|
|
2639
2735
|
margin-bottom: 0px;
|
|
2640
2736
|
margin-inline-start: -1px;
|
|
2641
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
|
+
}
|
|
2642
2774
|
.steps-horizontal .step {
|
|
2643
2775
|
grid-template-rows: 40px 1fr;
|
|
2644
2776
|
grid-template-columns: auto;
|
|
@@ -2764,9 +2896,15 @@ input.tab:checked + .tab-content,
|
|
|
2764
2896
|
.relative {
|
|
2765
2897
|
position: relative;
|
|
2766
2898
|
}
|
|
2899
|
+
.right-2 {
|
|
2900
|
+
right: 0.5rem;
|
|
2901
|
+
}
|
|
2767
2902
|
.right-6 {
|
|
2768
2903
|
right: 1.5rem;
|
|
2769
2904
|
}
|
|
2905
|
+
.top-2 {
|
|
2906
|
+
top: 0.5rem;
|
|
2907
|
+
}
|
|
2770
2908
|
.top-8 {
|
|
2771
2909
|
top: 2rem;
|
|
2772
2910
|
}
|
|
@@ -3003,6 +3141,10 @@ input.tab:checked + .tab-content,
|
|
|
3003
3141
|
padding-top: 0.5rem;
|
|
3004
3142
|
padding-bottom: 0.5rem;
|
|
3005
3143
|
}
|
|
3144
|
+
.py-4 {
|
|
3145
|
+
padding-top: 1rem;
|
|
3146
|
+
padding-bottom: 1rem;
|
|
3147
|
+
}
|
|
3006
3148
|
.text-justify {
|
|
3007
3149
|
text-align: justify;
|
|
3008
3150
|
}
|
|
@@ -3083,6 +3225,10 @@ input.tab:checked + .tab-content,
|
|
|
3083
3225
|
--tw-text-opacity: 1;
|
|
3084
3226
|
color: rgb(30 64 175 / var(--tw-text-opacity));
|
|
3085
3227
|
}
|
|
3228
|
+
.hover\:text-gray-300:hover {
|
|
3229
|
+
--tw-text-opacity: 1;
|
|
3230
|
+
color: rgb(209 213 219 / var(--tw-text-opacity));
|
|
3231
|
+
}
|
|
3086
3232
|
.hover\:text-gray-700:hover {
|
|
3087
3233
|
--tw-text-opacity: 1;
|
|
3088
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.
|
|
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",
|
|
@@ -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://
|
|
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`;
|
package/src/lapisApi/lapisApi.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
width: '100%',
|
|
54
|
+
height: '700px',
|
|
53
55
|
headline: 'Aggregate',
|
|
54
56
|
},
|
|
55
57
|
};
|
|
@@ -12,14 +12,15 @@ import Headline from '../components/headline';
|
|
|
12
12
|
import Info from '../components/info';
|
|
13
13
|
import { LoadingDisplay } from '../components/loading-display';
|
|
14
14
|
import { NoDataDisplay } from '../components/no-data-display';
|
|
15
|
-
import { ResizeContainer
|
|
15
|
+
import { ResizeContainer } from '../components/resize-container';
|
|
16
16
|
import Tabs from '../components/tabs';
|
|
17
17
|
import { useQuery } from '../useQuery';
|
|
18
18
|
|
|
19
19
|
export type View = 'table';
|
|
20
20
|
|
|
21
21
|
export type AggregateProps = {
|
|
22
|
-
|
|
22
|
+
width: string;
|
|
23
|
+
height: string;
|
|
23
24
|
headline?: string;
|
|
24
25
|
} & AggregateInnerProps;
|
|
25
26
|
|
|
@@ -31,16 +32,17 @@ export interface AggregateInnerProps {
|
|
|
31
32
|
|
|
32
33
|
export const Aggregate: FunctionComponent<AggregateProps> = ({
|
|
33
34
|
views,
|
|
34
|
-
|
|
35
|
+
width,
|
|
36
|
+
height,
|
|
35
37
|
headline = 'Mutations',
|
|
36
38
|
filter,
|
|
37
39
|
fields,
|
|
38
40
|
}) => {
|
|
39
|
-
const
|
|
41
|
+
const size = { height, width };
|
|
40
42
|
|
|
41
43
|
return (
|
|
42
|
-
<ErrorBoundary size={size}
|
|
43
|
-
<ResizeContainer size={size}
|
|
44
|
+
<ErrorBoundary size={size} headline={headline}>
|
|
45
|
+
<ResizeContainer size={size}>
|
|
44
46
|
<Headline heading={headline}>
|
|
45
47
|
<AggregateInner fields={fields} filter={filter} views={views} />
|
|
46
48
|
</Headline>
|
|
@@ -12,22 +12,20 @@ const meta: Meta = {
|
|
|
12
12
|
defaultSize: { control: 'object' },
|
|
13
13
|
headline: { control: 'text' },
|
|
14
14
|
},
|
|
15
|
+
args: {
|
|
16
|
+
size: { height: '600px', width: '100%' },
|
|
17
|
+
headline: 'Some headline',
|
|
18
|
+
},
|
|
15
19
|
};
|
|
16
20
|
|
|
17
21
|
export default meta;
|
|
18
22
|
|
|
19
23
|
export const ErrorBoundaryWithoutErrorStory: StoryObj = {
|
|
20
24
|
render: (args) => (
|
|
21
|
-
<ErrorBoundary size={args.size}
|
|
25
|
+
<ErrorBoundary size={args.size} headline={args.headline}>
|
|
22
26
|
<div>Some content</div>
|
|
23
27
|
</ErrorBoundary>
|
|
24
28
|
),
|
|
25
|
-
args: {
|
|
26
|
-
size: { height: '600px', width: '100%' },
|
|
27
|
-
defaultSize: { height: '600px', width: '100%' },
|
|
28
|
-
headline: 'Some headline',
|
|
29
|
-
},
|
|
30
|
-
|
|
31
29
|
play: async ({ canvasElement }) => {
|
|
32
30
|
const canvas = within(canvasElement);
|
|
33
31
|
const content = canvas.getByText('Some content', { exact: false });
|
|
@@ -38,16 +36,10 @@ export const ErrorBoundaryWithoutErrorStory: StoryObj = {
|
|
|
38
36
|
|
|
39
37
|
export const ErrorBoundaryWithErrorStory: StoryObj = {
|
|
40
38
|
render: (args) => (
|
|
41
|
-
<ErrorBoundary size={args.size}
|
|
39
|
+
<ErrorBoundary size={args.size} headline={args.headline}>
|
|
42
40
|
<ContentThatThrowsError />
|
|
43
41
|
</ErrorBoundary>
|
|
44
42
|
),
|
|
45
|
-
args: {
|
|
46
|
-
size: { height: '600px', width: '100%' },
|
|
47
|
-
defaultSize: { height: '600px', width: '100%' },
|
|
48
|
-
headline: 'Some headline',
|
|
49
|
-
},
|
|
50
|
-
|
|
51
43
|
play: async ({ canvasElement }) => {
|
|
52
44
|
const canvas = within(canvasElement);
|
|
53
45
|
const content = canvas.queryByText('Some content.', { exact: false });
|
|
@@ -5,21 +5,12 @@ import { ErrorDisplay } from './error-display';
|
|
|
5
5
|
import { ResizeContainer, type Size } from './resize-container';
|
|
6
6
|
import Headline from '../components/headline';
|
|
7
7
|
|
|
8
|
-
export const ErrorBoundary: FunctionComponent<{ size
|
|
9
|
-
size,
|
|
10
|
-
defaultSize,
|
|
11
|
-
headline,
|
|
12
|
-
children,
|
|
13
|
-
}) => {
|
|
8
|
+
export const ErrorBoundary: FunctionComponent<{ size: Size; headline?: string }> = ({ size, headline, children }) => {
|
|
14
9
|
const [internalError] = useErrorBoundary();
|
|
15
10
|
|
|
16
|
-
if (internalError) {
|
|
17
|
-
console.error(internalError);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
11
|
if (internalError) {
|
|
21
12
|
return (
|
|
22
|
-
<ResizeContainer
|
|
13
|
+
<ResizeContainer size={size}>
|
|
23
14
|
<Headline heading={headline}>
|
|
24
15
|
<ErrorDisplay error={internalError} />
|
|
25
16
|
</Headline>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
-
import { expect, waitFor, within } from '@storybook/test';
|
|
2
|
+
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
|
3
3
|
|
|
4
4
|
import { ErrorDisplay, UserFacingError } from './error-display';
|
|
5
5
|
import { ResizeContainer } from './resize-container';
|
|
@@ -14,7 +14,7 @@ export default meta;
|
|
|
14
14
|
|
|
15
15
|
export const ErrorStory: StoryObj = {
|
|
16
16
|
render: () => (
|
|
17
|
-
<ResizeContainer
|
|
17
|
+
<ResizeContainer size={{ height: '600px', width: '100%' }}>
|
|
18
18
|
<ErrorDisplay error={new Error('some message')} />
|
|
19
19
|
</ResizeContainer>
|
|
20
20
|
),
|
|
@@ -29,15 +29,22 @@ export const ErrorStory: StoryObj = {
|
|
|
29
29
|
|
|
30
30
|
export const UserFacingErrorStory: StoryObj = {
|
|
31
31
|
render: () => (
|
|
32
|
-
<ResizeContainer
|
|
33
|
-
<ErrorDisplay error={new UserFacingError('some message')} />
|
|
32
|
+
<ResizeContainer size={{ height: '600px', width: '100%' }}>
|
|
33
|
+
<ErrorDisplay error={new UserFacingError('Error Title', 'some message')} />
|
|
34
34
|
</ResizeContainer>
|
|
35
35
|
),
|
|
36
36
|
|
|
37
37
|
play: async ({ canvasElement }) => {
|
|
38
38
|
const canvas = within(canvasElement);
|
|
39
39
|
const error = canvas.getByText('Oops! Something went wrong.', { exact: false });
|
|
40
|
+
const detailMessage = () => canvas.getByText('some message');
|
|
40
41
|
await waitFor(() => expect(error).toBeInTheDocument());
|
|
41
|
-
await waitFor(() =>
|
|
42
|
+
await waitFor(() => {
|
|
43
|
+
expect(detailMessage()).not.toBeVisible();
|
|
44
|
+
});
|
|
45
|
+
await userEvent.click(canvas.getByText('Show details.'));
|
|
46
|
+
await waitFor(() => {
|
|
47
|
+
expect(detailMessage()).toBeVisible();
|
|
48
|
+
});
|
|
42
49
|
},
|
|
43
50
|
};
|
|
@@ -1,18 +1,52 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
|
+
import { useRef } from 'preact/hooks';
|
|
2
3
|
|
|
3
4
|
export class UserFacingError extends Error {
|
|
4
|
-
constructor(
|
|
5
|
+
constructor(
|
|
6
|
+
public readonly headline: string,
|
|
7
|
+
message: string,
|
|
8
|
+
) {
|
|
5
9
|
super(message);
|
|
6
10
|
this.name = 'UserFacingError';
|
|
7
11
|
}
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
export const ErrorDisplay: FunctionComponent<{ error: Error }> = ({ error }) => {
|
|
15
|
+
console.error(error);
|
|
16
|
+
|
|
17
|
+
const ref = useRef<HTMLDialogElement>(null);
|
|
18
|
+
|
|
11
19
|
return (
|
|
12
20
|
<div className='h-full w-full rounded-md border-2 border-gray-100 p-2 flex items-center justify-center flex-col'>
|
|
13
21
|
<div className='text-red-700 font-bold'>Error</div>
|
|
14
|
-
<div>
|
|
15
|
-
|
|
22
|
+
<div>
|
|
23
|
+
Oops! Something went wrong.
|
|
24
|
+
{error instanceof UserFacingError && (
|
|
25
|
+
<>
|
|
26
|
+
{' '}
|
|
27
|
+
<button
|
|
28
|
+
className='text-sm text-gray-600 hover:text-gray-300'
|
|
29
|
+
onClick={() => ref.current?.showModal()}
|
|
30
|
+
>
|
|
31
|
+
Show details.
|
|
32
|
+
</button>
|
|
33
|
+
<dialog ref={ref} class='modal'>
|
|
34
|
+
<div class='modal-box'>
|
|
35
|
+
<form method='dialog'>
|
|
36
|
+
<button className='btn btn-sm btn-circle btn-ghost absolute right-2 top-2'>
|
|
37
|
+
✕
|
|
38
|
+
</button>
|
|
39
|
+
</form>
|
|
40
|
+
<h1 class='text-lg'>{error.headline}</h1>
|
|
41
|
+
<p class='py-4'>{error.message}</p>
|
|
42
|
+
</div>
|
|
43
|
+
<form method='dialog' class='modal-backdrop'>
|
|
44
|
+
<button>close</button>
|
|
45
|
+
</form>
|
|
46
|
+
</dialog>
|
|
47
|
+
</>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
16
50
|
</div>
|
|
17
51
|
);
|
|
18
52
|
};
|
|
@@ -13,7 +13,7 @@ export default meta;
|
|
|
13
13
|
|
|
14
14
|
export const LoadingStory: StoryObj = {
|
|
15
15
|
render: () => (
|
|
16
|
-
<ResizeContainer
|
|
16
|
+
<ResizeContainer size={{ height: '600px', width: '100%' }}>
|
|
17
17
|
<LoadingDisplay />
|
|
18
18
|
</ResizeContainer>
|
|
19
19
|
),
|
|
@@ -1,23 +1,14 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
2
|
|
|
3
3
|
export type Size = {
|
|
4
|
-
width
|
|
5
|
-
height
|
|
4
|
+
width: string;
|
|
5
|
+
height: string;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
export interface ResizeContainerProps {
|
|
9
|
-
size
|
|
10
|
-
defaultSize: Size;
|
|
9
|
+
size: Size;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
export const ResizeContainer: FunctionComponent<ResizeContainerProps> = ({ children, size
|
|
14
|
-
return <div style={
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const extendByDefault = (size: Size | undefined, defaultSize: Size) => {
|
|
18
|
-
if (size === undefined) {
|
|
19
|
-
return defaultSize;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return { ...defaultSize, ...size };
|
|
12
|
+
export const ResizeContainer: FunctionComponent<ResizeContainerProps> = ({ children, size }) => {
|
|
13
|
+
return <div style={size}>{children}</div>;
|
|
23
14
|
};
|
|
@@ -60,6 +60,7 @@ const meta: Meta<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
|
60
60
|
customSelectOptions: [{ label: 'CustomDateRange', dateFrom: '2021-01-01', dateTo: '2021-12-31' }],
|
|
61
61
|
earliestDate: '1970-01-01',
|
|
62
62
|
initialValue: PRESET_VALUE_LAST_3_MONTHS,
|
|
63
|
+
dateColumn: 'aDateColumn',
|
|
63
64
|
width: '100%',
|
|
64
65
|
},
|
|
65
66
|
decorators: [withActions],
|
|
@@ -75,6 +76,7 @@ export const Primary: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
|
75
76
|
earliestDate={args.earliestDate}
|
|
76
77
|
initialValue={args.initialValue}
|
|
77
78
|
width={args.width}
|
|
79
|
+
dateColumn={args.dateColumn}
|
|
78
80
|
/>
|
|
79
81
|
</LapisUrlContext.Provider>
|
|
80
82
|
),
|