Skip to content

Commit 5c66f78

Browse files
committed
Added "View plate" button
1 parent 27e758d commit 5c66f78

File tree

3 files changed

+239
-0
lines changed

3 files changed

+239
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
# Unreleased
44

5+
* Added "View plate" button on dataset page (\#562);
56
* Improved stability of end to end tests (\#560);
67

78
# 1.6.0

src/routes/v2/projects/[projectId]/datasets/[datasetId]/+page.svelte

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,74 @@
4848
/** @type {CreateUpdateImageModal|undefined} */
4949
let imageModal = undefined;
5050
51+
$: plates = imagePage.attributes['plate'] || [];
52+
let selectedPlate = '';
53+
let platePath = '';
54+
let platePathLoading = false;
55+
let platePathError = '';
56+
57+
$: if (plates.length > 0 && selectedPlate !== '') {
58+
computePlatePath();
59+
} else {
60+
platePath = '';
61+
platePathError = '';
62+
}
63+
64+
async function computePlatePath() {
65+
platePathError = '';
66+
if (!plates.includes(selectedPlate)) {
67+
selectedPlate = '';
68+
return;
69+
}
70+
let imageWithPlate = imagePage.images.find((i) => i.attributes['plate'] === selectedPlate);
71+
if (!imageWithPlate) {
72+
platePathLoading = true;
73+
imageWithPlate = await loadImageForSelectedPlate();
74+
platePathLoading = false;
75+
}
76+
if (imageWithPlate) {
77+
// Removes the last 3 elements from the path
78+
platePath = imageWithPlate.zarr_url.split('/').slice(0, -3).join('/');
79+
if (!platePath) {
80+
platePathError = `Unable to load plate URL from zarr URL ${imageWithPlate.zarr_url}`;
81+
}
82+
} else {
83+
platePath = '';
84+
platePathError = 'Unable to load plate URL. No image found for the selected plate.';
85+
}
86+
}
87+
88+
/**
89+
* @returns {Promise<import('$lib/types-v2').Image|undefined>}
90+
*/
91+
async function loadImageForSelectedPlate() {
92+
const params = { filters: { attributes: { plate: selectedPlate } } };
93+
const headers = new Headers();
94+
headers.set('Content-Type', 'application/json');
95+
const response = await fetch(
96+
`/api/v2/project/${projectId}/dataset/${dataset.id}/images/query?page=1&page_size=1`,
97+
{
98+
method: 'POST',
99+
headers,
100+
credentials: 'include',
101+
body: JSON.stringify(params)
102+
}
103+
);
104+
if (!response.ok) {
105+
console.error(`Unable to load image for plate ${selectedPlate}`);
106+
return undefined;
107+
}
108+
/** @type {import('$lib/types-v2').ImagePage}*/
109+
const result = await response.json();
110+
if (result.images.length === 0) {
111+
console.error(
112+
`Unable to load image for plate ${selectedPlate}. Server replied with empty list`
113+
);
114+
return undefined;
115+
}
116+
return result.images[0];
117+
}
118+
51119
onMount(() => {
52120
loadAttributesSelectors();
53121
loadTypesSelector();
@@ -429,6 +497,46 @@
429497
</div>
430498
</div>
431499
500+
{#if plates.length > 0}
501+
<div class="border border-info rounded bg-light p-3 mt-2">
502+
<div class="row mb-2">
503+
<div class="col">
504+
Detected {plates.length}
505+
{plates.length === 1 ? 'plate' : 'plates'}. Select one plate to display the "View plate"
506+
button.
507+
</div>
508+
</div>
509+
<div class="row row-cols-md-auto g-3 align-items-center">
510+
<div class="col-12">
511+
<select class="form-select" aria-label="Select plate" bind:value={selectedPlate}>
512+
<option value="">Select...</option>
513+
{#each plates as plate}
514+
<option>{plate}</option>
515+
{/each}
516+
</select>
517+
</div>
518+
<div class="col-12">
519+
{#if platePath}
520+
<a
521+
href="{vizarrViewerUrl}?source={vizarrViewerUrl}data{platePath}"
522+
class="btn btn-info me-2"
523+
target="_blank"
524+
class:disabled={platePathLoading}
525+
>
526+
<i class="bi bi-eye" />
527+
View plate
528+
</a>
529+
{:else if platePathError}
530+
<span class="text-danger">{platePathError}</span>
531+
{/if}
532+
{#if platePathLoading}
533+
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" />
534+
{/if}
535+
</div>
536+
</div>
537+
</div>
538+
{/if}
539+
432540
{#if !showTable}
433541
<p class="fw-bold ms-4 mt-5">No entries in the image list yet</p>
434542
<button class="btn btn-outline-secondary ms-4" on:click={() => imageModal?.openForCreate()}>

tests/v2/view_plate.spec.js

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { waitModalClosed, waitPageLoading } from '../utils.js';
2+
import { expect, test } from './project_fixture.js';
3+
4+
test('View plate', async ({ page, project }) => {
5+
await page.waitForURL(project.url);
6+
await waitPageLoading(page);
7+
8+
const randomPath = `/tmp/${Math.random().toString(36).substring(7)}`;
9+
10+
await test.step('Create test dataset', async () => {
11+
const createDatasetButton = page.getByRole('button', { name: 'Create new dataset' });
12+
await createDatasetButton.click();
13+
const modal = page.locator('.modal.show');
14+
await modal.waitFor();
15+
await modal.getByRole('textbox', { name: 'Dataset Name' }).fill('test-dataset');
16+
await modal.getByRole('textbox', { name: 'Zarr dir' }).fill(randomPath);
17+
await modal.getByRole('button', { name: 'Save' }).click();
18+
await waitModalClosed(page);
19+
});
20+
21+
await test.step('Open test dataset', async () => {
22+
await page.getByRole('link', { name: 'test-dataset' }).click();
23+
await page.waitForURL(/\/v2\/projects\/\d+\/datasets\/\d+/);
24+
expect(await page.getByText('No entries in the image list yet').isVisible()).toEqual(true);
25+
});
26+
27+
await test.step('Create test images', async () => {
28+
for (let i = 0; i < 5; i++) {
29+
await createImageWithPlate(page, `${randomPath}/plate1.zarr/B/03/${i}`, 'plate1');
30+
}
31+
for (let i = 5; i < 11; i++) {
32+
await createImageWithPlate(page, `${randomPath}/plate2.zarr/B/03/${i}`, 'plate2');
33+
}
34+
await createImageWithPlate(page, `${randomPath}/plate3.zarr/B/03/11`, 'plate3');
35+
});
36+
37+
await test.step('Check plate selector', async () => {
38+
await checkPlateSelector(page, 'plate1', 'plate2', 'plate3');
39+
});
40+
41+
await test.step('Select plates', async () => {
42+
for (let i = 1; i <= 3; i++) {
43+
await page.getByRole('combobox', { name: 'Select plate' }).selectOption(`plate${i}`);
44+
await expect(page.getByRole('link', { name: 'View plate' })).toHaveAttribute(
45+
'href',
46+
new RegExp(`\\/plate${i}\\.zarr$`)
47+
);
48+
await expect(page.getByRole('combobox', { name: 'Select plate' })).toHaveValue(`plate${i}`);
49+
}
50+
});
51+
52+
await test.step('Delete image with plate3 and check selection', async () => {
53+
// go to page 2
54+
await page.getByRole('button', { name: '2', exact: true }).click();
55+
await page
56+
.getByRole('row', { name: 'plate3.zarr' })
57+
.getByRole('button', { name: 'Delete' })
58+
.click();
59+
const modal = page.locator('.modal.show');
60+
await modal.waitFor();
61+
await modal.getByRole('button', { name: 'Confirm' }).click();
62+
await waitModalClosed(page);
63+
await expect(page.getByRole('combobox', { name: 'Select plate' })).toHaveValue('');
64+
});
65+
66+
await test.step('Check plate selector', async () => {
67+
await checkPlateSelector(page, 'plate1', 'plate2');
68+
});
69+
70+
await test.step('Create another test dataset with invalid zarr dir', async () => {
71+
await page.getByRole('link', { name: project.projectName }).click();
72+
await waitPageLoading(page);
73+
const createDatasetButton = page.getByRole('button', { name: 'Create new dataset' });
74+
await createDatasetButton.click();
75+
const modal = page.locator('.modal.show');
76+
await modal.waitFor();
77+
await modal.getByRole('textbox', { name: 'Dataset Name' }).fill('test-dataset-2');
78+
await modal.getByRole('textbox', { name: 'Zarr dir' }).fill('/tmp');
79+
await modal.getByRole('button', { name: 'Save' }).click();
80+
await waitModalClosed(page);
81+
});
82+
83+
await test.step('Open test dataset 2 and create new image', async () => {
84+
await page.getByRole('link', { name: 'test-dataset-2' }).click();
85+
await page.waitForURL(/\/v2\/projects\/\d+\/datasets\/\d+/);
86+
expect(await page.getByText('No entries in the image list yet').isVisible()).toEqual(true);
87+
await createImageWithPlate(page, '/tmp/invalid', 'plate1');
88+
await checkPlateSelector(page, 'plate1');
89+
await page.getByRole('combobox', { name: 'Select plate' }).selectOption('plate1');
90+
await expect(
91+
page.getByText('Unable to load plate URL from zarr URL /tmp/invalid')
92+
).toBeVisible();
93+
});
94+
});
95+
96+
/**
97+
* @param {import('@playwright/test').Page} page
98+
* @param {string} zarrUrl
99+
* @param {string} plate
100+
*/
101+
async function createImageWithPlate(page, zarrUrl, plate) {
102+
const newImageBtn = page.getByRole('button', { name: 'Add an image list entry' });
103+
await newImageBtn.waitFor();
104+
await newImageBtn.click();
105+
const modal = page.locator('.modal.show');
106+
await modal.waitFor();
107+
await modal.getByRole('textbox', { name: 'Zarr URL' }).fill(zarrUrl);
108+
await modal.getByRole('button', { name: 'Add attribute' }).click();
109+
await modal.getByPlaceholder('Key').fill('plate');
110+
await modal.getByPlaceholder('Value').fill(plate);
111+
await modal.getByRole('button', { name: 'Save' }).click();
112+
await waitModalClosed(page);
113+
}
114+
115+
/**
116+
* @param {import('@playwright/test').Page} page
117+
* @param {...string} expectedOptions
118+
*/
119+
async function checkPlateSelector(page, ...expectedOptions) {
120+
await expect(page.getByText(`Detected ${expectedOptions.length} plate`)).toBeVisible();
121+
const options = await page
122+
.getByRole('combobox', { name: 'Select plate' })
123+
.getByRole('option')
124+
.all();
125+
expect(options).toHaveLength(expectedOptions.length + 1);
126+
expect(options[0]).toHaveText('Select...');
127+
for (let i = 0; i < expectedOptions.length; i++) {
128+
expect(options[i + 1]).toHaveText(expectedOptions[i]);
129+
}
130+
}

0 commit comments

Comments
 (0)