1import io, sys, subprocess, psutil, time, traceback
2import base64, webbrowser, json, ast
3
4import pandas as pd
5import sqlalchemy as sa
6from dash import dash, dcc, html, dash_table, Input, Output, State, ctx
7from dash.exceptions import PreventUpdate
8import dash_bootstrap_components as dbc
9from pydrive2.auth import GoogleAuth
10from pydrive2.drive import GoogleDrive
11
12from ms_autoqc.PlotGeneration import *
13from ms_autoqc.AcquisitionListener import *
14import ms_autoqc.DatabaseFunctions as db
15import ms_autoqc.AutoQCProcessing as qc
16import ms_autoqc.SlackNotifications as bot
17
18# Set ms_autoqc/src as the working directory
19src_folder = os.path.dirname(os.path.realpath(__file__))
20os.chdir(src_folder)
21
22# Initialize directories
23root_directory = os.getcwd()
24data_directory = os.path.join(root_directory, "data")
25methods_directory = os.path.join(data_directory, "methods")
26auth_directory = os.path.join(root_directory, "auth")
27
28for directory in [data_directory, auth_directory, methods_directory]:
29 if not os.path.exists(directory):
30 os.makedirs(directory)
31
32# Google Drive authentication files
33credentials_file = os.path.join(auth_directory, "credentials.txt")
34drive_settings_file = os.path.join(auth_directory, "settings.yaml")
35
36local_stylesheet = {
37 "href": "https://fonts.googleapis.com/css2?"
38 "family=Lato:wght@400;700&display=swap",
39 "rel": "stylesheet"
40}
41
42"""
43Dash app layout
44"""
45
46# Initialize Dash app
47app = dash.Dash(__name__, title="MS-AutoQC", suppress_callback_exceptions=True,
48 external_stylesheets=[local_stylesheet, dbc.themes.BOOTSTRAP, dbc.icons.BOOTSTRAP],
49 meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}])
50
51def serve_layout():
52
53 biohub_logo = "https://user-images.githubusercontent.com/7220175/184942387-0acf5deb-d81e-4962-ab27-05b453c7a688.png"
54
55 return html.Div(className="app-layout", children=[
56
57 # Navigation bar
58 dbc.Navbar(
59 dbc.Container(style={"height": "50px"}, children=[
60 # Logo and title
61 html.A(
62 dbc.Row([
63 dbc.Col(html.Img(src=biohub_logo, height="30px")),
64 dbc.Col(dbc.NavbarBrand(id="header", children="MS-AutoQC", className="ms-2")),
65 ], align="center", className="g-0",
66 ), href="https://biohub.org", style={"textDecoration": "none"},
67 ),
68 # Settings button
69 dbc.Row([
70 dbc.Nav([
71 dbc.NavItem(dbc.NavLink("About", href="https://github.com/czbiohub/MS-AutoQC", className="navbar-button", target="_blank")),
72 dbc.NavItem(dbc.NavLink("Support", href="https://github.com/czbiohub/MS-AutoQC/wiki", className="navbar-button", target="_blank")),
73 dbc.NavItem(dbc.NavLink("Settings", href="#", id="settings-button", className="navbar-button")),
74 ], className="me-auto")
75 ], className="g-0 ms-auto flex-nowrap mt-3 mt-md-0")
76 ]), color="dark", dark=True
77 ),
78
79 # App layout
80 html.Div(className="page", children=[
81
82 dbc.Row(justify="center", children=[
83
84 dbc.Col(width=11, children=[
85
86 dbc.Row(justify="center", children=[
87
88 # Tabs to switch between instruments
89 dcc.Tabs(id="tabs", className="instrument-tabs"),
90
91 dbc.Col(width=12, lg=4, children=[
92
93 html.Div(id="table-container", className="table-container", style={"display": "none"}, children=[
94
95 # Table of past/active instrument runs
96 dash_table.DataTable(id="instrument-run-table", page_action="none",
97 fixed_rows={"headers": True},
98 cell_selectable=True,
99 style_cell={
100 "textAlign": "left",
101 "fontSize": "15px",
102 "fontFamily": "sans-serif",
103 "lineHeight": "25px",
104 "padding": "10px",
105 "borderRadius": "5px"},
106 style_data={"whiteSpace": "normal",
107 "textOverflow": "ellipsis",
108 "maxWidth": 0},
109 style_table={
110 "max-height": "285px",
111 "overflowY": "auto"},
112 style_data_conditional=[
113 {"if": {"state": "active"},
114 "backgroundColor": bootstrap_colors[
115 "blue-low-opacity"],
116 "border": "1px solid " + bootstrap_colors["blue"]
117 }],
118 style_cell_conditional=[
119 {"if": {"column_id": "Run ID"},
120 "width": "40%"},
121 {"if": {"column_id": "Chromatography"},
122 "width": "35%"},
123 {"if": {"column_id": "Status"},
124 "width": "25%"}
125 ]
126 ),
127
128 # Progress bar for instrument run
129 dbc.Card(id="active-run-progress-card", style={"display": "none"},
130 className="margin-top-15", children=[
131 dbc.CardHeader(id="active-run-progress-header", style={"padding": "0.75rem"}),
132 dbc.CardBody([
133
134 # Instrument run progress
135 dcc.Interval(id="refresh-interval", n_intervals=0, interval=30000, disabled=True),
136 dbc.Progress(id="active-run-progress-bar", animated=False),
137
138 # Buttons for managing MS-AutoQC jobs
139 html.Div(id="job-controller-panel", children=[
140 html.Div(className="d-flex justify-content-center btn-toolbar", children=[
141 # Button to mark current job as complete
142 html.Div(className="me-1", children=[
143 dbc.Button("Mark as Completed",
144 id="mark-as-completed-button",
145 className="run-button",
146 outline=True,
147 color="success"),
148 ]),
149
150 # Button to restart job
151 html.Div(className="me-1", children=[
152 dbc.Button("Restart Job",
153 id="restart-job-button",
154 className="run-button",
155 outline=True,
156 color="warning"),
157 ]),
158
159 # Button to delete job
160 html.Div(className="me-1", children=[
161 dbc.Button("Delete Job",
162 id="delete-job-button",
163 className="run-button",
164 outline=True,
165 color="danger"),
166 ]),
167 ]),
168 ]),
169 ])
170 ]),
171
172 # Button to start new MS-AutoQC job
173 html.Div(className="d-grid gap-2", children=[
174 dbc.Button("Setup New QC Job",
175 id="setup-new-run-button",
176 style={"margin-top": "15px",
177 "line-height": "1.75"},
178 outline=True,
179 color="primary"),
180 ]),
181
182 # Polarity filtering options
183 html.Div(className="radio-group-container", children=[
184 html.Div(className="radio-group margin-top-30", children=[
185 dbc.RadioItems(
186 id="polarity-options",
187 className="btn-group",
188 inputClassName="btn-check",
189 labelClassName="btn btn-outline-primary",
190 inputCheckedClassName="active",
191 options=[
192 {"label": "Positive Mode", "value": "Pos"},
193 {"label": "Negative Mode", "value": "Neg"}],
194 value="Pos"
195 ),
196 ])
197 ]),
198
199 # Sample / blank / pool / treatment filtering options
200 html.Div(className="radio-group-container", children=[
201 html.Div(className="radio-group margin-top-30", children=[
202 dbc.RadioItems(
203 id="sample-filtering-options",
204 className="btn-group",
205 inputClassName="btn-check",
206 labelClassName="btn btn-outline-primary",
207 inputCheckedClassName="active",
208 value="all",
209 options=[
210 {"label": "All", "value": "all"},
211 {"label": "Samples", "value": "samples"},
212 {"label": "Pools", "value": "pools"},
213 {"label": "Blanks", "value": "blanks"}],
214 ),
215 ])
216 ]),
217
218 # Table of samples run for a particular study
219 dash_table.DataTable(id="sample-table", page_action="none",
220 fixed_rows={"headers": True},
221 # cell_selectable=True,
222 style_cell={
223 "textAlign": "left",
224 "fontSize": "15px",
225 "fontFamily": "sans-serif",
226 "lineHeight": "25px",
227 "whiteSpace": "normal",
228 "padding": "10px",
229 "borderRadius": "5px"},
230 style_data={
231 "whiteSpace": "normal",
232 "textOverflow": "ellipsis",
233 "maxWidth": 0},
234 style_table={
235 "height": "475px",
236 "overflowY": "auto"},
237 style_data_conditional=[
238 {"if": {"filter_query": "{QC} = 'Fail'"},
239 "backgroundColor": bootstrap_colors[
240 "red-low-opacity"],
241 "font-weight": "bold"
242 },
243 {"if": {"filter_query": "{QC} = 'Check'"},
244 "backgroundColor": bootstrap_colors[
245 "yellow-low-opacity"]
246 },
247 {"if": {"state": "active"},
248 "backgroundColor": bootstrap_colors[
249 "blue-low-opacity"],
250 "border": "1px solid " + bootstrap_colors["blue"]
251 }
252 ],
253 style_cell_conditional=[
254 {"if": {"column_id": "Sample"},
255 "width": "60%"},
256 {"if": {"column_id": "Position"},
257 "width": "20%"},
258 {"if": {"column_id": "QC"},
259 "width": "20%"},
260 ]
261 )
262 ]),
263 ]),
264
265 dbc.Col(width=12, lg=8, children=[
266
267 # Container for all plots
268 html.Div(id="plot-container", className="all-plots-container", style={"display": "none"}, children=[
269
270 html.Div(className="istd-plot-div", children=[
271
272 html.Div(id="istd-rt-div", className="plot-container", children=[
273
274 # Internal standard selection controls
275 html.Div(style={"width": "100%"}, children=[
276 # Dropdown for selecting an internal standard for the RT vs. sample plot
277 html.Div(className="istd-dropdown-style", children=[
278 dcc.Dropdown(
279 id="istd-rt-dropdown",
280 options=[],
281 placeholder="Select internal standards...",
282 style={"text-align": "left",
283 "height": "1.5",
284 "width": "100%"}
285 )]
286 ),
287
288 # Buttons for skipping through the internal standards
289 html.Div(className="istd-button-style", children=[
290 dbc.Button(html.I(className="bi bi-arrow-left"),
291 id="rt-prev-button", color="light", className="me-1"),
292 dbc.Button(html.I(className="bi bi-arrow-right"),
293 id="rt-next-button", color="light", className="me-1"),
294 ]),
295 ]),
296
297 # Dropdown for filtering by sample for the RT vs. sample plot
298 dcc.Dropdown(
299 id="rt-plot-sample-dropdown",
300 options=[],
301 placeholder="Select samples...",
302 style={"text-align": "left",
303 "height": "1.5",
304 "width": "100%",
305 "display": "inline-block"},
306 multi=True),
307
308 # Scatter plot of internal standard retention times vs. samples
309 dcc.Graph(id="istd-rt-plot"),
310 ]),
311
312 html.Div(id="istd-intensity-div", className="plot-container", children=[
313
314 # Internal standard selection controls
315 html.Div(style={"width": "100%"}, children=[
316 # Dropdown for selecting an internal standard for the intensity vs. sample plot
317 html.Div(className="istd-dropdown-style", children=[
318 dcc.Dropdown(
319 id="istd-intensity-dropdown",
320 options=[],
321 placeholder="Select internal standards...",
322 style={"text-align": "left",
323 "height": "1.5",
324 "width": "100%"}
325 )]
326 ),
327
328 # Buttons for skipping through the internal standards
329 html.Div(className="istd-button-style", children=[
330 dbc.Button(html.I(className="bi bi-arrow-left"),
331 id="intensity-prev-button", color="light", className="me-1"),
332 dbc.Button(html.I(className="bi bi-arrow-right"),
333 id="intensity-next-button", color="light", className="me-1"),
334 ]),
335 ]),
336
337 # Dropdown for filtering by sample for the intensity vs. sample plot
338 dcc.Dropdown(
339 id="intensity-plot-sample-dropdown",
340 options=[],
341 placeholder="Select samples...",
342 style={"text-align": "left",
343 "height": "1.5",
344 "width": "100%",
345 "display": "inline-block"},
346 multi=True,
347 ),
348
349 # Bar plot of internal standard intensity vs. samples
350 dcc.Graph(id="istd-intensity-plot")
351 ]),
352
353 html.Div(id="istd-mz-div", className="plot-container", children=[
354
355 # Internal standard selection controls
356 html.Div(style={"width": "100%"}, children=[
357 # Dropdown for selecting an internal standard for the delta m/z vs. sample plot
358 html.Div(className="istd-dropdown-style", children=[
359 dcc.Dropdown(
360 id="istd-mz-dropdown",
361 options=[],
362 placeholder="Select internal standards...",
363 style={"text-align": "left",
364 "height": "1.5",
365 "width": "100%"}
366 )]
367 ),
368
369 # Buttons for skipping through the internal standards
370 html.Div(className="istd-button-style", children=[
371 dbc.Button(html.I(className="bi bi-arrow-left"),
372 id="mz-prev-button", color="light", className="me-1"),
373 dbc.Button(html.I(className="bi bi-arrow-right"),
374 id="mz-next-button", color="light", className="me-1"),
375 ]),
376 ]),
377
378 # Dropdown for filtering by sample for the delta m/z vs. sample plot
379 dcc.Dropdown(
380 id="mz-plot-sample-dropdown",
381 options=[],
382 placeholder="Select samples...",
383 style={"text-align": "left",
384 "height": "1.5",
385 "width": "100%",
386 "display": "inline-block"},
387 multi=True),
388
389 # Scatter plot of internal standard delta m/z vs. samples
390 dcc.Graph(id="istd-mz-plot")
391 ]),
392
393 ]),
394
395 html.Div(className="bio-plot-div", children=[
396
397 # Scatter plot for biological standard m/z vs. RT
398 html.Div(id="bio-standard-mz-rt-div", className="plot-container", children=[
399
400 # Dropdown for selecting a biological standard to view
401 dcc.Dropdown(id="bio-standards-plot-dropdown",
402 options=[], placeholder="Select biological standard...",
403 style={"text-align": "left", "height": "1.5", "font-size": "1rem",
404 "width": "100%", "display": "inline-block"}),
405
406 dcc.Graph(id="bio-standard-mz-rt-plot")
407 ]),
408
409 # Bar plot for biological standard feature intensity vs. run
410 html.Div(id="bio-standard-benchmark-div", className="plot-container", children=[
411
412 # Dropdown for biological standard feature intensity plot
413 dcc.Dropdown(
414 id="bio-standard-benchmark-dropdown",
415 options=[],
416 placeholder="Select targeted metabolite...",
417 style={"text-align": "left",
418 "height": "35px",
419 "width": "100%",
420 "display": "inline-block"}
421 ),
422
423 dcc.Graph(id="bio-standard-benchmark-plot", animate=False)
424 ])
425 ])
426 ]),
427 ]),
428
429 # Modal for sample information card
430 dbc.Modal(id="sample-info-modal", size="xl", centered=True, is_open=False, scrollable=True, children=[
431 dbc.ModalHeader(dbc.ModalTitle(id="sample-modal-title"), close_button=True),
432 dbc.ModalBody(id="sample-modal-body")
433 ]),
434
435 # Modal for alerting user that data is loading
436 dbc.Modal(id="loading-modal", size="md", centered=True, is_open=False, scrollable=True,
437 keyboard=False, backdrop="static", children=[
438 dbc.ModalHeader(dbc.ModalTitle(id="loading-modal-title"), close_button=False),
439 dbc.ModalBody(id="loading-modal-body")
440 ]),
441
442 # Modal for job completion / restart / deletion confirmation
443 dbc.Modal(id="job-controller-modal", size="md", centered=True, is_open=False, children=[
444 dbc.ModalHeader(dbc.ModalTitle(id="job-controller-modal-title")),
445 dbc.ModalBody(id="job-controller-modal-body"),
446 dbc.ModalFooter(children=[
447 dbc.Button("Cancel", color="secondary", id="job-controller-cancel-button"),
448 dbc.Button(id="job-controller-confirm-button")
449 ]),
450 ]),
451
452 # Modal for progress feedback while database syncs to Google Drive
453 dbc.Modal(id="google-drive-sync-modal", size="md", centered=True, is_open=False, scrollable=True,
454 keyboard=True, backdrop="static", children=[
455 dbc.ModalHeader(dbc.ModalTitle(
456 html.Div(children=[
457 dbc.Spinner(color="primary"), " Syncing to Google Drive"])),
458 close_button=False),
459 dbc.ModalBody("This may take a few seconds...")
460 ]),
461
462 # Custom file explorer modal for new job setup
463 dbc.Modal(id="file-explorer-modal", size="md", centered=True, is_open=False, scrollable=True,
464 keyboard=True, children=[
465 dbc.ModalHeader(dbc.ModalTitle(id="file-explorer-modal-title")),
466 dbc.ModalBody(id="file-explorer-modal-body"),
467 dbc.ModalFooter(children=[
468 dbc.Button("Go Back", id="file-explorer-back-button", color="secondary"),
469 dbc.Button("Select Current Folder", id="file-explorer-select-button")
470 ])
471 ]),
472
473 # Modal for first-time workspace setup
474 dbc.Modal(id="workspace-setup-modal", size="lg", centered=True, scrollable=True,
475 keyboard=False, backdrop="static", children=[
476 dbc.ModalHeader(dbc.ModalTitle("Welcome to MS-AutoQC", id="setup-user-modal-title"), close_button=False),
477 dbc.ModalBody(id="setup-user-modal-body", className="modal-styles-2", children=[
478
479 html.Div([
480 html.H5("Let's help you get started."),
481 html.P("Looks like this is a new installation. What would you like to do today?"),
482 dbc.Accordion(start_collapsed=True, children=[
483
484 # Setting up MS-AutoQC for the first time
485 dbc.AccordionItem(title="I'm setting up MS-AutoQC on a new instrument", children=[
486 html.Div(className="modal-styles-3", children=[
487
488 # Instrument name text field
489 html.Div([
490 dbc.Label("Instrument name"),
491 dbc.InputGroup([
492 dbc.Input(id="first-time-instrument-id", type="text",
493 placeholder="Ex: Thermo Q-Exactive HF 1"),
494 dbc.DropdownMenu(id="first-time-instrument-vendor",
495 label="Choose Vendor", color="primary", children=[
496 dbc.DropdownMenuItem("Thermo Fisher", id="thermo-fisher-item"),
497 dbc.DropdownMenuItem("Agilent", id="agilent-item"),
498 dbc.DropdownMenuItem("Bruker", id="bruker-item"),
499 dbc.DropdownMenuItem("Sciex", id="sciex-item"),
500 dbc.DropdownMenuItem("Waters", id="waters-item")
501 ]),
502 ]),
503 dbc.FormText("Please choose a name and vendor for this instrument."),
504 ]),
505
506 html.Br(),
507
508 # Google Drive authentication button
509 html.Div([
510 dbc.Label("Sync with Google Drive (recommended)"),
511 html.Br(),
512 dbc.InputGroup([
513 dbc.Input(placeholder="Client ID", id="gdrive-client-id-1"),
514 dbc.Input(placeholder="Client secret", id="gdrive-client-secret-1"),
515 dbc.Button("Sign in to Google Drive", id="setup-google-drive-button-1",
516 color="primary", outline=True),
517 ]),
518 dbc.FormText("This will allow you to access your QC results from any device."),
519 dbc.Tooltip("If you have Google Drive sync enabled on an instrument already, " +
520 "please sign in with the same Google account to merge workspaces.",
521 target="setup-google-drive-button-1", placement="left"),
522 dbc.Popover(id="google-drive-button-1-popover", is_open=False,
523 target="setup-google-drive-button-1", placement="right")
524 ]),
525
526 html.Br(),
527
528 # Complete setup button
529 html.Div([
530 html.Div([
531 dbc.Button(children="Complete setup", id="first-time-complete-setup-button",
532 disabled=True, style={"line-height": "1.75"}, color="success"),
533 ], className="d-grid gap-2 col-12 mx-auto"),
534 ])
535 ]),
536 ]),
537
538 # Signing in from another device
539 dbc.AccordionItem(title="I'm signing in to an existing MS-AutoQC workspace", children=[
540 html.Div(className="modal-styles-3", children=[
541
542 # Google Drive authentication button
543 html.Div([
544 dbc.Label("Sign in to access MS-AutoQC"), html.Br(),
545 dbc.InputGroup([
546 dbc.Input(placeholder="Client ID", id="gdrive-client-id-2"),
547 dbc.Input(placeholder="Client secret", id="gdrive-client-secret-2"),
548 dbc.Button("Sign in to Google Drive", id="setup-google-drive-button-2",
549 color="primary", outline=False),
550 ]),
551 dbc.FormText(
552 "Please ensure that your Google account has been registered to " +
553 "access your MS-AutoQC workspace by visiting Settings > General."),
554 dbc.Popover(id="google-drive-button-2-popover", is_open=False,
555 target="setup-google-drive-button-2", placement="right")
556 ]),
557
558 # Checkbox for logging in to instrument computer
559 dbc.Checkbox(id="device-identity-checkbox", className="checkbox-margin",
560 label="I am signing in from an instrument computer", value=False),
561
562 # Dropdown for selecting an instrument
563 dbc.Select(id="device-identity-selection", value=None,
564 placeholder="Which instrument?", disabled=True),
565
566 html.Br(),
567
568 # Workspace sign-in button
569 html.Div([
570 html.Div([
571 dbc.Button("Sign in to MS-AutoQC workspace", id="first-time-sign-in-button",
572 disabled=True, style={"line-height": "1.75"}, color="success"),
573 ], className="d-grid gap-2 col-12 mx-auto"),
574 ])
575 ]),
576 ]),
577 ]),
578 ]),
579 ])
580 ]),
581
582 # Modal for starting an instrument run listener
583 dbc.Modal(id="setup-new-run-modal", size="lg", centered=True, is_open=False, scrollable=True, children=[
584 dbc.ModalHeader(dbc.ModalTitle(id="setup-new-run-modal-title", children="New QC Job"), close_button=True),
585 dbc.ModalBody(id="setup-new-run-modal-body", className="modal-styles-2", children=[
586
587 # Text field for entering your run ID
588 html.Div([
589 dbc.Label("Instrument run ID"),
590 dbc.Input(id="instrument-run-id", placeholder="Give your instrument run a unique name", type="text"),
591 dbc.FormFeedback("Looks good!", type="valid"),
592 dbc.FormFeedback("Please enter a unique ID for this run.", type="invalid"),
593 ]),
594
595 html.Br(),
596
597 # Select chromatography
598 html.Div([
599 dbc.Label("Select chromatography"),
600 dbc.Select(id="start-run-chromatography-dropdown",
601 placeholder="No chromatography selected"),
602 dbc.FormFeedback("Looks good!", type="valid"),
603 dbc.FormFeedback(
604 "Please ensure that your chromatography method has identification files "
605 "(MSP or CSV) configured for positive and negative mode in Settings > "
606 "Internal Standards and Settings > Biological Standards.", type="invalid")
607 ]),
608
609 html.Br(),
610
611 # Select biological standard used in this study
612 html.Div(children=[
613 dbc.Label("Select biological standards (optional)"),
614 dcc.Dropdown(id="start-run-bio-standards-dropdown",
615 options=[], placeholder="Select biological standards...",
616 style={"text-align": "left", "height": "1.5", "font-size": "1rem",
617 "width": "100%", "display": "inline-block"},
618 multi=True)
619 ]),
620
621 html.Br(),
622
623 # Select AutoQC configuration
624 html.Div(children=[
625 dbc.Label("Select MS-AutoQC configuration"),
626 dbc.Select(id="start-run-qc-configs-dropdown",
627 placeholder="No configuration selected"),
628 ]),
629
630 html.Br(),
631
632 # Button and field for selecting a sequence file
633 html.Div([
634 dbc.Label("Acquisition sequence (.csv)"),
635 dbc.InputGroup([
636 dbc.Input(id="sequence-path",
637 placeholder="No file selected"),
638 dbc.Button(dcc.Upload(
639 id="sequence-upload-button",
640 accept="text/plain, application/vnd.ms-excel, .csv",
641 children=[html.A("Browse Files")]),
642 color="secondary"),
643 dbc.FormFeedback("Looks good!", type="valid"),
644 dbc.FormFeedback("Please ensure that the sequence file is a CSV file "
645 "and in the correct vendor format.", type="invalid"),
646 ]),
647 ]),
648
649 html.Br(),
650
651 # Button and field for selecting a sample metadata file
652 html.Div([
653 dbc.Label("Sample metadata (.csv) (optional)"),
654 dbc.InputGroup([
655 dbc.Input(id="metadata-path",
656 placeholder="No file selected"),
657 dbc.Button(dcc.Upload(
658 id="metadata-upload-button",
659 accept="text/plain, application/vnd.ms-excel, .csv",
660 children=[html.A("Browse Files")]),
661 color="secondary"),
662 dbc.FormFeedback("Looks good!", type="valid"),
663 dbc.FormFeedback("Please ensure that the metadata file is a CSV and contains "
664 "the following columns: Sample Name, Species, Matrix, Treatment, "
665 "and Growth-Harvest Conditions", type="invalid"),
666 ]),
667 ]),
668
669 html.Br(),
670
671 # Button and field for selecting the data acquisition directory
672 html.Div([
673 dbc.Label("Data file directory", id="data-acquisition-path-title"),
674 dbc.InputGroup([
675 dbc.Input(placeholder="Browse folders or enter the folder path",
676 id="data-acquisition-folder-path"),
677 dbc.Button("Browse Folders", id="data-acquisition-folder-button",
678 color="secondary"),
679 dbc.FormFeedback("Looks good!", type="valid"),
680 dbc.FormFeedback(
681 "This path does not exist. Please enter a valid path.", type="invalid"),
682 ]),
683 dbc.FormText(id="data-acquisition-path-form-text",
684 children="Please type the folder path to which incoming data files will be saved."),
685
686 ]),
687
688 html.Br(),
689
690 # Switch between running AutoQC on a live run vs. past completed run
691 html.Div(children=[
692 dbc.Label("Is this an active or completed instrument run?"),
693 dbc.RadioItems(id="ms_autoqc-job-type", value="active", options=[
694 {"label": "Monitor an active instrument run",
695 "value": "active"},
696 {"label": "QC a completed instrument run",
697 "value": "completed"}],
698 ),
699 ]),
700
701 html.Br(),
702
703 html.Div([
704 dbc.Button("Start monitoring instrument run", id="monitor-new-run-button", disabled=True,
705 style={"line-height": "1.75"}, color="primary")],
706 className="d-grid gap-2")
707 ]),
708 ]),
709
710 # Modal to alert user that run monitoring has started
711 dbc.Modal(id="start-run-monitor-modal", size="md", centered=True, is_open=False, children=[
712 dbc.ModalHeader(dbc.ModalTitle(id="start-run-monitor-modal-title", children="Success!"), close_button=True),
713 dbc.ModalBody(id="start-run-monitor-modal-body", className="modal-styles", children=[
714 dbc.Alert("MS-AutoQC will start monitoring your run. Please do not restart your computer.", color="success")
715 ]),
716 ]),
717
718 # Error modal for new AutoQC job setup
719 dbc.Modal(id="new-job-error-modal", size="md", centered=True, is_open=False, children=[
720 dbc.ModalHeader(dbc.ModalTitle(id="new-job-error-modal-title"), close_button=False),
721 dbc.ModalBody(id="new-job-error-modal-body", className="modal-styles"),
722 ]),
723
724 # MS-AutoQC settings
725 dbc.Modal(id="settings-modal", fullscreen=True, centered=True, is_open=False, scrollable=True, children=[
726 dbc.ModalHeader(dbc.ModalTitle(children="Settings"), close_button=True),
727 dbc.ModalBody(id="settings-modal-body", className="modal-styles-fullscreen", children=[
728
729 # Tabbed interface
730 dbc.Tabs(children=[
731
732 # General settings
733 dbc.Tab(label="General", className="modal-styles", children=[
734
735 html.Br(),
736
737 dbc.Alert(id="google-drive-sign-in-from-settings-alert", is_open=False,
738 dismissable=True, color="danger", children=[
739 html.H4(
740 "This Google account already has an MS-AutoQC workspace."),
741 html.P(
742 "Please sign in with a different Google account to enable cloud "
743 "sync for this workspace."),
744 html.P(
745 "Or, if you'd like to add a new instrument to an existing MS-AutoQC "
746 "workspace, please reinstall MS-AutoQC on this instrument and enable "
747 "cloud sync during setup.")
748 ]),
749
750 dbc.Alert(id="gdrive-credentials-saved-alert", is_open=False, duration=5000),
751
752 dbc.Label("Manage workspace access", style={"font-weight": "bold"}),
753 html.Br(),
754
755 # Google Drive cloud storage
756 dbc.Label("Google API client credentials"),
757 html.Br(),
758 dbc.InputGroup([
759 dbc.Input(placeholder="Client ID", id="gdrive-client-id"),
760 dbc.Input(placeholder="Client secret", id="gdrive-client-secret"),
761 dbc.Button("Set credentials",
762 id="set-gdrive-credentials-button", color="primary", outline=True),
763 ]),
764 dbc.FormText(children=[
765 "You can get these credentials from the ",
766 html.A("Google Cloud console",
767 href="https://console.cloud.google.com/apis/credentials", target="_blank"),
768 " in Credentials > OAuth 2.0 Client ID's."]),
769 html.Br(), html.Br(),
770
771 dbc.Label("Enable cloud sync with Google Drive"),
772 html.Br(),
773 dbc.Button("Sync with Google Drive",
774 id="google-drive-sync-button", color="primary", outline=False),
775 html.Br(),
776 dbc.FormText(id="google-drive-sync-form-text", children=
777 "This will allow you to monitor your instrument runs on other devices."),
778 html.Br(), html.Br(),
779
780 # Alerts for modifying workspace access
781 dbc.Alert(id="user-addition-alert", color="success", is_open=False, duration=5000),
782 dbc.Alert(id="user-deletion-alert", color="primary", is_open=False, duration=5000),
783
784 # Google Drive sharing
785 dbc.Label("Add / remove workspace users"),
786 html.Br(),
787 dbc.InputGroup([
788 dbc.Input(placeholder="example@gmail.com", id="add-user-text-field"),
789 dbc.Button("Add user", color="primary", outline=True,
790 id="add-user-button", n_clicks=0),
791 dbc.Button("Delete user", color="danger", outline=True,
792 id="delete-user-button", n_clicks=0),
793 dbc.Popover("This will revoke user access to the MS-AutoQC workspace. "
794 "Are you sure?", target="delete-user-button", trigger="hover", body=True)
795 ]),
796 dbc.FormText(
797 "Adding new users grants full read-and-write access to this MS-AutoQC workspace."),
798 html.Br(), html.Br(),
799
800 # Table of users with workspace access
801 html.Div(id="workspace-users-table"),
802 html.Br(),
803
804 dbc.Label("Slack notifications", style={"font-weight": "bold"}),
805 html.Br(),
806
807 # Alerts for modifying workspace access
808 dbc.Alert(id="slack-token-save-alert", is_open=False, duration=5000),
809
810 # Channel for Slack notifications
811 dbc.Label("Slack API client credentials"),
812 html.Br(),
813 dbc.InputGroup([
814 dbc.Input(placeholder="Slack bot user OAuth token", id="slack-bot-token"),
815 dbc.Button("Save bot token", color="primary", outline=True,
816 id="save-slack-token-button", n_clicks=0),
817 ]),
818 dbc.FormText(children=[
819 "You can get the Slack bot token from the ",
820 html.A("Slack API website",
821 href="https://api.slack.com/apps", target="_blank"),
822 " in Your App > Settings > Install App."]),
823 html.Br(), html.Br(),
824
825 dbc.Alert(id="slack-notifications-toggle-alert", is_open=False, duration=5000),
826
827 dbc.Label("Register Slack channel for notifications"),
828 dbc.InputGroup(children=[
829 dbc.Input(id="slack-channel", placeholder="#my-slack-channel"),
830 dbc.InputGroupText(
831 dbc.Switch(id="slack-notifications-enabled", label="Enable notifications")),
832 ]),
833 dbc.FormText(
834 "Please enter the Slack channel you'd like to register for notifications."),
835 html.Br(), html.Br(),
836
837 dbc.Label("Email notifications", style={"font-weight": "bold"}),
838 html.Br(),
839
840 # Alerts for modifying email notification list
841 dbc.Alert(id="email-addition-alert", is_open=False, duration=5000),
842 dbc.Alert(id="email-deletion-alert", is_open=False, duration=5000),
843
844 # Register recipients for email notifications
845 dbc.Label("Register recipients for email notifications"),
846 html.Br(),
847 dbc.InputGroup([
848 dbc.Input(placeholder="recipient@example.com",
849 id="email-notifications-text-field"),
850 dbc.Button("Register email", color="primary", outline=True,
851 id="add-email-button", n_clicks=0),
852 dbc.Button("Remove email", color="danger", outline=True,
853 id="delete-email-button", n_clicks=0),
854 dbc.Popover("This will un-register the email account from MS-AutoQC "
855 "notifications. Are you sure?", target="delete-email-button",
856 trigger="hover", body=True)
857 ]),
858 dbc.FormText(
859 "Please enter a valid email address to register for email notifications."),
860 html.Br(), html.Br(),
861
862 # Table of users registered for email notifications
863 html.Div(id="email-notifications-table")
864 ]),
865
866 # Internal standards
867 dbc.Tab(label="Chromatography methods", className="modal-styles", children=[
868
869 html.Br(),
870
871 # Alerts for user feedback on biological standard addition/removal
872 dbc.Alert(id="chromatography-addition-alert", color="success", is_open=False, duration=5000),
873 dbc.Alert(id="chromatography-removal-alert", color="primary", is_open=False, duration=5000),
874
875 dbc.Label("Manage chromatography methods", style={"font-weight": "bold"}),
876 html.Br(),
877
878 # Add new chromatography method
879 html.Div([
880 dbc.Label("Add new chromatography method"),
881 dbc.InputGroup([
882 dbc.Input(id="add-chromatography-text-field", type="text",
883 placeholder="Name of chromatography to add"),
884 dbc.Button("Add method", color="primary", outline=True,
885 id="add-chromatography-button", n_clicks=0),
886 ]),
887 dbc.FormText("Example: HILIC, Reverse Phase, RP (30 mins)"),
888 ]), html.Br(),
889
890 # Chromatography methods table
891 dbc.Label("Chromatography methods", style={"font-weight": "bold"}),
892 html.Br(),
893 html.Div(id="chromatography-methods-table"),
894 html.Br(),
895
896 dbc.Label("Configure chromatography methods", style={"font-weight": "bold"}),
897 html.Br(),
898
899 # Select chromatography
900 html.Div([
901 dbc.Label("Select chromatography to modify"),
902 dbc.InputGroup([
903 dbc.Select(id="select-istd-chromatography-dropdown",
904 placeholder="No chromatography selected"),
905 dbc.Button("Remove", color="danger", outline=True,
906 id="remove-chromatography-method-button", n_clicks=0),
907 dbc.Popover("You are about to delete this chromatography method and "
908 "all of its corresponding MSP files. Are you sure?",
909 target="remove-chromatography-method-button", trigger="hover", body=True)
910 ]),
911 ]),
912
913 html.Br(),
914
915 # Select polarity
916 html.Div([
917 dbc.Label("Select polarity to modify"),
918 dbc.Select(id="select-istd-polarity-dropdown", options=[
919 {"label": "Positive Mode", "value": "Positive Mode"},
920 {"label": "Negative Mode", "value": "Negative Mode"},
921 ], placeholder="No polarity selected"),
922 ]),
923
924 html.Br(),
925
926 dbc.Alert(id="istd-config-success-alert", color="success", is_open=False, duration=5000),
927
928 # Set MS-DIAL configuration for selected chromatography
929 html.Div(children=[
930 dbc.Label("Set MS-DIAL processing configuration",
931 id="istd-medial-configs-label"),
932 dbc.InputGroup([
933 dbc.Select(id="istd-msdial-configs-dropdown",
934 placeholder="No configuration selected"),
935 dbc.Button("Set configuration", color="primary", outline=True,
936 id="istd-msdial-configs-button", n_clicks=0),
937 ])
938 ]),
939
940 html.Br(),
941
942 # UI feedback on adding MSP to chromatography method
943 dbc.Alert(id="chromatography-msp-success-alert", color="success", is_open=False,
944 duration=5000),
945 dbc.Alert(id="chromatography-msp-error-alert", color="danger", is_open=False,
946 duration=5000),
947
948 dbc.Label("Add internal standard identification files", style={"font-weight": "bold"}),
949 html.Br(),
950
951 html.Div([
952 dbc.Label("Add internal standards (MSP or CSV format)"),
953 dbc.InputGroup([
954 dbc.Input(placeholder="No file selected",
955 id="add-istd-msp-text-field"),
956 dbc.Button(dcc.Upload(
957 id="add-istd-msp-button",
958 accept="text/plain, application/vnd.ms-excel, .msp, .csv",
959 children=[html.A("Browse Files")]),
960 color="secondary"),
961 ]),
962 dbc.FormText(
963 "Please ensure that each internal standard has a name, m/z, RT, and MS/MS spectrum."),
964 ]),
965
966 html.Br(),
967
968 html.Div([
969 html.Div([
970 dbc.Button("Save changes", id="msp-save-changes-button",
971 style={"line-height": "1.75"}, color="primary"),
972 ], className="d-grid gap-2 col-12 mx-auto"),
973 ]),
974 ]),
975
976 # Biological standards
977 dbc.Tab(label="Biological standards", className="modal-styles", children=[
978
979 html.Br(),
980
981 # UI feedback for biological standard addition/removal
982 dbc.Alert(id="bio-standard-addition-alert", is_open=False, duration=5000),
983
984 dbc.Label("Manage biological standards", style={"font-weight": "bold"}),
985 html.Br(),
986
987 html.Div([
988 dbc.Label("Add new biological standard"),
989 dbc.InputGroup([
990 dbc.Input(id="add-bio-standard-text-field",
991 placeholder="Name of biological standard"),
992 dbc.Input(id="add-bio-standard-identifier-text-field",
993 placeholder="Sequence identifier"),
994 dbc.Button("Add biological standard", color="primary", outline=True,
995 id="add-bio-standard-button", n_clicks=0),
996 ]),
997 dbc.FormText(
998 "The sequence identifier is the label that denotes your biological standard in the sequence."),
999 ]),
1000
1001 html.Br(),
1002
1003 # Table of biological standards
1004 dbc.Label("Biological standards", style={"font-weight": "bold"}),
1005 html.Br(),
1006
1007 html.Div(id="biological-standards-table"),
1008 html.Br(),
1009
1010 dbc.Alert(id="bio-standard-removal-alert", color="primary", is_open=False, duration=5000),
1011
1012 dbc.Label("Configure biological standards and add MSP files",
1013 style={"font-weight": "bold"}),
1014 html.Br(),
1015
1016 # Select biological standard
1017 html.Div([
1018 dbc.Label("Select biological standard to modify"),
1019 dbc.InputGroup([
1020 dbc.Select(id="select-bio-standard-dropdown",
1021 placeholder="No biological standard selected"),
1022 dbc.Button("Remove", color="danger", outline=True,
1023 id="remove-bio-standard-button", n_clicks=0),
1024 dbc.Popover("You are about to delete this biological standard and "
1025 "all of its corresponding MSP files. Are you sure?",
1026 target="remove-bio-standard-button", trigger="hover",
1027 body=True)
1028 ]),
1029 ]),
1030
1031 html.Br(),
1032
1033 html.Div([
1034 dbc.Label("Select chromatography and polarity to modify"),
1035 html.Div(className="parent-container", children=[
1036 # Select chromatography
1037 html.Div(className="child-container", children=[
1038 dbc.Select(id="select-bio-chromatography-dropdown",
1039 placeholder="No chromatography selected"),
1040 ]),
1041
1042 # Select polarity
1043 html.Div(className="child-container", children=[
1044 dbc.Select(id="select-bio-polarity-dropdown", options=[
1045 {"label": "Positive Mode", "value": "Positive Mode"},
1046 {"label": "Negative Mode", "value": "Negative Mode"},
1047 ], placeholder="No polarity selected"),
1048 html.Br(),
1049 ]),
1050 ]),
1051 ]),
1052
1053 html.Br(), html.Br(),
1054
1055 dbc.Alert(id="bio-config-success-alert", color="success", is_open=False, duration=5000),
1056
1057 # Set MS-DIAL configuration for selected biological standard
1058 html.Div(children=[
1059 dbc.Label("Set MS-DIAL processing configuration",
1060 id="bio-standard-msdial-configs-label"),
1061 dbc.InputGroup([
1062 dbc.Select(id="bio-standard-msdial-configs-dropdown",
1063 placeholder="No configuration selected"),
1064 dbc.Button("Set configuration", color="primary", outline=True,
1065 id="bio-standard-msdial-configs-button", n_clicks=0),
1066 ])
1067 ]),
1068
1069 html.Br(),
1070
1071 # UI feedback on adding MSP to biological standard
1072 dbc.Alert(id="bio-msp-success-alert", color="success", is_open=False,
1073 duration=5000),
1074 dbc.Alert(id="bio-msp-error-alert", color="danger", is_open=False,
1075 duration=5000),
1076
1077 html.Div([
1078 dbc.Label("Edit targeted metabolites list (MSP format)"),
1079 html.Br(),
1080 dbc.InputGroup([
1081 dbc.Input(placeholder="No MSP file selected",
1082 id="add-bio-msp-text-field"),
1083 dbc.Button(dcc.Upload(
1084 id="add-bio-msp-button",
1085 accept=".msp",
1086 children=[html.A("Browse Files")]),
1087 color="secondary"),
1088 ]),
1089 dbc.FormText(
1090 "Please ensure that each feature has a name, m/z, RT, and MS/MS spectrum."),
1091 ]),
1092
1093 html.Br(),
1094
1095 html.Div([
1096 html.Div([
1097 dbc.Button("Save changes", id="bio-standard-save-changes-button",
1098 style={"line-height": "1.75"}, color="primary"),
1099 ], className="d-grid gap-2 col-12 mx-auto"),
1100 ]),
1101 ]),
1102
1103 # AutoQC parameters
1104 dbc.Tab(label="QC configurations", className="modal-styles", children=[
1105
1106 html.Br(),
1107
1108 # UI feedback on adding / removing QC configurations
1109 dbc.Alert(id="qc-config-addition-alert", is_open=False, duration=5000),
1110 dbc.Alert(id="qc-config-removal-alert", is_open=False, duration=5000),
1111
1112 dbc.Label("Manage QC configurations", style={"font-weight": "bold"}),
1113 html.Br(),
1114
1115 html.Div([
1116 dbc.Label("Add new QC configuration"),
1117 dbc.InputGroup([
1118 dbc.Input(id="add-qc-configuration-text-field",
1119 placeholder="Name of configuration to add"),
1120 dbc.Button("Add new config", color="primary", outline=True,
1121 id="add-qc-configuration-button", n_clicks=0),
1122 ]),
1123 dbc.FormText("Give your custom QC configuration a unique name"),
1124 ]),
1125
1126 html.Br(),
1127
1128 # Select configuration
1129 html.Div(children=[
1130 dbc.Label("Select QC configuration to edit"),
1131 dbc.InputGroup([
1132 dbc.Select(id="qc-configs-dropdown",
1133 placeholder="No configuration selected"),
1134 dbc.Button("Remove", color="danger", outline=True,
1135 id="remove-qc-config-button", n_clicks=0),
1136 dbc.Popover("You are about to delete this QC configuration. Are you sure?",
1137 target="remove-qc-config-button", trigger="hover", body=True)
1138 ])
1139 ]),
1140
1141 html.Br(),
1142
1143 dbc.Label("Edit QC configuration parameters", style={"font-weight": "bold"}),
1144 html.Br(),
1145
1146 html.Div([
1147 dbc.Label("Cutoff for intensity dropouts"),
1148 dbc.InputGroup(children=[
1149 dbc.Input(
1150 id="intensity-dropouts-cutoff", type="number", placeholder="4"),
1151 dbc.InputGroupText(
1152 dbc.Switch(id="intensity-cutoff-enabled", label="Enabled")),
1153 ]),
1154 dbc.FormText("The minimum number of missing internal " +
1155 "standards in a sample to trigger a QC fail."),
1156 ]),
1157
1158 html.Br(),
1159
1160 html.Div([
1161 dbc.Label("Cutoff for RT shift from library value"),
1162 dbc.InputGroup(children=[
1163 dbc.Input(id="library-rt-shift-cutoff", type="number", placeholder="0.1"),
1164 dbc.InputGroupText(
1165 dbc.Switch(id="library-rt-shift-cutoff-enabled", label="Enabled")),
1166 ]),
1167 dbc.FormText(
1168 "The minimum shift in retention time (in minutes) from " +
1169 "the library value to trigger a QC fail."),
1170 ]),
1171
1172 html.Br(),
1173
1174 html.Div([
1175 dbc.Label("Cutoff for RT shift from in-run average"),
1176 dbc.InputGroup(children=[
1177 dbc.Input(id="in-run-rt-shift-cutoff", type="number", placeholder="0.05"),
1178 dbc.InputGroupText(
1179 dbc.Switch(id="in-run-rt-shift-cutoff-enabled", label="Enabled")),
1180 ]),
1181 dbc.FormText(
1182 "The minimum shift in retention time (in minutes) from " +
1183 "the in-run average to trigger a QC fail."),
1184 ]),
1185
1186 html.Br(),
1187
1188 html.Div([
1189 dbc.Label("Cutoff for m/z shift from library value"),
1190 dbc.InputGroup(children=[
1191 dbc.Input(id="library-mz-shift-cutoff", type="number", placeholder="0.005"),
1192 dbc.InputGroupText(
1193 dbc.Switch(id="library-mz-shift-cutoff-enabled", label="Enabled")),
1194 ]),
1195 dbc.FormText(
1196 "The minimum shift in precursor m/z (in minutes) from " +
1197 "the library value to trigger a QC fail."),
1198 ]),
1199
1200 html.Br(),
1201
1202 # UI feedback on saving changes to MS-DIAL parameters
1203 dbc.Alert(id="qc-parameters-success-alert",
1204 color="success", is_open=False, duration=5000),
1205 dbc.Alert(id="qc-parameters-reset-alert",
1206 color="primary", is_open=False, duration=5000),
1207 dbc.Alert(id="qc-parameters-error-alert",
1208 color="danger", is_open=False, duration=5000),
1209
1210 html.Div([
1211 html.Div([
1212 dbc.Button("Save changes", id="save-changes-qc-parameters-button",
1213 style={"line-height": "1.75"}, color="primary"),
1214 dbc.Button("Reset default settings", id="reset-default-qc-parameters-button",
1215 style={"line-height": "1.75"}, color="secondary"),
1216 ], className="d-grid gap-2 col-12 mx-auto"),
1217 ]),
1218 ]),
1219
1220 # MS-DIAL parameters
1221 dbc.Tab(label="MS-DIAL configurations", className="modal-styles", children=[
1222
1223 html.Br(),
1224
1225 # UI feedback on configuration addition/removal
1226 dbc.Alert(id="msdial-config-addition-alert", is_open=False, duration=5000),
1227 dbc.Alert(id="msdial-config-removal-alert", is_open=False, duration=5000),
1228 dbc.Alert(id="msdial-directory-saved-alert", is_open=False, duration=5000),
1229
1230 dbc.Label("MS-DIAL installation", style={"font-weight": "bold"}),
1231 html.Br(),
1232
1233 # Button and field for selecting the data acquisition directory
1234 html.Div([
1235 dbc.Label("MS-DIAL download location"),
1236 dbc.InputGroup([
1237 dbc.Input(placeholder="C:/Users/Me/Downloads/MS-DIAL",
1238 id="msdial-directory"),
1239 dbc.Button("Browse Folders", id="msdial-folder-button",
1240 color="secondary", outline=True),
1241 dbc.Button("Save changes", id="msdial-folder-save-button",
1242 color="primary", outline=True)
1243 ]),
1244 dbc.FormText(
1245 "Browse for (or type) the path of your downloaded MS-DIAL folder."),
1246 ]),
1247
1248 html.Br(),
1249
1250 dbc.Label("Manage configurations", style={"font-weight": "bold"}),
1251 html.Br(),
1252
1253 html.Div([
1254 dbc.Label("Add new MS-DIAL configuration"),
1255 dbc.InputGroup([
1256 dbc.Input(id="add-msdial-configuration-text-field",
1257 placeholder="Name of configuration to add"),
1258 dbc.Button("Add new config", color="primary", outline=True,
1259 id="add-msdial-configuration-button", n_clicks=0),
1260 ]),
1261 dbc.FormText("Give your custom configuration a unique name"),
1262 ]), html.Br(),
1263
1264 # Select configuration
1265 html.Div(children=[
1266 dbc.Label("Select configuration to edit"),
1267 dbc.InputGroup([
1268 dbc.Select(id="msdial-configs-dropdown",
1269 placeholder="No configuration selected"),
1270 dbc.Button("Remove", color="danger", outline=True,
1271 id="remove-config-button", n_clicks=0),
1272 dbc.Popover("You are about to delete this configuration. Are you sure?",
1273 target="remove-config-button", trigger="hover", body=True)
1274 ])
1275 ]), html.Br(),
1276
1277 # Data collection parameters
1278 dbc.Label("Data collection parameters", style={"font-weight": "bold"}),
1279 html.Br(),
1280
1281 html.Div(className="parent-container", children=[
1282 # Retention time begin
1283 html.Div(className="child-container", children=[
1284 dbc.Label("Retention time begin"),
1285 dbc.Input(id="retention-time-begin", placeholder="0"),
1286 ]),
1287 # Retention time end
1288 html.Div(className="child-container", children=[
1289 dbc.Label("Retention time end"),
1290 dbc.Input(id="retention-time-end", placeholder="100"),
1291 html.Br(),
1292 ]),
1293 ]),
1294
1295 html.Div(className="parent-container", children=[
1296 # Mass range begin
1297 html.Div(className="child-container", children=[
1298 dbc.Label("Mass range begin"),
1299 dbc.Input(id="mass-range-begin", placeholder="0"),
1300 ]),
1301 # Mass range end
1302 html.Div(className="child-container", children=[
1303 dbc.Label("Mass range end"),
1304 dbc.Input(id="mass-range-end", placeholder="2000"),
1305 html.Br(),
1306 ]),
1307 ]),
1308
1309 # Centroid parameters
1310 dbc.Label("Centroid parameters", style={"font-weight": "bold"}),
1311 html.Br(),
1312
1313 html.Div(className="parent-container", children=[
1314 # MS1 centroid tolerance
1315 html.Div(className="child-container", children=[
1316 dbc.Label("MS1 centroid tolerance"),
1317 dbc.Input(id="ms1-centroid-tolerance", placeholder="0.008"),
1318 ]),
1319 # MS2 centroid tolerance
1320 html.Div(className="child-container", children=[
1321 dbc.Label("MS2 centroid tolerance"),
1322 dbc.Input(id="ms2-centroid-tolerance", placeholder="0.01"),
1323 html.Br(),
1324 ]),
1325 ]),
1326
1327 # Peak detection parameters
1328 dbc.Label("Peak detection parameters", style={"font-weight": "bold"}),
1329 html.Br(),
1330
1331 dbc.Label("Smoothing method"),
1332 dbc.Select(id="select-smoothing-dropdown", options=[
1333 {"label": "Simple moving average",
1334 "value": "SimpleMovingAverage"},
1335 {"label": "Linear weighted moving average",
1336 "value": "LinearWeightedMovingAverage"},
1337 {"label": "Savitzky-Golay filter",
1338 "value": "SavitzkyGolayFilter"},
1339 {"label": "Binomial filter",
1340 "value": "BinomialFilter"},
1341 ], placeholder="Linear weighted moving average"),
1342 html.Br(),
1343
1344 html.Div(className="parent-container", children=[
1345 # Smoothing level
1346 html.Div(className="child-container", children=[
1347 dbc.Label("Smoothing level"),
1348 dbc.Input(id="smoothing-level", placeholder="3"),
1349 ]),
1350 # Mass slice width
1351 html.Div(className="child-container", children=[
1352 dbc.Label("Mass slice width"),
1353 dbc.Input(id="mass-slice-width", placeholder="0.1"),
1354 html.Br(),
1355 ]),
1356 ]),
1357 html.Br(),
1358
1359 html.Div(className="parent-container", children=[
1360 # Minimum peak width
1361 html.Div(className="child-container", children=[
1362 dbc.Label("Minimum peak width"),
1363 dbc.Input(id="min-peak-width", placeholder="4"),
1364 ]),
1365 # Minimum peak height
1366 html.Div(className="child-container", children=[
1367 dbc.Label("Minimum peak height"),
1368 dbc.Input(id="min-peak-height", placeholder="50000"),
1369 html.Br(),
1370 ]),
1371 ]),
1372 html.Br(),
1373
1374 # Identification parameters
1375 dbc.Label("Identification parameters", style={"font-weight": "bold"}),
1376 html.Br(),
1377
1378 html.Div(className="parent-container", children=[
1379 # Retention time tolerance
1380 html.Div(className="child-container", children=[
1381 dbc.Label("Post-identification retention time tolerance"),
1382 dbc.Input(id="post-id-rt-tolerance", placeholder="0.3"),
1383 ]),
1384 # Accurate mass tolerance
1385 html.Div(className="child-container", children=[
1386 dbc.Label("Post-identification accurate MS1 tolerance"),
1387 dbc.Input(id="post-id-mz-tolerance", placeholder="0.008"),
1388 html.Br(),
1389 ]),
1390 ]),
1391 html.Br(),
1392
1393 html.Div([
1394 dbc.Label("Identification score cutoff"),
1395 dbc.Input(id="post-id-score-cutoff", placeholder="85"),
1396 ]),
1397 html.Br(),
1398
1399 # Alignment parameters
1400 dbc.Label("Alignment parameters", style={"font-weight": "bold"}),
1401 html.Br(),
1402
1403 html.Div(className="parent-container", children=[
1404 # Retention time tolerance
1405 html.Div(className="child-container", children=[
1406 dbc.Label("Alignment retention time tolerance"),
1407 dbc.Input(id="alignment-rt-tolerance", placeholder="0.05"),
1408 ]),
1409 # Accurate mass tolerance
1410 html.Div(className="child-container", children=[
1411 dbc.Label("Alignment MS1 tolerance"),
1412 dbc.Input(id="alignment-mz-tolerance", placeholder="0.008"),
1413 html.Br(),
1414 ]),
1415 ]),
1416 html.Br(),
1417
1418 html.Div(className="parent-container", children=[
1419 # Retention time factor
1420 html.Div(className="child-container", children=[
1421 dbc.Label("Alignment retention time factor"),
1422 dbc.Input(id="alignment-rt-factor", placeholder="0.5"),
1423 ]),
1424 # Accurate mass factor
1425 html.Div(className="child-container", children=[
1426 dbc.Label("Alignment MS1 factor"),
1427 dbc.Input(id="alignment-mz-factor", placeholder="0.5"),
1428 html.Br(),
1429 ]),
1430 ]),
1431 html.Br(),
1432
1433 html.Div(className="parent-container", children=[
1434 # Peak count filter
1435 html.Div(className="child-container", children=[
1436 dbc.Label("Peak count filter"),
1437 dbc.Input(id="peak-count-filter", placeholder="0"),
1438 ]),
1439 # QC at least filter
1440 html.Div(className="child-container", children=[
1441 dbc.Label("QC at least filter"),
1442 dbc.Select(id="qc-at-least-filter-dropdown", options=[
1443 {"label": "True", "value": "True"},
1444 {"label": "False", "value": "False"},
1445 ], placeholder="True"),
1446 html.Br(),
1447 ]),
1448 ]),
1449
1450 html.Br(), html.Br(), html.Br(), html.Br(), html.Br(),
1451 html.Br(), html.Br(), html.Br(), html.Br(), html.Br(),
1452
1453 html.Div([
1454 # UI feedback on saving changes to MS-DIAL parameters
1455 dbc.Alert(id="msdial-parameters-success-alert",
1456 color="success", is_open=False, duration=5000),
1457 dbc.Alert(id="msdial-parameters-reset-alert",
1458 color="primary", is_open=False, duration=5000),
1459 dbc.Alert(id="msdial-parameters-error-alert",
1460 color="danger", is_open=False, duration=5000),
1461 ]),
1462
1463 html.Div([
1464 html.Div([
1465 dbc.Button("Save changes", id="save-changes-msdial-parameters-button",
1466 style={"line-height": "1.75"}, color="primary"),
1467 dbc.Button("Reset default settings", id="reset-default-msdial-parameters-button",
1468 style={"line-height": "1.75"}, color="secondary"),
1469 ], className="d-grid gap-2 col-12 mx-auto"),
1470 ]),
1471 ]),
1472 ])
1473 ])
1474 ]),
1475 ]),
1476 ]),
1477 ]),
1478
1479 # Dummy input object for callbacks on page load
1480 dcc.Store(id="on-page-load"),
1481 dcc.Store(id="google-drive-authenticated"),
1482
1483 # Storage of all DataFrames necessary for QC plot generation
1484 dcc.Store(id="istd-rt-pos"),
1485 dcc.Store(id="istd-rt-neg"),
1486 dcc.Store(id="istd-intensity-pos"),
1487 dcc.Store(id="istd-intensity-neg"),
1488 dcc.Store(id="istd-mz-pos"),
1489 dcc.Store(id="istd-mz-neg"),
1490 dcc.Store(id="istd-delta-rt-pos"),
1491 dcc.Store(id="istd-delta-rt-neg"),
1492 dcc.Store(id="istd-in-run-delta-rt-pos"),
1493 dcc.Store(id="istd-in-run-delta-rt-neg"),
1494 dcc.Store(id="istd-delta-mz-pos"),
1495 dcc.Store(id="istd-delta-mz-neg"),
1496 dcc.Store(id="qc-warnings-pos"),
1497 dcc.Store(id="qc-warnings-neg"),
1498 dcc.Store(id="qc-fails-pos"),
1499 dcc.Store(id="qc-fails-neg"),
1500 dcc.Store(id="sequence"),
1501 dcc.Store(id="metadata"),
1502 dcc.Store(id="bio-rt-pos"),
1503 dcc.Store(id="bio-rt-neg"),
1504 dcc.Store(id="bio-intensity-pos"),
1505 dcc.Store(id="bio-intensity-neg"),
1506 dcc.Store(id="bio-mz-pos"),
1507 dcc.Store(id="bio-mz-neg"),
1508 dcc.Store(id="study-resources"),
1509 dcc.Store(id="samples"),
1510 dcc.Store(id="pos-internal-standards"),
1511 dcc.Store(id="neg-internal-standards"),
1512 dcc.Store(id="instruments"),
1513 dcc.Store(id="load-finished"),
1514 dcc.Store(id="close-load-modal"),
1515
1516 # Data for starting a new AutoQC job
1517 dcc.Store(id="new-sequence"),
1518 dcc.Store(id="new-metadata"),
1519
1520 # Dummy inputs for UI update callbacks
1521 dcc.Store(id="chromatography-added"),
1522 dcc.Store(id="chromatography-removed"),
1523 dcc.Store(id="chromatography-msdial-config-added"),
1524 dcc.Store(id="istd-msp-added"),
1525 dcc.Store(id="bio-standard-added"),
1526 dcc.Store(id="bio-standard-removed"),
1527 dcc.Store(id="bio-msp-added"),
1528 dcc.Store(id="bio-standard-msdial-config-added"),
1529 dcc.Store(id="qc-config-added"),
1530 dcc.Store(id="qc-config-removed"),
1531 dcc.Store(id="qc-parameters-saved"),
1532 dcc.Store(id="qc-parameters-reset"),
1533 dcc.Store(id="msdial-config-added"),
1534 dcc.Store(id="msdial-config-removed"),
1535 dcc.Store(id="msdial-parameters-saved"),
1536 dcc.Store(id="msdial-parameters-reset"),
1537 dcc.Store(id="msdial-directory-saved"),
1538 dcc.Store(id="google-drive-sync-finished"),
1539 dcc.Store(id="close-sync-modal"),
1540 dcc.Store(id="database-md5"),
1541 dcc.Store(id="selected-data-folder"),
1542 dcc.Store(id="selected-msdial-folder"),
1543 dcc.Store(id="google-drive-user-added"),
1544 dcc.Store(id="google-drive-user-deleted"),
1545 dcc.Store(id="email-added"),
1546 dcc.Store(id="email-deleted"),
1547 dcc.Store(id="gdrive-credentials-saved"),
1548 dcc.Store(id="slack-bot-token-saved"),
1549 dcc.Store(id="slack-channel-saved"),
1550 dcc.Store(id="google-drive-sync-update"),
1551 dcc.Store(id="job-marked-completed"),
1552 dcc.Store(id="job-restarted"),
1553 dcc.Store(id="job-deleted"),
1554 dcc.Store(id="job-action-failed"),
1555
1556 # Dummy inputs for Google Drive authentication
1557 dcc.Store(id="google-drive-download-database"),
1558 dcc.Store(id="workspace-has-been-setup-1"),
1559 dcc.Store(id="workspace-has-been-setup-2"),
1560 dcc.Store(id="google-drive-authenticated-1"),
1561 dcc.Store(id="gdrive-folder-id-1"),
1562 dcc.Store(id="gdrive-database-file-id-1"),
1563 dcc.Store(id="gdrive-methods-zip-id-1"),
1564 dcc.Store(id="google-drive-authenticated-2"),
1565 dcc.Store(id="gdrive-folder-id-2"),
1566 dcc.Store(id="gdrive-database-file-id-2"),
1567 dcc.Store(id="gdrive-methods-zip-id-2"),
1568 dcc.Store(id="google-drive-authenticated-3"),
1569 dcc.Store(id="gdrive-folder-id-3"),
1570 dcc.Store(id="gdrive-database-file-id-3"),
1571 dcc.Store(id="gdrive-methods-zip-id-3"),
1572 ])
1573 ])
1574
1575# Serve app layout
1576app.layout = serve_layout
1577
1578"""
1579Dash callbacks
1580"""
1581
1582
1583@app.callback(Output("google-drive-sync-update", "data"),
1584 Input("on-page-load", "data"))
1585def sync_with_google_drive(on_page_load):
1586
1587 """
1588 For users signed in to MS-AutoQC from an external device, this will download the database on page load
1589 """
1590
1591 # Download database on page load (or refresh) if sync is enabled
1592 if db.sync_is_enabled():
1593
1594 # Sync methods directory
1595 db.download_methods()
1596
1597 # Download instrument database
1598 instrument_id = db.get_instruments_list()[0]
1599 if instrument_id != db.get_device_identity():
1600 return db.download_database(instrument_id)
1601 else:
1602 return None
1603
1604 # If Google Drive sync is not enabled, perform no action
1605 else:
1606 raise PreventUpdate
1607
1608
1609@app.callback(Output("google-drive-download-database", "data"),
1610 Input("tabs", "value"), prevent_initial_call=True)
1611def sync_with_google_drive(instrument_id):
1612
1613 """
1614 For users signed in to MS-AutoQC from an external device, this will download the selected instrument database
1615 """
1616
1617 # Download database on page load (or refresh) if sync is enabled
1618 if db.sync_is_enabled():
1619 if instrument_id != db.get_device_identity():
1620 return db.download_database(instrument_id)
1621 else:
1622 return None
1623
1624 # If Google Drive sync is not enabled, perform no action
1625 else:
1626 raise PreventUpdate
1627
1628
1629@app.callback(Output("google-drive-authenticated", "data"),
1630 Input("on-page-load", "data"))
1631def authenticate_with_google_drive(on_page_load):
1632
1633 """
1634 Authenticates with Google Drive if the credentials file is found
1635 """
1636
1637 # Initialize Google Drive if sync is enabled
1638 if db.sync_is_enabled():
1639 return db.initialize_google_drive()
1640 else:
1641 raise PreventUpdate
1642
1643
1644@app.callback(Output("google-drive-authenticated-1", "data"),
1645 Output("google-drive-authenticated-2", "data"),
1646 Output("google-drive-authenticated-3", "data"),
1647 Input("setup-google-drive-button-1", "n_clicks"),
1648 Input("setup-google-drive-button-2", "n_clicks"),
1649 Input("google-drive-sync-button", "n_clicks"),
1650 State("gdrive-client-id-1", "value"),
1651 State("gdrive-client-id-2", "value"),
1652 State("gdrive-client-id", "value"),
1653 State("gdrive-client-secret-1", "value"),
1654 State("gdrive-client-secret-2", "value"),
1655 State("gdrive-client-secret", "value"), prevent_initial_call=True)
1656def launch_google_drive_authentication(setup_auth_button_clicks, sign_in_auth_button_clicks, settings_button_clicks,
1657 client_id_1, client_id_2, client_id_3, client_secret_1, client_secret_2, client_secret_3):
1658
1659 """
1660 Launches Google Drive authentication window from first-time setup
1661 """
1662
1663 # Get the correct authentication button
1664 button_id = ctx.triggered_id
1665
1666 # If user clicks a sign-in button, launch Google authentication page
1667 if button_id is not None:
1668
1669 # Create a settings.yaml file to access Drive API
1670 if button_id == "setup-google-drive-button-1":
1671 db.generate_client_settings_yaml(client_id_1, client_secret_1)
1672 elif button_id == "setup-google-drive-button-2":
1673 db.generate_client_settings_yaml(client_id_2, client_secret_2)
1674 elif button_id == "google-drive-sync-button":
1675 # Regenerate Drive settings file
1676 if not os.path.exists(drive_settings_file):
1677 db.generate_client_settings_yaml(client_id_3, client_secret_3)
1678
1679 # Authenticate, then save the credentials to a file
1680 db.launch_google_drive_authentication()
1681
1682 if button_id == "setup-google-drive-button-1":
1683 return True, None, None
1684 elif button_id == "setup-google-drive-button-2":
1685 return None, True, None
1686 elif button_id == "google-drive-sync-button":
1687 return None, None, True
1688 else:
1689 raise PreventUpdate
1690
1691
1692@app.callback(Output("setup-google-drive-button-1", "children"),
1693 Output("setup-google-drive-button-1", "color"),
1694 Output("setup-google-drive-button-1", "outline"),
1695 Output("google-drive-button-1-popover", "children"),
1696 Output("google-drive-button-1-popover", "is_open"),
1697 Output("gdrive-folder-id-1", "data"),
1698 Output("gdrive-methods-zip-id-1", "data"),
1699 Input("google-drive-authenticated-1", "data"), prevent_initial_call=True)
1700def check_first_time_google_drive_authentication(google_drive_is_authenticated):
1701
1702 """
1703 UI feedback for Google Drive authentication in Welcome > Setup New Instrument page
1704 """
1705
1706 if google_drive_is_authenticated:
1707
1708 drive = db.get_drive_instance()
1709
1710 # Initial values
1711 gdrive_folder_id = None
1712 gdrive_methods_zip_id = None
1713 popover_message = [dbc.PopoverHeader("No existing workspace found."),
1714 dbc.PopoverBody("A new MS-AutoQC workspace will be created.")]
1715
1716 # Check for workspace in Google Drive
1717 for file in drive.ListFile({"q": "'root' in parents and trashed=false"}).GetList():
1718 if file["title"] == "MS-AutoQC":
1719 gdrive_folder_id = file["id"]
1720 break
1721
1722 # If Google Drive folder is found, look for settings database next
1723 if gdrive_folder_id is not None:
1724 for file in drive.ListFile({"q": "'" + gdrive_folder_id + "' in parents and trashed=false"}).GetList():
1725 if file["title"] == "methods.zip":
1726 os.chdir(data_directory) # Switch to data directory
1727 file.GetContentFile(file["title"]) # Download methods ZIP archive
1728 gdrive_methods_zip_id = file["id"] # Get methods ZIP file ID
1729 os.chdir(root_directory) # Switch back to root directory
1730 db.unzip_methods() # Unzip methods ZIP archive
1731
1732 if gdrive_methods_zip_id is not None:
1733 popover_message = [dbc.PopoverHeader("Workspace found!"),
1734 dbc.PopoverBody("This instrument will be added to the existing MS-AutoQC workspace.")]
1735
1736 return "You're signed in!", "success", False, popover_message, True, gdrive_folder_id, gdrive_methods_zip_id
1737
1738 else:
1739 return "Sign in to Google Drive", "primary", True, "", False, None, None
1740
1741
1742@app.callback(Output("first-time-instrument-vendor", "label"),
1743 Output("thermo-fisher-item", "n_clicks"),
1744 Output("agilent-item", "n_clicks"),
1745 Output("bruker-item", "n_clicks"),
1746 Output("sciex-item", "n_clicks"),
1747 Output("waters-item", "n_clicks"),
1748 Input("thermo-fisher-item", "n_clicks"),
1749 Input("agilent-item", "n_clicks"),
1750 Input("bruker-item", "n_clicks"),
1751 Input("sciex-item", "n_clicks"),
1752 Input("waters-item", "n_clicks"), prevent_initial_call=True)
1753def vendor_dropdown_handling(thermo_fisher_click, agilent_click, bruker_click, sciex_click, waters_click):
1754
1755 """
1756 Why didn't Dash Bootstrap Components implement this themselves?
1757 The world may never know...
1758 """
1759
1760 thermo_selected = "Thermo Fisher", 0, 0, 0, 0, 0
1761 agilent_selected = "Agilent", 0, 0, 0, 0, 0,
1762 bruker_selected = "Bruker", 0, 0, 0, 0, 0
1763 sciex_selected = "Sciex", 0, 0, 0, 0, 0
1764 waters_selected = "Waters", 0, 0, 0, 0, 0
1765
1766 inputs = [thermo_fisher_click, agilent_click, bruker_click, sciex_click, waters_click]
1767 outputs = [thermo_selected, agilent_selected, bruker_selected, sciex_selected, waters_selected]
1768
1769 for index, input in enumerate(inputs):
1770 if input is not None:
1771 if input > 0:
1772 return outputs[index]
1773
1774
1775@app.callback(Output("first-time-complete-setup-button", "disabled"),
1776 Output("first-time-instrument-id", "valid"),
1777 Input("first-time-instrument-id", "value"),
1778 Input("first-time-instrument-vendor", "label"), prevent_initial_call=True)
1779def enable_complete_setup_button(instrument_name, instrument_vendor):
1780
1781 """
1782 Enables "Complete setup" button upon form completion in Welcome > Setup New Instrument page
1783 """
1784
1785 valid = False, True
1786 invalid = True, False
1787
1788 if instrument_name is not None:
1789 if len(instrument_name) > 3 and instrument_vendor != "Choose Vendor":
1790 return valid
1791 else:
1792 return invalid
1793 else:
1794 return invalid
1795
1796
1797@app.callback(Output("first-time-complete-setup-button", "children"),
1798 Input("first-time-complete-setup-button", "n_clicks"), prevent_initial_call=True)
1799def ui_feedback_for_complete_setup_button(button_click):
1800
1801 """
1802 Returns loading feedback on complete setup button
1803 """
1804
1805 return [dbc.Spinner(size="sm"), " Finishing up, please wait..."]
1806
1807
1808@app.callback(Output("workspace-has-been-setup-1", "data"),
1809 Input("first-time-complete-setup-button", "children"),
1810 State("first-time-instrument-id", "value"),
1811 State("first-time-instrument-vendor", "label"),
1812 State("google-drive-authenticated-1", "data"),
1813 State("gdrive-folder-id-1", "data"),
1814 State("gdrive-methods-zip-id-1", "data"), prevent_initial_call=True)
1815def complete_first_time_setup(button_click, instrument_id, instrument_vendor, google_drive_authenticated,
1816 gdrive_folder_id, methods_zip_file_id):
1817
1818 """
1819 Upon "Complete setup" button click, this callback completes the following:
1820 1. If databases DO exist in Google Drive, downloads databases
1821 2. If databases DO NOT exist in Google Drive, initializes new SQLite database
1822 3. Adds instrument to "instruments" table
1823 4. Uploads database to Google Drive folder
1824 5. Dismisses setup window
1825 """
1826
1827 if button_click:
1828
1829 # Initialize a new database if one does not exist
1830 if not db.is_valid(instrument_id=instrument_id):
1831 if methods_zip_file_id is not None:
1832 db.create_databases(instrument_id=instrument_id, new_instrument=True)
1833 else:
1834 db.create_databases(instrument_id=instrument_id)
1835
1836 # Handle Google Drive sync
1837 if google_drive_authenticated:
1838
1839 drive = db.get_drive_instance()
1840
1841 # Create necessary folders if not found
1842 if gdrive_folder_id is None:
1843
1844 # Create MS-AutoQC folder
1845 folder_metadata = {
1846 "title": "MS-AutoQC",
1847 "mimeType": "application/vnd.google-apps.folder"
1848 }
1849 folder = drive.CreateFile(folder_metadata)
1850 folder.Upload()
1851
1852 # Get Google Drive ID of folder
1853 gdrive_folder_id = folder["id"]
1854
1855 # Add instrument to database
1856 db.insert_new_instrument(instrument_id, instrument_vendor)
1857
1858 # Download other instrument databases
1859 for file in drive.ListFile({"q": "'" + gdrive_folder_id + "' in parents and trashed=false"}).GetList():
1860 if file["title"] != "methods.zip":
1861 os.chdir(data_directory) # Switch to data directory
1862 file.GetContentFile(file["title"]) # Download database ZIP archive
1863 os.chdir(root_directory) # Switch back to root directory
1864 db.unzip_database(filename=file["title"]) # Unzip database ZIP archive
1865
1866 # Sync newly created instrument database to Google Drive folder
1867 db.zip_database(instrument_id=instrument_id)
1868 filename = instrument_id.replace(" ", "_") + ".zip"
1869
1870 metadata = {
1871 "title": filename,
1872 "parents": [{"id": gdrive_folder_id}],
1873 }
1874 file = drive.CreateFile(metadata=metadata)
1875 file.SetContentFile(db.get_database_file(instrument_id, zip=True))
1876 file.Upload()
1877
1878 # Grab Google Drive file ID
1879 main_db_file_id = file["id"]
1880
1881 # Create local methods directory
1882 if not os.path.exists(methods_directory):
1883 os.makedirs(methods_directory)
1884
1885 # Upload/update local methods directory to Google Drive
1886 methods_zip_file = db.zip_methods()
1887
1888 if methods_zip_file_id is not None:
1889 file = drive.CreateFile({"id": methods_zip_file_id, "title": "methods.zip"})
1890 else:
1891 metadata = {
1892 "title": "methods.zip",
1893 "parents": [{"id": gdrive_folder_id}],
1894 }
1895 file = drive.CreateFile(metadata=metadata)
1896
1897 file.SetContentFile(methods_zip_file)
1898 file.Upload()
1899
1900 # Grab Google Drive file ID
1901 methods_zip_file_id = file["id"]
1902
1903 # Save user credentials
1904 db.save_google_drive_credentials()
1905
1906 # Save Google Drive ID's for each file
1907 db.insert_google_drive_ids(instrument_id, gdrive_folder_id, main_db_file_id, methods_zip_file_id)
1908
1909 # Sync database with Drive again to save Google Drive ID's
1910 db.upload_database(instrument_id, sync_settings=True)
1911
1912 else:
1913 # Add instrument to database
1914 db.insert_new_instrument(instrument_id, instrument_vendor)
1915
1916 # Dismiss setup window by returning True for workspace_has_been_setup boolean
1917 return db.is_valid()
1918
1919 else:
1920 raise PreventUpdate
1921
1922
1923@app.callback(Output("setup-google-drive-button-2", "children"),
1924 Output("setup-google-drive-button-2", "color"),
1925 Output("setup-google-drive-button-2", "outline"),
1926 Output("google-drive-button-2-popover", "children"),
1927 Output("google-drive-button-2-popover", "is_open"),
1928 Output("gdrive-folder-id-2", "data"),
1929 Output("device-identity-selection", "options"),
1930 Input("google-drive-authenticated-2", "data"), prevent_initial_call=True)
1931def check_workspace_login_google_drive_authentication(google_drive_is_authenticated):
1932
1933 """
1934 UI feedback for Google Drive authentication in Welcome > Sign In To Workspace page
1935 """
1936
1937 if google_drive_is_authenticated:
1938 drive = db.get_drive_instance()
1939
1940 # Initial values
1941 gdrive_folder_id = None
1942
1943 # Failed popover message
1944 button_text = "Sign in to Google Drive"
1945 button_color = "danger"
1946 popover_message = [dbc.PopoverHeader("No workspace found"),
1947 dbc.PopoverBody("Double-check that your Google account has access in " +
1948 "Settings > General, or sign in from a different account.")]
1949
1950 # Check for MS-AutoQC folder in Google Drive root directory
1951 for file in drive.ListFile({"q": "'root' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false"}).GetList():
1952 if file["title"] == "MS-AutoQC":
1953 gdrive_folder_id = file["id"]
1954 break
1955
1956 # If it's not there, check "Shared With Me" and copy it over to root directory
1957 if gdrive_folder_id is None:
1958 for file in drive.ListFile({"q": "sharedWithMe and mimeType='application/vnd.google-apps.folder' and trashed=false"}).GetList():
1959 if file["title"] == "MS-AutoQC":
1960 gdrive_folder_id = file["id"]
1961 break
1962
1963 # If Google Drive folder is found, download methods directory and all databases next
1964 if gdrive_folder_id is not None:
1965 for file in drive.ListFile({"q": "'" + gdrive_folder_id + "' in parents and trashed=false"}).GetList():
1966
1967 # Download and unzip instrument databases
1968 if file["title"] != "methods.zip":
1969 os.chdir(data_directory) # Switch to data directory
1970 file.GetContentFile(file["title"]) # Download database ZIP archive
1971 os.chdir(root_directory) # Switch back to root directory
1972 db.unzip_database(filename=file["title"]) # Unzip database ZIP archive
1973
1974 # Download and unzip methods directory
1975 else:
1976 os.chdir(data_directory) # Switch to data directory
1977 file.GetContentFile(file["title"]) # Download methods ZIP archive
1978 os.chdir(root_directory) # Switch back to root directory
1979 db.unzip_methods() # Unzip methods ZIP archive
1980
1981 # Popover alert
1982 button_text = "Signed in to Google Drive"
1983 button_color = "success"
1984 popover_message = [dbc.PopoverHeader("Workspace found!"),
1985 dbc.PopoverBody("Click the button below to sign in.")]
1986
1987 # Fill instrument identity dropdown
1988 instruments = db.get_instruments_list()
1989 instrument_options = []
1990 for instrument in instruments:
1991 instrument_options.append({"label": instrument, "value": instrument})
1992
1993 return button_text, button_color, False, popover_message, True, gdrive_folder_id, instrument_options
1994
1995 else:
1996 return "Sign in to Google Drive", "primary", True, "", False, None, []
1997
1998
1999@app.callback(Output("device-identity-selection", "disabled"),
2000 Input("device-identity-checkbox", "value"), prevent_initial_call=True)
2001def enable_instrument_id_selection(is_instrument_computer):
2002
2003 """
2004 In Welcome > Sign In To Workspace page, enables instrument dropdown selection if user is signing in to instrument
2005 """
2006
2007 if is_instrument_computer:
2008 return False
2009 else:
2010 return True
2011
2012
2013@app.callback(Output("first-time-sign-in-button", "disabled"),
2014 Input("setup-google-drive-button-2", "children"),
2015 Input("device-identity-checkbox", "value"),
2016 Input("device-identity-selection", "value"), prevent_initial_call=True)
2017def enable_workspace_login_button(button_text, is_instrument_computer, instrument_id):
2018
2019 """
2020 Enables "Sign in to workspace" button upon form completion in Welcome > Sign In To Workspace page
2021 """
2022
2023 if button_text is not None:
2024 if button_text == "Signed in to Google Drive":
2025 if is_instrument_computer:
2026 if instrument_id is not None:
2027 return False
2028 else:
2029 return True
2030 else:
2031 return False
2032 else:
2033 return True
2034 else:
2035 return True
2036
2037
2038@app.callback(Output("first-time-sign-in-button", "children"),
2039 Input("first-time-sign-in-button", "n_clicks"), prevent_initial_call=True)
2040def ui_feedback_for_workspace_login_button(button_click):
2041
2042 """
2043 UI feedback for workspace sign in button in Setup > Login To Workspace
2044 """
2045
2046 return [dbc.Spinner(size="sm"), " Signing in, this may take a moment..."]
2047
2048
2049@app.callback(Output("workspace-has-been-setup-2", "data"),
2050 Input("first-time-sign-in-button", "children"),
2051 State("device-identity-checkbox", "value"),
2052 State("device-identity-selection", "value"), prevent_initial_call=True)
2053def ui_feedback_for_login_button(button_click, is_instrument_computer, instrument_id):
2054
2055 """
2056 Dismisses setup window and signs in to MS-AutoQC workspace
2057 """
2058
2059 if button_click:
2060
2061 # Set device identity and proceed
2062 db.set_device_identity(is_instrument_computer, instrument_id)
2063
2064 # Save Google Drive credentials
2065 db.save_google_drive_credentials()
2066 return True
2067
2068 else:
2069 raise PreventUpdate
2070
2071
2072@app.callback(Output("workspace-setup-modal", "is_open"),
2073 Output("on-page-load", "data"),
2074 Input("workspace-has-been-setup-1", "data"),
2075 Input("workspace-has-been-setup-2", "data"))
2076def dismiss_setup_window(workspace_has_been_setup_1, workspace_has_been_setup_2):
2077
2078 """
2079 Checks for a valid database on every start and dismisses setup window if found
2080 """
2081
2082 # Check if setup is complete
2083 is_valid = db.is_valid()
2084 return not is_valid, is_valid
2085
2086
2087@app.callback(Output("google-drive-sync-button", "color"),
2088 Output("google-drive-sync-button", "children"),
2089 Output("google-drive-sync-form-text", "children"),
2090 Output("google-drive-sign-in-from-settings-alert", "is_open"),
2091 Output("gdrive-client-id", "placeholder"),
2092 Output("gdrive-client-secret", "placeholder"),
2093 Input("google-drive-authenticated-3", "data"),
2094 Input("google-drive-authenticated", "data"),
2095 Input("settings-modal", "is_open"),
2096 State("google-drive-sync-form-text", "children"),
2097 State("tabs", "value"), prevent_initial_call=True)
2098def update_google_drive_sync_status_in_settings(google_drive_authenticated, google_drive_authenticated_on_start,
2099 settings_is_open, form_text, instrument_id):
2100
2101 """
2102 Updates Google Drive sync status in user settings on user authentication
2103 """
2104
2105 trigger = ctx.triggered_id
2106
2107 if not settings_is_open:
2108 raise PreventUpdate
2109
2110 # Authenticated on app startup
2111 if (trigger == "google-drive-authenticated" or trigger == "settings-modal") and google_drive_authenticated_on_start is not None:
2112 form_text = "Cloud sync is enabled! You can now sign in to this MS-AutoQC workspace from any device."
2113 return "success", "Signed in to Google Drive", form_text, False, "Client ID (saved)", "Client secret (saved)"
2114
2115 # Authenticated from "Sign in to Google Drive" button in Settings > General
2116 elif trigger == "google-drive-authenticated-3" and google_drive_authenticated_on_start is None:
2117
2118 drive = db.get_drive_instance()
2119 gdrive_folder_id = None
2120 main_db_file_id = None
2121
2122 # Check for MS-AutoQC folder in Google Drive root directory
2123 for file in drive.ListFile({"q": "'root' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false"}).GetList():
2124 if file["title"] == "MS-AutoQC":
2125 gdrive_folder_id = file["id"]
2126 break
2127
2128 # If it's not there, check "Shared With Me" and copy it over to root directory
2129 if gdrive_folder_id is None:
2130 for file in drive.ListFile({"q": "sharedWithMe and mimeType='application/vnd.google-apps.folder' and trashed=false"}).GetList():
2131 if file["title"] == "MS-AutoQC":
2132 gdrive_folder_id = file["id"]
2133 break
2134
2135 # If Google Drive folder is found, alert user that they need to sign in with a different Google account
2136 if gdrive_folder_id is not None:
2137 os.remove(credentials_file)
2138 return "danger", "Sign in to Google Drive", form_text, True, "Client ID", "Client secret"
2139
2140 # If no workspace found, all good to create one
2141 else:
2142 # Create MS-AutoQC folder
2143 folder_metadata = {
2144 "title": "MS-AutoQC",
2145 "mimeType": "application/vnd.google-apps.folder"
2146 }
2147 folder = drive.CreateFile(folder_metadata)
2148 folder.Upload()
2149
2150 # Get Google Drive ID of folder
2151 for file in drive.ListFile({"q": "'root' in parents and trashed=false"}).GetList():
2152 if file["title"] == "MS-AutoQC":
2153 gdrive_folder_id = file["id"]
2154 break
2155
2156 # Upload database to Google Drive folder
2157 db.zip_database(instrument_id=instrument_id)
2158
2159 metadata = {
2160 "title": instrument_id.replace(" ", "_") + ".zip",
2161 "parents": [{"id": gdrive_folder_id}],
2162 }
2163
2164 file = drive.CreateFile(metadata=metadata)
2165 file.SetContentFile(db.get_database_file(instrument_id, zip=True))
2166 file.Upload()
2167 main_db_file_id = file["id"]
2168
2169 # Create local methods directory
2170 if not os.path.exists(methods_directory):
2171 os.makedirs(methods_directory)
2172
2173 # Upload local methods directory to Google Drive
2174 methods_zip_file = db.zip_methods()
2175
2176 metadata = {
2177 "title": "methods.zip",
2178 "parents": [{"id": gdrive_folder_id}],
2179 }
2180
2181 file = drive.CreateFile(metadata=metadata)
2182 file.SetContentFile(methods_zip_file)
2183 file.Upload()
2184 methods_zip_file_id = file["id"]
2185
2186 # Put Google Drive ID's into database
2187 db.insert_google_drive_ids(instrument_id, gdrive_folder_id, main_db_file_id, methods_zip_file_id)
2188
2189 # Sync database
2190 db.upload_database(instrument_id, sync_settings=True)
2191
2192 # Save user credentials
2193 db.save_google_drive_credentials()
2194
2195 form_text = "Cloud sync is enabled! You can now sign in to this MS-AutoQC workspace from any device."
2196 return "success", "Signed in to Google Drive", form_text, False, "Client ID (saved)", "Client secret (saved)"
2197
2198 else:
2199 raise PreventUpdate
2200
2201
2202@app.callback(Output("gdrive-credentials-saved", "data"),
2203 Input("set-gdrive-credentials-button", "n_clicks"),
2204 State("gdrive-client-id", "value"),
2205 State("gdrive-client-secret", "value"), prevent_initial_call=True)
2206def regenerate_settings_yaml_file(button_click, client_id, client_secret):
2207
2208 """
2209 Regenerates settings.yaml file with new credentials
2210 """
2211
2212 # Ensure user has entered client ID and client secret
2213 if client_id is not None and client_secret is not None:
2214
2215 # Delete existing settings.yaml file (if it exists)
2216 if os.path.exists(drive_settings_file):
2217 os.remove(drive_settings_file)
2218
2219 # Regenerate file
2220 db.generate_client_settings_yaml(client_id, client_secret)
2221 return "Success"
2222
2223 else:
2224 return "Error"
2225
2226
2227@app.callback(Output("gdrive-credentials-saved-alert", "is_open"),
2228 Output("gdrive-credentials-saved-alert", "children"),
2229 Output("gdrive-credentials-saved-alert", "color"),
2230 Input("gdrive-credentials-saved", "data"), prevent_initial_call=True)
2231def ui_alert_on_gdrive_credential_save(credential_save_result):
2232
2233 """
2234 Displays UI alert on Google API credential save
2235 """
2236
2237 if credential_save_result is not None:
2238 if credential_save_result == "Success":
2239 return True, "Your Google API credentials were successfully saved.", "success"
2240 elif credential_save_result == "Error":
2241 return True, "Error: Please enter both the client ID and client secret first.", "danger"
2242 else:
2243 raise PreventUpdate
2244
2245
2246@app.callback(Output("tabs", "children"),
2247 Output("tabs", "value"),
2248 Input("instruments", "data"),
2249 Input("workspace-setup-modal", "is_open"),
2250 Input("google-drive-sync-update", "data"))
2251def get_instrument_tabs(instruments, check_workspace_setup, sync_update):
2252
2253 """
2254 Retrieves all instruments on a user installation of MS-AutoQC
2255 """
2256
2257 if db.is_valid():
2258
2259 # Get list of instruments from database
2260 instrument_list = db.get_instruments_list()
2261
2262 # Create tabs for each instrument
2263 instrument_tabs = []
2264 for instrument in instrument_list:
2265 instrument_tabs.append(dcc.Tab(label=instrument, value=instrument))
2266
2267 return instrument_tabs, instrument_list[0]
2268
2269 else:
2270 raise PreventUpdate
2271
2272
2273@app.callback(Output("instrument-run-table", "active_cell"),
2274 Output("instrument-run-table", "selected_cells"),
2275 Input("tabs", "value"),
2276 Input("job-deleted", "data"), prevent_initial_call=True)
2277def reset_instrument_table(instrument, job_deleted):
2278
2279 """
2280 Removes selected cell highlight upon tab switch to different instrument
2281 (A case study in insane side missions during frontend development)
2282 """
2283
2284 return None, []
2285
2286
2287@app.callback(Output("instrument-run-table", "data"),
2288 Output("table-container", "style"),
2289 Output("plot-container", "style"),
2290 Input("tabs", "value"),
2291 Input("refresh-interval", "n_intervals"),
2292 State("study-resources", "data"),
2293 Input("google-drive-sync-update", "data"),
2294 Input("start-run-monitor-modal", "is_open"),
2295 Input("job-marked-completed", "data"),
2296 Input("job-deleted", "data"))
2297def populate_instrument_runs_table(instrument_id, refresh, resources, sync_update, new_job_started, job_marked_completed, job_deleted):
2298
2299 """
2300 Dash callback for populating tables with list of past/active instrument runs
2301 """
2302
2303 trigger = ctx.triggered_id
2304
2305 # Ensure that refresh does not trigger data parsing if no new samples processed
2306 if trigger == "refresh-interval":
2307 resources = json.loads(resources)
2308 run_id = resources["run_id"]
2309 status = resources["status"]
2310
2311 if db.get_device_identity() != instrument_id:
2312 if db.sync_is_enabled() and status != "Complete":
2313 db.download_qc_results(instrument_id, run_id)
2314
2315 completed_count_in_cache = resources["samples_completed"]
2316 actual_completed_count, total = db.get_completed_samples_count(instrument_id, run_id, status)
2317
2318 if completed_count_in_cache == actual_completed_count:
2319 raise PreventUpdate
2320
2321 if instrument_id != "tab-1":
2322 # Get instrument runs from database
2323 df_instrument_runs = db.get_instrument_runs(instrument_id)
2324
2325 if len(df_instrument_runs) == 0:
2326 empty_table = [{"Run ID": "N/A", "Chromatography": "N/A", "Status": "N/A"}]
2327 return empty_table, {"display": "block"}, {"display": "none"}
2328
2329 # DataFrame refactoring
2330 df_instrument_runs = df_instrument_runs[["run_id", "chromatography", "status"]]
2331 df_instrument_runs = df_instrument_runs.rename(
2332 columns={"run_id": "Run ID",
2333 "chromatography": "Chromatography",
2334 "status": "Status"})
2335 df_instrument_runs = df_instrument_runs[::-1]
2336
2337 # Convert DataFrame into a dictionary
2338 instrument_runs = df_instrument_runs.to_dict("records")
2339 return instrument_runs, {"display": "block"}, {"display": "block"}
2340
2341 else:
2342 raise PreventUpdate
2343
2344
2345@app.callback(Output("loading-modal", "is_open"),
2346 Output("loading-modal-title", "children"),
2347 Output("loading-modal-body", "children"),
2348 Input("instrument-run-table", "active_cell"),
2349 State("instrument-run-table", "data"),
2350 Input("close-load-modal", "data"), prevent_initial_call=True, suppress_callback_exceptions=True)
2351def open_loading_modal(active_cell, table_data, load_finished):
2352
2353 """
2354 Shows loading modal on selection of an instrument run
2355 """
2356
2357 trigger = ctx.triggered_id
2358
2359 if active_cell:
2360 run_id = table_data[active_cell["row"]]["Run ID"]
2361
2362 title = html.Div([
2363 html.Div(children=[dbc.Spinner(color="primary"), " Loading QC results for " + run_id])])
2364 body = "This may take a few seconds..."
2365
2366 if trigger == "instrument-run-table":
2367 modal_is_open = True
2368 elif trigger == "close-load-modal":
2369 modal_is_open = False
2370
2371 return modal_is_open, title, body
2372
2373 else:
2374 raise PreventUpdate
2375
2376
2377@app.callback(Output("istd-rt-pos", "data"),
2378 Output("istd-rt-neg", "data"),
2379 Output("istd-intensity-pos", "data"),
2380 Output("istd-intensity-neg", "data"),
2381 Output("istd-mz-pos", "data"),
2382 Output("istd-mz-neg", "data"),
2383 Output("sequence", "data"),
2384 Output("metadata", "data"),
2385 Output("bio-rt-pos", "data"),
2386 Output("bio-rt-neg", "data"),
2387 Output("bio-intensity-pos", "data"),
2388 Output("bio-intensity-neg", "data"),
2389 Output("bio-mz-pos", "data"),
2390 Output("bio-mz-neg", "data"),
2391 Output("study-resources", "data"),
2392 Output("samples", "data"),
2393 Output("pos-internal-standards", "data"),
2394 Output("neg-internal-standards", "data"),
2395 Output("istd-delta-rt-pos", "data"),
2396 Output("istd-delta-rt-neg", "data"),
2397 Output("istd-in-run-delta-rt-pos", "data"),
2398 Output("istd-in-run-delta-rt-neg", "data"),
2399 Output("istd-delta-mz-pos", "data"),
2400 Output("istd-delta-mz-neg", "data"),
2401 Output("qc-warnings-pos", "data"),
2402 Output("qc-warnings-neg", "data"),
2403 Output("qc-fails-pos", "data"),
2404 Output("qc-fails-neg", "data"),
2405 Output("load-finished", "data"),
2406 Input("refresh-interval", "n_intervals"),
2407 Input("instrument-run-table", "active_cell"),
2408 State("instrument-run-table", "data"),
2409 State("study-resources", "data"),
2410 State("tabs", "value"), prevent_initial_call=True, suppress_callback_exceptions=True)
2411def load_data(refresh, active_cell, table_data, resources, instrument_id):
2412
2413 """
2414 Updates and stores QC results in dcc.Store objects (user's browser session)
2415 """
2416
2417 trigger = ctx.triggered_id
2418
2419 if active_cell:
2420
2421 # Get run ID and status
2422 run_id = table_data[active_cell["row"]]["Run ID"]
2423 status = table_data[active_cell["row"]]["Status"]
2424
2425 # Ensure that refresh does not trigger data parsing if no new samples processed
2426 if trigger == "refresh-interval":
2427 try:
2428 if db.get_device_identity() != instrument_id:
2429 if db.sync_is_enabled() and status != "Complete":
2430 db.download_qc_results(instrument_id, run_id)
2431
2432 completed_count_in_cache = json.loads(resources)["samples_completed"]
2433 actual_completed_count, total = db.get_completed_samples_count(instrument_id, run_id, status)
2434
2435 if completed_count_in_cache == actual_completed_count:
2436 raise PreventUpdate
2437 except:
2438 raise PreventUpdate
2439
2440 # If the acquisition listener was stopped for some reason, start a new process and pass remaining samples
2441 if status == "Active" and os.name == "nt":
2442
2443 # Check that device is the instrument that the run is on
2444 if db.get_device_identity() == instrument_id:
2445
2446 # Get listener process ID from database; if process is not running, restart it
2447 listener_id = db.get_pid(instrument_id, run_id)
2448 if not qc.subprocess_is_running(listener_id):
2449
2450 # Retrieve acquisition path
2451 acquisition_path = db.get_acquisition_path(instrument_id, run_id).replace("\\", "/")
2452 acquisition_path = acquisition_path + "/" if acquisition_path[-1] != "/" else acquisition_path
2453
2454 # Delete temporary data file directory
2455 db.delete_temp_directory(instrument_id, run_id)
2456
2457 # Restart AcquisitionListener and store process ID
2458 process = psutil.Popen(["py", "AcquisitionListener.py", acquisition_path, instrument_id, run_id])
2459 db.store_pid(instrument_id, run_id, process.pid)
2460
2461 # If new sample, route raw data -> parsed data -> user session cache -> plots
2462 return get_qc_results(instrument_id, run_id, status) + (True,)
2463
2464 else:
2465 return (None, None, None, None, None, None, None, None, None, None,
2466 None, None, None, None, None, None, None, None, None, None,
2467 None, None, None, None, None, None, None, None, None)
2468
2469
2470@app.callback(Output("close-load-modal", "data"),
2471 Input("load-finished", "data"), prevent_initial_call=True)
2472def signal_load_finished(load_finished):
2473
2474 # Welcome to Dash callback hell :D
2475 return True
2476
2477
2478@app.callback(Output("sample-table", "data"),
2479 Input("samples", "data"), prevent_initial_call=True)
2480def populate_sample_tables(samples):
2481
2482 """
2483 Populates table with list of samples for selected run from instrument runs table
2484 """
2485
2486 if samples is not None:
2487 df_samples = pd.DataFrame(json.loads(samples))
2488 df_samples = df_samples[["Sample", "Position", "QC"]]
2489 return df_samples.to_dict("records")
2490 else:
2491 return None
2492
2493
2494@app.callback(Output("istd-rt-dropdown", "options"),
2495 Output("istd-mz-dropdown", "options"),
2496 Output("istd-intensity-dropdown", "options"),
2497 Output("bio-standard-benchmark-dropdown", "options"),
2498 Output("rt-plot-sample-dropdown", "options"),
2499 Output("mz-plot-sample-dropdown", "options"),
2500 Output("intensity-plot-sample-dropdown", "options"),
2501 Input("polarity-options", "value"),
2502 State("sample-table", "data"),
2503 Input("samples", "data"),
2504 State("bio-intensity-pos", "data"),
2505 State("bio-intensity-neg", "data"),
2506 State("pos-internal-standards", "data"),
2507 State("neg-internal-standards", "data"))
2508def update_dropdowns_on_polarity_change(polarity, table_data, samples, bio_intensity_pos, bio_intensity_neg,
2509 pos_internal_standards, neg_internal_standards):
2510
2511 """
2512 Updates dropdown lists with correct items for user-selected polarity
2513 """
2514
2515 if samples is not None:
2516 df_samples = pd.DataFrame(json.loads(samples))
2517
2518 if polarity == "Neg":
2519 istd_dropdown = json.loads(neg_internal_standards)
2520
2521 if bio_intensity_neg is not None:
2522 df = pd.DataFrame(json.loads(bio_intensity_neg))
2523 df.drop(columns=["Name"], inplace=True)
2524 bio_dropdown = df.columns.tolist()
2525 else:
2526 bio_dropdown = []
2527
2528 df_samples = df_samples.loc[df_samples["Sample"].str.contains("Neg")]
2529 sample_dropdown = df_samples["Sample"].tolist()
2530
2531 elif polarity == "Pos":
2532 istd_dropdown = json.loads(pos_internal_standards)
2533
2534 if bio_intensity_pos is not None:
2535 df = pd.DataFrame(json.loads(bio_intensity_pos))
2536 df.drop(columns=["Name"], inplace=True)
2537 bio_dropdown = df.columns.tolist()
2538 else:
2539 bio_dropdown = []
2540
2541 df_samples = df_samples.loc[(df_samples["Sample"].str.contains("Pos"))]
2542 sample_dropdown = df_samples["Sample"].tolist()
2543
2544 return istd_dropdown, istd_dropdown, istd_dropdown, bio_dropdown, sample_dropdown, sample_dropdown, sample_dropdown
2545
2546 else:
2547 return [], [], [], [], [], [], []
2548
2549
2550@app.callback(Output("rt-plot-sample-dropdown", "value"),
2551 Output("mz-plot-sample-dropdown", "value"),
2552 Output("intensity-plot-sample-dropdown", "value"),
2553 Input("sample-filtering-options", "value"),
2554 Input("polarity-options", "value"),
2555 Input("samples", "data"),
2556 State("metadata", "data"), prevent_initial_call=True)
2557def apply_sample_filter_to_plots(filter, polarity, samples, metadata):
2558
2559 """
2560 Apply sample filter to internal standard plots, options are:
2561 1. All samples
2562 2. Filter by samples only
2563 3. Filter by treatments / classes
2564 4. Filter by pools
2565 5. Filter by blanks
2566 """
2567
2568 # Get complete list of samples (including blanks + pools) in polarity
2569 if samples is not None:
2570 df_samples = pd.DataFrame(json.loads(samples))
2571 df_samples = df_samples.loc[df_samples["Polarity"].str.contains(polarity)]
2572 sample_list = df_samples["Sample"].tolist()
2573 else:
2574 raise PreventUpdate
2575
2576 if filter is not None:
2577 # Return all samples, blanks, and pools
2578 if filter == "all":
2579 return [], [], []
2580
2581 # Return samples only
2582 elif filter == "samples":
2583 df_metadata = pd.read_json(metadata, orient="split")
2584 df_metadata = df_metadata.loc[df_metadata["Filename"].isin(sample_list)]
2585 samples_only = df_metadata["Filename"].tolist()
2586 return samples_only, samples_only, samples_only
2587
2588 # Return pools only
2589 elif filter == "pools":
2590 pools = [sample for sample in sample_list if "QC" in sample]
2591 return pools, pools, pools
2592
2593 # Return blanks only
2594 elif filter == "blanks":
2595 blanks = [sample for sample in sample_list if "BK" in sample]
2596 return blanks, blanks, blanks
2597
2598 else:
2599 return [], [], []
2600
2601
2602@app.callback(Output("istd-rt-plot", "figure"),
2603 Output("rt-prev-button", "n_clicks"),
2604 Output("rt-next-button", "n_clicks"),
2605 Output("istd-rt-dropdown", "value"),
2606 Output("istd-rt-div", "style"),
2607 Input("polarity-options", "value"),
2608 Input("istd-rt-dropdown", "value"),
2609 Input("rt-plot-sample-dropdown", "value"),
2610 Input("istd-rt-pos", "data"),
2611 Input("istd-rt-neg", "data"),
2612 State("samples", "data"),
2613 State("study-resources", "data"),
2614 State("pos-internal-standards", "data"),
2615 State("neg-internal-standards", "data"),
2616 Input("rt-prev-button", "n_clicks"),
2617 Input("rt-next-button", "n_clicks"), prevent_initial_call=True)
2618def populate_istd_rt_plot(polarity, internal_standard, selected_samples, rt_pos, rt_neg, samples, resources,
2619 pos_internal_standards, neg_internal_standards, previous, next):
2620
2621 """
2622 Populates internal standard retention time vs. sample plot
2623 """
2624
2625 if rt_pos is None and rt_neg is None:
2626 return {}, None, None, None, {"display": "none"}
2627
2628 trigger = ctx.triggered_id
2629
2630 # Get internal standard RT data
2631 df_istd_rt_pos = pd.DataFrame()
2632 df_istd_rt_neg = pd.DataFrame()
2633
2634 if rt_pos is not None:
2635 df_istd_rt_pos = pd.DataFrame(json.loads(rt_pos))
2636
2637 if rt_neg is not None:
2638 df_istd_rt_neg = pd.DataFrame(json.loads(rt_neg))
2639
2640 # Get samples
2641 df_samples = pd.DataFrame(json.loads(samples))
2642 samples = df_samples.loc[df_samples["Polarity"] == polarity]["Sample"].astype(str).tolist()
2643
2644 # Filter out biological standards
2645 identifiers = db.get_biological_standard_identifiers()
2646 for identifier in identifiers:
2647 samples = [x for x in samples if identifier not in x]
2648
2649 # Filter samples and internal standards by polarity
2650 if polarity == "Pos":
2651 internal_standards = json.loads(pos_internal_standards)
2652 df_istd_rt = df_istd_rt_pos
2653 elif polarity == "Neg":
2654 internal_standards = json.loads(neg_internal_standards)
2655 df_istd_rt = df_istd_rt_neg
2656
2657 # Get retention times
2658 retention_times = json.loads(resources)["retention_times_dict"]
2659
2660 # Set initial dropdown values when none are selected
2661 if not internal_standard or trigger == "polarity-options":
2662 internal_standard = internal_standards[0]
2663
2664 if not selected_samples:
2665 selected_samples = samples
2666
2667 # Calculate index of internal standard from button clicks
2668 if trigger == "rt-prev-button" or trigger == "rt-next-button":
2669 index = get_internal_standard_index(previous, next, len(internal_standards))
2670 internal_standard = internal_standards[index]
2671 else:
2672 index = next
2673
2674 try:
2675 # Generate internal standard RT vs. sample plot
2676 return load_istd_rt_plot(dataframe=df_istd_rt, samples=selected_samples,
2677 internal_standard=internal_standard, retention_times=retention_times), \
2678 None, index, internal_standard, {"display": "block"}
2679
2680 except Exception as error:
2681 print("Error in loading RT vs. sample plot:", error)
2682 return {}, None, None, None, {"display": "none"}
2683
2684
2685@app.callback(Output("istd-intensity-plot", "figure"),
2686 Output("intensity-prev-button", "n_clicks"),
2687 Output("intensity-next-button", "n_clicks"),
2688 Output("istd-intensity-dropdown", "value"),
2689 Output("istd-intensity-div", "style"),
2690 Input("polarity-options", "value"),
2691 Input("istd-intensity-dropdown", "value"),
2692 Input("intensity-plot-sample-dropdown", "value"),
2693 Input("istd-intensity-pos", "data"),
2694 Input("istd-intensity-neg", "data"),
2695 State("samples", "data"),
2696 State("metadata", "data"),
2697 State("pos-internal-standards", "data"),
2698 State("neg-internal-standards", "data"),
2699 Input("intensity-prev-button", "n_clicks"),
2700 Input("intensity-next-button", "n_clicks"), prevent_initial_call=True)
2701def populate_istd_intensity_plot(polarity, internal_standard, selected_samples, intensity_pos, intensity_neg, samples, metadata,
2702 pos_internal_standards, neg_internal_standards, previous, next):
2703
2704 """
2705 Populates internal standard intensity vs. sample plot
2706 """
2707
2708 if intensity_pos is None and intensity_neg is None:
2709 return {}, None, None, None, {"display": "none"}
2710
2711 trigger = ctx.triggered_id
2712
2713 # Get internal standard intensity data
2714 df_istd_intensity_pos = pd.DataFrame()
2715 df_istd_intensity_neg = pd.DataFrame()
2716
2717 if intensity_pos is not None:
2718 df_istd_intensity_pos = pd.DataFrame(json.loads(intensity_pos))
2719
2720 if intensity_neg is not None:
2721 df_istd_intensity_neg = pd.DataFrame(json.loads(intensity_neg))
2722
2723 # Get samples
2724 df_samples = pd.DataFrame(json.loads(samples))
2725 samples = df_samples.loc[df_samples["Polarity"] == polarity]["Sample"].astype(str).tolist()
2726
2727 identifiers = db.get_biological_standard_identifiers()
2728 for identifier in identifiers:
2729 samples = [x for x in samples if identifier not in x]
2730
2731 # Get sample metadata
2732 df_metadata = pd.read_json(metadata, orient="split")
2733
2734 # Filter samples and internal standards by polarity
2735 if polarity == "Pos":
2736 internal_standards = json.loads(pos_internal_standards)
2737 df_istd_intensity = df_istd_intensity_pos
2738 elif polarity == "Neg":
2739 internal_standards = json.loads(neg_internal_standards)
2740 df_istd_intensity = df_istd_intensity_neg
2741
2742 # Set initial internal standard dropdown value when none are selected
2743 if not internal_standard or trigger == "polarity-options":
2744 internal_standard = internal_standards[0]
2745
2746 # Set initial sample dropdown value when none are selected
2747 if not selected_samples:
2748 selected_samples = samples
2749 treatments = pd.DataFrame()
2750 else:
2751 df_metadata = df_metadata.loc[df_metadata["Filename"].isin(selected_samples)]
2752 treatments = df_metadata[["Filename", "Treatment"]]
2753 if len(df_metadata) == len(selected_samples):
2754 selected_samples = df_metadata["Filename"].tolist()
2755
2756 # Calculate index of internal standard from button clicks
2757 if trigger == "intensity-prev-button" or trigger == "intensity-next-button":
2758 index = get_internal_standard_index(previous, next, len(internal_standards))
2759 internal_standard = internal_standards[index]
2760 else:
2761 index = next
2762
2763 try:
2764 # Generate internal standard intensity vs. sample plot
2765 return load_istd_intensity_plot(dataframe=df_istd_intensity, samples=selected_samples,
2766 internal_standard=internal_standard, treatments=treatments), \
2767 None, index, internal_standard, {"display": "block"}
2768
2769 except Exception as error:
2770 print("Error in loading intensity vs. sample plot:", error)
2771 return {}, None, None, None, {"display": "none"}
2772
2773
2774@app.callback(Output("istd-mz-plot", "figure"),
2775 Output("mz-prev-button", "n_clicks"),
2776 Output("mz-next-button", "n_clicks"),
2777 Output("istd-mz-dropdown", "value"),
2778 Output("istd-mz-div", "style"),
2779 Input("polarity-options", "value"),
2780 Input("istd-mz-dropdown", "value"),
2781 Input("mz-plot-sample-dropdown", "value"),
2782 Input("istd-delta-mz-pos", "data"),
2783 Input("istd-delta-mz-neg", "data"),
2784 State("samples", "data"),
2785 State("pos-internal-standards", "data"),
2786 State("neg-internal-standards", "data"),
2787 State("study-resources", "data"),
2788 Input("mz-prev-button", "n_clicks"),
2789 Input("mz-next-button", "n_clicks"), prevent_initial_call=True)
2790def populate_istd_mz_plot(polarity, internal_standard, selected_samples, delta_mz_pos, delta_mz_neg, samples,
2791 pos_internal_standards, neg_internal_standards, resources, previous, next):
2792
2793 """
2794 Populates internal standard delta m/z vs. sample plot
2795 """
2796
2797 if delta_mz_pos is None and delta_mz_neg is None:
2798 return {}, None, None, None, {"display": "none"}
2799
2800 trigger = ctx.triggered_id
2801
2802 # Get internal standard RT data
2803 df_istd_mz_pos = pd.DataFrame()
2804 df_istd_mz_neg = pd.DataFrame()
2805
2806 if delta_mz_pos is not None:
2807 df_istd_mz_pos = pd.DataFrame(json.loads(delta_mz_pos))
2808
2809 if delta_mz_neg is not None:
2810 df_istd_mz_neg = pd.DataFrame(json.loads(delta_mz_neg))
2811
2812 # Get samples (and filter out biological standards)
2813 df_samples = pd.DataFrame(json.loads(samples))
2814 samples = df_samples.loc[df_samples["Polarity"] == polarity]["Sample"].astype(str).tolist()
2815
2816 identifiers = db.get_biological_standard_identifiers()
2817 for identifier in identifiers:
2818 samples = [x for x in samples if identifier not in x]
2819
2820 # Filter samples and internal standards by polarity
2821 if polarity == "Pos":
2822 internal_standards = json.loads(pos_internal_standards)
2823 df_istd_mz = df_istd_mz_pos
2824
2825 elif polarity == "Neg":
2826 internal_standards = json.loads(neg_internal_standards)
2827 df_istd_mz = df_istd_mz_neg
2828
2829 # Set initial dropdown values when none are selected
2830 if not internal_standard or trigger == "polarity-options":
2831 internal_standard = internal_standards[0]
2832 if not selected_samples:
2833 selected_samples = samples
2834
2835 # Calculate index of internal standard from button clicks
2836 if trigger == "mz-prev-button" or trigger == "mz-next-button":
2837 index = get_internal_standard_index(previous, next, len(internal_standards))
2838 internal_standard = internal_standards[index]
2839 else:
2840 index = next
2841
2842 try:
2843 # Generate internal standard delta m/z vs. sample plot
2844 return load_istd_delta_mz_plot(dataframe=df_istd_mz, samples=selected_samples, internal_standard=internal_standard), \
2845 None, index, internal_standard, {"display": "block"}
2846
2847 except Exception as error:
2848 print("Error in loading delta m/z vs. sample plot:", error)
2849 return {}, None, None, None, {"display": "none"}
2850
2851
2852@app.callback(Output("bio-standards-plot-dropdown", "options"),
2853 Input("study-resources", "data"), prevent_initial_call=True)
2854def populate_biological_standards_dropdown(resources):
2855
2856 """
2857 Retrieves list of biological standards included in run
2858 """
2859
2860 try:
2861 return ast.literal_eval(ast.literal_eval(resources)["biological_standards"])
2862 except:
2863 return []
2864
2865
2866@app.callback(Output("bio-standard-mz-rt-plot", "figure"),
2867 Output("bio-standard-benchmark-dropdown", "value"),
2868 Output("bio-standard-mz-rt-plot", "clickData"),
2869 Output("bio-standard-mz-rt-div", "style"),
2870 Input("polarity-options", "value"),
2871 Input("bio-rt-pos", "data"),
2872 Input("bio-rt-neg", "data"),
2873 State("bio-intensity-pos", "data"),
2874 State("bio-intensity-neg", "data"),
2875 State("bio-mz-pos", "data"),
2876 State("bio-mz-neg", "data"),
2877 State("study-resources", "data"),
2878 Input("bio-standard-mz-rt-plot", "clickData"),
2879 Input("bio-standards-plot-dropdown", "value"), prevent_initial_call=True)
2880def populate_bio_standard_mz_rt_plot(polarity, rt_pos, rt_neg, intensity_pos, intensity_neg, mz_pos, mz_neg,
2881 resources, click_data, selected_bio_standard):
2882
2883 """
2884 Populates biological standard m/z vs. RT plot
2885 """
2886
2887 if rt_pos is None and rt_neg is None:
2888 if mz_pos is None and mz_neg is None:
2889 return {}, None, None, {"display": "none"}
2890
2891 # Get run ID and status
2892 resources = json.loads(resources)
2893 instrument_id = resources["instrument"]
2894 run_id = resources["run_id"]
2895 status = resources["status"]
2896
2897 # Get Google Drive instance
2898 drive = None
2899 if status == "Active" and db.sync_is_enabled():
2900 drive = db.get_drive_instance()
2901
2902 # Toggle a different biological standard
2903 if selected_bio_standard is not None:
2904 rt_pos, rt_neg, intensity_pos, intensity_neg, mz_pos, mz_neg = get_qc_results(instrument_id=instrument_id,
2905 run_id=run_id, status=status, biological_standard=selected_bio_standard, biological_standards_only=True)
2906
2907 # Get biological standard m/z, RT, and intensity data
2908 if polarity == "Pos":
2909 if rt_pos is not None and intensity_pos is not None and mz_pos is not None:
2910 df_bio_rt = pd.DataFrame(json.loads(rt_pos))
2911 df_bio_intensity = pd.DataFrame(json.loads(intensity_pos))
2912 df_bio_mz = pd.DataFrame(json.loads(mz_pos))
2913
2914 elif polarity == "Neg":
2915 if rt_neg is not None and intensity_neg is not None and mz_neg is not None:
2916 df_bio_rt = pd.DataFrame(json.loads(rt_neg))
2917 df_bio_intensity = pd.DataFrame(json.loads(intensity_neg))
2918 df_bio_mz = pd.DataFrame(json.loads(mz_neg))
2919
2920 if click_data is not None:
2921 selected_feature = click_data["points"][0]["hovertext"]
2922 else:
2923 selected_feature = None
2924
2925 try:
2926 # Biological standard metabolites – m/z vs. retention time
2927 return load_bio_feature_plot(run_id=run_id, df_rt=df_bio_rt, df_mz=df_bio_mz, df_intensity=df_bio_intensity), \
2928 selected_feature, None, {"display": "block"}
2929 except Exception as error:
2930 print("Error in loading biological standard m/z-RT plot:", error)
2931 return {}, None, None, {"display": "none"}
2932
2933
2934@app.callback(Output("bio-standard-benchmark-plot", "figure"),
2935 Output("bio-standard-benchmark-div", "style"),
2936 Input("polarity-options", "value"),
2937 Input("bio-standard-benchmark-dropdown", "value"),
2938 Input("bio-intensity-pos", "data"),
2939 Input("bio-intensity-neg", "data"),
2940 Input("bio-standards-plot-dropdown", "value"),
2941 State("study-resources", "data"), prevent_initial_call=True)
2942def populate_bio_standard_benchmark_plot(polarity, selected_feature, intensity_pos, intensity_neg, selected_bio_standard, resources):
2943
2944 """
2945 Populates biological standard benchmark plot
2946 """
2947
2948 if intensity_pos is None and intensity_neg is None:
2949 return {}, {"display": "none"}
2950
2951 # Get run ID and status
2952 resources = json.loads(resources)
2953 instrument_id = resources["instrument"]
2954 run_id = resources["run_id"]
2955 status = resources["status"]
2956
2957 # Get Google Drive instance
2958 drive = None
2959 if status == "Active" and db.sync_is_enabled():
2960 drive = db.get_drive_instance()
2961
2962 # Toggle a different biological standard
2963 if selected_bio_standard is not None:
2964 intensity_pos, intensity_neg = get_qc_results(instrument_id=instrument_id, run_id=run_id,
2965 status=status, drive=drive, biological_standard=selected_bio_standard, for_benchmark_plot=True)
2966
2967 # Get intensity data
2968 if polarity == "Pos":
2969 if intensity_pos is not None:
2970 df_bio_intensity = pd.DataFrame(json.loads(intensity_pos))
2971
2972 elif polarity == "Neg":
2973 if intensity_neg is not None:
2974 df_bio_intensity = pd.DataFrame(json.loads(intensity_neg))
2975
2976 # Get clicked or selected feature from biological standard m/z-RT plot
2977 if not selected_feature:
2978 selected_feature = df_bio_intensity.columns[1]
2979
2980 try:
2981 # Generate biological standard metabolite intensity vs. instrument run plot
2982 return load_bio_benchmark_plot(dataframe=df_bio_intensity,
2983 metabolite_name=selected_feature), {"display": "block"}
2984
2985 except Exception as error:
2986 print("Error loading biological standard intensity plot:", error)
2987 return {}, {"display": "none"}
2988
2989
2990@app.callback(Output("sample-info-modal", "is_open"),
2991 Output("sample-modal-title", "children"),
2992 Output("sample-modal-body", "children"),
2993 Output("sample-table", "selected_cells"),
2994 Output("sample-table", "active_cell"),
2995 Output("istd-rt-plot", "clickData"),
2996 Output("istd-intensity-plot", "clickData"),
2997 Output("istd-mz-plot", "clickData"),
2998 State("sample-info-modal", "is_open"),
2999 Input("sample-table", "active_cell"),
3000 State("sample-table", "data"),
3001 Input("istd-rt-plot", "clickData"),
3002 Input("istd-intensity-plot", "clickData"),
3003 Input("istd-mz-plot", "clickData"),
3004 State("istd-rt-pos", "data"),
3005 State("istd-rt-neg", "data"),
3006 State("istd-intensity-pos", "data"),
3007 State("istd-intensity-neg", "data"),
3008 State("istd-mz-pos", "data"),
3009 State("istd-mz-neg", "data"),
3010 State("istd-delta-rt-pos", "data"),
3011 State("istd-delta-rt-neg", "data"),
3012 State("istd-in-run-delta-rt-pos", "data"),
3013 State("istd-in-run-delta-rt-neg", "data"),
3014 State("istd-delta-mz-pos", "data"),
3015 State("istd-delta-mz-neg", "data"),
3016 State("qc-warnings-pos", "data"),
3017 State("qc-warnings-neg", "data"),
3018 State("qc-fails-pos", "data"),
3019 State("qc-fails-neg", "data"),
3020 State("bio-rt-pos", "data"),
3021 State("bio-rt-neg", "data"),
3022 State("bio-intensity-pos", "data"),
3023 State("bio-intensity-neg", "data"),
3024 State("bio-mz-pos", "data"),
3025 State("bio-mz-neg", "data"),
3026 State("sequence", "data"),
3027 State("metadata", "data"),
3028 State("study-resources", "data"), prevent_initial_call=True)
3029def toggle_sample_card(is_open, active_cell, table_data, rt_click, intensity_click, mz_click, rt_pos, rt_neg, intensity_pos,
3030 intensity_neg, mz_pos, mz_neg, delta_rt_pos, delta_rt_neg, in_run_delta_rt_pos, in_run_delta_rt_neg, delta_mz_pos, delta_mz_neg,
3031 qc_warnings_pos, qc_warnings_neg, qc_fails_pos, qc_fails_neg, bio_rt_pos, bio_rt_neg, bio_intensity_pos, bio_intensity_neg,
3032 bio_mz_pos, bio_mz_neg, sequence, metadata, resources):
3033
3034 """
3035 Opens information modal when a sample is clicked from the sample table
3036 """
3037
3038 # Get selected sample from table
3039 if active_cell:
3040 clicked_sample = table_data[active_cell["row"]][active_cell["column_id"]]
3041
3042 # Get selected sample from plots
3043 if rt_click:
3044 clicked_sample = rt_click["points"][0]["x"]
3045 clicked_sample = clicked_sample.replace(": RT Info", "")
3046
3047 if intensity_click:
3048 clicked_sample = intensity_click["points"][0]["x"]
3049 clicked_sample = clicked_sample.replace(": Height", "")
3050
3051 if mz_click:
3052 clicked_sample = mz_click["points"][0]["x"]
3053 clicked_sample = clicked_sample.replace(": Precursor m/z Info", "")
3054
3055 # Get instrument ID and run ID
3056 resources = json.loads(resources)
3057 instrument_id = resources["instrument"]
3058 run_id = resources["run_id"]
3059 status = resources["status"]
3060
3061 # Get sequence and metadata
3062 df_sequence = pd.read_json(sequence, orient="split")
3063 try:
3064 df_metadata = pd.read_json(metadata, orient="split")
3065 except:
3066 df_metadata = pd.DataFrame()
3067
3068 # Check whether sample is a biological standard or not
3069 is_bio_standard = False
3070 identifiers = db.get_biological_standard_identifiers()
3071
3072 for identifier in identifiers.keys():
3073 if identifier in clicked_sample:
3074 is_bio_standard = True
3075 break
3076
3077 # Get polarity
3078 polarity = db.get_polarity_for_sample(instrument_id, run_id, clicked_sample, status)
3079
3080 # Generate DataFrames with quantified features and metadata for selected sample
3081 if not is_bio_standard:
3082
3083 if polarity == "Pos":
3084 df_rt = pd.DataFrame(json.loads(rt_pos))
3085 df_intensity = pd.DataFrame(json.loads(intensity_pos))
3086 df_mz = pd.DataFrame(json.loads(mz_pos))
3087 df_delta_rt = pd.DataFrame(json.loads(delta_rt_pos))
3088 df_in_run_delta_rt = pd.DataFrame(json.loads(in_run_delta_rt_pos))
3089 df_delta_mz = pd.DataFrame(json.loads(delta_mz_pos))
3090 df_warnings = pd.DataFrame(json.loads(qc_warnings_pos))
3091 df_fails = pd.DataFrame(json.loads(qc_fails_pos))
3092
3093 elif polarity == "Neg":
3094 df_rt = pd.DataFrame(json.loads(rt_neg))
3095 df_intensity = pd.DataFrame(json.loads(intensity_neg))
3096 df_mz = pd.DataFrame(json.loads(mz_neg))
3097 df_delta_rt = pd.DataFrame(json.loads(delta_rt_neg))
3098 df_in_run_delta_rt = pd.DataFrame(json.loads(in_run_delta_rt_neg))
3099 df_delta_mz = pd.DataFrame(json.loads(delta_mz_neg))
3100 df_warnings = pd.DataFrame(json.loads(qc_warnings_neg))
3101 df_fails = pd.DataFrame(json.loads(qc_fails_neg))
3102
3103 df_sample_features, df_sample_info = generate_sample_metadata_dataframe(clicked_sample, df_rt, df_mz, df_intensity,
3104 df_delta_rt, df_in_run_delta_rt, df_delta_mz, df_warnings, df_fails, df_sequence, df_metadata)
3105
3106 elif is_bio_standard:
3107
3108 if polarity == "Pos":
3109 df_rt = pd.DataFrame(json.loads(bio_rt_pos))
3110 df_intensity = pd.DataFrame(json.loads(bio_intensity_pos))
3111 df_mz = pd.DataFrame(json.loads(bio_mz_pos))
3112
3113 elif polarity == "Neg":
3114 df_rt = pd.DataFrame(json.loads(bio_rt_neg))
3115 df_intensity = pd.DataFrame(json.loads(bio_intensity_neg))
3116 df_mz = pd.DataFrame(json.loads(bio_mz_neg))
3117
3118 df_sample_features, df_sample_info = generate_bio_standard_dataframe(clicked_sample, instrument_id, run_id, df_rt, df_mz, df_intensity)
3119
3120 # Create tables from DataFrames
3121 metadata_table = dbc.Table.from_dataframe(df_sample_info, striped=True, bordered=True, hover=True)
3122 feature_table = dbc.Table.from_dataframe(df_sample_features, striped=True, bordered=True, hover=True)
3123
3124 # Add tables to sample information modal
3125 title = clicked_sample
3126 body = html.Div(children=[metadata_table, feature_table])
3127
3128 # Toggle modal
3129 if is_open:
3130 return False, title, body, [], None, None, None, None
3131 else:
3132 return True, title, body, [], None, None, None, None
3133
3134
3135@app.callback(Output("settings-modal", "is_open"),
3136 Input("settings-button", "n_clicks"), prevent_initial_call=True)
3137def toggle_settings_modal(button_click):
3138
3139 """
3140 Toggles global settings modal
3141 """
3142
3143 if db.sync_is_enabled():
3144 db.download_methods()
3145
3146 return True
3147
3148
3149@app.callback(Output("google-drive-sync-modal", "is_open"),
3150 Output("database-md5", "data"),
3151 Input("settings-modal", "is_open"),
3152 State("google-drive-authenticated", "data"),
3153 State("google-drive-sync-modal", "is_open"),
3154 Input("close-sync-modal", "data"),
3155 State("database-md5", "data"), prevent_initial_call=True)
3156def show_sync_modal(settings_is_open, google_drive_authenticated, sync_modal_is_open, sync_finished, md5_checksum):
3157
3158 """
3159 Launches progress modal, which syncs database and methods directory to Google Drive
3160 """
3161
3162 # If sync modal is open
3163 if sync_modal_is_open:
3164 # If sync is finished
3165 if sync_finished:
3166 # Close the modal
3167 return False, None
3168
3169 # Check if settings modal has been closed
3170 if settings_is_open:
3171 return False, db.get_md5_for_settings_db()
3172
3173 elif not settings_is_open:
3174
3175 # Check if user is logged into Google Drive
3176 if google_drive_authenticated:
3177
3178 # Get MD5 checksum after use closes settings
3179 new_md5_checksum = db.get_md5_for_settings_db()
3180
3181 # Compare new MD5 checksum to old MD5 checksum
3182 if md5_checksum != new_md5_checksum:
3183 return True, new_md5_checksum
3184 else:
3185 return False, new_md5_checksum
3186
3187 else:
3188 return False, None
3189
3190
3191@app.callback(Output("google-drive-sync-finished", "data"),
3192 Input("settings-modal", "is_open"),
3193 State("google-drive-authenticated", "data"),
3194 State("google-drive-authenticated-3", "data"),
3195 State("database-md5", "data"), prevent_initial_call=True)
3196def sync_settings_to_google_drive(settings_modal_is_open, google_drive_authenticated, auth_in_app, md5_checksum):
3197
3198 """
3199 Syncs settings and methods files to Google Drive
3200 """
3201
3202 if not settings_modal_is_open:
3203 if google_drive_authenticated or auth_in_app:
3204 if db.settings_were_modified(md5_checksum):
3205 db.upload_methods()
3206 return True
3207
3208 return False
3209
3210
3211@app.callback(Output("close-sync-modal", "data"),
3212 Input("google-drive-sync-finished", "data"), prevent_initial_call=True)
3213def close_sync_modal(sync_finished):
3214
3215 # You've reached Dash callback purgatory :/
3216 if sync_finished:
3217 return True
3218
3219
3220@app.callback(Output("workspace-users-table", "children"),
3221 Input("on-page-load", "data"),
3222 Input("google-drive-user-added", "data"),
3223 Input("google-drive-user-deleted", "data"),
3224 Input("google-drive-sync-update", "data"))
3225def get_users_with_workspace_access(on_page_load, user_added, user_deleted, sync_update):
3226
3227 """
3228 Returns table of users that have access to the MS-AutoQC workspace
3229 """
3230
3231 # Get users from database
3232 if db.is_valid():
3233 df_gdrive_users = db.get_table("Settings", "gdrive_users")
3234 df_gdrive_users = df_gdrive_users.rename(
3235 columns={"id": "User",
3236 "name": "Name",
3237 "email_address": "Google Account Email Address"})
3238 df_gdrive_users.drop(["permission_id"], inplace=True, axis=1)
3239
3240 # Generate and return table
3241 if len(df_gdrive_users) > 0:
3242 table = dbc.Table.from_dataframe(df_gdrive_users, striped=True, hover=True)
3243 return table
3244 else:
3245 return None
3246 else:
3247 raise PreventUpdate
3248
3249
3250@app.callback(Output("google-drive-user-added", "data"),
3251 Input("add-user-button", "n_clicks"),
3252 State("add-user-text-field", "value"),
3253 State("google-drive-authenticated", "data"), prevent_initial_call=True)
3254def add_user_to_workspace(button_click, user_email_address, google_drive_is_authenticated):
3255
3256 """
3257 Grants user permission to MS-AutoQC workspace in Google Drive
3258 """
3259
3260 if user_email_address in db.get_workspace_users_list():
3261 return "User already exists"
3262
3263 if db.sync_is_enabled():
3264 db.add_user_to_workspace(user_email_address)
3265
3266 if user_email_address in db.get_workspace_users_list():
3267 return user_email_address
3268 else:
3269 return "Error"
3270
3271
3272@app.callback(Output("google-drive-user-deleted", "data"),
3273 Input("delete-user-button", "n_clicks"),
3274 State("add-user-text-field", "value"),
3275 State("google-drive-authenticated", "data"), prevent_initial_call=True)
3276def delete_user_from_workspace(button_click, user_email_address, google_drive_is_authenticated):
3277
3278 """
3279 Revokes user permission to MS-AutoQC workspace in Google Drive
3280 """
3281
3282 if user_email_address not in db.get_workspace_users_list():
3283 return "User does not exist"
3284
3285 if db.sync_is_enabled():
3286 db.delete_user_from_workspace(user_email_address)
3287
3288 if user_email_address in db.get_workspace_users_list():
3289 return "Error"
3290 else:
3291 return user_email_address
3292
3293
3294@app.callback(Output("user-addition-alert", "is_open"),
3295 Output("user-addition-alert", "children"),
3296 Output("user-addition-alert", "color"),
3297 Input("google-drive-user-added", "data"), prevent_initial_call=True)
3298def ui_feedback_for_adding_gdrive_user(user_added_result):
3299
3300 """
3301 UI alert upon adding a new user to MS-AutoQC workspace
3302 """
3303
3304 if user_added_result is not None:
3305 if user_added_result != "Error" and user_added_result != "User already exists":
3306 return True, user_added_result + " has been granted access to the workspace.", "success"
3307 elif user_added_result == "User already exists":
3308 return True, "Error: This user already has access to the workspace.", "danger"
3309 else:
3310 return True, "Error: Could not grant access.", "danger"
3311
3312
3313@app.callback(Output("user-deletion-alert", "is_open"),
3314 Output("user-deletion-alert", "children"),
3315 Output("user-deletion-alert", "color"),
3316 Input("google-drive-user-deleted", "data"), prevent_initial_call=True)
3317def ui_feedback_for_deleting_gdrive_user(user_deleted_result):
3318
3319 """
3320 UI alert upon deleting a user from the MS-AutoQC workspace
3321 """
3322
3323 if user_deleted_result is not None:
3324 if user_deleted_result != "Error" and user_deleted_result != "User does not exist":
3325 return True, "Revoked workspace access for " + user_deleted_result + ".", "primary"
3326 elif user_deleted_result == "User does not exist":
3327 return True, "Error: this user cannot be deleted because they are not in the workspace.", "danger"
3328 else:
3329 return True, "Error: Could not revoke access.", "danger"
3330
3331
3332@app.callback(Output("slack-bot-token", "placeholder"),
3333 Input("slack-bot-token-saved", "data"),
3334 Input("google-drive-sync-update", "data"))
3335def get_slack_bot_token(token_save_result, sync_update):
3336
3337 """
3338 Get Slack bot token saved in database
3339 """
3340
3341 if db.is_valid():
3342 if db.get_slack_bot_token() != "None":
3343 return "Slack bot user OAuth token (saved)"
3344 else:
3345 raise PreventUpdate
3346 else:
3347 raise PreventUpdate
3348
3349
3350@app.callback(Output("slack-bot-token-saved", "data"),
3351 Input("save-slack-token-button", "n_clicks"),
3352 State("slack-bot-token", "value"), prevent_initial_call=True)
3353def save_slack_bot_token(button_click, slack_bot_token):
3354
3355 """
3356 Saves Slack bot user OAuth token in database
3357 """
3358
3359 if slack_bot_token is not None:
3360 db.update_slack_bot_token(slack_bot_token)
3361 return "Success"
3362 else:
3363 return "Error"
3364
3365
3366@app.callback(Output("slack-token-save-alert", "is_open"),
3367 Output("slack-token-save-alert", "children"),
3368 Output("slack-token-save-alert", "color"),
3369 Input("slack-bot-token-saved", "data"), prevent_initial_call=True)
3370def ui_alert_on_slack_token_save(token_save_result):
3371
3372 """
3373 Displays UI alert on Slack bot token save
3374 """
3375
3376 if token_save_result is not None:
3377 if token_save_result == "Success":
3378 return True, "Your Slack bot token was successfully saved.", "success"
3379 elif token_save_result == "Error":
3380 return True, "Error: Please enter your Slack bot token first.", "danger"
3381 else:
3382 raise PreventUpdate
3383
3384
3385@app.callback(Output("slack-channel", "value"),
3386 Output("slack-notifications-enabled", "value"),
3387 Input("slack-channel-saved", "data"),
3388 Input("google-drive-sync-update", "data"))
3389def get_slack_channel(result, sync_update):
3390
3391 """
3392 Gets Slack channel and notification toggle setting from database
3393 """
3394
3395 if db.is_valid():
3396 slack_channel = db.get_slack_channel()
3397 slack_notifications_enabled = db.get_slack_notifications_toggled()
3398
3399 if slack_notifications_enabled == 1:
3400 return "#" + slack_channel, slack_notifications_enabled
3401 else:
3402 raise PreventUpdate
3403 else:
3404 raise PreventUpdate
3405
3406
3407@app.callback(Output("slack-channel-saved", "data"),
3408 Input("slack-notifications-enabled", "value"),
3409 State("slack-channel", "value"), prevent_initial_call=True)
3410def save_slack_channel(notifications_enabled, slack_channel):
3411
3412 """
3413 1. Registers Slack channel for MS-AutoQC notifications
3414 2. Sends a Slack message to confirm registration
3415 """
3416
3417 if slack_channel is not None:
3418 if notifications_enabled == 1:
3419 if db.get_slack_bot_token() != "None":
3420 db.update_slack_channel(slack_channel, notifications_enabled)
3421 return "Enabled"
3422 else:
3423 return "No token"
3424 elif notifications_enabled == 0:
3425 db.update_slack_channel(slack_channel, notifications_enabled)
3426 return "Disabled"
3427 else:
3428 raise PreventUpdate
3429 else:
3430 raise PreventUpdate
3431
3432
3433@app.callback(Output("slack-notifications-toggle-alert", "is_open"),
3434 Output("slack-notifications-toggle-alert", "children"),
3435 Output("slack-notifications-toggle-alert", "color"),
3436 Input("slack-channel-saved", "data"), prevent_initial_call=True)
3437def ui_alert_on_slack_notifications_toggle(result):
3438
3439 """
3440 UI alert on setting Slack channel and toggling Slack notifications
3441 """
3442
3443 if result is not None:
3444 if result == "Enabled":
3445 return True, "Success! Slack notifications have been enabled.", "success"
3446 elif result == "Disabled":
3447 return True, "Slack notifications have been disabled.", "primary"
3448 elif result == "No token":
3449 return True, "Error: Please save your Slack bot token first.", "danger"
3450 else:
3451 raise PreventUpdate
3452
3453
3454@app.callback(Output("email-notifications-table", "children"),
3455 Input("on-page-load", "data"),
3456 Input("email-added", "data"),
3457 Input("email-deleted", "data"),
3458 Input("google-drive-sync-update", "data"))
3459def get_emails_registered_for_notifications(on_page_load, email_added, email_deleted, sync_update):
3460
3461 """
3462 Returns table of emails that are registered for email notifications
3463 """
3464
3465 # Get emails from database
3466 if db.is_valid():
3467 df_emails = pd.DataFrame()
3468 df_emails["Registered Email Addresses"] = db.get_email_notifications_list()
3469
3470 # Generate and return table
3471 if len(df_emails) > 0:
3472 table = dbc.Table.from_dataframe(df_emails, striped=True, hover=True)
3473 return table
3474 else:
3475 return None
3476 else:
3477 raise PreventUpdate
3478
3479
3480@app.callback(Output("email-added", "data"),
3481 Input("add-email-button", "n_clicks"),
3482 State("email-notifications-text-field", "value"), prevent_initial_call=True)
3483def register_email_for_notifications(button_click, user_email_address):
3484
3485 """
3486 Registers email address for MS-AutoQC notifications
3487 """
3488
3489 if user_email_address in db.get_email_notifications_list():
3490 return "Email already exists"
3491
3492 db.register_email_for_notifications(user_email_address)
3493
3494 if user_email_address in db.get_email_notifications_list():
3495 return user_email_address
3496 else:
3497 return "Error"
3498
3499
3500@app.callback(Output("email-deleted", "data"),
3501 Input("delete-email-button", "n_clicks"),
3502 State("email-notifications-text-field", "value"), prevent_initial_call=True)
3503def delete_email_from_notifications(button_click, user_email_address):
3504
3505 """
3506 Unsubscribes email address from MS-AutoQC notifications
3507 """
3508
3509 if user_email_address not in db.get_email_notifications_list():
3510 return "Email does not exist"
3511
3512 db.delete_email_from_notifications(user_email_address)
3513
3514 if user_email_address in db.get_email_notifications_list():
3515 return "Error"
3516 else:
3517 return user_email_address
3518
3519
3520@app.callback(Output("email-addition-alert", "is_open"),
3521 Output("email-addition-alert", "children"),
3522 Output("email-addition-alert", "color"),
3523 Input("email-added", "data"), prevent_initial_call=True)
3524def ui_feedback_for_registering_email(email_added_result):
3525
3526 """
3527 UI alert upon registering email for email notifications
3528 """
3529
3530 if email_added_result is not None:
3531 if email_added_result != "Error" and email_added_result != "Email already exists":
3532 return True, email_added_result + " has been registered for MS-AutoQC notifications.", "success"
3533 elif email_added_result == "Email already exists":
3534 return True, "Error: This email is already registered for MS-AutoQC notifications.", "danger"
3535 else:
3536 return True, "Error: Could not register email for MS-AutoQC notifications.", "danger"
3537
3538
3539@app.callback(Output("email-deletion-alert", "is_open"),
3540 Output("email-deletion-alert", "children"),
3541 Output("email-deletion-alert", "color"),
3542 Input("email-deleted", "data"), prevent_initial_call=True)
3543def ui_feedback_for_deleting_email(email_deleted_result):
3544
3545 """
3546 UI alert upon deleting email from email notifications list
3547 """
3548
3549 if email_deleted_result is not None:
3550 if email_deleted_result != "Error" and email_deleted_result != "Email does not exist":
3551 return True, "Unsubscribed " + email_deleted_result + " from email notifications.", "primary"
3552 elif email_deleted_result == "Email does not exist":
3553 message = "Error: Email cannot be deleted because it isn't registered for notifications."
3554 return True, message, "danger"
3555 else:
3556 return True, "Error: Could not unsubscribe email from MS-AutoQC notifications.", "danger"
3557
3558
3559@app.callback(Output("chromatography-methods-table", "children"),
3560 Output("select-istd-chromatography-dropdown", "options"),
3561 Output("select-bio-chromatography-dropdown", "options"),
3562 Output("add-chromatography-text-field", "value"),
3563 Output("chromatography-added", "data"),
3564 Input("on-page-load", "data"),
3565 Input("add-chromatography-button", "n_clicks"),
3566 State("add-chromatography-text-field", "value"),
3567 Input("istd-msp-added", "data"),
3568 Input("chromatography-removed", "data"),
3569 Input("chromatography-msdial-config-added", "data"),
3570 Input("google-drive-sync-update", "data"))
3571def add_chromatography_method(on_page_load, button_click, chromatography_method, msp_added, method_removed, config_added, sync_update):
3572
3573 """
3574 Add chromatography method to database
3575 """
3576
3577 if db.is_valid():
3578
3579 # Add chromatography method to database
3580 method_added = ""
3581 if chromatography_method is not None:
3582 db.insert_chromatography_method(chromatography_method)
3583 method_added = "Added"
3584
3585 # Update table
3586 df_methods = db.get_chromatography_methods()
3587
3588 df_methods = df_methods.rename(
3589 columns={"method_id": "Method ID",
3590 "num_pos_standards": "Pos (+) Standards",
3591 "num_neg_standards": "Neg (–) Standards",
3592 "msdial_config_id": "MS-DIAL Config"})
3593
3594 df_methods = df_methods[["Method ID", "Pos (+) Standards", "Neg (–) Standards", "MS-DIAL Config"]]
3595
3596 methods_table = dbc.Table.from_dataframe(df_methods, striped=True, hover=True)
3597
3598 # Update dropdown
3599 dropdown_options = []
3600 for method in df_methods["Method ID"].astype(str).tolist():
3601 dropdown_options.append({"label": method, "value": method})
3602
3603 return methods_table, dropdown_options, dropdown_options, None, method_added
3604
3605 else:
3606 raise PreventUpdate
3607
3608
3609@app.callback(Output("chromatography-removed", "data"),
3610 Input("remove-chromatography-method-button", "n_clicks"),
3611 State("select-istd-chromatography-dropdown", "value"), prevent_initial_call=True)
3612def remove_chromatography_method(button_click, chromatography):
3613
3614 """
3615 Remove chromatography method from database
3616 """
3617
3618 if chromatography is not None:
3619 db.remove_chromatography_method(chromatography)
3620 return "Removed"
3621
3622 else:
3623 return ""
3624
3625
3626@app.callback(Output("chromatography-addition-alert", "is_open"),
3627 Output("chromatography-addition-alert", "children"),
3628 Input("chromatography-added", "data"))
3629def show_alert_on_chromatography_addition(chromatography_added):
3630
3631 """
3632 UI feedback for adding a chromatography method
3633 """
3634
3635 if chromatography_added is not None:
3636 if chromatography_added == "Added":
3637 return True, "The chromatography method was added successfully."
3638
3639 return False, None
3640
3641
3642@app.callback(Output("chromatography-removal-alert", "is_open"),
3643 Output("chromatography-removal-alert", "children"),
3644 Input("chromatography-removed", "data"))
3645def show_alert_on_chromatography_addition(chromatography_removed):
3646
3647 """
3648 UI feedback for removing a chromatography method
3649 """
3650
3651 if chromatography_removed is not None:
3652 if chromatography_removed == "Removed":
3653 return True, "The selected chromatography method was removed."
3654
3655 return False, None
3656
3657
3658@app.callback(Output("msp-save-changes-button", "children"),
3659 Input("select-istd-chromatography-dropdown", "value"),
3660 Input("select-istd-polarity-dropdown", "value"))
3661def add_msp_to_chromatography_button_feedback(chromatography, polarity):
3662
3663 """
3664 "Save changes" button UI feedback for Settings > Internal Standards
3665 """
3666
3667 if chromatography is not None and polarity is not None:
3668 return "Add MSP to " + chromatography + " " + polarity
3669 else:
3670 return "Add MSP"
3671
3672
3673@app.callback(Output("add-istd-msp-text-field", "value"),
3674 Input("add-istd-msp-button", "filename"), prevent_intitial_call=True)
3675def bio_standard_msp_text_field_feedback(filename):
3676
3677 """
3678 UI feedback for selecting an MSP to save for a chromatography method
3679 """
3680
3681 return filename
3682
3683
3684@app.callback(Output("istd-msp-added", "data"),
3685 Input("msp-save-changes-button", "n_clicks"),
3686 State("add-istd-msp-button", "contents"),
3687 State("add-istd-msp-button", "filename"),
3688 State("select-istd-chromatography-dropdown", "value"),
3689 State("select-istd-polarity-dropdown", "value"), prevent_initial_call=True)
3690def capture_uploaded_istd_msp(button_click, contents, filename, chromatography, polarity):
3691
3692 """
3693 In Settings > Internal Standards, captures contents of uploaded MSP file and calls add_msp_to_database()
3694 """
3695
3696 if contents is not None and chromatography is not None and polarity is not None:
3697
3698 # Decode file contents
3699 content_type, content_string = contents.split(",")
3700 decoded = base64.b64decode(content_string)
3701 file = io.StringIO(decoded.decode("utf-8"))
3702
3703 # Add identification file to database
3704 if button_click is not None and chromatography is not None and polarity is not None:
3705 if filename.endswith(".msp"):
3706 db.add_msp_to_database(file, chromatography, polarity) # Parse MSP files
3707 elif filename.endswith(".csv") or filename.endswith(".txt"):
3708 db.add_csv_to_database(file, chromatography, polarity) # Parse CSV files
3709 return "Success! " + filename + " has been added to " + chromatography + " " + polarity + "."
3710 else:
3711 return "Error"
3712
3713 return "Ready"
3714
3715 # Update dummy dcc.Store object to update chromatography methods table
3716 return None
3717
3718
3719@app.callback(Output("chromatography-msp-success-alert", "is_open"),
3720 Output("chromatography-msp-success-alert", "children"),
3721 Output("chromatography-msp-error-alert", "is_open"),
3722 Output("chromatography-msp-error-alert", "children"),
3723 Input("istd-msp-added", "data"), prevent_initial_call=True)
3724def ui_feedback_for_adding_msp_to_chromatography(msp_added):
3725
3726 """
3727 UI feedback for adding an MSP to a chromatography method
3728 """
3729
3730 if msp_added is not None:
3731 if "Success" in msp_added:
3732 return True, msp_added, False, ""
3733 elif msp_added == "Error":
3734 return False, "", True, "Error: Please select a chromatography and polarity."
3735 else:
3736 return False, "", False, ""
3737
3738
3739@app.callback(Output("msdial-directory", "value"),
3740 Input("file-explorer-select-button", "n_clicks"),
3741 Input("settings-modal", "is_open"),
3742 State("selected-msdial-folder", "data"),
3743 Input("google-drive-sync-update", "data"))
3744def get_msdial_directory(select_folder_button, settings_modal_is_open, selected_folder, sync_update):
3745
3746 """
3747 Returns (previously inputted by user) location of MS-DIAL directory
3748 """
3749
3750 selected_component = ctx.triggered_id
3751
3752 if selected_component == "file-explorer-select-button":
3753 return selected_folder
3754
3755 if db.is_valid():
3756 return db.get_msdial_directory()
3757 else:
3758 raise PreventUpdate
3759
3760
3761@app.callback(Output("msdial-directory-saved", "data"),
3762 Input("msdial-folder-save-button", "n_clicks"),
3763 State("msdial-directory", "value"), prevent_initial_call=True)
3764def update_msdial_directory(button_click, msdial_directory):
3765
3766 """
3767 Updates MS-DIAL directory
3768 """
3769
3770 if msdial_directory is not None:
3771 if os.path.exists(msdial_directory):
3772 db.update_msdial_directory(msdial_directory)
3773 return "Success"
3774 else:
3775 return "Does not exist"
3776 else:
3777 return "Error"
3778
3779
3780@app.callback(Output("msdial-directory-saved-alert", "is_open"),
3781 Output("msdial-directory-saved-alert", "children"),
3782 Output("msdial-directory-saved-alert", "color"),
3783 Input("msdial-directory-saved", "data"), prevent_initial_call=True)
3784def ui_alert_for_msdial_directory_save(msdial_folder_save_result):
3785
3786 """
3787 Displays alert on MS-DIAL directory update
3788 """
3789
3790 if msdial_folder_save_result is not None:
3791 if msdial_folder_save_result == "Success":
3792 return True, "The MS-DIAL location was successfully saved.", "success"
3793 elif msdial_folder_save_result == "Does not exist":
3794 return True, "Error: This directory does not exist on your computer.", "danger"
3795 else:
3796 return True, "Error: Could not set MS-DIAL directory.", "danger"
3797
3798
3799@app.callback(Output("msdial-config-added", "data"),
3800 Output("add-msdial-configuration-text-field", "value"),
3801 Input("add-msdial-configuration-button", "n_clicks"),
3802 State("add-msdial-configuration-text-field", "value"), prevent_initial_call=True)
3803def add_msdial_configuration(button_click, msdial_config_id):
3804
3805 """
3806 Adds new MS-DIAL configuration to the database
3807 """
3808
3809 if msdial_config_id is not None:
3810 db.add_msdial_configuration(msdial_config_id)
3811 return "Added", None
3812 else:
3813 return "", None
3814
3815
3816@app.callback(Output("msdial-config-removed", "data"),
3817 Input("remove-config-button", "n_clicks"),
3818 State("msdial-configs-dropdown", "value"), prevent_initial_call=True)
3819def delete_msdial_configuration(button_click, msdial_config_id):
3820
3821 """
3822 Removes dropdown-selected MS-DIAL configuration from database
3823 """
3824
3825 if msdial_config_id is not None:
3826 if msdial_config_id != "Default":
3827 db.remove_msdial_configuration(msdial_config_id)
3828 return "Removed"
3829 else:
3830 return "Cannot remove"
3831 else:
3832 return ""
3833
3834
3835@app.callback(Output("msdial-configs-dropdown", "options"),
3836 Output("msdial-configs-dropdown", "value"),
3837 Input("on-page-load", "data"),
3838 Input("msdial-config-added", "data"),
3839 Input("msdial-config-removed", "data"),
3840 Input("google-drive-sync-update", "data"))
3841def get_msdial_configs_for_dropdown(on_page_load, on_config_added, on_config_removed, sync_update):
3842
3843 """
3844 Retrieves list of user-created configurations of MS-DIAL parameters from database
3845 """
3846
3847 if db.is_valid():
3848
3849 # Get MS-DIAL configurations from database
3850 msdial_configurations = db.get_msdial_configurations()
3851
3852 # Create and return options for dropdown
3853 config_options = []
3854
3855 for config in msdial_configurations:
3856 config_options.append({"label": config, "value": config})
3857
3858 return config_options, "Default"
3859
3860 else:
3861 raise PreventUpdate
3862
3863
3864@app.callback(Output("msdial-config-addition-alert", "is_open"),
3865 Output("msdial-config-addition-alert", "children"),
3866 Output("msdial-config-addition-alert", "color"),
3867 Input("msdial-config-added", "data"), prevent_initial_call=True)
3868def show_alert_on_msdial_config_addition(config_added):
3869
3870 """
3871 UI feedback on MS-DIAL configuration addition
3872 """
3873
3874 if config_added is not None:
3875 if config_added == "Added":
3876 return True, "Success! New MS-DIAL configuration added.", "success"
3877
3878 return False, None, "success"
3879
3880
3881@app.callback(Output("msdial-config-removal-alert", "is_open"),
3882 Output("msdial-config-removal-alert", "children"),
3883 Output("msdial-config-removal-alert", "color"),
3884 Input("msdial-config-removed", "data"),
3885 State("msdial-configs-dropdown", "value"), prevent_initial_call=True)
3886def show_alert_on_msdial_config_removal(config_removed, selected_config):
3887
3888 """
3889 UI feedback on MS-DIAL configuration removal
3890 """
3891
3892 if config_removed is not None:
3893 if config_removed == "Removed":
3894 message = "The selected MS-DIAL configuration was deleted."
3895 color = "primary"
3896 if selected_config == "Default":
3897 message = "Error: The default configuration cannot be deleted."
3898 color = "danger"
3899 return True, message, color
3900 else:
3901 return False, "", "danger"
3902
3903
3904@app.callback(Output("retention-time-begin", "value"),
3905 Output("retention-time-end", "value"),
3906 Output("mass-range-begin", "value"),
3907 Output("mass-range-end", "value"),
3908 Output("ms1-centroid-tolerance", "value"),
3909 Output("ms2-centroid-tolerance", "value"),
3910 Output("select-smoothing-dropdown", "value"),
3911 Output("smoothing-level", "value"),
3912 Output("min-peak-width", "value"),
3913 Output("min-peak-height", "value"),
3914 Output("mass-slice-width", "value"),
3915 Output("post-id-rt-tolerance", "value"),
3916 Output("post-id-mz-tolerance", "value"),
3917 Output("post-id-score-cutoff", "value"),
3918 Output("alignment-rt-tolerance", "value"),
3919 Output("alignment-mz-tolerance", "value"),
3920 Output("alignment-rt-factor", "value"),
3921 Output("alignment-mz-factor", "value"),
3922 Output("peak-count-filter", "value"),
3923 Output("qc-at-least-filter-dropdown", "value"),
3924 Input("msdial-configs-dropdown", "value"),
3925 Input("msdial-parameters-saved", "data"),
3926 Input("msdial-parameters-reset", "data"), prevent_initial_call=True)
3927def get_msdial_parameters_for_config(msdial_config_id, on_parameters_saved, on_parameters_reset):
3928
3929 """
3930 In Settings > MS-DIAL parameters, fills text fields with placeholders
3931 of current parameter values stored in the database.
3932 """
3933
3934 return db.get_msdial_configuration_parameters(msdial_config_id)
3935
3936
3937@app.callback(Output("msdial-parameters-saved", "data"),
3938 Input("save-changes-msdial-parameters-button", "n_clicks"),
3939 State("msdial-configs-dropdown", "value"),
3940 State("retention-time-begin", "value"),
3941 State("retention-time-end", "value"),
3942 State("mass-range-begin", "value"),
3943 State("mass-range-end", "value"),
3944 State("ms1-centroid-tolerance", "value"),
3945 State("ms2-centroid-tolerance", "value"),
3946 State("select-smoothing-dropdown", "value"),
3947 State("smoothing-level", "value"),
3948 State("mass-slice-width", "value"),
3949 State("min-peak-width", "value"),
3950 State("min-peak-height", "value"),
3951 State("post-id-rt-tolerance", "value"),
3952 State("post-id-mz-tolerance", "value"),
3953 State("post-id-score-cutoff", "value"),
3954 State("alignment-rt-tolerance", "value"),
3955 State("alignment-mz-tolerance", "value"),
3956 State("alignment-rt-factor", "value"),
3957 State("alignment-mz-factor", "value"),
3958 State("peak-count-filter", "value"),
3959 State("qc-at-least-filter-dropdown", "value"), prevent_initial_call=True)
3960def write_msdial_parameters_to_database(button_clicks, config_name, rt_begin, rt_end, mz_begin, mz_end,
3961 ms1_centroid_tolerance, ms2_centroid_tolerance, smoothing_method, smoothing_level, mass_slice_width, min_peak_width,
3962 min_peak_height, post_id_rt_tolerance, post_id_mz_tolerance, post_id_score_cutoff, alignment_rt_tolerance,
3963 alignment_mz_tolerance, alignment_rt_factor, alignment_mz_factor, peak_count_filter, qc_at_least_filter):
3964
3965 """
3966 Saves MS-DIAL parameters to respective configuration in database
3967 """
3968
3969 db.update_msdial_configuration(config_name, rt_begin, rt_end, mz_begin, mz_end, ms1_centroid_tolerance,
3970 ms2_centroid_tolerance, smoothing_method, smoothing_level, mass_slice_width, min_peak_width, min_peak_height,
3971 post_id_rt_tolerance, post_id_mz_tolerance, post_id_score_cutoff, alignment_rt_tolerance, alignment_mz_tolerance,
3972 alignment_rt_factor, alignment_mz_factor, peak_count_filter, qc_at_least_filter)
3973
3974 return "Saved"
3975
3976
3977@app.callback(Output("msdial-parameters-reset", "data"),
3978 Input("reset-default-msdial-parameters-button", "n_clicks"),
3979 State("msdial-configs-dropdown", "value"), prevent_initial_call=True)
3980def reset_msdial_parameters_to_default(button_clicks, msdial_config_name):
3981
3982 """
3983 Resets parameters for selected MS-DIAL configuration to default settings
3984 """
3985
3986 db.update_msdial_configuration(msdial_config_name, 0, 100, 0, 2000, 0.008, 0.01, "LinearWeightedMovingAverage",
3987 3, 3, 35000, 0.1, 0.1, 0.008, 85, 0.05, 0.008, 0.5, 0.5, 0, "True")
3988
3989 return "Reset"
3990
3991
3992@app.callback(Output("msdial-parameters-success-alert", "is_open"),
3993 Output("msdial-parameters-success-alert", "children"),
3994 Input("msdial-parameters-saved", "data"), prevent_initial_call=True)
3995def show_alert_on_parameter_save(parameters_saved):
3996
3997 """
3998 UI feedback for saving changes to MS-DIAL parameters
3999 """
4000
4001 if parameters_saved is not None:
4002 if parameters_saved == "Saved":
4003 return True, "Your changes were successfully saved."
4004
4005
4006@app.callback(Output("msdial-parameters-reset-alert", "is_open"),
4007 Output("msdial-parameters-reset-alert", "children"),
4008 Input("msdial-parameters-reset", "data"), prevent_initial_call=True)
4009def show_alert_on_parameter_reset(parameters_reset):
4010
4011 """
4012 UI feedback for resetting MS-DIAL parameters in a configuration
4013 """
4014
4015 if parameters_reset is not None:
4016 if parameters_reset == "Reset":
4017 return True, "Your configuration has been reset to its default settings."
4018
4019
4020@app.callback(Output("qc-config-added", "data"),
4021 Output("add-qc-configuration-text-field", "value"),
4022 Input("add-qc-configuration-button", "n_clicks"),
4023 State("add-qc-configuration-text-field", "value"), prevent_initial_call=True)
4024def add_qc_configuration(button_click, qc_config_id):
4025
4026 """
4027 Adds new QC configuration to the database
4028 """
4029
4030 if qc_config_id is not None:
4031 db.add_qc_configuration(qc_config_id)
4032 return "Added", None
4033 else:
4034 return "", None
4035
4036
4037@app.callback(Output("qc-config-removed", "data"),
4038 Input("remove-qc-config-button", "n_clicks"),
4039 State("qc-configs-dropdown", "value"), prevent_initial_call=True)
4040def delete_qc_configuration(button_click, qc_config_id):
4041
4042 """
4043 Removes dropdown-selected QC configuration from database
4044 """
4045
4046 if qc_config_id is not None:
4047 if qc_config_id != "Default":
4048 db.remove_qc_configuration(qc_config_id)
4049 return "Removed"
4050 else:
4051 return "Cannot remove"
4052 else:
4053 return ""
4054
4055
4056@app.callback(Output("qc-configs-dropdown", "options"),
4057 Output("qc-configs-dropdown", "value"),
4058 Input("on-page-load", "data"),
4059 Input("qc-config-added", "data"),
4060 Input("qc-config-removed", "data"),
4061 Input("google-drive-sync-update", "data"))
4062def get_qc_configs_for_dropdown(on_page_load, qc_config_added, qc_config_removed, sync_upda