Skip to content

Commit 3f37c20

Browse files
authored
feat(website): make LinkOuts usable without data use terms (#4121)
If DUTs are disabled when clicking on a LinkOut / Tool on the search page, no modal to select the data use terms will be shown. Instead, the tool will be opened directly.
1 parent ff87dee commit 3f37c20

4 files changed

Lines changed: 133 additions & 85 deletions

File tree

kubernetes/loculus/values.schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@
350350
"groups": ["schema"],
351351
"docsIncludePrefix": false,
352352
"type": "array",
353-
"description": "[NB: This feature will currently only work correctly if data use terms are enabled]. An array of external tools that can be linked to from the search page. Each linkOut has a name and URL. The URL can contain placeholders in the format [dataType] or [dataType:segment] or [dataType+rich] or [dataType:segment+rich|format] which will be replaced with the corresponding data URLs. `rich` means display name faster headers. Valid dataTypes are: unalignedNucleotideSequences, metadata, and alignedNucleotideSequences. You can also use {{this}} notation to URL-encode the contents of the double brackets, and you can {{nest {{these}} }}.",
353+
"description": "An array of external tools that can be linked to from the search page. Each linkOut has a name and URL. The URL can contain placeholders in the format [dataType] or [dataType:segment] or [dataType+rich] or [dataType:segment+rich|format] which will be replaced with the corresponding data URLs. `rich` means display name faster headers. Valid dataTypes are: unalignedNucleotideSequences, metadata, and alignedNucleotideSequences. You can also use {{this}} notation to URL-encode the contents of the double brackets, and you can {{nest {{these}} }}.",
354354
"items": {
355355
"type": "object",
356356
"properties": {

website/src/components/SearchPage/DownloadDialog/LinkOutMenu.spec.tsx

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,37 +18,38 @@ const realDownloadUrlGenerator = new DownloadUrlGenerator('test', 'http://testur
1818

1919
const mockSequenceFilter = FieldFilterSet.empty();
2020

21-
describe('LinkOutMenu', () => {
22-
const linkOuts = [
23-
{
24-
name: 'Basic',
25-
url: 'http://example.com/tool?data=[unalignedNucleotideSequences]',
26-
description: 'Basic tool description',
27-
},
28-
{ name: 'Format', url: 'http://example.com/tool?data=[unalignedNucleotideSequences|json]' },
29-
{ name: 'Segment', url: 'http://example.com/tool?data=[unalignedNucleotideSequences:S]' },
30-
{ name: 'SegmentFormat', url: 'http://example.com/tool?data=[unalignedNucleotideSequences:S|json]' },
31-
{ name: 'Rich', url: 'http://example.com/tool?data=[unalignedNucleotideSequences+rich]' },
32-
{ name: 'RichFormat', url: 'http://example.com/tool?data=[unalignedNucleotideSequences+rich|json]' },
33-
{ name: 'SegmentRich', url: 'http://example.com/tool?data=[unalignedNucleotideSequences:S+rich]' },
34-
{
35-
name: 'Complete',
36-
url: 'http://example.com/tool?data=[unalignedNucleotideSequences:S+rich|json]',
37-
description: 'Complete tool with all options',
38-
},
39-
{
40-
name: 'Multiple',
41-
url: 'http://example.com/tool?data1=[unalignedNucleotideSequences]&data2=[metadata|json]',
42-
},
43-
{ name: 'Invalid', url: 'http://example.com/tool?data=[invalidType]&valid=[metadata]' },
44-
];
45-
21+
const linkOuts = [
22+
{
23+
name: 'Basic',
24+
url: 'http://example.com/tool?data=[unalignedNucleotideSequences]',
25+
description: 'Basic tool description',
26+
},
27+
{ name: 'Format', url: 'http://example.com/tool?data=[unalignedNucleotideSequences|json]' },
28+
{ name: 'Segment', url: 'http://example.com/tool?data=[unalignedNucleotideSequences:S]' },
29+
{ name: 'SegmentFormat', url: 'http://example.com/tool?data=[unalignedNucleotideSequences:S|json]' },
30+
{ name: 'Rich', url: 'http://example.com/tool?data=[unalignedNucleotideSequences+rich]' },
31+
{ name: 'RichFormat', url: 'http://example.com/tool?data=[unalignedNucleotideSequences+rich|json]' },
32+
{ name: 'SegmentRich', url: 'http://example.com/tool?data=[unalignedNucleotideSequences:S+rich]' },
33+
{
34+
name: 'Complete',
35+
url: 'http://example.com/tool?data=[unalignedNucleotideSequences:S+rich|json]',
36+
description: 'Complete tool with all options',
37+
},
38+
{
39+
name: 'Multiple',
40+
url: 'http://example.com/tool?data1=[unalignedNucleotideSequences]&data2=[metadata|json]',
41+
},
42+
{ name: 'Invalid', url: 'http://example.com/tool?data=[invalidType]&valid=[metadata]' },
43+
];
44+
45+
describe('LinkOutMenu with enabled data use terms', () => {
4646
test('opens modal when a tool is clicked', () => {
4747
render(
4848
<LinkOutMenu
4949
downloadUrlGenerator={realDownloadUrlGenerator}
5050
sequenceFilter={mockSequenceFilter}
5151
linkOuts={linkOuts}
52+
dataUseTermsEnabled={true}
5253
/>,
5354
);
5455

@@ -67,6 +68,7 @@ describe('LinkOutMenu', () => {
6768
downloadUrlGenerator={realDownloadUrlGenerator}
6869
sequenceFilter={mockSequenceFilter}
6970
linkOuts={linkOuts}
71+
dataUseTermsEnabled={true}
7072
/>,
7173
);
7274

@@ -91,6 +93,7 @@ describe('LinkOutMenu', () => {
9193
downloadUrlGenerator={realDownloadUrlGenerator}
9294
sequenceFilter={mockSequenceFilter}
9395
linkOuts={linkOuts}
96+
dataUseTermsEnabled={true}
9497
/>,
9598
);
9699

@@ -115,6 +118,7 @@ describe('LinkOutMenu', () => {
115118
downloadUrlGenerator={realDownloadUrlGenerator}
116119
sequenceFilter={mockSequenceFilter}
117120
linkOuts={[{ name: 'Basic', url: 'http://example.com/tool?data=[unalignedNucleotideSequences]' }]}
121+
dataUseTermsEnabled={true}
118122
/>,
119123
);
120124

@@ -126,3 +130,21 @@ describe('LinkOutMenu', () => {
126130
expect(vi.mocked(window.open).mock.calls[0][0]).not.toBeUndefined();
127131
});
128132
});
133+
134+
describe('LinkOutMenu with disabled data use terms', () => {
135+
test('opens tool when it is clicked', () => {
136+
render(
137+
<LinkOutMenu
138+
downloadUrlGenerator={realDownloadUrlGenerator}
139+
sequenceFilter={mockSequenceFilter}
140+
linkOuts={linkOuts}
141+
dataUseTermsEnabled={false}
142+
/>,
143+
);
144+
145+
fireEvent.click(screen.getByRole('button', { name: /Tools/ }));
146+
fireEvent.click(screen.getByText('Basic'));
147+
148+
expect(window.open).toHaveBeenCalled();
149+
});
150+
});

website/src/components/SearchPage/DownloadDialog/LinkOutMenu.tsx

Lines changed: 84 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -21,49 +21,54 @@ type LinkOutMenuProps = {
2121
downloadUrlGenerator: DownloadUrlGenerator;
2222
sequenceFilter: SequenceFilter;
2323
linkOuts: LinkOut[];
24+
dataUseTermsEnabled: boolean;
2425
};
2526

26-
export const LinkOutMenu: FC<LinkOutMenuProps> = ({ downloadUrlGenerator, sequenceFilter, linkOuts }) => {
27+
export const LinkOutMenu: FC<LinkOutMenuProps> = ({
28+
downloadUrlGenerator,
29+
sequenceFilter,
30+
linkOuts,
31+
dataUseTermsEnabled,
32+
}) => {
2733
const [isOpen, setIsOpen] = useState(false);
28-
const [isModalVisible, setModalVisible] = useState(false);
34+
const [isDataUseTermsModalVisible, setDataUseTermsModalVisible] = useState(false);
2935
const currentLinkOut = useRef<LinkOut | null>(null);
3036

3137
const handleLinkClick = (linkOut: LinkOut) => {
3238
currentLinkOut.current = linkOut;
33-
setModalVisible(true);
39+
if (dataUseTermsEnabled) {
40+
setDataUseTermsModalVisible(true);
41+
} else {
42+
const url = generateLinkOutUrl(currentLinkOut.current);
43+
openUrl(url);
44+
}
3445
};
3546

3647
const generateLinkOutUrl = (linkOut: LinkOut, includeRestricted = false) => {
3748
const placeholders = matchPlaceholders(linkOut.url);
3849

39-
const urlMap = placeholders.reduce(
40-
(acc, match) => {
41-
const { fullMatch, dataType, segment, richHeaders, dataFormat } = match;
42-
43-
if (!DATA_TYPES.includes(dataType as DataType)) {
44-
return acc;
45-
}
46-
47-
const downloadOption: DownloadOption = {
48-
includeRestricted: includeRestricted,
49-
dataType: {
50-
type: dataType as DataType,
51-
segment: segment,
52-
includeRichFastaHeaders: richHeaders ? true : undefined,
53-
},
54-
compression: undefined,
55-
dataFormat: dataFormat,
56-
};
57-
58-
const { url } = downloadUrlGenerator.generateDownloadUrl(sequenceFilter, downloadOption);
59-
60-
return {
61-
...acc,
62-
[fullMatch.slice(1, -1)]: url,
63-
};
64-
},
65-
{} as Record<string, string>,
66-
);
50+
const urlMap: Record<string, string> = {};
51+
for (const match of placeholders) {
52+
const { fullMatch, dataType, segment, richHeaders, dataFormat } = match;
53+
54+
if (!DATA_TYPES.includes(dataType as DataType)) {
55+
continue;
56+
}
57+
58+
const downloadOption: DownloadOption = {
59+
includeRestricted: includeRestricted,
60+
dataType: {
61+
type: dataType as DataType,
62+
segment: segment,
63+
includeRichFastaHeaders: richHeaders ? true : undefined,
64+
},
65+
compression: undefined,
66+
dataFormat: dataFormat,
67+
};
68+
69+
const { url } = downloadUrlGenerator.generateDownloadUrl(sequenceFilter, downloadOption);
70+
urlMap[fullMatch.slice(1, -1)] = url;
71+
}
6772

6873
return processTemplate(linkOut.url, urlMap);
6974
};
@@ -82,15 +87,15 @@ export const LinkOutMenu: FC<LinkOutMenuProps> = ({ downloadUrlGenerator, sequen
8287
const url = generateLinkOutUrl(currentLinkOut.current, true);
8388
openUrl(url);
8489
}
85-
setModalVisible(false);
90+
setDataUseTermsModalVisible(false);
8691
};
8792

8893
const handleOpenLinkWithOpenOnly = () => {
8994
if (currentLinkOut.current) {
9095
const url = generateLinkOutUrl(currentLinkOut.current, false);
9196
openUrl(url);
9297
}
93-
setModalVisible(false);
98+
setDataUseTermsModalVisible(false);
9499
};
95100

96101
return (
@@ -126,32 +131,52 @@ export const LinkOutMenu: FC<LinkOutMenuProps> = ({ downloadUrlGenerator, sequen
126131
</MenuItems>
127132
</Menu>
128133

129-
<BasicModal isModalVisible={isModalVisible} setModalVisible={setModalVisible}>
130-
<div className='p-6'>
131-
<h2 className='text-xl font-bold mb-2'>
132-
Options for launching {currentLinkOut.current?.name ?? 'Tool'}
133-
</h2>
134-
<h3 className='text-lg font-medium text-gray-700 mb-4 mt-6'>Data use terms</h3>
135-
<p className='mb-6 text-gray-600'>
136-
Would you like to include restricted-use sequences in this analysis? (If you do, you must comply
137-
with the Restricted-Use terms.)
138-
</p>
139-
<div className='flex justify-end space-x-4'>
140-
<button
141-
className='px-4 py-2 border border-gray-300 rounded-md text-gray-700 font-medium hover:bg-gray-50 transition-colors'
142-
onClick={handleOpenLinkWithOpenOnly}
143-
>
144-
Open sequences only
145-
</button>
146-
<button
147-
className='px-4 py-2 bg-primary-600 text-white rounded-md font-medium hover:bg-primary-700 transition-colors'
148-
onClick={handleIncludeRestricted}
149-
>
150-
Include Restricted-Use
151-
</button>
152-
</div>
153-
</div>
154-
</BasicModal>
134+
{dataUseTermsEnabled && (
135+
<LinkOutMenuDataUseTermModal
136+
modalVisible={isDataUseTermsModalVisible}
137+
setModalVisible={setDataUseTermsModalVisible}
138+
currentLinkOut={currentLinkOut}
139+
onClick={handleOpenLinkWithOpenOnly}
140+
onClick1={handleIncludeRestricted}
141+
/>
142+
)}
155143
</>
156144
);
157145
};
146+
147+
function LinkOutMenuDataUseTermModal(props: {
148+
modalVisible: boolean;
149+
setModalVisible: (value: ((prevState: boolean) => boolean) | boolean) => void;
150+
currentLinkOut: React.MutableRefObject<LinkOut | null>;
151+
onClick: () => void;
152+
onClick1: () => void;
153+
}) {
154+
return (
155+
<BasicModal isModalVisible={props.modalVisible} setModalVisible={props.setModalVisible}>
156+
<div className='p-6'>
157+
<h2 className='text-xl font-bold mb-2'>
158+
Options for launching {props.currentLinkOut.current?.name ?? 'Tool'}
159+
</h2>
160+
<h3 className='text-lg font-medium text-gray-700 mb-4 mt-6'>Data use terms</h3>
161+
<p className='mb-6 text-gray-600'>
162+
Would you like to include restricted-use sequences in this analysis? (If you do, you must comply
163+
with the Restricted-Use terms.)
164+
</p>
165+
<div className='flex justify-end space-x-4'>
166+
<button
167+
className='px-4 py-2 border border-gray-300 rounded-md text-gray-700 font-medium hover:bg-gray-50 transition-colors'
168+
onClick={props.onClick}
169+
>
170+
Open sequences only
171+
</button>
172+
<button
173+
className='px-4 py-2 bg-primary-600 text-white rounded-md font-medium hover:bg-primary-700 transition-colors'
174+
onClick={props.onClick1}
175+
>
176+
Include Restricted-Use
177+
</button>
178+
</div>
179+
</div>
180+
</BasicModal>
181+
);
182+
}

website/src/components/SearchPage/SearchFullUI.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@ export const InnerSearchFullUI = ({
459459
downloadUrlGenerator={downloadUrlGenerator}
460460
sequenceFilter={downloadFilter}
461461
linkOuts={linkOuts}
462+
dataUseTermsEnabled={dataUseTermsEnabled}
462463
/>
463464
)}
464465
</div>

0 commit comments

Comments
 (0)