genelastic 0.8.0__py3-none-any.whl → 0.9.0__py3-none-any.whl

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.
Files changed (86) hide show
  1. genelastic/api/.env +4 -0
  2. genelastic/api/cli_start_api.py +2 -2
  3. genelastic/api/errors.py +52 -0
  4. genelastic/api/extends/example.py +0 -6
  5. genelastic/api/extends/example.yml +0 -20
  6. genelastic/api/routes.py +313 -181
  7. genelastic/api/server.py +8 -3
  8. genelastic/api/specification.yml +343 -181
  9. genelastic/common/__init__.py +0 -44
  10. genelastic/common/cli.py +48 -0
  11. genelastic/common/elastic.py +374 -46
  12. genelastic/common/exceptions.py +34 -2
  13. genelastic/common/server.py +9 -1
  14. genelastic/common/types.py +1 -14
  15. genelastic/import_data/__init__.py +0 -27
  16. genelastic/import_data/checker.py +99 -0
  17. genelastic/import_data/checker_observer.py +13 -0
  18. genelastic/import_data/cli/__init__.py +0 -0
  19. genelastic/import_data/cli/cli_check.py +136 -0
  20. genelastic/import_data/{cli_gen_data.py → cli/gen_data.py} +4 -4
  21. genelastic/import_data/cli/import_data.py +346 -0
  22. genelastic/import_data/cli/info.py +247 -0
  23. genelastic/import_data/{cli_integrity.py → cli/integrity.py} +29 -7
  24. genelastic/import_data/cli/validate.py +146 -0
  25. genelastic/import_data/collect.py +185 -0
  26. genelastic/import_data/constants.py +136 -11
  27. genelastic/import_data/import_bundle.py +102 -59
  28. genelastic/import_data/import_bundle_factory.py +70 -149
  29. genelastic/import_data/importers/__init__.py +0 -0
  30. genelastic/import_data/importers/importer_base.py +131 -0
  31. genelastic/import_data/importers/importer_factory.py +85 -0
  32. genelastic/import_data/importers/importer_types.py +223 -0
  33. genelastic/import_data/logger.py +2 -1
  34. genelastic/import_data/models/__init__.py +0 -0
  35. genelastic/import_data/models/analyses.py +178 -0
  36. genelastic/import_data/models/analysis.py +144 -0
  37. genelastic/import_data/models/data_file.py +110 -0
  38. genelastic/import_data/models/process.py +45 -0
  39. genelastic/import_data/models/processes.py +84 -0
  40. genelastic/import_data/models/tags.py +170 -0
  41. genelastic/import_data/models/unique_list.py +109 -0
  42. genelastic/import_data/models/validate.py +26 -0
  43. genelastic/import_data/patterns.py +90 -0
  44. genelastic/import_data/random_bundle.py +10 -8
  45. genelastic/import_data/resolve.py +157 -0
  46. genelastic/ui/.env +1 -0
  47. genelastic/ui/cli_start_ui.py +4 -2
  48. genelastic/ui/routes.py +289 -42
  49. genelastic/ui/static/cea-cnrgh.ico +0 -0
  50. genelastic/ui/static/cea.ico +0 -0
  51. genelastic/ui/static/layout.ico +0 -0
  52. genelastic/ui/static/novaseq6000.png +0 -0
  53. genelastic/ui/static/style.css +430 -0
  54. genelastic/ui/static/ui.js +458 -0
  55. genelastic/ui/templates/analyses.html +96 -9
  56. genelastic/ui/templates/analysis_detail.html +44 -0
  57. genelastic/ui/templates/bi_process_detail.html +129 -0
  58. genelastic/ui/templates/bi_processes.html +114 -9
  59. genelastic/ui/templates/explorer.html +356 -0
  60. genelastic/ui/templates/home.html +205 -2
  61. genelastic/ui/templates/layout.html +148 -29
  62. genelastic/ui/templates/version.html +19 -7
  63. genelastic/ui/templates/wet_process_detail.html +131 -0
  64. genelastic/ui/templates/wet_processes.html +114 -9
  65. genelastic-0.9.0.dist-info/METADATA +686 -0
  66. genelastic-0.9.0.dist-info/RECORD +76 -0
  67. genelastic-0.9.0.dist-info/WHEEL +4 -0
  68. genelastic-0.9.0.dist-info/entry_points.txt +10 -0
  69. genelastic-0.9.0.dist-info/licenses/LICENSE +519 -0
  70. genelastic/import_data/analyses.py +0 -69
  71. genelastic/import_data/analysis.py +0 -205
  72. genelastic/import_data/bi_process.py +0 -27
  73. genelastic/import_data/bi_processes.py +0 -49
  74. genelastic/import_data/cli_import.py +0 -379
  75. genelastic/import_data/cli_info.py +0 -256
  76. genelastic/import_data/cli_validate.py +0 -54
  77. genelastic/import_data/data_file.py +0 -87
  78. genelastic/import_data/filename_pattern.py +0 -57
  79. genelastic/import_data/tags.py +0 -123
  80. genelastic/import_data/wet_process.py +0 -28
  81. genelastic/import_data/wet_processes.py +0 -53
  82. genelastic-0.8.0.dist-info/METADATA +0 -109
  83. genelastic-0.8.0.dist-info/RECORD +0 -52
  84. genelastic-0.8.0.dist-info/WHEEL +0 -5
  85. genelastic-0.8.0.dist-info/entry_points.txt +0 -8
  86. genelastic-0.8.0.dist-info/top_level.txt +0 -1
@@ -0,0 +1,458 @@
1
+ console.log('Script loaded');
2
+
3
+ document.addEventListener("DOMContentLoaded", () => {
4
+
5
+ // === UTILS ===
6
+ const $ = selector => document.querySelector(selector);
7
+ const $$ = selector => Array.from(document.querySelectorAll(selector));
8
+
9
+ // === FONCTION POUR INITIALISER LES CHARTS DANS LES CARDS ===
10
+ const initChartsInCards = () => {
11
+ const palette = [
12
+ 'rgb(255,99,132)', 'rgb(54,162,235)', 'rgb(255,205,86)',
13
+ 'rgb(75,192,192)', 'rgb(153,102,255)', 'rgb(255,159,64)',
14
+ 'rgb(201,203,207)', 'rgb(100,149,237)', 'rgb(255,69,0)', 'rgb(0,128,0)'
15
+ ];
16
+
17
+ $$('canvas[id^="chart_"]').forEach(canvas => {
18
+ const card = canvas.closest('.metadata-table');
19
+ if (!card) return;
20
+
21
+ // Récupérer les données JSON dans un attribut data-values (doit être ajouté côté serveur)
22
+ const valuesAttr = card.dataset.values;
23
+ if (!valuesAttr) {
24
+ console.warn(`Pas de data-values pour la carte ${card.id}`);
25
+ return;
26
+ }
27
+ let values;
28
+ try {
29
+ values = JSON.parse(valuesAttr);
30
+ } catch (e) {
31
+ console.error(`Erreur parsing JSON data-values pour ${card.id}`, e);
32
+ return;
33
+ }
34
+
35
+ const labels = Object.keys(values);
36
+ const data = Object.values(values);
37
+
38
+ const colors = labels.map((_, i) => palette[i % palette.length]);
39
+
40
+ // Créer le chart
41
+ new Chart(canvas.getContext('2d'), {
42
+ type: 'pie',
43
+ data: {
44
+ labels: labels,
45
+ datasets: [{
46
+ data: data,
47
+ backgroundColor: colors,
48
+ hoverOffset: 4
49
+ }]
50
+ },
51
+ options: {
52
+ responsive: false,
53
+ plugins: {
54
+ legend: { display: true, position: 'bottom', labels: {boxWidth: 12}},
55
+ tooltip: {
56
+ callbacks: {
57
+ label: ctx => `${ctx.label}: ${ctx.raw}`
58
+ }
59
+ }
60
+ }
61
+ }
62
+ });
63
+ });
64
+ };
65
+
66
+ initChartsInCards();
67
+
68
+ // === GESTION FILTRES PAR CHECKBOX ===
69
+ const setupCheckboxFilter = name => {
70
+ $$(`input[name="${name}"]`).forEach(cb => {
71
+ cb.addEventListener('change', () => {
72
+ const params = new URLSearchParams(window.location.search);
73
+ const selected = $$(`input[name="${name}"]:checked`).map(cb => cb.value);
74
+ params.delete(name);
75
+ selected.forEach(val => params.append(name, val));
76
+ window.location.search = params.toString();
77
+ });
78
+ });
79
+ };
80
+ setupCheckboxFilter('bi_processes');
81
+ setupCheckboxFilter('wet_processes');
82
+
83
+ // === VARIABLES MODAL & CHART ===
84
+ const modal = $('#chartModal');
85
+ const closeBtn = $('#closeModalBtn');
86
+ const ctx = $('#occurrencesChart')?.getContext('2d');
87
+ const legendContainer = $('#chartLegend');
88
+ const toggleChartTypeBtn = $('#toggleChartTypeBtn');
89
+
90
+ let chartInstance = null;
91
+ let currentChartType = 'doughnut';
92
+ let lastChartData = null;
93
+ let lastChartTitle = null;
94
+
95
+ const renderLegend = (labels, data, colors) => {
96
+ legendContainer.innerHTML = '';
97
+ labels.forEach((label, i) => {
98
+ const legendItem = document.createElement('div');
99
+ legendItem.style = "display: flex; align-items: center; margin-bottom: 6px; gap: 8px;";
100
+ legendItem.innerHTML = `
101
+ <span style="display: inline-block; width: 18px; height: 18px; background-color: ${colors[i]}; border-radius: 3px;"></span>
102
+ <span>${label} (${data[i]})</span>
103
+ `;
104
+ legendContainer.appendChild(legendItem);
105
+ });
106
+ };
107
+
108
+ const showChart = (labels, data, colors, title) => {
109
+ if (chartInstance) chartInstance.destroy();
110
+ chartInstance = new Chart(ctx, {
111
+ type: currentChartType,
112
+ data: {
113
+ labels,
114
+ datasets: [{
115
+ label: title,
116
+ data,
117
+ backgroundColor: colors,
118
+ hoverOffset: 4
119
+ }]
120
+ },
121
+ options: {
122
+ responsive: false,
123
+ plugins: {legend: {display: false}, title: {display: false}}
124
+ }
125
+ });
126
+ renderLegend(labels, data, colors);
127
+ };
128
+
129
+ const showChartWithData = (metadataCounts, title) => {
130
+ document.body.classList.add('modal-open');
131
+ modal.style.display = 'flex';
132
+ lastChartData = metadataCounts;
133
+ lastChartTitle = title;
134
+
135
+ if (toggleChartTypeBtn) {
136
+ toggleChartTypeBtn.textContent = currentChartType === 'doughnut' ? 'Diagramme en bâtons' : 'Camembert';
137
+ }
138
+
139
+ const labels = [], data = [], colors = [];
140
+ const palette = [
141
+ 'rgb(255,99,132)', 'rgb(54,162,235)', 'rgb(255,205,86)',
142
+ 'rgb(75,192,192)', 'rgb(153,102,255)', 'rgb(255,159,64)',
143
+ 'rgb(201,203,207)', 'rgb(100,149,237)', 'rgb(255,69,0)', 'rgb(0,128,0)'
144
+ ];
145
+
146
+ let i = 0;
147
+ for (const [field, values] of Object.entries(metadataCounts)) {
148
+ const label = field.split('.').pop().replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
149
+ for (const [val, count] of Object.entries(values)) {
150
+ labels.push(`${label} → ${val}`);
151
+ data.push(count);
152
+ colors.push(palette[i++ % palette.length]);
153
+ }
154
+ }
155
+ showChart(labels, data, colors, title);
156
+ };
157
+
158
+ const showChartWithCustomData = (chartData, title) => {
159
+ document.body.classList.add('modal-open');
160
+ modal.style.display = 'flex';
161
+ lastChartData = chartData;
162
+ lastChartTitle = title;
163
+
164
+ const {labels, data} = chartData;
165
+ const colors = labels.map((_, i) => `hsl(${(i * 360 / labels.length)}, 70%, 60%)`);
166
+
167
+ if (toggleChartTypeBtn) {
168
+ toggleChartTypeBtn.textContent = currentChartType === 'doughnut' ? 'Diagramme en bâtons' : 'Camembert';
169
+ }
170
+
171
+ showChart(labels, data, colors, title);
172
+ };
173
+
174
+ // === BOUTONS CHART ===
175
+ document.body.addEventListener('click', e => {
176
+ if (e.target.id === 'showOccurrencesWetBtn') {
177
+ showChartWithData(metadataCountsWet, 'Occurrences des métadonnées wet-process');
178
+ } else if (e.target.id === 'showOccurrencesBiBtn') {
179
+ showChartWithData(metadataCountsBi, 'Occurrences des métadonnées bi-process');
180
+ } else if (e.target.classList.contains('show-occurrences-btn')) {
181
+ const label = e.target.dataset.label;
182
+ const values = JSON.parse(e.target.dataset.values);
183
+ showChartWithCustomData({
184
+ labels: Object.keys(values),
185
+ data: Object.values(values)
186
+ }, `Occurrences pour ${label}`);
187
+ }
188
+ });
189
+
190
+ toggleChartTypeBtn?.addEventListener('click', () => {
191
+ if (!lastChartData || !lastChartTitle) return;
192
+ currentChartType = currentChartType === 'doughnut' ? 'bar' : 'doughnut';
193
+ toggleChartTypeBtn.textContent = currentChartType === 'doughnut' ? 'Diagramme en bâtons' : 'Camembert';
194
+ lastChartData.labels
195
+ ? showChartWithCustomData(lastChartData, lastChartTitle)
196
+ : showChartWithData(lastChartData, lastChartTitle);
197
+ });
198
+
199
+ const closeModal = () => {
200
+ modal.style.display = 'none';
201
+ document.body.classList.remove('modal-open');
202
+ };
203
+ closeBtn.addEventListener('click', closeModal);
204
+ modal.addEventListener('click', e => {
205
+ if (e.target === modal) closeModal();
206
+ });
207
+ document.addEventListener('keydown', e => {
208
+ if (modal.style.display === 'flex' && e.key === 'Escape') closeModal();
209
+ });
210
+
211
+ // === TOGGLES VISIBILITÉ TABLES ===
212
+ const toggles = $$('.meta-toggle');
213
+ const allFiltersButton = $('#toggle-all-filters');
214
+ const sectionButtons = [
215
+ {button: $('#toggle-wet'), prefix: 'table_wet_', label: 'Index Wet-Processes'},
216
+ {button: $('#toggle-bi'), prefix: 'table_bi_', label: 'Index Bi-Processes'}
217
+ ];
218
+ const allTables = {};
219
+ $$('.metadata-table, .metadata-list').forEach(el => allTables[el.id] = el);
220
+
221
+ const updateTablesVisibility = () => {
222
+ toggles.forEach(t => {
223
+ const el = allTables[t.dataset.target];
224
+ if (el) el.style.display = t.checked ? '' : 'none';
225
+ });
226
+ updateAllFiltersButton();
227
+ updateSectionButtons();
228
+ };
229
+
230
+ const updateAllFiltersButton = () => {
231
+ const allChecked = toggles.every(t => t.checked);
232
+ allFiltersButton.textContent = allChecked ? 'Tout masquer' : 'Tout afficher';
233
+ };
234
+
235
+ const updateSectionButtons = () => {
236
+ sectionButtons.forEach(({button, prefix, label}) => {
237
+ const related = toggles.filter(t => t.dataset.target.startsWith(prefix));
238
+ const allChecked = related.every(t => t.checked);
239
+ button.textContent = allChecked ? `Masquer ${label}` : `Afficher ${label}`;
240
+ });
241
+ };
242
+
243
+ allFiltersButton.addEventListener('click', () => {
244
+ const allChecked = toggles.every(t => t.checked);
245
+ toggles.forEach(t => t.checked = !allChecked);
246
+ updateTablesVisibility();
247
+ });
248
+
249
+ sectionButtons.forEach(({button, prefix}) => {
250
+ button.addEventListener('click', () => {
251
+ const related = toggles.filter(t => t.dataset.target.startsWith(prefix));
252
+ const allChecked = related.every(t => t.checked);
253
+ related.forEach(t => t.checked = !allChecked);
254
+ updateTablesVisibility();
255
+ });
256
+ });
257
+
258
+ toggles.forEach(t => t.addEventListener('change', updateTablesVisibility));
259
+ updateTablesVisibility();
260
+
261
+ // === SWITCH VUE TABLE/LISTE ===
262
+ const viewTableBtn = $('#viewTableBtn');
263
+ const viewListBtn = $('#viewListBtn');
264
+ const tableView = $('#tableView');
265
+ const listView = $('#listView');
266
+
267
+ const switchView = isTable => {
268
+ tableView.style.display = isTable ? '' : 'none';
269
+ listView.style.display = isTable ? 'none' : '';
270
+ viewTableBtn.classList.toggle('active', isTable);
271
+ viewListBtn.classList.toggle('active', !isTable);
272
+ };
273
+
274
+ viewTableBtn?.addEventListener('click', () => switchView(true));
275
+ viewListBtn?.addEventListener('click', () => switchView(false));
276
+
277
+ // === ACCORDÉONS ===
278
+ $$('.expand-all-btn').forEach(btn =>
279
+ btn.addEventListener('click', () =>
280
+ $$(`#accordion_${btn.dataset.target} .accordion-collapse`).forEach(el =>
281
+ bootstrap.Collapse.getOrCreateInstance(el).show()
282
+ )
283
+ )
284
+ );
285
+ $$('.collapse-all-btn').forEach(btn =>
286
+ btn.addEventListener('click', () =>
287
+ $$(`#accordion_${btn.dataset.target} .accordion-collapse`).forEach(el =>
288
+ bootstrap.Collapse.getOrCreateInstance(el).hide()
289
+ )
290
+ )
291
+ );
292
+ $$('.toggle-all-btn').forEach(btn =>
293
+ btn.addEventListener('click', () =>
294
+ $$(`#accordion_${btn.dataset.target} .accordion-collapse`).forEach(el => {
295
+ const inst = bootstrap.Collapse.getOrCreateInstance(el);
296
+ el.classList.contains('show') ? inst.hide() : inst.show();
297
+ })
298
+ )
299
+ );
300
+
301
+ // === TRI DES TABLEAUX PAR OCCURRENCE ===
302
+ $$('.sort-btn').forEach(button => {
303
+ let asc = true;
304
+ button.addEventListener('click', () => {
305
+ const tbody = button.closest('.card')?.querySelector('tbody');
306
+ if (!tbody) return;
307
+
308
+ const rows = Array.from(tbody.querySelectorAll('tr'));
309
+ rows.sort((a, b) => {
310
+ const aVal = parseInt(a.cells[1].textContent) || 0;
311
+ const bVal = parseInt(b.cells[1].textContent) || 0;
312
+ return asc ? aVal - bVal : bVal - aVal;
313
+ });
314
+
315
+ rows.forEach(row => tbody.appendChild(row));
316
+ asc = !asc;
317
+ button.textContent = asc ? 'Tri croissant' : 'Tri décroissant';
318
+ });
319
+ });
320
+ const allCanvases = document.querySelectorAll('canvas[id^="chart_"]');
321
+ allCanvases.forEach(canvas => {
322
+ const ctx = canvas.getContext('2d');
323
+ const label = canvas.dataset.label || '';
324
+ const labels = JSON.parse(canvas.dataset.labels || '[]');
325
+ const values = JSON.parse(canvas.dataset.values || '[]');
326
+ if (labels.length && values.length) {
327
+ new Chart(ctx, {
328
+ type: 'pie',
329
+ data: {
330
+ labels: labels,
331
+ datasets: [{
332
+ label: label,
333
+ data: values,
334
+ backgroundColor: labels.map((_, i) => `hsl(${Math.floor(i * 360 / labels.length)}, 70%, 60%)`)
335
+ }]
336
+ },
337
+ options: {
338
+ plugins: {
339
+ legend: { display: true, position: 'bottom', labels: { boxWidth: 12 }},
340
+ tooltip: {
341
+ callbacks: {
342
+ label: ctx => `${ctx.label}: ${ctx.raw}`
343
+ }
344
+ }
345
+ }
346
+ }
347
+ });
348
+ }
349
+ });
350
+ const selectedSet = new Set();
351
+ const selectedContainer = document.getElementById('selectedMetadata');
352
+ const cartCountBadge = document.getElementById('cartCount');
353
+
354
+ function updateCartUI() {
355
+ selectedContainer.innerHTML = '';
356
+ if(selectedSet.size === 0) {
357
+ selectedContainer.innerHTML = '<p class="text-muted fst-italic text-center">Aucune sélection</p>';
358
+ }
359
+ selectedSet.forEach(item => {
360
+ const [field, value] = item.split('||');
361
+ const cleanName = field.replace('metadata.', '').replace(/_/g, ' ').replace(/\./g, ' ');
362
+ const displayText = `${cleanName.trim().replace(/\b\w/g, c => c.toUpperCase())}: ${value}`;
363
+
364
+ const badge = document.createElement('div');
365
+ badge.className = 'badge bg-primary d-flex justify-content-between align-items-center rounded-pill px-3 py-2';
366
+ badge.style.cursor = 'default';
367
+ badge.textContent = displayText;
368
+
369
+ const btnClose = document.createElement('button');
370
+ btnClose.type = 'button';
371
+ btnClose.className = 'btn-close btn-close-white btn-sm ms-3';
372
+ btnClose.setAttribute('aria-label', `Retirer ${displayText}`);
373
+ btnClose.style.filter = 'drop-shadow(0 0 1px rgba(0,0,0,0.3))';
374
+ btnClose.addEventListener('click', () => {
375
+ selectedSet.delete(item);
376
+ updateCartUI();
377
+ updateCheckboxes();
378
+ });
379
+
380
+ badge.appendChild(btnClose);
381
+ selectedContainer.appendChild(badge);
382
+ });
383
+
384
+ cartCountBadge.textContent = selectedSet.size;
385
+ updateCheckboxes();
386
+ }
387
+
388
+ function updateCheckboxes() {
389
+ document.querySelectorAll('.metadata-checkbox').forEach(checkbox => {
390
+ const field = checkbox.dataset.field;
391
+ const value = checkbox.dataset.value;
392
+ const key = `${field}||${value}`;
393
+ checkbox.checked = selectedSet.has(key);
394
+ });
395
+ }
396
+
397
+ document.querySelectorAll('.metadata-checkbox').forEach(checkbox => {
398
+ checkbox.addEventListener('change', () => {
399
+ const field = checkbox.dataset.field;
400
+ const value = checkbox.dataset.value;
401
+ const key = `${field}||${value}`;
402
+ if (checkbox.checked) {
403
+ selectedSet.add(key);
404
+ } else {
405
+ selectedSet.delete(key);
406
+ }
407
+ updateCartUI();
408
+ });
409
+ });
410
+
411
+ document.getElementById('validateBtn').addEventListener('click', () => {
412
+ console.log('Sélection validée:', Array.from(selectedSet));
413
+ // TODO : envoyer la sélection au serveur ou agir en conséquence
414
+ });
415
+
416
+ document.getElementById('clearCartBtn').addEventListener('click', () => {
417
+ selectedSet.clear();
418
+ updateCartUI();
419
+ });
420
+
421
+ const toggleAllBtn = document.getElementById('toggleAllBtn');
422
+ const accordionItems = document.querySelectorAll('.accordion-collapse');
423
+
424
+ toggleAllBtn.addEventListener('click', () => {
425
+ const allExpanded = Array.from(accordionItems).every(item => item.classList.contains('show'));
426
+
427
+ if (allExpanded) {
428
+ accordionItems.forEach(item => {
429
+ const bsCollapse = bootstrap.Collapse.getInstance(item);
430
+ if (bsCollapse) {
431
+ bsCollapse.hide();
432
+ } else {
433
+ new bootstrap.Collapse(item, {toggle: false}).hide();
434
+ }
435
+ });
436
+ toggleAllBtn.textContent = 'Tout dérouler';
437
+ } else {
438
+ accordionItems.forEach(item => {
439
+ const bsCollapse = bootstrap.Collapse.getInstance(item);
440
+ if (bsCollapse) {
441
+ bsCollapse.show();
442
+ } else {
443
+ new bootstrap.Collapse(item, {toggle: false}).show();
444
+ }
445
+ });
446
+ toggleAllBtn.textContent = 'Tout replier';
447
+ }
448
+ });
449
+
450
+ // 👇 Afficher / masquer le panier au clic sur le bouton flottant
451
+ document.getElementById('toggleCartBtn').addEventListener('click', () => {
452
+ const cart = document.getElementById('floatingCart');
453
+ cart.style.display = (cart.style.display === 'none') ? 'block' : 'none';
454
+ });
455
+
456
+ updateCartUI();
457
+
458
+ });
@@ -1,11 +1,98 @@
1
1
  {% extends "layout.html" %}
2
- {% block title %}Analyses{% endblock %}
2
+ {% block title %}
3
+ Analyses - BEx
4
+ {% endblock title %}
3
5
  {% block content %}
4
- <h2>List of Analyses</h2>
5
- <ul>
6
- {% for analyse in analyses %}
7
- <li>{{ analyse }}</li>
8
- {% endfor %}
9
- </ul>
10
- <a href="/">Back to Home</a>
11
- {% endblock %}
6
+ <div class="container mt-5">
7
+ <div class="text-center mb-4">
8
+ <h1 class="display-5 title">Liste des Analyses</h1>
9
+ <p class="lead">Cliquer sur une analyse pour afficher les détails associés.</p>
10
+ </div>
11
+ <div class="card shadow-sm mb-4">
12
+ <div class="card-body">
13
+ <div class="input-group">
14
+ <input list="analyses-list"
15
+ id="search-analyses"
16
+ class="form-control"
17
+ placeholder="Rechercher une analyse..."
18
+ autocomplete="off" />
19
+ <button id="apply-btn" class="btn btn-primary" type="button" disabled>Appliquer</button>
20
+ </div>
21
+ <datalist id="analyses-list"></datalist>
22
+ </div>
23
+ </div>
24
+ <div class="card shadow-sm">
25
+ <div class="card-body">
26
+ <h3 class="mb-4">Analyses disponibles :</h3>
27
+ <ul class="list-group" id="analyses-ul">
28
+ {% for analyse in analyses %}
29
+ <li class="list-group-item">
30
+ <a href="{{ url_for('routes.show_analysis_detail', analysis_id=analyse) }}"
31
+ class="text-decoration-none">
32
+ <div class="d-flex justify-content-between align-items-center">
33
+ <span>{{ analyse }}</span>
34
+ <i class="bi bi-chevron-right"></i>
35
+ </div>
36
+ </a>
37
+ </li>
38
+ {% endfor %}
39
+ </ul>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ <script>
44
+ const input = document.getElementById('search-analyses');
45
+ const datalist = document.getElementById('analyses-list');
46
+ const applyBtn = document.getElementById('apply-btn');
47
+
48
+ let currentOptions = [];
49
+
50
+ function updateApplyButton() {
51
+ const val = input.value.trim();
52
+ applyBtn.disabled = !currentOptions.includes(val);
53
+ }
54
+
55
+ input.addEventListener('input', async () => {
56
+ const query = input.value.trim();
57
+
58
+ if (query.length === 0) {
59
+ datalist.innerHTML = '';
60
+ currentOptions = [];
61
+ updateApplyButton();
62
+ return;
63
+ }
64
+
65
+ try {
66
+ const res = await fetch(
67
+ `/search_analyses?q=${encodeURIComponent(query)}`,
68
+ );
69
+ const data = await res.json();
70
+
71
+ datalist.innerHTML = '';
72
+ currentOptions = [];
73
+
74
+ data.results.forEach((analysis) => {
75
+ const option = document.createElement('option');
76
+ option.value = analysis;
77
+ datalist.appendChild(option);
78
+ currentOptions.push(analysis);
79
+ });
80
+
81
+ updateApplyButton();
82
+ } catch (e) {
83
+ console.error('Erreur recherche analyses:', e);
84
+ }
85
+ });
86
+
87
+ input.addEventListener('change', () => {
88
+ updateApplyButton();
89
+ });
90
+
91
+ applyBtn.addEventListener('click', () => {
92
+ const val = input.value.trim();
93
+ if (currentOptions.includes(val)) {
94
+ window.location.href = `/analysis/${encodeURIComponent(val)}`;
95
+ }
96
+ });
97
+ </script>
98
+ {% endblock content %}
@@ -0,0 +1,44 @@
1
+ {% extends "layout.html" %}
2
+ {% block title %}
3
+ Détails : {{ analysis_id }} - BEx
4
+ {% endblock title %}
5
+ {% block content %}
6
+ <div class="container mt-5">
7
+ <div class="card shadow-sm">
8
+ <div class="card-body">
9
+ <h2 class="text-center mb-4">
10
+ Détails de l'analyse :
11
+ <code class="analysis-name">{{ analysis_id }}</code>
12
+ </h2>
13
+ <div class="row">
14
+ <div class="col-md-6 mb-4">
15
+ <h5>Condition expérimentale</h5>
16
+ <ul class="list-group">
17
+ {% for wet in wet_processes %}
18
+ <li class="list-group-item">
19
+ <a href="{{ url_for('routes.wet_process_detail', wet_process_id=wet) }}"
20
+ class="text-decoration-none">{{ wet }}</a>
21
+ </li>
22
+ {% else %}
23
+ <li class="list-group-item text-muted">Aucun wet process trouvé.</li>
24
+ {% endfor %}
25
+ </ul>
26
+ </div>
27
+ <div class="col-md-6 mb-4">
28
+ <h5>Condition bio-informatique</h5>
29
+ <ul class="list-group">
30
+ {% for bi in bi_processes %}
31
+ <li class="list-group-item">
32
+ <a href="{{ url_for('routes.bi_process_detail', bi_process_id=bi) }}"
33
+ class="text-decoration-none">{{ bi }}</a>
34
+ </li>
35
+ {% else %}
36
+ <li class="list-group-item text-muted">Aucun bi process trouvé.</li>
37
+ {% endfor %}
38
+ </ul>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ {% endblock content %}