@genspectrum/dashboard-components 0.18.4 → 0.18.6
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/README.md +12 -0
- package/custom-elements.json +1 -1
- package/dist/components.d.ts +44 -44
- package/dist/components.js +826 -343
- package/dist/components.js.map +1 -1
- package/dist/style.css +2 -2
- package/dist/util.d.ts +44 -44
- package/package.json +2 -2
- package/src/preact/MutationAnnotationsContext.tsx +34 -27
- package/src/preact/components/dropdown.tsx +1 -1
- package/src/preact/components/info.tsx +1 -1
- package/src/preact/components/mutations-over-time-text-filter.stories.tsx +57 -0
- package/src/preact/components/mutations-over-time-text-filter.tsx +63 -0
- package/src/preact/components/segment-selector.stories.tsx +12 -5
- package/src/preact/components/segment-selector.tsx +11 -7
- package/src/preact/mutationComparison/mutation-comparison.tsx +5 -1
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +169 -50
- package/src/preact/mutationFilter/mutation-filter.tsx +239 -234
- package/src/preact/mutationFilter/parseAndValidateMutation.ts +62 -10
- package/src/preact/mutationFilter/parseMutation.spec.ts +62 -47
- package/src/preact/mutations/mutations.tsx +5 -1
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +128 -0
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +39 -2
- package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +9 -12
- package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +27 -0
- package/src/preact/mutationsOverTime/mutations-over-time.tsx +31 -6
- package/src/preact/sequencesByLocation/__mockData__/worldAtlas.json +1 -1
- package/src/preact/shared/tanstackTable/pagination-context.tsx +30 -0
- package/src/preact/shared/tanstackTable/pagination.tsx +41 -21
- package/src/preact/shared/tanstackTable/tanstackTable.tsx +17 -3
- package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +22 -4
- package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +11 -2
- package/src/web-components/input/gs-mutation-filter.stories.ts +4 -4
- package/src/web-components/visualization/gs-prevalence-over-time.stories.ts +1 -1
- package/standalone-bundle/dashboard-components.js +12896 -13334
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/standalone-bundle/style.css +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genspectrum/dashboard-components",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.6",
|
|
4
4
|
"description": "GenSpectrum web components for building dashboards",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"eslint-config-preact": "^1.3.0",
|
|
133
133
|
"eslint-plugin-import": "^2.29.1",
|
|
134
134
|
"eslint-plugin-jest": "^28.2.0",
|
|
135
|
-
"eslint-plugin-storybook": "^0.
|
|
135
|
+
"eslint-plugin-storybook": "^0.12.0",
|
|
136
136
|
"happy-dom": "^17.1.1",
|
|
137
137
|
"http-server": "^14.1.1",
|
|
138
138
|
"lit-analyzer": "^2.0.3",
|
|
@@ -37,33 +37,7 @@ export const MutationAnnotationsContextProvider: FunctionalComponent<
|
|
|
37
37
|
return parseResult;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
const nucleotidePositions = new Map<string, MutationAnnotations>();
|
|
42
|
-
const aminoAcidMap = new Map<string, MutationAnnotations>();
|
|
43
|
-
const aminoAcidPositions = new Map<string, MutationAnnotations>();
|
|
44
|
-
|
|
45
|
-
value.forEach((annotation) => {
|
|
46
|
-
new Set(annotation.nucleotideMutations).forEach((code) => {
|
|
47
|
-
addAnnotationToMap(nucleotideMap, code, annotation);
|
|
48
|
-
});
|
|
49
|
-
new Set(annotation.aminoAcidMutations).forEach((code) => {
|
|
50
|
-
addAnnotationToMap(aminoAcidMap, code, annotation);
|
|
51
|
-
});
|
|
52
|
-
new Set(annotation.nucleotidePositions).forEach((position) => {
|
|
53
|
-
addAnnotationToMap(nucleotidePositions, position, annotation);
|
|
54
|
-
});
|
|
55
|
-
new Set(annotation.aminoAcidPositions).forEach((position) => {
|
|
56
|
-
addAnnotationToMap(aminoAcidPositions, position, annotation);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
success: true as const,
|
|
62
|
-
value: {
|
|
63
|
-
nucleotide: { mutation: nucleotideMap, position: nucleotidePositions },
|
|
64
|
-
'amino acid': { mutation: aminoAcidMap, position: aminoAcidPositions },
|
|
65
|
-
},
|
|
66
|
-
};
|
|
40
|
+
return { success: true as const, value: getMutationAnnotationsContext(value) };
|
|
67
41
|
}, [value]);
|
|
68
42
|
|
|
69
43
|
if (!parseResult.success) {
|
|
@@ -79,6 +53,33 @@ export const MutationAnnotationsContextProvider: FunctionalComponent<
|
|
|
79
53
|
);
|
|
80
54
|
};
|
|
81
55
|
|
|
56
|
+
export function getMutationAnnotationsContext(value: MutationAnnotations) {
|
|
57
|
+
const nucleotideMap = new Map<string, MutationAnnotations>();
|
|
58
|
+
const nucleotidePositions = new Map<string, MutationAnnotations>();
|
|
59
|
+
const aminoAcidMap = new Map<string, MutationAnnotations>();
|
|
60
|
+
const aminoAcidPositions = new Map<string, MutationAnnotations>();
|
|
61
|
+
|
|
62
|
+
value.forEach((annotation) => {
|
|
63
|
+
new Set(annotation.nucleotideMutations).forEach((code) => {
|
|
64
|
+
addAnnotationToMap(nucleotideMap, code, annotation);
|
|
65
|
+
});
|
|
66
|
+
new Set(annotation.aminoAcidMutations).forEach((code) => {
|
|
67
|
+
addAnnotationToMap(aminoAcidMap, code, annotation);
|
|
68
|
+
});
|
|
69
|
+
new Set(annotation.nucleotidePositions).forEach((position) => {
|
|
70
|
+
addAnnotationToMap(nucleotidePositions, position, annotation);
|
|
71
|
+
});
|
|
72
|
+
new Set(annotation.aminoAcidPositions).forEach((position) => {
|
|
73
|
+
addAnnotationToMap(aminoAcidPositions, position, annotation);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
nucleotide: { mutation: nucleotideMap, position: nucleotidePositions },
|
|
79
|
+
'amino acid': { mutation: aminoAcidMap, position: aminoAcidPositions },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
82
83
|
function addAnnotationToMap(map: Map<string, MutationAnnotations>, code: string, annotation: MutationAnnotation) {
|
|
83
84
|
const oldAnnotations = map.get(code.toUpperCase()) ?? [];
|
|
84
85
|
map.set(code.toUpperCase(), [...oldAnnotations, annotation]);
|
|
@@ -87,6 +88,12 @@ function addAnnotationToMap(map: Map<string, MutationAnnotations>, code: string,
|
|
|
87
88
|
export function useMutationAnnotationsProvider() {
|
|
88
89
|
const mutationAnnotations = useContext(MutationAnnotationsContext);
|
|
89
90
|
|
|
91
|
+
return getMutationAnnotationsProvider(mutationAnnotations);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getMutationAnnotationsProvider(
|
|
95
|
+
mutationAnnotations: Record<SequenceType, MutationAnnotationPerSequenceType>,
|
|
96
|
+
) {
|
|
90
97
|
return (mutation: Mutation, sequenceType: SequenceType) => {
|
|
91
98
|
const position =
|
|
92
99
|
mutation.segment === undefined
|
|
@@ -30,7 +30,7 @@ export const Dropdown: FunctionComponent<DropdownProps> = ({ children, buttonTit
|
|
|
30
30
|
return (
|
|
31
31
|
<>
|
|
32
32
|
<button type='button' className='btn btn-xs whitespace-nowrap w-full' onClick={toggle} ref={referenceRef}>
|
|
33
|
-
{buttonTitle}
|
|
33
|
+
<span className={'w-full truncate'}>{buttonTitle}</span>
|
|
34
34
|
</button>
|
|
35
35
|
<div ref={floatingRef} className={`${dropdownClass} ${showContent ? '' : 'hidden'}`}>
|
|
36
36
|
{children}
|
|
@@ -21,7 +21,7 @@ export const InfoHeadline2: FunctionComponent = ({ children }) => {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
export const InfoParagraph: FunctionComponent = ({ children }) => {
|
|
24
|
-
return <p className='text-justify text-base font-normal my-1'>{children}</p>;
|
|
24
|
+
return <p className='text-justify text-base font-normal my-1 text-wrap'>{children}</p>;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
export const InfoLink: FunctionComponent<{ href: string }> = ({ children, href }) => {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type StoryObj } from '@storybook/preact';
|
|
2
|
+
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
|
3
|
+
import { type Meta } from '@storybook/web-components';
|
|
4
|
+
import { useState } from 'preact/hooks';
|
|
5
|
+
|
|
6
|
+
import { MutationsOverTimeTextFilter, type TextFilterProps } from './mutations-over-time-text-filter';
|
|
7
|
+
|
|
8
|
+
const meta: Meta = {
|
|
9
|
+
title: 'Component/Mutations over time text filter',
|
|
10
|
+
component: 'MutationsOverTimeTextFilter',
|
|
11
|
+
parameters: { fetchMock: {} },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default meta;
|
|
15
|
+
|
|
16
|
+
const WrapperWithState = ({ setFilterValue, value }: { setFilterValue: (value: string) => void; value: string }) => {
|
|
17
|
+
const [state, setState] = useState(value);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<MutationsOverTimeTextFilter
|
|
21
|
+
setFilterValue={(value) => {
|
|
22
|
+
setFilterValue(value);
|
|
23
|
+
setState(value);
|
|
24
|
+
}}
|
|
25
|
+
value={state}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const MutationsOverTimeTextFilterStory: StoryObj<TextFilterProps> = {
|
|
31
|
+
render: (args) => {
|
|
32
|
+
return <WrapperWithState setFilterValue={args.setFilterValue} value={args.value} />;
|
|
33
|
+
},
|
|
34
|
+
args: {
|
|
35
|
+
setFilterValue: fn(),
|
|
36
|
+
value: 'Test',
|
|
37
|
+
},
|
|
38
|
+
play: async ({ canvasElement, step }) => {
|
|
39
|
+
const canvas = within(canvasElement);
|
|
40
|
+
|
|
41
|
+
await step('Expect initial value to show on the button', async () => {
|
|
42
|
+
const button = canvas.getByRole('button');
|
|
43
|
+
await expect(button).toHaveTextContent('Test');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await step('Change filter and expect it to show on the button', async () => {
|
|
47
|
+
const button = canvas.getByRole('button');
|
|
48
|
+
await userEvent.click(button);
|
|
49
|
+
|
|
50
|
+
const inputField = canvas.getByRole('textbox');
|
|
51
|
+
await userEvent.clear(inputField);
|
|
52
|
+
await userEvent.type(inputField, 'OtherText');
|
|
53
|
+
|
|
54
|
+
await waitFor(() => expect(button).toHaveTextContent('OtherText'));
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { h } from 'preact';
|
|
2
|
+
import { useCallback, useEffect, useState } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
import { Dropdown } from './dropdown';
|
|
5
|
+
import { DeleteIcon } from '../shared/icons/DeleteIcon';
|
|
6
|
+
|
|
7
|
+
export type TextFilterProps = { setFilterValue: (newValue: string) => void; value: string };
|
|
8
|
+
|
|
9
|
+
export function MutationsOverTimeTextFilter({ setFilterValue, value }: TextFilterProps) {
|
|
10
|
+
const onInput = (newValue: string) => {
|
|
11
|
+
setFilterValue(newValue);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const onDeleteClick = () => setFilterValue('');
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className={'w-28 inline-flex'}>
|
|
18
|
+
<Dropdown buttonTitle={value === '' ? `Filter mutations` : value} placement={'bottom-start'}>
|
|
19
|
+
<div>
|
|
20
|
+
<label className='flex gap-1 input input-xs'>
|
|
21
|
+
<DebouncedInput placeholder={'Filter'} onInput={onInput} value={value} type='text' />
|
|
22
|
+
{value !== undefined && value !== '' && (
|
|
23
|
+
<button className={'cursor-pointer'} onClick={onDeleteClick}>
|
|
24
|
+
<DeleteIcon />
|
|
25
|
+
</button>
|
|
26
|
+
)}
|
|
27
|
+
</label>
|
|
28
|
+
</div>
|
|
29
|
+
</Dropdown>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function DebouncedInput({
|
|
35
|
+
value: initialValue,
|
|
36
|
+
onInput,
|
|
37
|
+
debounce = 500,
|
|
38
|
+
...props
|
|
39
|
+
}: {
|
|
40
|
+
onInput: (value: string) => void;
|
|
41
|
+
debounce?: number;
|
|
42
|
+
value?: string;
|
|
43
|
+
} & Omit<h.JSX.IntrinsicElements['input'], 'onInput'>) {
|
|
44
|
+
const [value, setValue] = useState<string | undefined>(initialValue);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
setValue(initialValue);
|
|
48
|
+
}, [initialValue]);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const timeout = setTimeout(() => {
|
|
52
|
+
onInput(value ?? '');
|
|
53
|
+
}, debounce);
|
|
54
|
+
|
|
55
|
+
return () => clearTimeout(timeout);
|
|
56
|
+
}, [value, debounce, onInput]);
|
|
57
|
+
|
|
58
|
+
const onChangeInput = useCallback((event: h.JSX.TargetedEvent<HTMLInputElement>) => {
|
|
59
|
+
setValue(event.currentTarget.value);
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
return <input {...props} value={value} onInput={onChangeInput} />;
|
|
63
|
+
}
|
|
@@ -4,6 +4,7 @@ import { type FunctionComponent } from 'preact';
|
|
|
4
4
|
import { useState } from 'preact/hooks';
|
|
5
5
|
|
|
6
6
|
import { type DisplayedSegment, SegmentSelector, type SegmentSelectorProps } from './segment-selector';
|
|
7
|
+
import type { SequenceType } from '../../types';
|
|
7
8
|
|
|
8
9
|
const meta: Meta<SegmentSelectorProps> = {
|
|
9
10
|
title: 'Component/Segment selector',
|
|
@@ -15,7 +16,8 @@ export default meta;
|
|
|
15
16
|
|
|
16
17
|
const WrapperWithState: FunctionComponent<{
|
|
17
18
|
displayedSegments: DisplayedSegment[];
|
|
18
|
-
|
|
19
|
+
sequenceType: SequenceType;
|
|
20
|
+
}> = ({ displayedSegments: initialDisplayedSegments, sequenceType }) => {
|
|
19
21
|
const [displayedSegments, setDisplayedSegments] = useState<DisplayedSegment[]>(initialDisplayedSegments);
|
|
20
22
|
|
|
21
23
|
return (
|
|
@@ -24,6 +26,7 @@ const WrapperWithState: FunctionComponent<{
|
|
|
24
26
|
setDisplayedSegments={(items: DisplayedSegment[]) => {
|
|
25
27
|
setDisplayedSegments(items);
|
|
26
28
|
}}
|
|
29
|
+
sequenceType={sequenceType}
|
|
27
30
|
/>
|
|
28
31
|
);
|
|
29
32
|
};
|
|
@@ -50,12 +53,13 @@ export const AllSegmentsSelected: StoryObj<SegmentSelectorProps> = {
|
|
|
50
53
|
checked: true,
|
|
51
54
|
},
|
|
52
55
|
],
|
|
56
|
+
sequenceType: 'amino acid',
|
|
53
57
|
},
|
|
54
58
|
play: async ({ canvasElement, step }) => {
|
|
55
59
|
const canvas = within(canvasElement);
|
|
56
60
|
|
|
57
|
-
await step("Show 'All
|
|
58
|
-
await expect(canvas.getByText('All
|
|
61
|
+
await step("Show 'All genes' as label", async () => {
|
|
62
|
+
await expect(canvas.getByText('All genes')).toBeInTheDocument();
|
|
59
63
|
});
|
|
60
64
|
},
|
|
61
65
|
};
|
|
@@ -80,6 +84,7 @@ export const NoSegmentsSelected: StoryObj<SegmentSelectorProps> = {
|
|
|
80
84
|
checked: false,
|
|
81
85
|
},
|
|
82
86
|
],
|
|
87
|
+
sequenceType: 'nucleotide',
|
|
83
88
|
},
|
|
84
89
|
play: async ({ canvasElement, step }) => {
|
|
85
90
|
const canvas = within(canvasElement);
|
|
@@ -110,12 +115,13 @@ export const LongSegmentsSelected: StoryObj<SegmentSelectorProps> = {
|
|
|
110
115
|
checked: true,
|
|
111
116
|
},
|
|
112
117
|
],
|
|
118
|
+
sequenceType: 'amino acid',
|
|
113
119
|
},
|
|
114
120
|
play: async ({ canvasElement, step }) => {
|
|
115
121
|
const canvas = within(canvasElement);
|
|
116
122
|
|
|
117
|
-
await step('Show number of active
|
|
118
|
-
await expect(canvas.getByText('2
|
|
123
|
+
await step('Show number of active genes as label', async () => {
|
|
124
|
+
await expect(canvas.getByText('2 genes')).toBeInTheDocument();
|
|
119
125
|
});
|
|
120
126
|
},
|
|
121
127
|
};
|
|
@@ -140,6 +146,7 @@ export const ShortSegmentsSelected: StoryObj<SegmentSelectorProps> = {
|
|
|
140
146
|
checked: false,
|
|
141
147
|
},
|
|
142
148
|
],
|
|
149
|
+
sequenceType: 'amino acid',
|
|
143
150
|
},
|
|
144
151
|
play: async ({ canvasElement, step }) => {
|
|
145
152
|
const canvas = within(canvasElement);
|
|
@@ -13,40 +13,44 @@ export type DisplayedSegment = CheckboxItem & {
|
|
|
13
13
|
export type SegmentSelectorProps = {
|
|
14
14
|
displayedSegments: DisplayedSegment[];
|
|
15
15
|
setDisplayedSegments: (items: DisplayedSegment[]) => void;
|
|
16
|
+
sequenceType: SequenceType;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
export const SegmentSelector: FunctionComponent<SegmentSelectorProps> = ({
|
|
19
20
|
displayedSegments,
|
|
20
21
|
setDisplayedSegments,
|
|
22
|
+
sequenceType,
|
|
21
23
|
}) => {
|
|
22
24
|
if (displayedSegments.length <= 1) {
|
|
23
25
|
return null;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
return (
|
|
27
|
-
<div className='w-24'>
|
|
29
|
+
<div className='w-24 inline-flex'>
|
|
28
30
|
<CheckboxSelector
|
|
29
31
|
items={displayedSegments}
|
|
30
|
-
label={getSegmentSelectorLabel(displayedSegments)}
|
|
32
|
+
label={getSegmentSelectorLabel(displayedSegments, sequenceType)}
|
|
31
33
|
setItems={(items) => setDisplayedSegments(items)}
|
|
32
34
|
/>
|
|
33
35
|
</div>
|
|
34
36
|
);
|
|
35
37
|
};
|
|
36
38
|
|
|
37
|
-
const getSegmentSelectorLabel = (displayedSegments: DisplayedSegment[]) => {
|
|
39
|
+
const getSegmentSelectorLabel = (displayedSegments: DisplayedSegment[], sequenceType: SequenceType) => {
|
|
38
40
|
const allSelectedSelected = displayedSegments
|
|
39
41
|
.filter((segment) => segment.checked)
|
|
40
42
|
.map((segment) => segment.segment);
|
|
41
43
|
|
|
44
|
+
const label = sequenceType === 'amino acid' ? 'gene' : 'segment';
|
|
45
|
+
|
|
42
46
|
if (allSelectedSelected.length === 0) {
|
|
43
|
-
return `No
|
|
47
|
+
return `No ${label}s`;
|
|
44
48
|
}
|
|
45
49
|
if (displayedSegments.length === allSelectedSelected.length) {
|
|
46
|
-
return `All
|
|
50
|
+
return `All ${label}s`;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
const longestDisplayString = `All
|
|
53
|
+
const longestDisplayString = `All ${label}s`;
|
|
50
54
|
|
|
51
55
|
const allSelectedSelectedString = allSelectedSelected.join(', ');
|
|
52
56
|
|
|
@@ -54,7 +58,7 @@ const getSegmentSelectorLabel = (displayedSegments: DisplayedSegment[]) => {
|
|
|
54
58
|
return allSelectedSelectedString;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
return `${allSelectedSelected.length} ${allSelectedSelected.length === 1 ?
|
|
61
|
+
return `${allSelectedSelected.length} ${allSelectedSelected.length === 1 ? label : `${label}s`}`;
|
|
58
62
|
};
|
|
59
63
|
|
|
60
64
|
export function useDisplayedSegments(sequenceType: SequenceType) {
|
|
@@ -172,7 +172,11 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
|
172
172
|
setMinProportion={(min) => setProportionInterval((prev) => ({ ...prev, min }))}
|
|
173
173
|
setMaxProportion={(max) => setProportionInterval((prev) => ({ ...prev, max }))}
|
|
174
174
|
/>
|
|
175
|
-
<SegmentSelector
|
|
175
|
+
<SegmentSelector
|
|
176
|
+
displayedSegments={displayedSegments}
|
|
177
|
+
setDisplayedSegments={setDisplayedSegments}
|
|
178
|
+
sequenceType={originalComponentProps.sequenceType}
|
|
179
|
+
/>
|
|
176
180
|
<MutationTypeSelector
|
|
177
181
|
displayedMutationTypes={displayedMutationTypes}
|
|
178
182
|
setDisplayedMutationTypes={setDisplayedMutationTypes}
|