@genspectrum/dashboard-components 0.16.2 → 0.16.3
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 +72 -7
- package/dist/assets/mutationOverTimeWorker-DJcZmEH9.js.map +1 -0
- package/dist/components.d.ts +69 -31
- package/dist/components.js +305 -148
- package/dist/components.js.map +1 -1
- package/dist/style.css +16 -0
- package/dist/util.d.ts +31 -31
- package/package.json +4 -2
- package/src/preact/MutationAnnotationsContext.spec.tsx +58 -0
- package/src/preact/MutationAnnotationsContext.tsx +72 -0
- package/src/preact/components/annotated-mutation.stories.tsx +163 -0
- package/src/preact/components/annotated-mutation.tsx +80 -0
- package/src/preact/components/error-display.tsx +9 -9
- package/src/preact/components/info.tsx +6 -13
- package/src/preact/components/modal.stories.tsx +7 -19
- package/src/preact/components/modal.tsx +35 -4
- package/src/preact/mutations/mutations-table.tsx +14 -2
- package/src/preact/mutations/mutations.stories.tsx +40 -2
- package/src/preact/mutations/mutations.tsx +1 -0
- package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +19 -8
- package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +34 -5
- package/src/preact/mutationsOverTime/mutations-over-time.tsx +13 -1
- package/src/preact/sequencesByLocation/sequences-by-location-map.tsx +28 -30
- package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +6 -1
- package/src/web-components/gs-app.spec-d.ts +10 -0
- package/src/web-components/gs-app.stories.ts +24 -6
- package/src/web-components/gs-app.ts +17 -0
- package/src/web-components/mutation-annotations-context.ts +16 -0
- package/src/web-components/visualization/gs-mutations-over-time.stories.ts +18 -1
- package/src/web-components/visualization/gs-mutations-over-time.tsx +22 -11
- package/src/web-components/visualization/gs-mutations.stories.ts +18 -1
- package/src/web-components/visualization/gs-mutations.tsx +20 -9
- package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.stories.ts +11 -1
- package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +18 -7
- package/standalone-bundle/assets/mutationOverTimeWorker-CERZSdcA.js.map +1 -0
- package/standalone-bundle/dashboard-components.js +7332 -7202
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/standalone-bundle/style.css +1 -1
- package/dist/assets/mutationOverTimeWorker-BL50C-yi.js.map +0 -1
- package/standalone-bundle/assets/mutationOverTimeWorker-CFB5-Mdk.js.map +0 -1
package/dist/style.css
CHANGED
|
@@ -1073,6 +1073,12 @@ html {
|
|
|
1073
1073
|
--tw-bg-opacity: 1;
|
|
1074
1074
|
background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
|
|
1075
1075
|
}
|
|
1076
|
+
|
|
1077
|
+
.table-zebra tr.hover:hover,
|
|
1078
|
+
.table-zebra tr.hover:nth-child(even):hover {
|
|
1079
|
+
--tw-bg-opacity: 1;
|
|
1080
|
+
background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));
|
|
1081
|
+
}
|
|
1076
1082
|
}
|
|
1077
1083
|
.btn {
|
|
1078
1084
|
display: inline-flex;
|
|
@@ -3187,6 +3193,9 @@ input.tab:checked + .tab-content,
|
|
|
3187
3193
|
.mt-4 {
|
|
3188
3194
|
margin-top: 1rem;
|
|
3189
3195
|
}
|
|
3196
|
+
.block {
|
|
3197
|
+
display: block;
|
|
3198
|
+
}
|
|
3190
3199
|
.inline {
|
|
3191
3200
|
display: inline;
|
|
3192
3201
|
}
|
|
@@ -3492,6 +3501,9 @@ input.tab:checked + .tab-content,
|
|
|
3492
3501
|
.font-medium {
|
|
3493
3502
|
font-weight: 500;
|
|
3494
3503
|
}
|
|
3504
|
+
.font-normal {
|
|
3505
|
+
font-weight: 400;
|
|
3506
|
+
}
|
|
3495
3507
|
.font-semibold {
|
|
3496
3508
|
font-weight: 600;
|
|
3497
3509
|
}
|
|
@@ -3522,6 +3534,10 @@ input.tab:checked + .tab-content,
|
|
|
3522
3534
|
--tw-text-opacity: 1;
|
|
3523
3535
|
color: rgb(115 115 115 / var(--tw-text-opacity, 1));
|
|
3524
3536
|
}
|
|
3537
|
+
.text-red-600 {
|
|
3538
|
+
--tw-text-opacity: 1;
|
|
3539
|
+
color: rgb(220 38 38 / var(--tw-text-opacity, 1));
|
|
3540
|
+
}
|
|
3525
3541
|
.text-red-700 {
|
|
3526
3542
|
--tw-text-opacity: 1;
|
|
3527
3543
|
color: rgb(185 28 28 / var(--tw-text-opacity, 1));
|
package/dist/util.d.ts
CHANGED
|
@@ -880,7 +880,7 @@ declare global {
|
|
|
880
880
|
|
|
881
881
|
declare global {
|
|
882
882
|
interface HTMLElementTagNameMap {
|
|
883
|
-
'gs-
|
|
883
|
+
'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
|
|
884
884
|
}
|
|
885
885
|
}
|
|
886
886
|
|
|
@@ -888,7 +888,7 @@ declare global {
|
|
|
888
888
|
declare global {
|
|
889
889
|
namespace JSX {
|
|
890
890
|
interface IntrinsicElements {
|
|
891
|
-
'gs-
|
|
891
|
+
'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
892
892
|
}
|
|
893
893
|
}
|
|
894
894
|
}
|
|
@@ -896,7 +896,7 @@ declare global {
|
|
|
896
896
|
|
|
897
897
|
declare global {
|
|
898
898
|
interface HTMLElementTagNameMap {
|
|
899
|
-
'gs-
|
|
899
|
+
'gs-prevalence-over-time': PrevalenceOverTimeComponent;
|
|
900
900
|
}
|
|
901
901
|
}
|
|
902
902
|
|
|
@@ -904,7 +904,7 @@ declare global {
|
|
|
904
904
|
declare global {
|
|
905
905
|
namespace JSX {
|
|
906
906
|
interface IntrinsicElements {
|
|
907
|
-
'gs-
|
|
907
|
+
'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
908
908
|
}
|
|
909
909
|
}
|
|
910
910
|
}
|
|
@@ -926,6 +926,22 @@ declare global {
|
|
|
926
926
|
}
|
|
927
927
|
|
|
928
928
|
|
|
929
|
+
declare global {
|
|
930
|
+
interface HTMLElementTagNameMap {
|
|
931
|
+
'gs-mutations-over-time': MutationsOverTimeComponent;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
declare global {
|
|
937
|
+
namespace JSX {
|
|
938
|
+
interface IntrinsicElements {
|
|
939
|
+
'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
|
|
929
945
|
declare global {
|
|
930
946
|
interface HTMLElementTagNameMap {
|
|
931
947
|
'gs-aggregate': AggregateComponent;
|
|
@@ -944,7 +960,7 @@ declare global {
|
|
|
944
960
|
|
|
945
961
|
declare global {
|
|
946
962
|
interface HTMLElementTagNameMap {
|
|
947
|
-
'gs-
|
|
963
|
+
'gs-statistics': StatisticsComponent;
|
|
948
964
|
}
|
|
949
965
|
}
|
|
950
966
|
|
|
@@ -952,7 +968,7 @@ declare global {
|
|
|
952
968
|
declare global {
|
|
953
969
|
namespace JSX {
|
|
954
970
|
interface IntrinsicElements {
|
|
955
|
-
'gs-
|
|
971
|
+
'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
956
972
|
}
|
|
957
973
|
}
|
|
958
974
|
}
|
|
@@ -960,7 +976,10 @@ declare global {
|
|
|
960
976
|
|
|
961
977
|
declare global {
|
|
962
978
|
interface HTMLElementTagNameMap {
|
|
963
|
-
'gs-
|
|
979
|
+
'gs-location-filter': LocationFilterComponent;
|
|
980
|
+
}
|
|
981
|
+
interface HTMLElementEventMap {
|
|
982
|
+
'gs-location-changed': LocationChangedEvent;
|
|
964
983
|
}
|
|
965
984
|
}
|
|
966
985
|
|
|
@@ -968,7 +987,7 @@ declare global {
|
|
|
968
987
|
declare global {
|
|
969
988
|
namespace JSX {
|
|
970
989
|
interface IntrinsicElements {
|
|
971
|
-
'gs-
|
|
990
|
+
'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
972
991
|
}
|
|
973
992
|
}
|
|
974
993
|
}
|
|
@@ -976,7 +995,7 @@ declare global {
|
|
|
976
995
|
|
|
977
996
|
declare global {
|
|
978
997
|
interface HTMLElementTagNameMap {
|
|
979
|
-
'gs-
|
|
998
|
+
'gs-sequences-by-location': SequencesByLocationComponent;
|
|
980
999
|
}
|
|
981
1000
|
}
|
|
982
1001
|
|
|
@@ -984,7 +1003,7 @@ declare global {
|
|
|
984
1003
|
declare global {
|
|
985
1004
|
namespace JSX {
|
|
986
1005
|
interface IntrinsicElements {
|
|
987
|
-
'gs-
|
|
1006
|
+
'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
988
1007
|
}
|
|
989
1008
|
}
|
|
990
1009
|
}
|
|
@@ -992,10 +1011,7 @@ declare global {
|
|
|
992
1011
|
|
|
993
1012
|
declare global {
|
|
994
1013
|
interface HTMLElementTagNameMap {
|
|
995
|
-
'gs-
|
|
996
|
-
}
|
|
997
|
-
interface HTMLElementEventMap {
|
|
998
|
-
'gs-location-changed': LocationChangedEvent;
|
|
1014
|
+
'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
|
|
999
1015
|
}
|
|
1000
1016
|
}
|
|
1001
1017
|
|
|
@@ -1003,7 +1019,7 @@ declare global {
|
|
|
1003
1019
|
declare global {
|
|
1004
1020
|
namespace JSX {
|
|
1005
1021
|
interface IntrinsicElements {
|
|
1006
|
-
'gs-
|
|
1022
|
+
'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1007
1023
|
}
|
|
1008
1024
|
}
|
|
1009
1025
|
}
|
|
@@ -1086,22 +1102,6 @@ declare global {
|
|
|
1086
1102
|
}
|
|
1087
1103
|
|
|
1088
1104
|
|
|
1089
|
-
declare global {
|
|
1090
|
-
interface HTMLElementTagNameMap {
|
|
1091
|
-
'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
declare global {
|
|
1097
|
-
namespace JSX {
|
|
1098
|
-
interface IntrinsicElements {
|
|
1099
|
-
'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
1105
|
declare module 'chart.js' {
|
|
1106
1106
|
interface CartesianScaleTypeRegistry {
|
|
1107
1107
|
logit: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genspectrum/dashboard-components",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.3",
|
|
4
4
|
"description": "GenSpectrum web components for building dashboards",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build": "vite --config vite.release.config.ts build && npm run generate-manifest && vite --config vite.release-standalone.config.ts build",
|
|
53
53
|
"build-and-pack": "npm run build && npm pack | xargs -I {} cp {} genspectrum-dashboard-components.tgz",
|
|
54
|
-
"test": "vitest",
|
|
54
|
+
"test": "vitest --typecheck",
|
|
55
55
|
"lint": "npm run lint:lit-analyzer && npm run lint:eslint",
|
|
56
56
|
"lint:eslint": "eslint 'src/**/*.{ts,tsx}'",
|
|
57
57
|
"lint:lit-analyzer": "lit-analyzer",
|
|
@@ -115,6 +115,7 @@
|
|
|
115
115
|
"@storybook/web-components": "^8.0.9",
|
|
116
116
|
"@storybook/web-components-vite": "^8.0.9",
|
|
117
117
|
"@tailwindcss/container-queries": "^0.1.1",
|
|
118
|
+
"@testing-library/preact": "^3.2.4",
|
|
118
119
|
"@types/geojson": "^7946.0.15",
|
|
119
120
|
"@types/leaflet": "^1.9.15",
|
|
120
121
|
"@types/node": "^22.0.0",
|
|
@@ -130,6 +131,7 @@
|
|
|
130
131
|
"eslint-plugin-import": "^2.29.1",
|
|
131
132
|
"eslint-plugin-jest": "^28.2.0",
|
|
132
133
|
"eslint-plugin-storybook": "^0.11.0",
|
|
134
|
+
"happy-dom": "^17.1.1",
|
|
133
135
|
"http-server": "^14.1.1",
|
|
134
136
|
"lit-analyzer": "^2.0.3",
|
|
135
137
|
"msw": "^2.2.14",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/preact';
|
|
2
|
+
import { type FunctionalComponent } from 'preact';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { MutationAnnotationsContextProvider, useMutationAnnotationsProvider } from './MutationAnnotationsContext';
|
|
6
|
+
import { type MutationAnnotations } from '../web-components/mutation-annotations-context';
|
|
7
|
+
|
|
8
|
+
describe('useMutationAnnotation', () => {
|
|
9
|
+
const mockAnnotations: MutationAnnotations = [
|
|
10
|
+
{
|
|
11
|
+
name: 'Annotation 1',
|
|
12
|
+
description: 'Description 1',
|
|
13
|
+
symbol: 'A1',
|
|
14
|
+
nucleotideMutations: ['A123', 'A456'],
|
|
15
|
+
aminoAcidMutations: ['B123'],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'Annotation 2',
|
|
19
|
+
description: 'Description 2',
|
|
20
|
+
symbol: 'A2',
|
|
21
|
+
nucleotideMutations: ['A456', 'A789'],
|
|
22
|
+
aminoAcidMutations: ['B456', 'B789'],
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const wrapper: FunctionalComponent = ({ children }) => (
|
|
27
|
+
<MutationAnnotationsContextProvider value={mockAnnotations}>{children}</MutationAnnotationsContextProvider>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
function renderAnnotationsHook() {
|
|
31
|
+
const { result } = renderHook(() => useMutationAnnotationsProvider(), { wrapper });
|
|
32
|
+
return result.current;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
it('should return the correct annotation for a given nucleotide mutation', () => {
|
|
36
|
+
const result = renderAnnotationsHook()('A123', 'nucleotide');
|
|
37
|
+
|
|
38
|
+
expect(result).toEqual([mockAnnotations[0]]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should return the correct annotations if multiple contain a mutation', () => {
|
|
42
|
+
const result = renderAnnotationsHook()('A456', 'nucleotide');
|
|
43
|
+
|
|
44
|
+
expect(result).toEqual([mockAnnotations[0], mockAnnotations[1]]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should return undefined for a non-existent mutation code', () => {
|
|
48
|
+
const result = renderAnnotationsHook()('NON_EXISTENT', 'nucleotide');
|
|
49
|
+
|
|
50
|
+
expect(result).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should return the correct mutation annotation for amino acid mutations', () => {
|
|
54
|
+
const result = renderAnnotationsHook()('B456', 'amino acid');
|
|
55
|
+
|
|
56
|
+
expect(result).toEqual([mockAnnotations[1]]);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type ComponentProps, createContext, type FunctionalComponent } from 'preact';
|
|
2
|
+
import { useContext, useMemo } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
import { type SequenceType } from '../types';
|
|
5
|
+
import {
|
|
6
|
+
type MutationAnnotation,
|
|
7
|
+
type MutationAnnotations,
|
|
8
|
+
mutationAnnotationsSchema,
|
|
9
|
+
} from '../web-components/mutation-annotations-context';
|
|
10
|
+
import { ErrorDisplay } from './components/error-display';
|
|
11
|
+
import { ResizeContainer } from './components/resize-container';
|
|
12
|
+
|
|
13
|
+
const MutationAnnotationsContext = createContext<Record<SequenceType, Map<string, MutationAnnotations>>>({
|
|
14
|
+
nucleotide: new Map(),
|
|
15
|
+
'amino acid': new Map(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const MutationAnnotationsContextProvider: FunctionalComponent<
|
|
19
|
+
Omit<ComponentProps<typeof MutationAnnotationsContext.Provider>, 'value'> & { value: MutationAnnotations }
|
|
20
|
+
> = ({ value, children }) => {
|
|
21
|
+
const parseResult = useMemo(() => {
|
|
22
|
+
const parseResult = mutationAnnotationsSchema.safeParse(value);
|
|
23
|
+
|
|
24
|
+
if (!parseResult.success) {
|
|
25
|
+
return parseResult;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const nucleotideMap = new Map<string, MutationAnnotations>();
|
|
29
|
+
const aminoAcidMap = new Map<string, MutationAnnotations>();
|
|
30
|
+
|
|
31
|
+
value.forEach((annotation) => {
|
|
32
|
+
new Set(annotation.nucleotideMutations).forEach((code) => {
|
|
33
|
+
addAnnotationToMap(nucleotideMap, code, annotation);
|
|
34
|
+
});
|
|
35
|
+
new Set(annotation.aminoAcidMutations).forEach((code) => {
|
|
36
|
+
addAnnotationToMap(aminoAcidMap, code, annotation);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
success: true as const,
|
|
42
|
+
value: {
|
|
43
|
+
nucleotide: nucleotideMap,
|
|
44
|
+
'amino acid': aminoAcidMap,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}, [value]);
|
|
48
|
+
|
|
49
|
+
if (!parseResult.success) {
|
|
50
|
+
return (
|
|
51
|
+
<ResizeContainer size={{ width: '100%' }}>
|
|
52
|
+
<ErrorDisplay error={parseResult.error} layout='vertical' />
|
|
53
|
+
</ResizeContainer>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<MutationAnnotationsContext.Provider value={parseResult.value}>{children}</MutationAnnotationsContext.Provider>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function addAnnotationToMap(map: Map<string, MutationAnnotations>, code: string, annotation: MutationAnnotation) {
|
|
63
|
+
const oldAnnotations = map.get(code.toUpperCase()) ?? [];
|
|
64
|
+
map.set(code.toUpperCase(), [...oldAnnotations, annotation]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function useMutationAnnotationsProvider() {
|
|
68
|
+
const mutationAnnotations = useContext(MutationAnnotationsContext);
|
|
69
|
+
|
|
70
|
+
return (mutationCode: string, sequenceType: SequenceType) =>
|
|
71
|
+
mutationAnnotations[sequenceType].get(mutationCode.toUpperCase());
|
|
72
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
+
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
|
3
|
+
|
|
4
|
+
import { AnnotatedMutation, type AnnotatedMutationProps } from './annotated-mutation';
|
|
5
|
+
import { type MutationAnnotations } from '../../web-components/mutation-annotations-context';
|
|
6
|
+
import { MutationAnnotationsContextProvider } from '../MutationAnnotationsContext';
|
|
7
|
+
|
|
8
|
+
const meta: Meta<AnnotatedMutationProps & { annotations: MutationAnnotations }> = {
|
|
9
|
+
title: 'Component/Annotated Mutation',
|
|
10
|
+
component: AnnotatedMutation,
|
|
11
|
+
parameters: { fetchMock: {} },
|
|
12
|
+
argTypes: {
|
|
13
|
+
annotations: { control: { type: 'object' } },
|
|
14
|
+
mutation: { control: { type: 'object' } },
|
|
15
|
+
sequenceType: {
|
|
16
|
+
options: ['nucleotide', 'amino acid'],
|
|
17
|
+
control: { type: 'radio' },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
|
|
24
|
+
export const MutationWithoutAnnotationEntry: StoryObj<AnnotatedMutationProps & { annotations: MutationAnnotations }> = {
|
|
25
|
+
render: (args) => {
|
|
26
|
+
const { annotations, ...annotatedMutationsArgs } = args;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<MutationAnnotationsContextProvider value={annotations}>
|
|
30
|
+
<AnnotatedMutation {...annotatedMutationsArgs} />
|
|
31
|
+
</MutationAnnotationsContextProvider>
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
args: {
|
|
35
|
+
mutation: {
|
|
36
|
+
type: 'substitution',
|
|
37
|
+
code: 'A23403G',
|
|
38
|
+
position: 23403,
|
|
39
|
+
valueAtReference: 'A',
|
|
40
|
+
substitutionValue: 'G',
|
|
41
|
+
},
|
|
42
|
+
sequenceType: 'nucleotide',
|
|
43
|
+
annotations: [
|
|
44
|
+
{
|
|
45
|
+
name: 'Test annotation',
|
|
46
|
+
description: 'This is a test annotation',
|
|
47
|
+
symbol: '*',
|
|
48
|
+
nucleotideMutations: ['123T'],
|
|
49
|
+
aminoAcidMutations: [],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
play: async ({ canvasElement }) => {
|
|
54
|
+
const canvas = within(canvasElement);
|
|
55
|
+
|
|
56
|
+
await waitFor(() => expect(canvas.getByText('A23403G')).toBeVisible());
|
|
57
|
+
await expect(getAnnotationIndicator(canvas)).not.toBeInTheDocument();
|
|
58
|
+
await expect(getAnnotationName(canvas)).not.toBeInTheDocument();
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const MutationWithAnnotationEntry: StoryObj<AnnotatedMutationProps & { annotations: MutationAnnotations }> = {
|
|
63
|
+
...MutationWithoutAnnotationEntry,
|
|
64
|
+
args: {
|
|
65
|
+
...MutationWithoutAnnotationEntry.args,
|
|
66
|
+
annotations: [
|
|
67
|
+
{
|
|
68
|
+
name: 'Test annotation',
|
|
69
|
+
description: 'This is a test annotation',
|
|
70
|
+
symbol: '*',
|
|
71
|
+
nucleotideMutations: ['A23403G'],
|
|
72
|
+
aminoAcidMutations: [],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
play: async ({ canvasElement }) => {
|
|
77
|
+
const canvas = within(canvasElement);
|
|
78
|
+
|
|
79
|
+
await waitFor(() => expect(canvas.getByText('A23403G')).toBeVisible());
|
|
80
|
+
await expect(getAnnotationIndicator(canvas)).toBeVisible();
|
|
81
|
+
|
|
82
|
+
await userEvent.click(canvas.getByText('A23403G'));
|
|
83
|
+
await waitFor(() => expect(getAnnotationName(canvas)).toBeVisible());
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const MutationWithMultipleAnnotationEntries: StoryObj<
|
|
88
|
+
AnnotatedMutationProps & { annotations: MutationAnnotations }
|
|
89
|
+
> = {
|
|
90
|
+
...MutationWithoutAnnotationEntry,
|
|
91
|
+
args: {
|
|
92
|
+
...MutationWithoutAnnotationEntry.args,
|
|
93
|
+
annotations: [
|
|
94
|
+
{
|
|
95
|
+
name: 'Test annotation',
|
|
96
|
+
description: 'This is a test annotation',
|
|
97
|
+
symbol: '*',
|
|
98
|
+
nucleotideMutations: ['A23403G'],
|
|
99
|
+
aminoAcidMutations: [],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'Another test annotation',
|
|
103
|
+
description: 'This is a test annotation',
|
|
104
|
+
symbol: '+',
|
|
105
|
+
nucleotideMutations: ['A23403G'],
|
|
106
|
+
aminoAcidMutations: [],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
play: async ({ canvasElement }) => {
|
|
111
|
+
const canvas = within(canvasElement);
|
|
112
|
+
|
|
113
|
+
await waitFor(() => expect(canvas.getByText('A23403G')).toBeVisible());
|
|
114
|
+
await expect(getAnnotationIndicator(canvas)).toBeVisible();
|
|
115
|
+
await expect(canvas.queryByText('+')).toBeVisible();
|
|
116
|
+
|
|
117
|
+
await userEvent.click(canvas.getByText('A23403G'));
|
|
118
|
+
await waitFor(() => expect(getAnnotationName(canvas)).toBeVisible());
|
|
119
|
+
await expect(canvas.queryByText('Another test annotation')).toBeVisible();
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const AminoAcidMutationWithAnnotationEntry: StoryObj<
|
|
124
|
+
AnnotatedMutationProps & { annotations: MutationAnnotations }
|
|
125
|
+
> = {
|
|
126
|
+
...MutationWithoutAnnotationEntry,
|
|
127
|
+
args: {
|
|
128
|
+
mutation: {
|
|
129
|
+
type: 'substitution',
|
|
130
|
+
code: 'S:A501G',
|
|
131
|
+
position: 501,
|
|
132
|
+
valueAtReference: 'A',
|
|
133
|
+
substitutionValue: 'G',
|
|
134
|
+
},
|
|
135
|
+
sequenceType: 'amino acid',
|
|
136
|
+
annotations: [
|
|
137
|
+
{
|
|
138
|
+
name: 'Test annotation',
|
|
139
|
+
description: 'This is a test annotation',
|
|
140
|
+
symbol: '*',
|
|
141
|
+
nucleotideMutations: [],
|
|
142
|
+
aminoAcidMutations: ['S:A501G'],
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
play: async ({ canvasElement }) => {
|
|
147
|
+
const canvas = within(canvasElement);
|
|
148
|
+
|
|
149
|
+
await waitFor(() => expect(canvas.getByText('S:A501G')).toBeVisible());
|
|
150
|
+
await expect(getAnnotationIndicator(canvas)).toBeVisible();
|
|
151
|
+
|
|
152
|
+
await userEvent.click(canvas.getByText('S:A501G'));
|
|
153
|
+
await waitFor(() => expect(getAnnotationName(canvas)).toBeVisible());
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
function getAnnotationIndicator(canvas: ReturnType<typeof within>) {
|
|
158
|
+
return canvas.queryByText('*');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getAnnotationName(canvas: ReturnType<typeof within>) {
|
|
162
|
+
return canvas.queryByText('Test annotation');
|
|
163
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useRef } from 'gridjs';
|
|
2
|
+
import { Fragment, type FunctionComponent, type RefObject } from 'preact';
|
|
3
|
+
|
|
4
|
+
import type { SequenceType } from '../../types';
|
|
5
|
+
import type { Deletion, Substitution } from '../../utils/mutations';
|
|
6
|
+
import { useMutationAnnotationsProvider } from '../MutationAnnotationsContext';
|
|
7
|
+
import { InfoHeadline1, InfoHeadline2, InfoParagraph } from './info';
|
|
8
|
+
import { ButtonWithModalDialog, useModalRef } from './modal';
|
|
9
|
+
|
|
10
|
+
export type AnnotatedMutationProps = {
|
|
11
|
+
mutation: Substitution | Deletion;
|
|
12
|
+
sequenceType: SequenceType;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const AnnotatedMutation: FunctionComponent<AnnotatedMutationProps> = (props) => {
|
|
16
|
+
const annotationsProvider = useMutationAnnotationsProvider();
|
|
17
|
+
const modalRef = useModalRef();
|
|
18
|
+
|
|
19
|
+
return <AnnotatedMutationWithoutContext {...props} annotationsProvider={annotationsProvider} modalRef={modalRef} />;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type GridJsAnnotatedMutationProps = AnnotatedMutationProps & {
|
|
23
|
+
annotationsProvider: ReturnType<typeof useMutationAnnotationsProvider>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* GridJS internally also uses Preact, but it uses its own Preact instance:
|
|
28
|
+
* - Our Preact contexts are not available in GridJS. We need to inject context content as long as we're in our Preact instance.
|
|
29
|
+
* - We must use the GridJS re-exports of the Preact hooks. I'm not sure why.
|
|
30
|
+
*/
|
|
31
|
+
export const GridJsAnnotatedMutation: FunctionComponent<GridJsAnnotatedMutationProps> = (props) => {
|
|
32
|
+
const modalRef = useRef<HTMLDialogElement>(null);
|
|
33
|
+
|
|
34
|
+
return <AnnotatedMutationWithoutContext {...props} modalRef={modalRef} />;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type AnnotatedMutationWithoutContextProps = GridJsAnnotatedMutationProps & {
|
|
38
|
+
modalRef: RefObject<HTMLDialogElement>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const AnnotatedMutationWithoutContext: FunctionComponent<AnnotatedMutationWithoutContextProps> = ({
|
|
42
|
+
mutation,
|
|
43
|
+
sequenceType,
|
|
44
|
+
annotationsProvider,
|
|
45
|
+
modalRef,
|
|
46
|
+
}) => {
|
|
47
|
+
const mutationAnnotations = annotationsProvider(mutation.code, sequenceType);
|
|
48
|
+
|
|
49
|
+
if (mutationAnnotations === undefined || mutationAnnotations.length === 0) {
|
|
50
|
+
return mutation.code;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const modalContent = (
|
|
54
|
+
<div className='block'>
|
|
55
|
+
<InfoHeadline1>Annotations for {mutation.code}</InfoHeadline1>
|
|
56
|
+
{mutationAnnotations.map((annotation) => (
|
|
57
|
+
<Fragment key={annotation.name}>
|
|
58
|
+
<InfoHeadline2>{annotation.name}</InfoHeadline2>
|
|
59
|
+
<InfoParagraph>{annotation.description}</InfoParagraph>
|
|
60
|
+
</Fragment>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<ButtonWithModalDialog modalContent={modalContent} modalRef={modalRef}>
|
|
67
|
+
{mutation.code}
|
|
68
|
+
<sup>
|
|
69
|
+
{mutationAnnotations
|
|
70
|
+
.map((annotation) => annotation.symbol)
|
|
71
|
+
.map((symbol, index) => (
|
|
72
|
+
<Fragment key={symbol}>
|
|
73
|
+
<span className='text-red-600'>{symbol}</span>
|
|
74
|
+
{index !== mutationAnnotations.length - 1 && ','}
|
|
75
|
+
</Fragment>
|
|
76
|
+
))}
|
|
77
|
+
</sup>
|
|
78
|
+
</ButtonWithModalDialog>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
@@ -3,7 +3,7 @@ import { useEffect, useRef } from 'preact/hooks';
|
|
|
3
3
|
import { type ZodError } from 'zod';
|
|
4
4
|
|
|
5
5
|
import { InfoHeadline1, InfoParagraph } from './info';
|
|
6
|
-
import { Modal
|
|
6
|
+
import { Modal } from './modal';
|
|
7
7
|
import { LapisError, UnknownLapisError } from '../../lapisApi/lapisApi';
|
|
8
8
|
|
|
9
9
|
export const GS_ERROR_EVENT_TYPE = 'gs-error';
|
|
@@ -48,7 +48,6 @@ export const ErrorDisplay: FunctionComponent<ErrorDisplayProps> = ({ error, rese
|
|
|
48
48
|
console.error(error);
|
|
49
49
|
|
|
50
50
|
const containerRef = useRef<HTMLInputElement>(null);
|
|
51
|
-
const modalRef = useModalRef();
|
|
52
51
|
|
|
53
52
|
useEffect(() => {
|
|
54
53
|
containerRef.current?.dispatchEvent(new ErrorEvent(error));
|
|
@@ -68,15 +67,16 @@ export const ErrorDisplay: FunctionComponent<ErrorDisplayProps> = ({ error, rese
|
|
|
68
67
|
{details !== undefined && (
|
|
69
68
|
<>
|
|
70
69
|
{' '}
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
<Modal
|
|
71
|
+
buttonClassName='underline hover:text-gray-400'
|
|
72
|
+
modalContent={
|
|
73
|
+
<>
|
|
74
|
+
<InfoHeadline1>{details.headline}</InfoHeadline1>
|
|
75
|
+
<InfoParagraph>{details.message}</InfoParagraph>
|
|
76
|
+
</>
|
|
77
|
+
}
|
|
74
78
|
>
|
|
75
79
|
Show details.
|
|
76
|
-
</button>
|
|
77
|
-
<Modal modalRef={modalRef}>
|
|
78
|
-
<InfoHeadline1>{details.headline}</InfoHeadline1>
|
|
79
|
-
<InfoParagraph>{details.message}</InfoParagraph>
|
|
80
80
|
</Modal>
|
|
81
81
|
</>
|
|
82
82
|
)}
|
|
@@ -1,34 +1,27 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
2
|
|
|
3
|
-
import { Modal
|
|
3
|
+
import { Modal } from './modal';
|
|
4
4
|
|
|
5
5
|
const Info: FunctionComponent = ({ children }) => {
|
|
6
|
-
const modalRef = useModalRef();
|
|
7
|
-
|
|
8
|
-
const toggleHelp = () => {
|
|
9
|
-
modalRef.current?.showModal();
|
|
10
|
-
};
|
|
11
|
-
|
|
12
6
|
return (
|
|
13
7
|
<div className='relative'>
|
|
14
|
-
<
|
|
8
|
+
<Modal buttonClassName='btn btn-xs' modalContent={children}>
|
|
15
9
|
?
|
|
16
|
-
</
|
|
17
|
-
<Modal modalRef={modalRef}>{children}</Modal>
|
|
10
|
+
</Modal>
|
|
18
11
|
</div>
|
|
19
12
|
);
|
|
20
13
|
};
|
|
21
14
|
|
|
22
15
|
export const InfoHeadline1: FunctionComponent = ({ children }) => {
|
|
23
|
-
return <h1 className='text-lg font-bold'>{children}</h1>;
|
|
16
|
+
return <h1 className='text-justify text-lg font-bold'>{children}</h1>;
|
|
24
17
|
};
|
|
25
18
|
|
|
26
19
|
export const InfoHeadline2: FunctionComponent = ({ children }) => {
|
|
27
|
-
return <h2 className='text-base font-bold mt-4'>{children}</h2>;
|
|
20
|
+
return <h2 className='text-justify text-base font-bold mt-4'>{children}</h2>;
|
|
28
21
|
};
|
|
29
22
|
|
|
30
23
|
export const InfoParagraph: FunctionComponent = ({ children }) => {
|
|
31
|
-
return <p className='text-justify my-1'>{children}</p>;
|
|
24
|
+
return <p className='text-justify text-base font-normal my-1'>{children}</p>;
|
|
32
25
|
};
|
|
33
26
|
|
|
34
27
|
export const InfoLink: FunctionComponent<{ href: string }> = ({ children, href }) => {
|