ms_autoqc.DashWebApp
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_update): 4063 4064 """ 4065 Retrieves list of user-created configurations of QC parameters from database 4066 """ 4067 4068 if db.is_valid(): 4069 4070 # Get QC configurations from database 4071 qc_configurations = db.get_qc_configurations_list() 4072 4073 # Create and return options for dropdown 4074 config_options = [] 4075 4076 for config in qc_configurations: 4077 config_options.append({"label": config, "value": config}) 4078 4079 return config_options, "Default" 4080 4081 else: 4082 raise PreventUpdate 4083 4084 4085@app.callback(Output("qc-config-addition-alert", "is_open"), 4086 Output("qc-config-addition-alert", "children"), 4087 Output("qc-config-addition-alert", "color"), 4088 Input("qc-config-added", "data"), prevent_initial_call=True) 4089def show_alert_on_qc_config_addition(config_added): 4090 4091 """ 4092 UI feedback on QC configuration addition 4093 """ 4094 4095 if config_added is not None: 4096 if config_added == "Added": 4097 return True, "Success! New QC configuration added.", "success" 4098 4099 return False, None, "success" 4100 4101 4102@app.callback(Output("qc-config-removal-alert", "is_open"), 4103 Output("qc-config-removal-alert", "children"), 4104 Output("qc-config-removal-alert", "color"), 4105 Input("qc-config-removed", "data"), 4106 State("qc-configs-dropdown", "value"), prevent_initial_call=True) 4107def show_alert_on_qc_config_removal(config_removed, selected_config): 4108 4109 """ 4110 UI feedback on QC configuration removal 4111 """ 4112 4113 if config_removed is not None: 4114 if config_removed == "Removed": 4115 message = "The selected QC configuration was deleted." 4116 color = "primary" 4117 if selected_config == "Default": 4118 message = "Error: The default configuration cannot be deleted." 4119 color = "danger" 4120 return True, message, color 4121 else: 4122 return False, "", "danger" 4123 4124 4125@app.callback(Output("intensity-dropouts-cutoff", "value"), 4126 Output("library-rt-shift-cutoff", "value"), 4127 Output("in-run-rt-shift-cutoff", "value"), 4128 Output("library-mz-shift-cutoff", "value"), 4129 Output("intensity-cutoff-enabled", "value"), 4130 Output("library-rt-shift-cutoff-enabled", "value"), 4131 Output("in-run-rt-shift-cutoff-enabled", "value"), 4132 Output("library-mz-shift-cutoff-enabled", "value"), 4133 Input("qc-configs-dropdown", "value"), 4134 Input("qc-parameters-saved", "data"), 4135 Input("qc-parameters-reset", "data"), prevent_initial_call=True) 4136def get_qc_parameters_for_config(qc_config_name, on_parameters_saved, on_parameters_reset): 4137 4138 """ 4139 In Settings > QC Configurations, fills text fields with placeholders 4140 of current parameter values stored in the database. 4141 """ 4142 4143 selected_config = db.get_qc_configuration_parameters(config_name=qc_config_name) 4144 return tuple(selected_config.to_records(index=False)[0]) 4145 4146 4147@app.callback(Output("qc-parameters-saved", "data"), 4148 Input("save-changes-qc-parameters-button", "n_clicks"), 4149 State("qc-configs-dropdown", "value"), 4150 State("intensity-dropouts-cutoff", "value"), 4151 State("library-rt-shift-cutoff", "value"), 4152 State("in-run-rt-shift-cutoff", "value"), 4153 State("library-mz-shift-cutoff", "value"), 4154 State("intensity-cutoff-enabled", "value"), 4155 State("library-rt-shift-cutoff-enabled", "value"), 4156 State("in-run-rt-shift-cutoff-enabled", "value"), 4157 State("library-mz-shift-cutoff-enabled", "value"), prevent_initial_call=True) 4158def write_qc_parameters_to_database(button_clicks, qc_config_name, intensity_dropouts_cutoff, library_rt_shift_cutoff, 4159 in_run_rt_shift_cutoff, library_mz_shift_cutoff, intensity_enabled, library_rt_enabled, in_run_rt_enabled, library_mz_enabled): 4160 4161 """ 4162 Saves QC parameters to respective configuration in database 4163 """ 4164 4165 db.update_qc_configuration(qc_config_name, intensity_dropouts_cutoff, library_rt_shift_cutoff, in_run_rt_shift_cutoff, 4166 library_mz_shift_cutoff, intensity_enabled, library_rt_enabled, in_run_rt_enabled, library_mz_enabled) 4167 return "Saved" 4168 4169 4170@app.callback(Output("qc-parameters-reset", "data"), 4171 Input("reset-default-qc-parameters-button", "n_clicks"), 4172 State("qc-configs-dropdown", "value"), prevent_initial_call=True) 4173def reset_msdial_parameters_to_default(button_clicks, qc_config_name): 4174 4175 """ 4176 Resets parameters for selected QC configuration to default settings 4177 """ 4178 4179 db.update_qc_configuration(config_name=qc_config_name, intensity_dropouts_cutoff=4, 4180 library_rt_shift_cutoff=0.1, in_run_rt_shift_cutoff=0.05, library_mz_shift_cutoff=0.005, 4181 intensity_enabled=True, library_rt_enabled=True, in_run_rt_enabled=True, library_mz_enabled=True) 4182 return "Reset" 4183 4184 4185@app.callback(Output("qc-parameters-success-alert", "is_open"), 4186 Output("qc-parameters-success-alert", "children"), 4187 Input("qc-parameters-saved", "data"), prevent_initial_call=True) 4188def show_alert_on_qc_parameter_save(parameters_saved): 4189 4190 """ 4191 UI feedback for saving changes to QC parameters 4192 """ 4193 4194 if parameters_saved is not None: 4195 if parameters_saved == "Saved": 4196 return True, "Your changes were successfully saved." 4197 4198 4199@app.callback(Output("qc-parameters-reset-alert", "is_open"), 4200 Output("qc-parameters-reset-alert", "children"), 4201 Input("qc-parameters-reset", "data"), prevent_initial_call=True) 4202def show_alert_on_qc_parameter_reset(parameters_reset): 4203 4204 """ 4205 UI feedback for resetting QC parameters in a configuration 4206 """ 4207 4208 if parameters_reset is not None: 4209 if parameters_reset == "Reset": 4210 return True, "Your QC configuration has been reset to its default settings." 4211 4212 4213@app.callback(Output("select-bio-standard-dropdown", "options"), 4214 Output("biological-standards-table", "children"), 4215 Input("on-page-load", "data"), 4216 Input("bio-standard-added", "data"), 4217 Input("bio-standard-removed", "data"), 4218 Input("chromatography-added", "data"), 4219 Input("chromatography-removed", "data"), 4220 Input("bio-msp-added", "data"), 4221 Input("bio-standard-msdial-config-added", "data"), 4222 Input("google-drive-sync-update", "data")) 4223def get_biological_standards(on_page_load, on_standard_added, on_standard_removed, on_method_added, on_method_removed, 4224 on_msp_added, on_bio_standard_msdial_config_added, sync_update): 4225 4226 """ 4227 Populates dropdown and table of biological standards 4228 """ 4229 4230 if db.is_valid(): 4231 4232 # Populate dropdown 4233 dropdown_options = [] 4234 for biological_standard in db.get_biological_standards_list(): 4235 dropdown_options.append({"label": biological_standard, "value": biological_standard}) 4236 4237 # Populate table 4238 df_biological_standards = db.get_biological_standards() 4239 4240 # DataFrame refactoring 4241 df_biological_standards = df_biological_standards.rename( 4242 columns={"name": "Name", 4243 "identifier": "Identifier", 4244 "chromatography": "Method ID", 4245 "num_pos_features": "Pos (+) Metabolites", 4246 "num_neg_features": "Neg (–) Metabolites", 4247 "msdial_config_id": "MS-DIAL Config"}) 4248 4249 df_biological_standards = df_biological_standards[ 4250 ["Name", "Identifier", "Method ID", "Pos (+) Metabolites", "Neg (–) Metabolites", "MS-DIAL Config"]] 4251 4252 biological_standards_table = dbc.Table.from_dataframe(df_biological_standards, striped=True, hover=True) 4253 4254 return dropdown_options, biological_standards_table 4255 4256 else: 4257 raise PreventUpdate 4258 4259 4260@app.callback(Output("bio-standard-added", "data"), 4261 Output("add-bio-standard-text-field", "value"), 4262 Output("add-bio-standard-identifier-text-field", "value"), 4263 Input("add-bio-standard-button", "n_clicks"), 4264 State("add-bio-standard-text-field", "value"), 4265 State("add-bio-standard-identifier-text-field", "value"), prevent_initial_call=True) 4266def add_biological_standard(button_click, name, identifier): 4267 4268 """ 4269 Adds biological standard to database 4270 """ 4271 4272 if name is not None and identifier is not None: 4273 4274 if len(db.get_chromatography_methods()) == 0: 4275 return "Error 2", name, identifier 4276 else: 4277 db.add_biological_standard(name, identifier) 4278 4279 return "Added", None, None 4280 4281 else: 4282 return "Error 1", name, identifier 4283 4284 4285@app.callback(Output("bio-standard-removed", "data"), 4286 Input("remove-bio-standard-button", "n_clicks"), 4287 State("select-bio-standard-dropdown", "value"), prevent_initial_call=True) 4288def remove_biological_standard(button_click, biological_standard_name): 4289 4290 """ 4291 Removes biological standard (and all corresponding MSPs) in the database 4292 """ 4293 4294 if biological_standard_name is not None: 4295 db.remove_biological_standard(biological_standard_name) 4296 return "Deleted " + biological_standard_name + " and all corresponding MSP files." 4297 else: 4298 return "Error" 4299 4300 4301@app.callback(Output("add-bio-msp-text-field", "value"), 4302 Input("add-bio-msp-button", "filename"), prevent_intitial_call=True) 4303def bio_standard_msp_text_field_ui_callback(filename): 4304 4305 """ 4306 UI feedback for selecting an MSP to save for a biological standard 4307 """ 4308 4309 return filename 4310 4311 4312@app.callback(Output("bio-msp-added", "data"), 4313 Input("bio-standard-save-changes-button", "n_clicks"), 4314 State("add-bio-msp-button", "contents"), 4315 State("add-bio-msp-button", "filename"), 4316 State("select-bio-chromatography-dropdown", "value"), 4317 State("select-bio-polarity-dropdown", "value"), 4318 State("select-bio-standard-dropdown", "value"), prevent_initial_call=True) 4319def capture_uploaded_bio_msp(button_click, contents, filename, chromatography, polarity, bio_standard): 4320 4321 """ 4322 In Settings > Biological Standards, captures contents of uploaded MSP file and calls add_msp_to_database(). 4323 """ 4324 4325 if contents is not None and chromatography is not None and polarity is not None: 4326 4327 # Decode file contents 4328 content_type, content_string = contents.split(",") 4329 decoded = base64.b64decode(content_string) 4330 file = io.StringIO(decoded.decode("utf-8")) 4331 4332 # Add MSP file to database 4333 if button_click is not None and chromatography is not None and polarity is not None and bio_standard is not None: 4334 if filename.endswith(".msp"): 4335 db.add_msp_to_database(file, chromatography, polarity, bio_standard=bio_standard) 4336 4337 # Check whether MSP was added successfully 4338 if bio_standard in db.get_biological_standards_list(): 4339 return "Success! Added " + filename + " to " + bio_standard + " in " + chromatography + " " + polarity + "." 4340 else: 4341 return "Error 1" 4342 else: 4343 return "Error 2" 4344 4345 return "Ready" 4346 4347 # Update dummy dcc.Store object to update chromatography methods table 4348 return "" 4349 4350 4351@app.callback(Output("bio-standard-addition-alert", "is_open"), 4352 Output("bio-standard-addition-alert", "children"), 4353 Output("bio-standard-addition-alert", "color"), 4354 Input("bio-standard-added", "data"), prevent_initial_call=True) 4355def show_alert_on_bio_standard_addition(bio_standard_added): 4356 4357 """ 4358 UI feedback for adding a biological standard 4359 """ 4360 4361 if bio_standard_added is not None: 4362 if bio_standard_added == "Added": 4363 return True, "Success! New biological standard added.", "success" 4364 elif bio_standard_added == "Error 2": 4365 return True, "Error: Please add a chromatography method first.", "danger" 4366 4367 return False, None, None 4368 4369 4370@app.callback(Output("bio-standard-removal-alert", "is_open"), 4371 Output("bio-standard-removal-alert", "children"), 4372 Input("bio-standard-removed", "data"), prevent_initial_call=True) 4373def show_alert_on_bio_standard_removal(bio_standard_removed): 4374 4375 """ 4376 UI feedback for removing a biological standard 4377 """ 4378 4379 if bio_standard_removed is not None: 4380 if "Deleted" in bio_standard_removed: 4381 return True, bio_standard_removed 4382 4383 return False, None 4384 4385 4386@app.callback(Output("bio-msp-success-alert", "is_open"), 4387 Output("bio-msp-success-alert", "children"), 4388 Output("bio-msp-error-alert", "is_open"), 4389 Output("bio-msp-error-alert", "children"), 4390 Input("bio-msp-added", "data"), prevent_initial_call=True) 4391def ui_feedback_for_adding_msp_to_bio_standard(bio_standard_msp_added): 4392 4393 """ 4394 UI feedback for adding an MSP to a biological standard 4395 """ 4396 4397 if bio_standard_msp_added is not None: 4398 if "Success" in bio_standard_msp_added: 4399 return True, bio_standard_msp_added, False, "" 4400 elif bio_standard_msp_added == "Error 1": 4401 return False, "", True, "Error: Unable to add MSP to biological standard." 4402 elif bio_standard_msp_added == "Error 2": 4403 return False, "", True, "Error: Please select a biological standard, chromatography, and polarity first." 4404 else: 4405 return False, "", False, "" 4406 4407 4408@app.callback(Output("bio-standard-save-changes-button", "children"), 4409 Input("select-bio-chromatography-dropdown", "value"), 4410 Input("select-bio-polarity-dropdown", "value"), 4411 Input("select-bio-standard-dropdown", "value")) 4412def add_msp_to_bio_standard_button_feedback(chromatography, polarity, bio_standard): 4413 4414 """ 4415 "Save changes" button UI feedback for Settings > Biological Standards 4416 """ 4417 4418 if bio_standard is not None and chromatography is not None and polarity is not None: 4419 return "Add MSP to " + bio_standard + " in " + chromatography + " " + polarity 4420 elif bio_standard is not None: 4421 return "Added MSP to " + bio_standard 4422 else: 4423 return "Add MSP" 4424 4425 4426@app.callback(Output("bio-standard-msdial-configs-dropdown", "options"), 4427 Output("istd-msdial-configs-dropdown", "options"), 4428 Input("msdial-config-added", "data"), 4429 Input("msdial-config-removed", "data"), 4430 Input("google-drive-sync-update", "data")) 4431def populate_msdial_configs_for_biological_standard(msdial_config_added, msdial_config_removed, sync_update): 4432 4433 """ 4434 In Settings > Biological Standards, populates the MS-DIAL configurations dropdown 4435 """ 4436 4437 if db.is_valid(): 4438 4439 options = [] 4440 4441 for config in db.get_msdial_configurations(): 4442 options.append({"label": config, "value": config}) 4443 4444 return options, options 4445 4446 else: 4447 raise PreventUpdate 4448 4449 4450@app.callback(Output("bio-standard-msdial-config-added", "data"), 4451 Input("bio-standard-msdial-configs-button", "n_clicks"), 4452 State("select-bio-standard-dropdown", "value"), 4453 State("select-bio-chromatography-dropdown", "value"), 4454 State("bio-standard-msdial-configs-dropdown", "value"), prevent_initial_call=True) 4455def add_msdial_config_for_bio_standard(button_click, biological_standard, chromatography, config_id): 4456 4457 """ 4458 In Settings > Biological Standards, sets the MS-DIAL configuration to be used for chromatography 4459 """ 4460 4461 if biological_standard is not None and chromatography is not None and config_id is not None: 4462 db.update_msdial_config_for_bio_standard(biological_standard, chromatography, config_id) 4463 return "Added" 4464 else: 4465 return "" 4466 4467 4468@app.callback(Output("bio-config-success-alert", "is_open"), 4469 Output("bio-config-success-alert", "children"), 4470 Input("bio-standard-msdial-config-added", "data"), 4471 State("select-bio-standard-dropdown", "value"), 4472 State("select-bio-chromatography-dropdown", "value"), prevent_initial_call=True) 4473def ui_feedback_for_setting_msdial_config_for_bio_standard(config_added, bio_standard, chromatography): 4474 4475 """ 4476 In Settings > Biological Standards, provides an alert when MS-DIAL config is successfully set for biological standard 4477 """ 4478 4479 if config_added is not None: 4480 if config_added == "Added": 4481 message = "MS-DIAL parameter configuration saved successfully for " + bio_standard + " (" + chromatography + " method)." 4482 return True, message 4483 4484 return False, "" 4485 4486 4487@app.callback(Output("chromatography-msdial-config-added", "data"), 4488 Input("istd-msdial-configs-button", "n_clicks"), 4489 State("select-istd-chromatography-dropdown", "value"), 4490 State("istd-msdial-configs-dropdown", "value"), prevent_initial_call=True) 4491def add_msdial_config_for_chromatography(button_click, chromatography, config_id): 4492 4493 """ 4494 In Settings > Internal Standards, sets the MS-DIAL configuration to be used for processing samples 4495 """ 4496 4497 if chromatography is not None and config_id is not None: 4498 db.update_msdial_config_for_internal_standards(chromatography, config_id) 4499 return "Added" 4500 else: 4501 return "" 4502 4503 4504@app.callback(Output("istd-config-success-alert", "is_open"), 4505 Output("istd-config-success-alert", "children"), 4506 Input("chromatography-msdial-config-added", "data"), 4507 State("select-istd-chromatography-dropdown", "value"), prevent_initial_call=True) 4508def ui_feedback_for_setting_msdial_config_for_chromatography(config_added, chromatography): 4509 4510 """ 4511 In Settings > Internal Standards, provides an alert when MS-DIAL config is successfully set for a chromatography 4512 """ 4513 4514 if config_added is not None: 4515 if config_added == "Added": 4516 message = "MS-DIAL parameter configuration saved successfully for " + chromatography + "." 4517 return True, message 4518 4519 return False, "" 4520 4521 4522@app.callback(Output("setup-new-run-modal", "is_open"), 4523 Output("setup-new-run-button", "n_clicks"), 4524 Output("setup-new-run-modal-title", "children"), 4525 Input("setup-new-run-button", "n_clicks"), 4526 Input("start-run-monitor-modal", "is_open"), 4527 State("tabs", "value"), 4528 Input("data-acquisition-folder-button", "n_clicks"), 4529 Input("file-explorer-select-button", "n_clicks"), 4530 State("settings-modal", "is_open"), prevent_initial_call=True) 4531def toggle_new_run_modal(button_clicks, success, instrument_name, browse_folder_button, file_explorer_button, settings_modal_is_open): 4532 4533 """ 4534 Toggles modal for setting up AutoQC monitoring for a new instrument run 4535 """ 4536 4537 button = ctx.triggered_id 4538 4539 modal_title = "New QC Job – " + instrument_name 4540 4541 open_modal = True, 1, modal_title 4542 close_modal = False, 0, modal_title 4543 4544 if button == "data-acquisition-folder-button": 4545 return close_modal 4546 4547 elif button == "file-explorer-select-button": 4548 if settings_modal_is_open: 4549 return close_modal 4550 else: 4551 return open_modal 4552 4553 if not success and button_clicks != 0: 4554 return open_modal 4555 else: 4556 return close_modal 4557 4558 4559@app.callback(Output("start-run-chromatography-dropdown", "options"), 4560 Output("start-run-bio-standards-dropdown", "options"), 4561 Output("start-run-qc-configs-dropdown", "options"), 4562 Input("setup-new-run-button", "n_clicks"), prevent_initial_call=True) 4563def populate_options_for_new_run(button_click): 4564 4565 """ 4566 Populates dropdowns and checklists for Setup New MS-AutoQC Job page 4567 """ 4568 4569 chromatography_methods = [] 4570 biological_standards = [] 4571 qc_configurations = [] 4572 4573 for method in db.get_chromatography_methods_list(): 4574 chromatography_methods.append({"value": method, "label": method}) 4575 4576 for bio_standard in db.get_biological_standards_list(): 4577 biological_standards.append({"value": bio_standard, "label": bio_standard}) 4578 4579 for qc_configuration in db.get_qc_configurations_list(): 4580 qc_configurations.append({"value": qc_configuration, "label": qc_configuration}) 4581 4582 return chromatography_methods, biological_standards, qc_configurations 4583 4584 4585@app.callback(Output("sequence-path", "value"), 4586 Output("new-sequence", "data"), 4587 Input("sequence-upload-button", "contents"), 4588 State("sequence-upload-button", "filename"), prevent_initial_call=True) 4589def capture_uploaded_sequence(contents, filename): 4590 4591 """ 4592 Converts sequence CSV file to JSON string and stores in dcc.Store object 4593 """ 4594 4595 # Decode sequence file contents 4596 content_type, content_string = contents.split(",") 4597 decoded = base64.b64decode(content_string) 4598 sequence_file_contents = io.StringIO(decoded.decode("utf-8")) 4599 4600 # Get sequence file as JSON string 4601 sequence = qc.convert_sequence_to_json(sequence_file_contents) 4602 4603 # Update UI and store sequence JSON string 4604 return filename, sequence 4605 4606 4607@app.callback(Output("metadata-path", "value"), 4608 Output("new-metadata", "data"), 4609 Input("metadata-upload-button", "contents"), 4610 State("metadata-upload-button", "filename"), prevent_initial_call=True) 4611def capture_uploaded_metadata(contents, filename): 4612 4613 """ 4614 Converts metadata CSV file to JSON string and stores in dcc.Store object 4615 """ 4616 4617 # Decode metadata file contents 4618 content_type, content_string = contents.split(",") 4619 decoded = base64.b64decode(content_string) 4620 metadata_file_contents = io.StringIO(decoded.decode("utf-8")) 4621 4622 # Get metadata file as JSON string 4623 metadata = qc.convert_metadata_to_json(metadata_file_contents) 4624 4625 # Update UI and store metadata JSON string 4626 return filename, metadata 4627 4628 4629@app.callback(Output("monitor-new-run-button", "children"), 4630 Output("data-acquisition-path-title", "children"), 4631 Output("data-acquisition-path-form-text", "children"), 4632 Input("ms_autoqc-job-type", "value")) 4633def update_new_job_button_text(job_type): 4634 4635 """ 4636 Updates New MS-AutoQC Job form submit button based on job type 4637 """ 4638 4639 if job_type == "active": 4640 button_text = "Start monitoring instrument run" 4641 text_field_title = "Data acquisition path" 4642 form_text = "Please enter the folder path to which incoming raw data files will be saved." 4643 elif job_type == "completed": 4644 button_text = "Start QC processing data files" 4645 text_field_title = "Data file path" 4646 form_text = "Please enter the folder path where your data files are saved." 4647 4648 msconvert_valid = db.pipeline_valid(module="msconvert") 4649 msdial_valid = db.pipeline_valid(module="msdial") 4650 4651 if not msconvert_valid and not msdial_valid: 4652 button_text = "Error: MSConvert and MS-DIAL installations not found" 4653 if not msdial_valid: 4654 button_text = "Error: Could not locate MS-DIAL console app" 4655 if not msconvert_valid: 4656 button_text = "Error: Could not locate MSConvert installation" 4657 4658 return button_text, text_field_title, form_text 4659 4660 4661@app.callback(Output("instrument-run-id", "valid"), 4662 Output("instrument-run-id", "invalid"), 4663 Output("start-run-chromatography-dropdown", "valid"), 4664 Output("start-run-chromatography-dropdown", "invalid"), 4665 Output("start-run-qc-configs-dropdown", "valid"), 4666 Output("start-run-qc-configs-dropdown", "invalid"), 4667 Output("sequence-path", "valid"), 4668 Output("sequence-path", "invalid"), 4669 Output("metadata-path", "valid"), 4670 Output("metadata-path", "invalid"), 4671 Output("data-acquisition-folder-path", "valid"), 4672 Output("data-acquisition-folder-path", "invalid"), 4673 Input("instrument-run-id", "value"), 4674 Input("start-run-chromatography-dropdown", "value"), 4675 Input("start-run-bio-standards-dropdown", "value"), 4676 Input("start-run-qc-configs-dropdown", "value"), 4677 Input("sequence-upload-button", "contents"), 4678 State("sequence-upload-button", "filename"), 4679 Input("metadata-upload-button", "contents"), 4680 State("metadata-upload-button", "filename"), 4681 Input("data-acquisition-folder-path", "value"), 4682 State("instrument-run-id", "valid"), 4683 State("instrument-run-id", "invalid"), 4684 State("start-run-chromatography-dropdown", "valid"), 4685 State("start-run-chromatography-dropdown", "invalid"), 4686 State("start-run-qc-configs-dropdown", "valid"), 4687 State("start-run-qc-configs-dropdown", "invalid"), 4688 State("sequence-path", "valid"), 4689 State("sequence-path", "invalid"), 4690 State("metadata-path", "valid"), 4691 State("metadata-path", "invalid"), 4692 State("data-acquisition-folder-path", "valid"), 4693 State("data-acquisition-folder-path", "invalid"), 4694 State("tabs", "value"), prevent_initial_call=True) 4695def validation_feedback_for_new_run_setup_form(run_id, chromatography, bio_standards, qc_config, sequence_contents, 4696 sequence_filename, metadata_contents, metadata_filename, data_acquisition_path, run_id_valid, run_id_invalid, 4697 chromatography_valid, chromatography_invalid, qc_config_valid, qc_config_invalid, sequence_valid, sequence_invalid, 4698 metadata_valid, metadata_invalid, path_valid, path_invalid, instrument): 4699 4700 """ 4701 Extensive form validation and feedback for setting up a new MS-AutoQC job 4702 """ 4703 4704 # Instrument run ID validation 4705 if run_id is not None: 4706 4707 # Get run ID's for instrument 4708 run_ids = db.get_instrument_runs(instrument)["run_id"].astype(str).tolist() 4709 4710 # Check if run ID is unique 4711 if run_id not in run_ids: 4712 run_id_valid, run_id_invalid = True, False 4713 else: 4714 run_id_valid, run_id_invalid = False, True 4715 4716 # Chromatography validation 4717 if chromatography is not None: 4718 if qc.chromatography_valid(chromatography): 4719 chromatography_valid, chromatography_invalid = True, False 4720 else: 4721 chromatography_valid, chromatography_invalid = False, True 4722 4723 # Biological standard validation 4724 if bio_standards is not None: 4725 if qc.biological_standards_valid(chromatography, bio_standards): 4726 chromatography_valid, chromatography_invalid = True, False 4727 else: 4728 chromatography_valid, chromatography_invalid = False, True 4729 elif chromatography is not None: 4730 if qc.chromatography_valid(chromatography): 4731 chromatography_valid, chromatography_invalid = True, False 4732 else: 4733 chromatography_valid, chromatography_invalid = False, True 4734 4735 # QC configuration validation 4736 if qc_config is not None: 4737 qc_config_valid = True 4738 4739 # Instrument sequence file validation 4740 if sequence_contents is not None: 4741 4742 content_type, content_string = sequence_contents.split(",") 4743 decoded = base64.b64decode(content_string) 4744 sequence_contents = io.StringIO(decoded.decode("utf-8")) 4745 4746 if qc.sequence_is_valid(sequence_filename, sequence_contents): 4747 sequence_valid, sequence_invalid = True, False 4748 else: 4749 sequence_valid, sequence_invalid = False, True 4750 4751 # Metadata file validation 4752 if metadata_contents is not None: 4753 4754 content_type, content_string = metadata_contents.split(",") 4755 decoded = base64.b64decode(content_string) 4756 metadata_contents = io.StringIO(decoded.decode("utf-8")) 4757 4758 if qc.metadata_is_valid(metadata_filename, metadata_contents): 4759 metadata_valid, metadata_invalid = True, False 4760 else: 4761 metadata_valid, metadata_invalid = False, True 4762 4763 # Validate that data acquisition path exists 4764 if data_acquisition_path is not None: 4765 if os.path.exists(data_acquisition_path): 4766 path_valid, path_invalid = True, False 4767 else: 4768 path_valid, path_invalid = False, True 4769 4770 return run_id_valid, run_id_invalid, chromatography_valid, chromatography_invalid, qc_config_valid, qc_config_invalid, \ 4771 sequence_valid, sequence_invalid, metadata_valid, metadata_invalid, path_valid, path_invalid 4772 4773 4774@app.callback(Output("monitor-new-run-button", "disabled"), 4775 Input("instrument-run-id", "valid"), 4776 Input("start-run-chromatography-dropdown", "valid"), 4777 Input("start-run-qc-configs-dropdown", "valid"), 4778 Input("sequence-path", "valid"), 4779 Input("data-acquisition-folder-path", "valid"), prevent_initial_call=True) 4780def enable_new_autoqc_job_button(run_id_valid, chromatography_valid, qc_config_valid, sequence_valid, path_valid): 4781 4782 """ 4783 Enables "submit" button for New MS-AutoQC Job form 4784 """ 4785 4786 if run_id_valid and chromatography_valid and qc_config_valid and sequence_valid and path_valid and db.pipeline_valid(): 4787 return False 4788 else: 4789 return True 4790 4791 4792@app.callback(Output("start-run-monitor-modal", "is_open"), 4793 Output("new-job-error-modal", "is_open"), 4794 Input("monitor-new-run-button", "n_clicks"), 4795 State("instrument-run-id", "value"), 4796 State("tabs", "value"), 4797 State("start-run-chromatography-dropdown", "value"), 4798 State("start-run-bio-standards-dropdown", "value"), 4799 State("new-sequence", "data"), 4800 State("new-metadata", "data"), 4801 State("data-acquisition-folder-path", "value"), 4802 State("start-run-qc-configs-dropdown", "value"), 4803 State("ms_autoqc-job-type", "value"), prevent_initial_call=True) 4804def new_autoqc_job_setup(button_clicks, run_id, instrument_id, chromatography, bio_standards, sequence, metadata, 4805 acquisition_path, qc_config_id, job_type): 4806 4807 """ 4808 This callback initiates the following: 4809 1. Writing a new instrument run to the database 4810 2. Generate parameters files for MS-DIAL processing 4811 3a. Initializing run monitoring at the given directory for an active run, or 4812 3b. Iterating through and processing data files for a completed run 4813 """ 4814 4815 if run_id not in db.get_instrument_runs(instrument_id, as_list=True): 4816 4817 # Write a new instrument run to the database 4818 db.insert_new_run(run_id, instrument_id, chromatography, bio_standards, acquisition_path, sequence, metadata, qc_config_id, job_type) 4819 4820 # Get MSPs and generate parameters files for MS-DIAL processing 4821 for polarity in ["Positive", "Negative"]: 4822 4823 # Generate parameters files for processing samples 4824 msp_file_path = db.get_msp_file_path(chromatography, polarity) 4825 db.generate_msdial_parameters_file(chromatography, polarity, msp_file_path) 4826 4827 # Generate parameters files for processing each biological standard 4828 if bio_standards is not None: 4829 for bio_standard in bio_standards: 4830 msp_file_path = db.get_msp_file_path(chromatography, polarity, bio_standard) 4831 db.generate_msdial_parameters_file(chromatography, polarity, msp_file_path, bio_standard) 4832 4833 # Start AcquisitionListener process in the background 4834 process = psutil.Popen(["py", "AcquisitionListener.py", acquisition_path, instrument_id, run_id]) 4835 db.store_pid(instrument_id, run_id, process.pid) 4836 4837 # Upload database to Google Drive 4838 if db.is_instrument_computer() and db.sync_is_enabled(): 4839 db.upload_database(instrument_id) 4840 4841 return True, False 4842 4843 4844@app.callback(Output("file-explorer-modal", "is_open"), 4845 Input("data-acquisition-folder-button", "n_clicks"), 4846 Input("file-explorer-select-button", "n_clicks"), 4847 State("setup-new-run-modal", "is_open"), 4848 Input("msdial-folder-button", "n_clicks"), prevent_initial_call=True) 4849def open_file_explorer(new_job_browse_folder_button, select_folder_button, new_run_modal_is_open, msdial_select_folder_button): 4850 4851 """ 4852 Opens custom file explorer modal 4853 """ 4854 4855 button = ctx.triggered_id 4856 4857 if button == "msdial-folder-button" or button == "data-acquisition-folder-button": 4858 return True 4859 elif button == "file-explorer-select-button": 4860 return False 4861 else: 4862 raise PreventUpdate 4863 4864 4865@app.callback(Output("file-explorer-modal-body", "children"), 4866 Input("file-explorer-modal", "is_open"), 4867 Input("selected-data-folder", "data"), 4868 Input("selected-msdial-folder", "data"), 4869 State("settings-modal", "is_open"), prevent_initial_call=True) 4870def list_directories_in_file_explorer(file_explorer_is_open, selected_data_folder, selected_msdial_folder, settings_is_open): 4871 4872 """ 4873 Lists directories for a user to select in the file explorer modal 4874 """ 4875 4876 if file_explorer_is_open: 4877 4878 link_components = [] 4879 start_folder = None 4880 4881 if not settings_is_open and selected_data_folder is not None: 4882 start_folder = selected_data_folder 4883 elif settings_is_open and selected_msdial_folder is not None: 4884 start_folder = selected_msdial_folder 4885 4886 if start_folder is None: 4887 if sys.platform == "win32": 4888 start_folder = "C:/" 4889 elif sys.platform == "darwin": 4890 start_folder = "/Users/" 4891 4892 folders = [f.path for f in os.scandir(start_folder) if f.is_dir()] 4893 4894 if len(folders) > 0: 4895 for index, folder in enumerate(folders): 4896 link = html.A(folder, href="#", id="dir-" + str(index + 1)) 4897 link_components.append(link) 4898 link_components.append(html.Br()) 4899 4900 for index in range(len(folders), 30): 4901 link_components.append(html.A("", id="dir-" + str(index + 1))) 4902 else: 4903 link_components = [] 4904 4905 return link_components 4906 4907 else: 4908 raise PreventUpdate 4909 4910 4911@app.callback(Output("selected-data-folder", "data"), 4912 Output("selected-msdial-folder", "data"), 4913 Input("dir-1", "n_clicks"), Input("dir-2", "n_clicks"), Input("dir-3", "n_clicks"), 4914 Input("dir-4", "n_clicks"), Input("dir-5", "n_clicks"), Input("dir-6", "n_clicks"), 4915 Input("dir-7", "n_clicks"), Input("dir-8", "n_clicks"), Input("dir-9", "n_clicks"), 4916 Input("dir-10", "n_clicks"), Input("dir-11", "n_clicks"), Input("dir-12", "n_clicks"), 4917 Input("dir-13", "n_clicks"), Input("dir-14", "n_clicks"), Input("dir-15", "n_clicks"), 4918 Input("dir-16", "n_clicks"), Input("dir-17", "n_clicks"), Input("dir-18", "n_clicks"), 4919 Input("dir-19", "n_clicks"), Input("dir-20", "n_clicks"), Input("dir-21", "n_clicks"), 4920 Input("dir-22", "n_clicks"), Input("dir-23", "n_clicks"), Input("dir-24", "n_clicks"), 4921 Input("dir-25", "n_clicks"), Input("dir-26", "n_clicks"), Input("dir-27", "n_clicks"), 4922 Input("dir-28", "n_clicks"), Input("dir-29", "n_clicks"), Input("dir-30", "n_clicks"), 4923 State("dir-1", "children"), State("dir-2", "children"), State("dir-3", "children"), 4924 State("dir-4", "children"), State("dir-5", "children"), State("dir-6", "children"), 4925 State("dir-7", "children"), State("dir-8", "children"), State("dir-9", "children"), 4926 State("dir-10", "children"), State("dir-11", "children"), State("dir-12", "children"), 4927 State("dir-13", "children"), State("dir-14", "children"), State("dir-15", "children"), 4928 State("dir-16", "children"), State("dir-17", "children"), State("dir-18", "children"), 4929 State("dir-19", "children"), State("dir-20", "children"), State("dir-21", "children"), 4930 State("dir-22", "children"), State("dir-23", "children"), State("dir-24", "children"), 4931 State("dir-25", "children"), State("dir-26", "children"), State("dir-27", "children"), 4932 State("dir-28", "children"), State("dir-29", "children"), State("dir-30", "children"), 4933 Input("selected-data-folder", "data"), 4934 Input("selected-msdial-folder", "data"), 4935 Input("file-explorer-back-button", "n_clicks"), 4936 State("settings-modal", "is_open"), prevent_initial_call=True) 4937def the_most_inefficient_callback_in_history(com_1, com_2, com_3, com_4, com_5, com_6, com_7, com_8, com_9, com_10, 4938 com_11, com_12, com_13, com_14, com_15, com_16, com_17, com_18, com_19, com_20, com_21, com_22, com_23, com_24, 4939 com_25, com_26, com_27, com_28, com_29, com_30, dir_1, dir_2, dir_3, dir_4, dir_5, dir_6, dir_7, dir_8, dir_9, dir_10, 4940 dir_11, dir_12, dir_13, dir_14, dir_15, dir_16, dir_17, dir_18, dir_19, dir_20, dir_21, dir_22, dir_23, dir_24, 4941 dir_25, dir_26, dir_27, dir_28, dir_29, dir_30, selected_data_folder, selected_msdial_folder, back_button, settings_is_open): 4942 4943 """ 4944 Handles user selection of folder in the file explorer modal (I'm sorry) 4945 """ 4946 4947 if settings_is_open: 4948 selected_folder = selected_msdial_folder 4949 else: 4950 selected_folder = selected_data_folder 4951 4952 if selected_folder is None: 4953 4954 if sys.platform == "win32": 4955 start = "C:/Users/" 4956 elif sys.platform == "darwin": 4957 start = "/Users/" 4958 4959 if settings_is_open: 4960 return None, start 4961 else: 4962 return start, None 4963 4964 # Get <a> component that triggered callback 4965 selected_component = ctx.triggered_id 4966 4967 if selected_component == "file-explorer-back-button": 4968 last_folder = "/" + selected_folder.split("/")[-1] 4969 previous = selected_folder.replace(last_folder, "") 4970 4971 if settings_is_open: 4972 return None, previous 4973 else: 4974 return previous, None 4975 4976 # Create a dictionary with all link components and their values 4977 components = ("dir-1", "dir-2", "dir-3", "dir-4", "dir-5", "dir-6", "dir-7", "dir-8", "dir-9", "dir-10", "dir-11", "dir-12", 4978 "dir-13", "dir-14", "dir-15", "dir-16", "dir-17", "dir-18", "dir-19", "dir-20", "dir-21", "dir-22", "dir-23", "dir-24", 4979 "dir-25", "dir-26", "dir-27", "dir-28", "dir-29", "dir-30") 4980 values = (dir_1, dir_2, dir_3, dir_4, dir_5, dir_6, dir_7, dir_8, dir_9, dir_10, dir_11, dir_12, dir_13, dir_14, dir_15, 4981 dir_16, dir_17, dir_18, dir_19, dir_20, dir_21, dir_22, dir_23, dir_24, dir_25, dir_26, dir_27, dir_28, dir_29, dir_30) 4982 folders = {components[i]: values[i] for i in range(len(components))} 4983 4984 # Append to selected folder path by indexing set 4985 selected_folder = folders[selected_component] 4986 4987 # Return selected folder and all folder values 4988 if settings_is_open: 4989 return None, selected_folder.replace("\\", "/") 4990 else: 4991 return selected_folder.replace("\\", "/"), None 4992 4993 4994@app.callback(Output("file-explorer-modal-title", "children"), 4995 Input("selected-data-folder", "data"), 4996 Input("selected-msdial-folder", "data"), 4997 State("settings-modal", "is_open"), prevent_initial_call=True) 4998def update_file_explorer_title(selected_data_folder, selected_msdial_folder, settings_is_open): 4999 5000 """ 5001 Populates data acquisition path text field with user selection 5002 """ 5003 5004 if not settings_is_open: 5005 return selected_data_folder 5006 elif settings_is_open: 5007 return selected_msdial_folder 5008 else: 5009 raise PreventUpdate 5010 5011 5012@app.callback(Output("data-acquisition-folder-path", "value"), 5013 Input("file-explorer-select-button", "n_clicks"), 5014 State("selected-data-folder", "data"), 5015 State("settings-modal", "is_open"), prevent_initial_call=True) 5016def update_folder_path_text_field(select_folder_button, selected_folder, settings_is_open): 5017 5018 """ 5019 Populates data acquisition path text field with user selection 5020 """ 5021 5022 if not settings_is_open: 5023 return selected_folder 5024 5025 5026@app.callback(Output("active-run-progress-card", "style"), 5027 Output("active-run-progress-header", "children"), 5028 Output("active-run-progress-bar", "value"), 5029 Output("active-run-progress-bar", "label"), 5030 Output("refresh-interval", "disabled"), 5031 Output("job-controller-panel", "style"), 5032 Input("instrument-run-table", "active_cell"), 5033 State("instrument-run-table", "data"), 5034 Input("refresh-interval", "n_intervals"), 5035 Input("tabs", "value"), 5036 Input("start-run-monitor-modal", "is_open"), prevent_initial_call=True) 5037def update_progress_bar_during_active_instrument_run(active_cell, table_data, refresh, instrument_id, new_job_started): 5038 5039 """ 5040 Displays and updates progress bar if an active instrument run was selected from the table 5041 """ 5042 5043 if active_cell: 5044 5045 # Get run ID 5046 run_id = table_data[active_cell["row"]]["Run ID"] 5047 status = table_data[active_cell["row"]]["Status"] 5048 5049 # Construct values for progress bar 5050 completed, total = db.get_completed_samples_count(instrument_id, run_id, status) 5051 percent_complete = db.get_run_progress(instrument_id, run_id, status) 5052 progress_label = str(percent_complete) + "%" 5053 header_text = run_id + " – " + str(completed) + " out of " + str(total) + " samples processed" 5054 5055 if status == "Complete": 5056 refresh_interval_disabled = True 5057 else: 5058 refresh_interval_disabled = False 5059 5060 if db.get_device_identity() == instrument_id: 5061 controller_panel_visibility = {"display": "block"} 5062 else: 5063 controller_panel_visibility = {"display": "none"} 5064 5065 return {"display": "block"}, header_text, percent_complete, progress_label, refresh_interval_disabled, controller_panel_visibility 5066 5067 else: 5068 return {"display": "none"}, None, None, None, True, {"display": "none"} 5069 5070 5071@app.callback(Output("setup-new-run-button", "style"), 5072 Input("tabs", "value"), prevent_initial_call=True) 5073def hide_elements_for_non_instrument_devices(instrument_id): 5074 5075 """ 5076 Hides job setup button for shared users 5077 """ 5078 5079 if db.is_valid(): 5080 if db.get_device_identity() != instrument_id: 5081 return {"display": "none"} 5082 else: 5083 return {"display": "block", "margin-top": "15px", "line-height": "1.75"} 5084 else: 5085 raise PreventUpdate 5086 5087 5088@app.callback(Output("job-controller-modal", "is_open"), 5089 Output("job-controller-modal-title", "children"), 5090 Output("job-controller-modal-body", "children"), 5091 Output("job-controller-confirm-button", "children"), 5092 Output("job-controller-confirm-button", "color"), 5093 Input("mark-as-completed-button", "n_clicks"), 5094 Input("job-marked-completed", "data"), 5095 Input("restart-job-button", "n_clicks"), 5096 Input("job-restarted", "data"), 5097 Input("delete-job-button", "n_clicks"), 5098 Input("job-deleted", "data"), 5099 State("study-resources", "data"), prevent_initial_call=True) 5100def confirm_action_on_job(mark_job_as_completed, job_completed, restart_job, job_restarted, delete_job, job_deleted, resources): 5101 5102 """ 5103 Shows an alert confirming that the user wants to perform an action on the selected MS-AutoQC job 5104 """ 5105 5106 trigger = ctx.triggered_id 5107 resources = json.loads(resources) 5108 instrument_id = resources["instrument"] 5109 run_id = resources["run_id"] 5110 5111 if trigger == "mark-as-completed-button": 5112 title = "Mark " + run_id + " as completed?" 5113 body = dbc.Label("This will save your QC results as-is and end the current job. Continue?") 5114 return True, title, body, "Mark Job as Completed", "success" 5115 5116 elif trigger == "restart-job-button": 5117 title = "Restart " + run_id + "?" 5118 body = dbc.Label("This will restart the acquisition listener process for " + run_id + ". Continue?") 5119 return True, title, body, "Restart Job", "warning" 5120 5121 elif trigger == "delete-job-button": 5122 title = "Delete " + run_id + " on " + instrument_id + "?" 5123 body = dbc.Label("This will delete all QC results for " + run_id + " on " + instrument_id + 5124 ". This process cannot be undone. Continue?") 5125 return True, title, body, "Delete Job", "danger" 5126 5127 elif trigger == "job-marked-completed" or trigger == "job-restarted" or trigger == "job-deleted" or trigger == "job-action-failed": 5128 return False, None, None, None, None 5129 5130 else: 5131 raise PreventUpdate 5132 5133 5134@app.callback(Output("job-marked-completed", "data"), 5135 Output("job-restarted", "data"), 5136 Output("job-deleted", "data"), 5137 Output("job-action-failed", "data"), 5138 Input("job-controller-confirm-button", "n_clicks"), 5139 State("job-controller-modal-title", "children"), 5140 State("study-resources", "data"), prevent_initial_call=True) 5141def perform_action_on_job(confirm_button, modal_title, resources): 5142 5143 """ 5144 Performs the selected action on the selected MS-AutoQC job 5145 """ 5146 5147 resources = json.loads(resources) 5148 instrument_id = resources["instrument"] 5149 run_id = resources["run_id"] 5150 acquisition_path = db.get_acquisition_path(instrument_id, run_id) 5151 5152 if "Mark" in modal_title: 5153 5154 try: 5155 # Mark instrument run as completed 5156 db.mark_run_as_completed(instrument_id, run_id) 5157 5158 # Sync database on run completion 5159 if db.sync_is_enabled(): 5160 db.sync_on_run_completion(instrument_id, run_id) 5161 5162 # Delete temporary data file directory 5163 db.delete_temp_directory(instrument_id, run_id) 5164 5165 # Kill acquisition listener 5166 pid = db.get_pid(instrument_id, run_id) 5167 qc.kill_subprocess(pid) 5168 return True, None, None, None 5169 5170 except: 5171 print("Could not mark instrument run as completed.") 5172 traceback.print_exc() 5173 return None, None, None, True 5174 5175 elif "Restart" in modal_title: 5176 5177 try: 5178 # Kill current acquisition listener (acquisition listener will be restarted automatically) 5179 pid = db.get_pid(instrument_id, run_id) 5180 qc.kill_subprocess(pid) 5181 5182 # Delete temporary data file directory 5183 db.delete_temp_directory(instrument_id, run_id) 5184 5185 # Restart AcquisitionListener and store process ID 5186 process = psutil.Popen(["py", "AcquisitionListener.py", acquisition_path, instrument_id, run_id]) 5187 db.store_pid(instrument_id, run_id, process.pid) 5188 return None, True, None, None 5189 5190 except: 5191 print("Could not restart listener.") 5192 traceback.print_exc() 5193 return None, None, None, True 5194 5195 elif "Delete" in modal_title: 5196 5197 try: 5198 # Delete instrument run from database 5199 db.delete_instrument_run(instrument_id, run_id) 5200 5201 # Sync with Google Drive 5202 if db.sync_is_enabled(): 5203 db.upload_database(instrument_id) 5204 db.delete_active_run_csv_files(instrument_id, run_id) 5205 5206 # Delete temporary data file directory 5207 db.delete_temp_directory(instrument_id, run_id) 5208 return None, None, True, None 5209 5210 except: 5211 print("Could not delete instrument run.") 5212 traceback.print_exc() 5213 return None, None, None, True 5214 5215 else: 5216 raise PreventUpdate
Dash app layout
52def serve_layout(): 53 54 biohub_logo = "https://user-images.githubusercontent.com/7220175/184942387-0acf5deb-d81e-4962-ab27-05b453c7a688.png" 55 56 return html.Div(className="app-layout", children=[ 57 58 # Navigation bar 59 dbc.Navbar( 60 dbc.Container(style={"height": "50px"}, children=[ 61 # Logo and title 62 html.A( 63 dbc.Row([ 64 dbc.Col(html.Img(src=biohub_logo, height="30px")), 65 dbc.Col(dbc.NavbarBrand(id="header", children="MS-AutoQC", className="ms-2")), 66 ], align="center", className="g-0", 67 ), href="https://biohub.org", style={"textDecoration": "none"}, 68 ), 69 # Settings button 70 dbc.Row([ 71 dbc.Nav([ 72 dbc.NavItem(dbc.NavLink("About", href="https://github.com/czbiohub/MS-AutoQC", className="navbar-button", target="_blank")), 73 dbc.NavItem(dbc.NavLink("Support", href="https://github.com/czbiohub/MS-AutoQC/wiki", className="navbar-button", target="_blank")), 74 dbc.NavItem(dbc.NavLink("Settings", href="#", id="settings-button", className="navbar-button")), 75 ], className="me-auto") 76 ], className="g-0 ms-auto flex-nowrap mt-3 mt-md-0") 77 ]), color="dark", dark=True 78 ), 79 80 # App layout 81 html.Div(className="page", children=[ 82 83 dbc.Row(justify="center", children=[ 84 85 dbc.Col(width=11, children=[ 86 87 dbc.Row(justify="center", children=[ 88 89 # Tabs to switch between instruments 90 dcc.Tabs(id="tabs", className="instrument-tabs"), 91 92 dbc.Col(width=12, lg=4, children=[ 93 94 html.Div(id="table-container", className="table-container", style={"display": "none"}, children=[ 95 96 # Table of past/active instrument runs 97 dash_table.DataTable(id="instrument-run-table", page_action="none", 98 fixed_rows={"headers": True}, 99 cell_selectable=True, 100 style_cell={ 101 "textAlign": "left", 102 "fontSize": "15px", 103 "fontFamily": "sans-serif", 104 "lineHeight": "25px", 105 "padding": "10px", 106 "borderRadius": "5px"}, 107 style_data={"whiteSpace": "normal", 108 "textOverflow": "ellipsis", 109 "maxWidth": 0}, 110 style_table={ 111 "max-height": "285px", 112 "overflowY": "auto"}, 113 style_data_conditional=[ 114 {"if": {"state": "active"}, 115 "backgroundColor": bootstrap_colors[ 116 "blue-low-opacity"], 117 "border": "1px solid " + bootstrap_colors["blue"] 118 }], 119 style_cell_conditional=[ 120 {"if": {"column_id": "Run ID"}, 121 "width": "40%"}, 122 {"if": {"column_id": "Chromatography"}, 123 "width": "35%"}, 124 {"if": {"column_id": "Status"}, 125 "width": "25%"} 126 ] 127 ), 128 129 # Progress bar for instrument run 130 dbc.Card(id="active-run-progress-card", style={"display": "none"}, 131 className="margin-top-15", children=[ 132 dbc.CardHeader(id="active-run-progress-header", style={"padding": "0.75rem"}), 133 dbc.CardBody([ 134 135 # Instrument run progress 136 dcc.Interval(id="refresh-interval", n_intervals=0, interval=30000, disabled=True), 137 dbc.Progress(id="active-run-progress-bar", animated=False), 138 139 # Buttons for managing MS-AutoQC jobs 140 html.Div(id="job-controller-panel", children=[ 141 html.Div(className="d-flex justify-content-center btn-toolbar", children=[ 142 # Button to mark current job as complete 143 html.Div(className="me-1", children=[ 144 dbc.Button("Mark as Completed", 145 id="mark-as-completed-button", 146 className="run-button", 147 outline=True, 148 color="success"), 149 ]), 150 151 # Button to restart job 152 html.Div(className="me-1", children=[ 153 dbc.Button("Restart Job", 154 id="restart-job-button", 155 className="run-button", 156 outline=True, 157 color="warning"), 158 ]), 159 160 # Button to delete job 161 html.Div(className="me-1", children=[ 162 dbc.Button("Delete Job", 163 id="delete-job-button", 164 className="run-button", 165 outline=True, 166 color="danger"), 167 ]), 168 ]), 169 ]), 170 ]) 171 ]), 172 173 # Button to start new MS-AutoQC job 174 html.Div(className="d-grid gap-2", children=[ 175 dbc.Button("Setup New QC Job", 176 id="setup-new-run-button", 177 style={"margin-top": "15px", 178 "line-height": "1.75"}, 179 outline=True, 180 color="primary"), 181 ]), 182 183 # Polarity filtering options 184 html.Div(className="radio-group-container", children=[ 185 html.Div(className="radio-group margin-top-30", children=[ 186 dbc.RadioItems( 187 id="polarity-options", 188 className="btn-group", 189 inputClassName="btn-check", 190 labelClassName="btn btn-outline-primary", 191 inputCheckedClassName="active", 192 options=[ 193 {"label": "Positive Mode", "value": "Pos"}, 194 {"label": "Negative Mode", "value": "Neg"}], 195 value="Pos" 196 ), 197 ]) 198 ]), 199 200 # Sample / blank / pool / treatment filtering options 201 html.Div(className="radio-group-container", children=[ 202 html.Div(className="radio-group margin-top-30", children=[ 203 dbc.RadioItems( 204 id="sample-filtering-options", 205 className="btn-group", 206 inputClassName="btn-check", 207 labelClassName="btn btn-outline-primary", 208 inputCheckedClassName="active", 209 value="all", 210 options=[ 211 {"label": "All", "value": "all"}, 212 {"label": "Samples", "value": "samples"}, 213 {"label": "Pools", "value": "pools"}, 214 {"label": "Blanks", "value": "blanks"}], 215 ), 216 ]) 217 ]), 218 219 # Table of samples run for a particular study 220 dash_table.DataTable(id="sample-table", page_action="none", 221 fixed_rows={"headers": True}, 222 # cell_selectable=True, 223 style_cell={ 224 "textAlign": "left", 225 "fontSize": "15px", 226 "fontFamily": "sans-serif", 227 "lineHeight": "25px", 228 "whiteSpace": "normal", 229 "padding": "10px", 230 "borderRadius": "5px"}, 231 style_data={ 232 "whiteSpace": "normal", 233 "textOverflow": "ellipsis", 234 "maxWidth": 0}, 235 style_table={ 236 "height": "475px", 237 "overflowY": "auto"}, 238 style_data_conditional=[ 239 {"if": {"filter_query": "{QC} = 'Fail'"}, 240 "backgroundColor": bootstrap_colors[ 241 "red-low-opacity"], 242 "font-weight": "bold" 243 }, 244 {"if": {"filter_query": "{QC} = 'Check'"}, 245 "backgroundColor": bootstrap_colors[ 246 "yellow-low-opacity"] 247 }, 248 {"if": {"state": "active"}, 249 "backgroundColor": bootstrap_colors[ 250 "blue-low-opacity"], 251 "border": "1px solid " + bootstrap_colors["blue"] 252 } 253 ], 254 style_cell_conditional=[ 255 {"if": {"column_id": "Sample"}, 256 "width": "60%"}, 257 {"if": {"column_id": "Position"}, 258 "width": "20%"}, 259 {"if": {"column_id": "QC"}, 260 "width": "20%"}, 261 ] 262 ) 263 ]), 264 ]), 265 266 dbc.Col(width=12, lg=8, children=[ 267 268 # Container for all plots 269 html.Div(id="plot-container", className="all-plots-container", style={"display": "none"}, children=[ 270 271 html.Div(className="istd-plot-div", children=[ 272 273 html.Div(id="istd-rt-div", className="plot-container", children=[ 274 275 # Internal standard selection controls 276 html.Div(style={"width": "100%"}, children=[ 277 # Dropdown for selecting an internal standard for the RT vs. sample plot 278 html.Div(className="istd-dropdown-style", children=[ 279 dcc.Dropdown( 280 id="istd-rt-dropdown", 281 options=[], 282 placeholder="Select internal standards...", 283 style={"text-align": "left", 284 "height": "1.5", 285 "width": "100%"} 286 )] 287 ), 288 289 # Buttons for skipping through the internal standards 290 html.Div(className="istd-button-style", children=[ 291 dbc.Button(html.I(className="bi bi-arrow-left"), 292 id="rt-prev-button", color="light", className="me-1"), 293 dbc.Button(html.I(className="bi bi-arrow-right"), 294 id="rt-next-button", color="light", className="me-1"), 295 ]), 296 ]), 297 298 # Dropdown for filtering by sample for the RT vs. sample plot 299 dcc.Dropdown( 300 id="rt-plot-sample-dropdown", 301 options=[], 302 placeholder="Select samples...", 303 style={"text-align": "left", 304 "height": "1.5", 305 "width": "100%", 306 "display": "inline-block"}, 307 multi=True), 308 309 # Scatter plot of internal standard retention times vs. samples 310 dcc.Graph(id="istd-rt-plot"), 311 ]), 312 313 html.Div(id="istd-intensity-div", className="plot-container", children=[ 314 315 # Internal standard selection controls 316 html.Div(style={"width": "100%"}, children=[ 317 # Dropdown for selecting an internal standard for the intensity vs. sample plot 318 html.Div(className="istd-dropdown-style", children=[ 319 dcc.Dropdown( 320 id="istd-intensity-dropdown", 321 options=[], 322 placeholder="Select internal standards...", 323 style={"text-align": "left", 324 "height": "1.5", 325 "width": "100%"} 326 )] 327 ), 328 329 # Buttons for skipping through the internal standards 330 html.Div(className="istd-button-style", children=[ 331 dbc.Button(html.I(className="bi bi-arrow-left"), 332 id="intensity-prev-button", color="light", className="me-1"), 333 dbc.Button(html.I(className="bi bi-arrow-right"), 334 id="intensity-next-button", color="light", className="me-1"), 335 ]), 336 ]), 337 338 # Dropdown for filtering by sample for the intensity vs. sample plot 339 dcc.Dropdown( 340 id="intensity-plot-sample-dropdown", 341 options=[], 342 placeholder="Select samples...", 343 style={"text-align": "left", 344 "height": "1.5", 345 "width": "100%", 346 "display": "inline-block"}, 347 multi=True, 348 ), 349 350 # Bar plot of internal standard intensity vs. samples 351 dcc.Graph(id="istd-intensity-plot") 352 ]), 353 354 html.Div(id="istd-mz-div", className="plot-container", children=[ 355 356 # Internal standard selection controls 357 html.Div(style={"width": "100%"}, children=[ 358 # Dropdown for selecting an internal standard for the delta m/z vs. sample plot 359 html.Div(className="istd-dropdown-style", children=[ 360 dcc.Dropdown( 361 id="istd-mz-dropdown", 362 options=[], 363 placeholder="Select internal standards...", 364 style={"text-align": "left", 365 "height": "1.5", 366 "width": "100%"} 367 )] 368 ), 369 370 # Buttons for skipping through the internal standards 371 html.Div(className="istd-button-style", children=[ 372 dbc.Button(html.I(className="bi bi-arrow-left"), 373 id="mz-prev-button", color="light", className="me-1"), 374 dbc.Button(html.I(className="bi bi-arrow-right"), 375 id="mz-next-button", color="light", className="me-1"), 376 ]), 377 ]), 378 379 # Dropdown for filtering by sample for the delta m/z vs. sample plot 380 dcc.Dropdown( 381 id="mz-plot-sample-dropdown", 382 options=[], 383 placeholder="Select samples...", 384 style={"text-align": "left", 385 "height": "1.5", 386 "width": "100%", 387 "display": "inline-block"}, 388 multi=True), 389 390 # Scatter plot of internal standard delta m/z vs. samples 391 dcc.Graph(id="istd-mz-plot") 392 ]), 393 394 ]), 395 396 html.Div(className="bio-plot-div", children=[ 397 398 # Scatter plot for biological standard m/z vs. RT 399 html.Div(id="bio-standard-mz-rt-div", className="plot-container", children=[ 400 401 # Dropdown for selecting a biological standard to view 402 dcc.Dropdown(id="bio-standards-plot-dropdown", 403 options=[], placeholder="Select biological standard...", 404 style={"text-align": "left", "height": "1.5", "font-size": "1rem", 405 "width": "100%", "display": "inline-block"}), 406 407 dcc.Graph(id="bio-standard-mz-rt-plot") 408 ]), 409 410 # Bar plot for biological standard feature intensity vs. run 411 html.Div(id="bio-standard-benchmark-div", className="plot-container", children=[ 412 413 # Dropdown for biological standard feature intensity plot 414 dcc.Dropdown( 415 id="bio-standard-benchmark-dropdown", 416 options=[], 417 placeholder="Select targeted metabolite...", 418 style={"text-align": "left", 419 "height": "35px", 420 "width": "100%", 421 "display": "inline-block"} 422 ), 423 424 dcc.Graph(id="bio-standard-benchmark-plot", animate=False) 425 ]) 426 ]) 427 ]), 428 ]), 429 430 # Modal for sample information card 431 dbc.Modal(id="sample-info-modal", size="xl", centered=True, is_open=False, scrollable=True, children=[ 432 dbc.ModalHeader(dbc.ModalTitle(id="sample-modal-title"), close_button=True), 433 dbc.ModalBody(id="sample-modal-body") 434 ]), 435 436 # Modal for alerting user that data is loading 437 dbc.Modal(id="loading-modal", size="md", centered=True, is_open=False, scrollable=True, 438 keyboard=False, backdrop="static", children=[ 439 dbc.ModalHeader(dbc.ModalTitle(id="loading-modal-title"), close_button=False), 440 dbc.ModalBody(id="loading-modal-body") 441 ]), 442 443 # Modal for job completion / restart / deletion confirmation 444 dbc.Modal(id="job-controller-modal", size="md", centered=True, is_open=False, children=[ 445 dbc.ModalHeader(dbc.ModalTitle(id="job-controller-modal-title")), 446 dbc.ModalBody(id="job-controller-modal-body"), 447 dbc.ModalFooter(children=[ 448 dbc.Button("Cancel", color="secondary", id="job-controller-cancel-button"), 449 dbc.Button(id="job-controller-confirm-button") 450 ]), 451 ]), 452 453 # Modal for progress feedback while database syncs to Google Drive 454 dbc.Modal(id="google-drive-sync-modal", size="md", centered=True, is_open=False, scrollable=True, 455 keyboard=True, backdrop="static", children=[ 456 dbc.ModalHeader(dbc.ModalTitle( 457 html.Div(children=[ 458 dbc.Spinner(color="primary"), " Syncing to Google Drive"])), 459 close_button=False), 460 dbc.ModalBody("This may take a few seconds...") 461 ]), 462 463 # Custom file explorer modal for new job setup 464 dbc.Modal(id="file-explorer-modal", size="md", centered=True, is_open=False, scrollable=True, 465 keyboard=True, children=[ 466 dbc.ModalHeader(dbc.ModalTitle(id="file-explorer-modal-title")), 467 dbc.ModalBody(id="file-explorer-modal-body"), 468 dbc.ModalFooter(children=[ 469 dbc.Button("Go Back", id="file-explorer-back-button", color="secondary"), 470 dbc.Button("Select Current Folder", id="file-explorer-select-button") 471 ]) 472 ]), 473 474 # Modal for first-time workspace setup 475 dbc.Modal(id="workspace-setup-modal", size="lg", centered=True, scrollable=True, 476 keyboard=False, backdrop="static", children=[ 477 dbc.ModalHeader(dbc.ModalTitle("Welcome to MS-AutoQC", id="setup-user-modal-title"), close_button=False), 478 dbc.ModalBody(id="setup-user-modal-body", className="modal-styles-2", children=[ 479 480 html.Div([ 481 html.H5("Let's help you get started."), 482 html.P("Looks like this is a new installation. What would you like to do today?"), 483 dbc.Accordion(start_collapsed=True, children=[ 484 485 # Setting up MS-AutoQC for the first time 486 dbc.AccordionItem(title="I'm setting up MS-AutoQC on a new instrument", children=[ 487 html.Div(className="modal-styles-3", children=[ 488 489 # Instrument name text field 490 html.Div([ 491 dbc.Label("Instrument name"), 492 dbc.InputGroup([ 493 dbc.Input(id="first-time-instrument-id", type="text", 494 placeholder="Ex: Thermo Q-Exactive HF 1"), 495 dbc.DropdownMenu(id="first-time-instrument-vendor", 496 label="Choose Vendor", color="primary", children=[ 497 dbc.DropdownMenuItem("Thermo Fisher", id="thermo-fisher-item"), 498 dbc.DropdownMenuItem("Agilent", id="agilent-item"), 499 dbc.DropdownMenuItem("Bruker", id="bruker-item"), 500 dbc.DropdownMenuItem("Sciex", id="sciex-item"), 501 dbc.DropdownMenuItem("Waters", id="waters-item") 502 ]), 503 ]), 504 dbc.FormText("Please choose a name and vendor for this instrument."), 505 ]), 506 507 html.Br(), 508 509 # Google Drive authentication button 510 html.Div([ 511 dbc.Label("Sync with Google Drive (recommended)"), 512 html.Br(), 513 dbc.InputGroup([ 514 dbc.Input(placeholder="Client ID", id="gdrive-client-id-1"), 515 dbc.Input(placeholder="Client secret", id="gdrive-client-secret-1"), 516 dbc.Button("Sign in to Google Drive", id="setup-google-drive-button-1", 517 color="primary", outline=True), 518 ]), 519 dbc.FormText("This will allow you to access your QC results from any device."), 520 dbc.Tooltip("If you have Google Drive sync enabled on an instrument already, " + 521 "please sign in with the same Google account to merge workspaces.", 522 target="setup-google-drive-button-1", placement="left"), 523 dbc.Popover(id="google-drive-button-1-popover", is_open=False, 524 target="setup-google-drive-button-1", placement="right") 525 ]), 526 527 html.Br(), 528 529 # Complete setup button 530 html.Div([ 531 html.Div([ 532 dbc.Button(children="Complete setup", id="first-time-complete-setup-button", 533 disabled=True, style={"line-height": "1.75"}, color="success"), 534 ], className="d-grid gap-2 col-12 mx-auto"), 535 ]) 536 ]), 537 ]), 538 539 # Signing in from another device 540 dbc.AccordionItem(title="I'm signing in to an existing MS-AutoQC workspace", children=[ 541 html.Div(className="modal-styles-3", children=[ 542 543 # Google Drive authentication button 544 html.Div([ 545 dbc.Label("Sign in to access MS-AutoQC"), html.Br(), 546 dbc.InputGroup([ 547 dbc.Input(placeholder="Client ID", id="gdrive-client-id-2"), 548 dbc.Input(placeholder="Client secret", id="gdrive-client-secret-2"), 549 dbc.Button("Sign in to Google Drive", id="setup-google-drive-button-2", 550 color="primary", outline=False), 551 ]), 552 dbc.FormText( 553 "Please ensure that your Google account has been registered to " + 554 "access your MS-AutoQC workspace by visiting Settings > General."), 555 dbc.Popover(id="google-drive-button-2-popover", is_open=False, 556 target="setup-google-drive-button-2", placement="right") 557 ]), 558 559 # Checkbox for logging in to instrument computer 560 dbc.Checkbox(id="device-identity-checkbox", className="checkbox-margin", 561 label="I am signing in from an instrument computer", value=False), 562 563 # Dropdown for selecting an instrument 564 dbc.Select(id="device-identity-selection", value=None, 565 placeholder="Which instrument?", disabled=True), 566 567 html.Br(), 568 569 # Workspace sign-in button 570 html.Div([ 571 html.Div([ 572 dbc.Button("Sign in to MS-AutoQC workspace", id="first-time-sign-in-button", 573 disabled=True, style={"line-height": "1.75"}, color="success"), 574 ], className="d-grid gap-2 col-12 mx-auto"), 575 ]) 576 ]), 577 ]), 578 ]), 579 ]), 580 ]) 581 ]), 582 583 # Modal for starting an instrument run listener 584 dbc.Modal(id="setup-new-run-modal", size="lg", centered=True, is_open=False, scrollable=True, children=[ 585 dbc.ModalHeader(dbc.ModalTitle(id="setup-new-run-modal-title", children="New QC Job"), close_button=True), 586 dbc.ModalBody(id="setup-new-run-modal-body", className="modal-styles-2", children=[ 587 588 # Text field for entering your run ID 589 html.Div([ 590 dbc.Label("Instrument run ID"), 591 dbc.Input(id="instrument-run-id", placeholder="Give your instrument run a unique name", type="text"), 592 dbc.FormFeedback("Looks good!", type="valid"), 593 dbc.FormFeedback("Please enter a unique ID for this run.", type="invalid"), 594 ]), 595 596 html.Br(), 597 598 # Select chromatography 599 html.Div([ 600 dbc.Label("Select chromatography"), 601 dbc.Select(id="start-run-chromatography-dropdown", 602 placeholder="No chromatography selected"), 603 dbc.FormFeedback("Looks good!", type="valid"), 604 dbc.FormFeedback( 605 "Please ensure that your chromatography method has identification files " 606 "(MSP or CSV) configured for positive and negative mode in Settings > " 607 "Internal Standards and Settings > Biological Standards.", type="invalid") 608 ]), 609 610 html.Br(), 611 612 # Select biological standard used in this study 613 html.Div(children=[ 614 dbc.Label("Select biological standards (optional)"), 615 dcc.Dropdown(id="start-run-bio-standards-dropdown", 616 options=[], placeholder="Select biological standards...", 617 style={"text-align": "left", "height": "1.5", "font-size": "1rem", 618 "width": "100%", "display": "inline-block"}, 619 multi=True) 620 ]), 621 622 html.Br(), 623 624 # Select AutoQC configuration 625 html.Div(children=[ 626 dbc.Label("Select MS-AutoQC configuration"), 627 dbc.Select(id="start-run-qc-configs-dropdown", 628 placeholder="No configuration selected"), 629 ]), 630 631 html.Br(), 632 633 # Button and field for selecting a sequence file 634 html.Div([ 635 dbc.Label("Acquisition sequence (.csv)"), 636 dbc.InputGroup([ 637 dbc.Input(id="sequence-path", 638 placeholder="No file selected"), 639 dbc.Button(dcc.Upload( 640 id="sequence-upload-button", 641 accept="text/plain, application/vnd.ms-excel, .csv", 642 children=[html.A("Browse Files")]), 643 color="secondary"), 644 dbc.FormFeedback("Looks good!", type="valid"), 645 dbc.FormFeedback("Please ensure that the sequence file is a CSV file " 646 "and in the correct vendor format.", type="invalid"), 647 ]), 648 ]), 649 650 html.Br(), 651 652 # Button and field for selecting a sample metadata file 653 html.Div([ 654 dbc.Label("Sample metadata (.csv) (optional)"), 655 dbc.InputGroup([ 656 dbc.Input(id="metadata-path", 657 placeholder="No file selected"), 658 dbc.Button(dcc.Upload( 659 id="metadata-upload-button", 660 accept="text/plain, application/vnd.ms-excel, .csv", 661 children=[html.A("Browse Files")]), 662 color="secondary"), 663 dbc.FormFeedback("Looks good!", type="valid"), 664 dbc.FormFeedback("Please ensure that the metadata file is a CSV and contains " 665 "the following columns: Sample Name, Species, Matrix, Treatment, " 666 "and Growth-Harvest Conditions", type="invalid"), 667 ]), 668 ]), 669 670 html.Br(), 671 672 # Button and field for selecting the data acquisition directory 673 html.Div([ 674 dbc.Label("Data file directory", id="data-acquisition-path-title"), 675 dbc.InputGroup([ 676 dbc.Input(placeholder="Browse folders or enter the folder path", 677 id="data-acquisition-folder-path"), 678 dbc.Button("Browse Folders", id="data-acquisition-folder-button", 679 color="secondary"), 680 dbc.FormFeedback("Looks good!", type="valid"), 681 dbc.FormFeedback( 682 "This path does not exist. Please enter a valid path.", type="invalid"), 683 ]), 684 dbc.FormText(id="data-acquisition-path-form-text", 685 children="Please type the folder path to which incoming data files will be saved."), 686 687 ]), 688 689 html.Br(), 690 691 # Switch between running AutoQC on a live run vs. past completed run 692 html.Div(children=[ 693 dbc.Label("Is this an active or completed instrument run?"), 694 dbc.RadioItems(id="ms_autoqc-job-type", value="active", options=[ 695 {"label": "Monitor an active instrument run", 696 "value": "active"}, 697 {"label": "QC a completed instrument run", 698 "value": "completed"}], 699 ), 700 ]), 701 702 html.Br(), 703 704 html.Div([ 705 dbc.Button("Start monitoring instrument run", id="monitor-new-run-button", disabled=True, 706 style={"line-height": "1.75"}, color="primary")], 707 className="d-grid gap-2") 708 ]), 709 ]), 710 711 # Modal to alert user that run monitoring has started 712 dbc.Modal(id="start-run-monitor-modal", size="md", centered=True, is_open=False, children=[ 713 dbc.ModalHeader(dbc.ModalTitle(id="start-run-monitor-modal-title", children="Success!"), close_button=True), 714 dbc.ModalBody(id="start-run-monitor-modal-body", className="modal-styles", children=[ 715 dbc.Alert("MS-AutoQC will start monitoring your run. Please do not restart your computer.", color="success") 716 ]), 717 ]), 718 719 # Error modal for new AutoQC job setup 720 dbc.Modal(id="new-job-error-modal", size="md", centered=True, is_open=False, children=[ 721 dbc.ModalHeader(dbc.ModalTitle(id="new-job-error-modal-title"), close_button=False), 722 dbc.ModalBody(id="new-job-error-modal-body", className="modal-styles"), 723 ]), 724 725 # MS-AutoQC settings 726 dbc.Modal(id="settings-modal", fullscreen=True, centered=True, is_open=False, scrollable=True, children=[ 727 dbc.ModalHeader(dbc.ModalTitle(children="Settings"), close_button=True), 728 dbc.ModalBody(id="settings-modal-body", className="modal-styles-fullscreen", children=[ 729 730 # Tabbed interface 731 dbc.Tabs(children=[ 732 733 # General settings 734 dbc.Tab(label="General", className="modal-styles", children=[ 735 736 html.Br(), 737 738 dbc.Alert(id="google-drive-sign-in-from-settings-alert", is_open=False, 739 dismissable=True, color="danger", children=[ 740 html.H4( 741 "This Google account already has an MS-AutoQC workspace."), 742 html.P( 743 "Please sign in with a different Google account to enable cloud " 744 "sync for this workspace."), 745 html.P( 746 "Or, if you'd like to add a new instrument to an existing MS-AutoQC " 747 "workspace, please reinstall MS-AutoQC on this instrument and enable " 748 "cloud sync during setup.") 749 ]), 750 751 dbc.Alert(id="gdrive-credentials-saved-alert", is_open=False, duration=5000), 752 753 dbc.Label("Manage workspace access", style={"font-weight": "bold"}), 754 html.Br(), 755 756 # Google Drive cloud storage 757 dbc.Label("Google API client credentials"), 758 html.Br(), 759 dbc.InputGroup([ 760 dbc.Input(placeholder="Client ID", id="gdrive-client-id"), 761 dbc.Input(placeholder="Client secret", id="gdrive-client-secret"), 762 dbc.Button("Set credentials", 763 id="set-gdrive-credentials-button", color="primary", outline=True), 764 ]), 765 dbc.FormText(children=[ 766 "You can get these credentials from the ", 767 html.A("Google Cloud console", 768 href="https://console.cloud.google.com/apis/credentials", target="_blank"), 769 " in Credentials > OAuth 2.0 Client ID's."]), 770 html.Br(), html.Br(), 771 772 dbc.Label("Enable cloud sync with Google Drive"), 773 html.Br(), 774 dbc.Button("Sync with Google Drive", 775 id="google-drive-sync-button", color="primary", outline=False), 776 html.Br(), 777 dbc.FormText(id="google-drive-sync-form-text", children= 778 "This will allow you to monitor your instrument runs on other devices."), 779 html.Br(), html.Br(), 780 781 # Alerts for modifying workspace access 782 dbc.Alert(id="user-addition-alert", color="success", is_open=False, duration=5000), 783 dbc.Alert(id="user-deletion-alert", color="primary", is_open=False, duration=5000), 784 785 # Google Drive sharing 786 dbc.Label("Add / remove workspace users"), 787 html.Br(), 788 dbc.InputGroup([ 789 dbc.Input(placeholder="example@gmail.com", id="add-user-text-field"), 790 dbc.Button("Add user", color="primary", outline=True, 791 id="add-user-button", n_clicks=0), 792 dbc.Button("Delete user", color="danger", outline=True, 793 id="delete-user-button", n_clicks=0), 794 dbc.Popover("This will revoke user access to the MS-AutoQC workspace. " 795 "Are you sure?", target="delete-user-button", trigger="hover", body=True) 796 ]), 797 dbc.FormText( 798 "Adding new users grants full read-and-write access to this MS-AutoQC workspace."), 799 html.Br(), html.Br(), 800 801 # Table of users with workspace access 802 html.Div(id="workspace-users-table"), 803 html.Br(), 804 805 dbc.Label("Slack notifications", style={"font-weight": "bold"}), 806 html.Br(), 807 808 # Alerts for modifying workspace access 809 dbc.Alert(id="slack-token-save-alert", is_open=False, duration=5000), 810 811 # Channel for Slack notifications 812 dbc.Label("Slack API client credentials"), 813 html.Br(), 814 dbc.InputGroup([ 815 dbc.Input(placeholder="Slack bot user OAuth token", id="slack-bot-token"), 816 dbc.Button("Save bot token", color="primary", outline=True, 817 id="save-slack-token-button", n_clicks=0), 818 ]), 819 dbc.FormText(children=[ 820 "You can get the Slack bot token from the ", 821 html.A("Slack API website", 822 href="https://api.slack.com/apps", target="_blank"), 823 " in Your App > Settings > Install App."]), 824 html.Br(), html.Br(), 825 826 dbc.Alert(id="slack-notifications-toggle-alert", is_open=False, duration=5000), 827 828 dbc.Label("Register Slack channel for notifications"), 829 dbc.InputGroup(children=[ 830 dbc.Input(id="slack-channel", placeholder="#my-slack-channel"), 831 dbc.InputGroupText( 832 dbc.Switch(id="slack-notifications-enabled", label="Enable notifications")), 833 ]), 834 dbc.FormText( 835 "Please enter the Slack channel you'd like to register for notifications."), 836 html.Br(), html.Br(), 837 838 dbc.Label("Email notifications", style={"font-weight": "bold"}), 839 html.Br(), 840 841 # Alerts for modifying email notification list 842 dbc.Alert(id="email-addition-alert", is_open=False, duration=5000), 843 dbc.Alert(id="email-deletion-alert", is_open=False, duration=5000), 844 845 # Register recipients for email notifications 846 dbc.Label("Register recipients for email notifications"), 847 html.Br(), 848 dbc.InputGroup([ 849 dbc.Input(placeholder="recipient@example.com", 850 id="email-notifications-text-field"), 851 dbc.Button("Register email", color="primary", outline=True, 852 id="add-email-button", n_clicks=0), 853 dbc.Button("Remove email", color="danger", outline=True, 854 id="delete-email-button", n_clicks=0), 855 dbc.Popover("This will un-register the email account from MS-AutoQC " 856 "notifications. Are you sure?", target="delete-email-button", 857 trigger="hover", body=True) 858 ]), 859 dbc.FormText( 860 "Please enter a valid email address to register for email notifications."), 861 html.Br(), html.Br(), 862 863 # Table of users registered for email notifications 864 html.Div(id="email-notifications-table") 865 ]), 866 867 # Internal standards 868 dbc.Tab(label="Chromatography methods", className="modal-styles", children=[ 869 870 html.Br(), 871 872 # Alerts for user feedback on biological standard addition/removal 873 dbc.Alert(id="chromatography-addition-alert", color="success", is_open=False, duration=5000), 874 dbc.Alert(id="chromatography-removal-alert", color="primary", is_open=False, duration=5000), 875 876 dbc.Label("Manage chromatography methods", style={"font-weight": "bold"}), 877 html.Br(), 878 879 # Add new chromatography method 880 html.Div([ 881 dbc.Label("Add new chromatography method"), 882 dbc.InputGroup([ 883 dbc.Input(id="add-chromatography-text-field", type="text", 884 placeholder="Name of chromatography to add"), 885 dbc.Button("Add method", color="primary", outline=True, 886 id="add-chromatography-button", n_clicks=0), 887 ]), 888 dbc.FormText("Example: HILIC, Reverse Phase, RP (30 mins)"), 889 ]), html.Br(), 890 891 # Chromatography methods table 892 dbc.Label("Chromatography methods", style={"font-weight": "bold"}), 893 html.Br(), 894 html.Div(id="chromatography-methods-table"), 895 html.Br(), 896 897 dbc.Label("Configure chromatography methods", style={"font-weight": "bold"}), 898 html.Br(), 899 900 # Select chromatography 901 html.Div([ 902 dbc.Label("Select chromatography to modify"), 903 dbc.InputGroup([ 904 dbc.Select(id="select-istd-chromatography-dropdown", 905 placeholder="No chromatography selected"), 906 dbc.Button("Remove", color="danger", outline=True, 907 id="remove-chromatography-method-button", n_clicks=0), 908 dbc.Popover("You are about to delete this chromatography method and " 909 "all of its corresponding MSP files. Are you sure?", 910 target="remove-chromatography-method-button", trigger="hover", body=True) 911 ]), 912 ]), 913 914 html.Br(), 915 916 # Select polarity 917 html.Div([ 918 dbc.Label("Select polarity to modify"), 919 dbc.Select(id="select-istd-polarity-dropdown", options=[ 920 {"label": "Positive Mode", "value": "Positive Mode"}, 921 {"label": "Negative Mode", "value": "Negative Mode"}, 922 ], placeholder="No polarity selected"), 923 ]), 924 925 html.Br(), 926 927 dbc.Alert(id="istd-config-success-alert", color="success", is_open=False, duration=5000), 928 929 # Set MS-DIAL configuration for selected chromatography 930 html.Div(children=[ 931 dbc.Label("Set MS-DIAL processing configuration", 932 id="istd-medial-configs-label"), 933 dbc.InputGroup([ 934 dbc.Select(id="istd-msdial-configs-dropdown", 935 placeholder="No configuration selected"), 936 dbc.Button("Set configuration", color="primary", outline=True, 937 id="istd-msdial-configs-button", n_clicks=0), 938 ]) 939 ]), 940 941 html.Br(), 942 943 # UI feedback on adding MSP to chromatography method 944 dbc.Alert(id="chromatography-msp-success-alert", color="success", is_open=False, 945 duration=5000), 946 dbc.Alert(id="chromatography-msp-error-alert", color="danger", is_open=False, 947 duration=5000), 948 949 dbc.Label("Add internal standard identification files", style={"font-weight": "bold"}), 950 html.Br(), 951 952 html.Div([ 953 dbc.Label("Add internal standards (MSP or CSV format)"), 954 dbc.InputGroup([ 955 dbc.Input(placeholder="No file selected", 956 id="add-istd-msp-text-field"), 957 dbc.Button(dcc.Upload( 958 id="add-istd-msp-button", 959 accept="text/plain, application/vnd.ms-excel, .msp, .csv", 960 children=[html.A("Browse Files")]), 961 color="secondary"), 962 ]), 963 dbc.FormText( 964 "Please ensure that each internal standard has a name, m/z, RT, and MS/MS spectrum."), 965 ]), 966 967 html.Br(), 968 969 html.Div([ 970 html.Div([ 971 dbc.Button("Save changes", id="msp-save-changes-button", 972 style={"line-height": "1.75"}, color="primary"), 973 ], className="d-grid gap-2 col-12 mx-auto"), 974 ]), 975 ]), 976 977 # Biological standards 978 dbc.Tab(label="Biological standards", className="modal-styles", children=[ 979 980 html.Br(), 981 982 # UI feedback for biological standard addition/removal 983 dbc.Alert(id="bio-standard-addition-alert", is_open=False, duration=5000), 984 985 dbc.Label("Manage biological standards", style={"font-weight": "bold"}), 986 html.Br(), 987 988 html.Div([ 989 dbc.Label("Add new biological standard"), 990 dbc.InputGroup([ 991 dbc.Input(id="add-bio-standard-text-field", 992 placeholder="Name of biological standard"), 993 dbc.Input(id="add-bio-standard-identifier-text-field", 994 placeholder="Sequence identifier"), 995 dbc.Button("Add biological standard", color="primary", outline=True, 996 id="add-bio-standard-button", n_clicks=0), 997 ]), 998 dbc.FormText( 999 "The sequence identifier is the label that denotes your biological standard in the sequence."), 1000 ]), 1001 1002 html.Br(), 1003 1004 # Table of biological standards 1005 dbc.Label("Biological standards", style={"font-weight": "bold"}), 1006 html.Br(), 1007 1008 html.Div(id="biological-standards-table"), 1009 html.Br(), 1010 1011 dbc.Alert(id="bio-standard-removal-alert", color="primary", is_open=False, duration=5000), 1012 1013 dbc.Label("Configure biological standards and add MSP files", 1014 style={"font-weight": "bold"}), 1015 html.Br(), 1016 1017 # Select biological standard 1018 html.Div([ 1019 dbc.Label("Select biological standard to modify"), 1020 dbc.InputGroup([ 1021 dbc.Select(id="select-bio-standard-dropdown", 1022 placeholder="No biological standard selected"), 1023 dbc.Button("Remove", color="danger", outline=True, 1024 id="remove-bio-standard-button", n_clicks=0), 1025 dbc.Popover("You are about to delete this biological standard and " 1026 "all of its corresponding MSP files. Are you sure?", 1027 target="remove-bio-standard-button", trigger="hover", 1028 body=True) 1029 ]), 1030 ]), 1031 1032 html.Br(), 1033 1034 html.Div([ 1035 dbc.Label("Select chromatography and polarity to modify"), 1036 html.Div(className="parent-container", children=[ 1037 # Select chromatography 1038 html.Div(className="child-container", children=[ 1039 dbc.Select(id="select-bio-chromatography-dropdown", 1040 placeholder="No chromatography selected"), 1041 ]), 1042 1043 # Select polarity 1044 html.Div(className="child-container", children=[ 1045 dbc.Select(id="select-bio-polarity-dropdown", options=[ 1046 {"label": "Positive Mode", "value": "Positive Mode"}, 1047 {"label": "Negative Mode", "value": "Negative Mode"}, 1048 ], placeholder="No polarity selected"), 1049 html.Br(), 1050 ]), 1051 ]), 1052 ]), 1053 1054 html.Br(), html.Br(), 1055 1056 dbc.Alert(id="bio-config-success-alert", color="success", is_open=False, duration=5000), 1057 1058 # Set MS-DIAL configuration for selected biological standard 1059 html.Div(children=[ 1060 dbc.Label("Set MS-DIAL processing configuration", 1061 id="bio-standard-msdial-configs-label"), 1062 dbc.InputGroup([ 1063 dbc.Select(id="bio-standard-msdial-configs-dropdown", 1064 placeholder="No configuration selected"), 1065 dbc.Button("Set configuration", color="primary", outline=True, 1066 id="bio-standard-msdial-configs-button", n_clicks=0), 1067 ]) 1068 ]), 1069 1070 html.Br(), 1071 1072 # UI feedback on adding MSP to biological standard 1073 dbc.Alert(id="bio-msp-success-alert", color="success", is_open=False, 1074 duration=5000), 1075 dbc.Alert(id="bio-msp-error-alert", color="danger", is_open=False, 1076 duration=5000), 1077 1078 html.Div([ 1079 dbc.Label("Edit targeted metabolites list (MSP format)"), 1080 html.Br(), 1081 dbc.InputGroup([ 1082 dbc.Input(placeholder="No MSP file selected", 1083 id="add-bio-msp-text-field"), 1084 dbc.Button(dcc.Upload( 1085 id="add-bio-msp-button", 1086 accept=".msp", 1087 children=[html.A("Browse Files")]), 1088 color="secondary"), 1089 ]), 1090 dbc.FormText( 1091 "Please ensure that each feature has a name, m/z, RT, and MS/MS spectrum."), 1092 ]), 1093 1094 html.Br(), 1095 1096 html.Div([ 1097 html.Div([ 1098 dbc.Button("Save changes", id="bio-standard-save-changes-button", 1099 style={"line-height": "1.75"}, color="primary"), 1100 ], className="d-grid gap-2 col-12 mx-auto"), 1101 ]), 1102 ]), 1103 1104 # AutoQC parameters 1105 dbc.Tab(label="QC configurations", className="modal-styles", children=[ 1106 1107 html.Br(), 1108 1109 # UI feedback on adding / removing QC configurations 1110 dbc.Alert(id="qc-config-addition-alert", is_open=False, duration=5000), 1111 dbc.Alert(id="qc-config-removal-alert", is_open=False, duration=5000), 1112 1113 dbc.Label("Manage QC configurations", style={"font-weight": "bold"}), 1114 html.Br(), 1115 1116 html.Div([ 1117 dbc.Label("Add new QC configuration"), 1118 dbc.InputGroup([ 1119 dbc.Input(id="add-qc-configuration-text-field", 1120 placeholder="Name of configuration to add"), 1121 dbc.Button("Add new config", color="primary", outline=True, 1122 id="add-qc-configuration-button", n_clicks=0), 1123 ]), 1124 dbc.FormText("Give your custom QC configuration a unique name"), 1125 ]), 1126 1127 html.Br(), 1128 1129 # Select configuration 1130 html.Div(children=[ 1131 dbc.Label("Select QC configuration to edit"), 1132 dbc.InputGroup([ 1133 dbc.Select(id="qc-configs-dropdown", 1134 placeholder="No configuration selected"), 1135 dbc.Button("Remove", color="danger", outline=True, 1136 id="remove-qc-config-button", n_clicks=0), 1137 dbc.Popover("You are about to delete this QC configuration. Are you sure?", 1138 target="remove-qc-config-button", trigger="hover", body=True) 1139 ]) 1140 ]), 1141 1142 html.Br(), 1143 1144 dbc.Label("Edit QC configuration parameters", style={"font-weight": "bold"}), 1145 html.Br(), 1146 1147 html.Div([ 1148 dbc.Label("Cutoff for intensity dropouts"), 1149 dbc.InputGroup(children=[ 1150 dbc.Input( 1151 id="intensity-dropouts-cutoff", type="number", placeholder="4"), 1152 dbc.InputGroupText( 1153 dbc.Switch(id="intensity-cutoff-enabled", label="Enabled")), 1154 ]), 1155 dbc.FormText("The minimum number of missing internal " + 1156 "standards in a sample to trigger a QC fail."), 1157 ]), 1158 1159 html.Br(), 1160 1161 html.Div([ 1162 dbc.Label("Cutoff for RT shift from library value"), 1163 dbc.InputGroup(children=[ 1164 dbc.Input(id="library-rt-shift-cutoff", type="number", placeholder="0.1"), 1165 dbc.InputGroupText( 1166 dbc.Switch(id="library-rt-shift-cutoff-enabled", label="Enabled")), 1167 ]), 1168 dbc.FormText( 1169 "The minimum shift in retention time (in minutes) from " + 1170 "the library value to trigger a QC fail."), 1171 ]), 1172 1173 html.Br(), 1174 1175 html.Div([ 1176 dbc.Label("Cutoff for RT shift from in-run average"), 1177 dbc.InputGroup(children=[ 1178 dbc.Input(id="in-run-rt-shift-cutoff", type="number", placeholder="0.05"), 1179 dbc.InputGroupText( 1180 dbc.Switch(id="in-run-rt-shift-cutoff-enabled", label="Enabled")), 1181 ]), 1182 dbc.FormText( 1183 "The minimum shift in retention time (in minutes) from " + 1184 "the in-run average to trigger a QC fail."), 1185 ]), 1186 1187 html.Br(), 1188 1189 html.Div([ 1190 dbc.Label("Cutoff for m/z shift from library value"), 1191 dbc.InputGroup(children=[ 1192 dbc.Input(id="library-mz-shift-cutoff", type="number", placeholder="0.005"), 1193 dbc.InputGroupText( 1194 dbc.Switch(id="library-mz-shift-cutoff-enabled", label="Enabled")), 1195 ]), 1196 dbc.FormText( 1197 "The minimum shift in precursor m/z (in minutes) from " + 1198 "the library value to trigger a QC fail."), 1199 ]), 1200 1201 html.Br(), 1202 1203 # UI feedback on saving changes to MS-DIAL parameters 1204 dbc.Alert(id="qc-parameters-success-alert", 1205 color="success", is_open=False, duration=5000), 1206 dbc.Alert(id="qc-parameters-reset-alert", 1207 color="primary", is_open=False, duration=5000), 1208 dbc.Alert(id="qc-parameters-error-alert", 1209 color="danger", is_open=False, duration=5000), 1210 1211 html.Div([ 1212 html.Div([ 1213 dbc.Button("Save changes", id="save-changes-qc-parameters-button", 1214 style={"line-height": "1.75"}, color="primary"), 1215 dbc.Button("Reset default settings", id="reset-default-qc-parameters-button", 1216 style={"line-height": "1.75"}, color="secondary"), 1217 ], className="d-grid gap-2 col-12 mx-auto"), 1218 ]), 1219 ]), 1220 1221 # MS-DIAL parameters 1222 dbc.Tab(label="MS-DIAL configurations", className="modal-styles", children=[ 1223 1224 html.Br(), 1225 1226 # UI feedback on configuration addition/removal 1227 dbc.Alert(id="msdial-config-addition-alert", is_open=False, duration=5000), 1228 dbc.Alert(id="msdial-config-removal-alert", is_open=False, duration=5000), 1229 dbc.Alert(id="msdial-directory-saved-alert", is_open=False, duration=5000), 1230 1231 dbc.Label("MS-DIAL installation", style={"font-weight": "bold"}), 1232 html.Br(), 1233 1234 # Button and field for selecting the data acquisition directory 1235 html.Div([ 1236 dbc.Label("MS-DIAL download location"), 1237 dbc.InputGroup([ 1238 dbc.Input(placeholder="C:/Users/Me/Downloads/MS-DIAL", 1239 id="msdial-directory"), 1240 dbc.Button("Browse Folders", id="msdial-folder-button", 1241 color="secondary", outline=True), 1242 dbc.Button("Save changes", id="msdial-folder-save-button", 1243 color="primary", outline=True) 1244 ]), 1245 dbc.FormText( 1246 "Browse for (or type) the path of your downloaded MS-DIAL folder."), 1247 ]), 1248 1249 html.Br(), 1250 1251 dbc.Label("Manage configurations", style={"font-weight": "bold"}), 1252 html.Br(), 1253 1254 html.Div([ 1255 dbc.Label("Add new MS-DIAL configuration"), 1256 dbc.InputGroup([ 1257 dbc.Input(id="add-msdial-configuration-text-field", 1258 placeholder="Name of configuration to add"), 1259 dbc.Button("Add new config", color="primary", outline=True, 1260 id="add-msdial-configuration-button", n_clicks=0), 1261 ]), 1262 dbc.FormText("Give your custom configuration a unique name"), 1263 ]), html.Br(), 1264 1265 # Select configuration 1266 html.Div(children=[ 1267 dbc.Label("Select configuration to edit"), 1268 dbc.InputGroup([ 1269 dbc.Select(id="msdial-configs-dropdown", 1270 placeholder="No configuration selected"), 1271 dbc.Button("Remove", color="danger", outline=True, 1272 id="remove-config-button", n_clicks=0), 1273 dbc.Popover("You are about to delete this configuration. Are you sure?", 1274 target="remove-config-button", trigger="hover", body=True) 1275 ]) 1276 ]), html.Br(), 1277 1278 # Data collection parameters 1279 dbc.Label("Data collection parameters", style={"font-weight": "bold"}), 1280 html.Br(), 1281 1282 html.Div(className="parent-container", children=[ 1283 # Retention time begin 1284 html.Div(className="child-container", children=[ 1285 dbc.Label("Retention time begin"), 1286 dbc.Input(id="retention-time-begin", placeholder="0"), 1287 ]), 1288 # Retention time end 1289 html.Div(className="child-container", children=[ 1290 dbc.Label("Retention time end"), 1291 dbc.Input(id="retention-time-end", placeholder="100"), 1292 html.Br(), 1293 ]), 1294 ]), 1295 1296 html.Div(className="parent-container", children=[ 1297 # Mass range begin 1298 html.Div(className="child-container", children=[ 1299 dbc.Label("Mass range begin"), 1300 dbc.Input(id="mass-range-begin", placeholder="0"), 1301 ]), 1302 # Mass range end 1303 html.Div(className="child-container", children=[ 1304 dbc.Label("Mass range end"), 1305 dbc.Input(id="mass-range-end", placeholder="2000"), 1306 html.Br(), 1307 ]), 1308 ]), 1309 1310 # Centroid parameters 1311 dbc.Label("Centroid parameters", style={"font-weight": "bold"}), 1312 html.Br(), 1313 1314 html.Div(className="parent-container", children=[ 1315 # MS1 centroid tolerance 1316 html.Div(className="child-container", children=[ 1317 dbc.Label("MS1 centroid tolerance"), 1318 dbc.Input(id="ms1-centroid-tolerance", placeholder="0.008"), 1319 ]), 1320 # MS2 centroid tolerance 1321 html.Div(className="child-container", children=[ 1322 dbc.Label("MS2 centroid tolerance"), 1323 dbc.Input(id="ms2-centroid-tolerance", placeholder="0.01"), 1324 html.Br(), 1325 ]), 1326 ]), 1327 1328 # Peak detection parameters 1329 dbc.Label("Peak detection parameters", style={"font-weight": "bold"}), 1330 html.Br(), 1331 1332 dbc.Label("Smoothing method"), 1333 dbc.Select(id="select-smoothing-dropdown", options=[ 1334 {"label": "Simple moving average", 1335 "value": "SimpleMovingAverage"}, 1336 {"label": "Linear weighted moving average", 1337 "value": "LinearWeightedMovingAverage"}, 1338 {"label": "Savitzky-Golay filter", 1339 "value": "SavitzkyGolayFilter"}, 1340 {"label": "Binomial filter", 1341 "value": "BinomialFilter"}, 1342 ], placeholder="Linear weighted moving average"), 1343 html.Br(), 1344 1345 html.Div(className="parent-container", children=[ 1346 # Smoothing level 1347 html.Div(className="child-container", children=[ 1348 dbc.Label("Smoothing level"), 1349 dbc.Input(id="smoothing-level", placeholder="3"), 1350 ]), 1351 # Mass slice width 1352 html.Div(className="child-container", children=[ 1353 dbc.Label("Mass slice width"), 1354 dbc.Input(id="mass-slice-width", placeholder="0.1"), 1355 html.Br(), 1356 ]), 1357 ]), 1358 html.Br(), 1359 1360 html.Div(className="parent-container", children=[ 1361 # Minimum peak width 1362 html.Div(className="child-container", children=[ 1363 dbc.Label("Minimum peak width"), 1364 dbc.Input(id="min-peak-width", placeholder="4"), 1365 ]), 1366 # Minimum peak height 1367 html.Div(className="child-container", children=[ 1368 dbc.Label("Minimum peak height"), 1369 dbc.Input(id="min-peak-height", placeholder="50000"), 1370 html.Br(), 1371 ]), 1372 ]), 1373 html.Br(), 1374 1375 # Identification parameters 1376 dbc.Label("Identification parameters", style={"font-weight": "bold"}), 1377 html.Br(), 1378 1379 html.Div(className="parent-container", children=[ 1380 # Retention time tolerance 1381 html.Div(className="child-container", children=[ 1382 dbc.Label("Post-identification retention time tolerance"), 1383 dbc.Input(id="post-id-rt-tolerance", placeholder="0.3"), 1384 ]), 1385 # Accurate mass tolerance 1386 html.Div(className="child-container", children=[ 1387 dbc.Label("Post-identification accurate MS1 tolerance"), 1388 dbc.Input(id="post-id-mz-tolerance", placeholder="0.008"), 1389 html.Br(), 1390 ]), 1391 ]), 1392 html.Br(), 1393 1394 html.Div([ 1395 dbc.Label("Identification score cutoff"), 1396 dbc.Input(id="post-id-score-cutoff", placeholder="85"), 1397 ]), 1398 html.Br(), 1399 1400 # Alignment parameters 1401 dbc.Label("Alignment parameters", style={"font-weight": "bold"}), 1402 html.Br(), 1403 1404 html.Div(className="parent-container", children=[ 1405 # Retention time tolerance 1406 html.Div(className="child-container", children=[ 1407 dbc.Label("Alignment retention time tolerance"), 1408 dbc.Input(id="alignment-rt-tolerance", placeholder="0.05"), 1409 ]), 1410 # Accurate mass tolerance 1411 html.Div(className="child-container", children=[ 1412 dbc.Label("Alignment MS1 tolerance"), 1413 dbc.Input(id="alignment-mz-tolerance", placeholder="0.008"), 1414 html.Br(), 1415 ]), 1416 ]), 1417 html.Br(), 1418 1419 html.Div(className="parent-container", children=[ 1420 # Retention time factor 1421 html.Div(className="child-container", children=[ 1422 dbc.Label("Alignment retention time factor"), 1423 dbc.Input(id="alignment-rt-factor", placeholder="0.5"), 1424 ]), 1425 # Accurate mass factor 1426 html.Div(className="child-container", children=[ 1427 dbc.Label("Alignment MS1 factor"), 1428 dbc.Input(id="alignment-mz-factor", placeholder="0.5"), 1429 html.Br(), 1430 ]), 1431 ]), 1432 html.Br(), 1433 1434 html.Div(className="parent-container", children=[ 1435 # Peak count filter 1436 html.Div(className="child-container", children=[ 1437 dbc.Label("Peak count filter"), 1438 dbc.Input(id="peak-count-filter", placeholder="0"), 1439 ]), 1440 # QC at least filter 1441 html.Div(className="child-container", children=[ 1442 dbc.Label("QC at least filter"), 1443 dbc.Select(id="qc-at-least-filter-dropdown", options=[ 1444 {"label": "True", "value": "True"}, 1445 {"label": "False", "value": "False"}, 1446 ], placeholder="True"), 1447 html.Br(), 1448 ]), 1449 ]), 1450 1451 html.Br(), html.Br(), html.Br(), html.Br(), html.Br(), 1452 html.Br(), html.Br(), html.Br(), html.Br(), html.Br(), 1453 1454 html.Div([ 1455 # UI feedback on saving changes to MS-DIAL parameters 1456 dbc.Alert(id="msdial-parameters-success-alert", 1457 color="success", is_open=False, duration=5000), 1458 dbc.Alert(id="msdial-parameters-reset-alert", 1459 color="primary", is_open=False, duration=5000), 1460 dbc.Alert(id="msdial-parameters-error-alert", 1461 color="danger", is_open=False, duration=5000), 1462 ]), 1463 1464 html.Div([ 1465 html.Div([ 1466 dbc.Button("Save changes", id="save-changes-msdial-parameters-button", 1467 style={"line-height": "1.75"}, color="primary"), 1468 dbc.Button("Reset default settings", id="reset-default-msdial-parameters-button", 1469 style={"line-height": "1.75"}, color="secondary"), 1470 ], className="d-grid gap-2 col-12 mx-auto"), 1471 ]), 1472 ]), 1473 ]) 1474 ]) 1475 ]), 1476 ]), 1477 ]), 1478 ]), 1479 1480 # Dummy input object for callbacks on page load 1481 dcc.Store(id="on-page-load"), 1482 dcc.Store(id="google-drive-authenticated"), 1483 1484 # Storage of all DataFrames necessary for QC plot generation 1485 dcc.Store(id="istd-rt-pos"), 1486 dcc.Store(id="istd-rt-neg"), 1487 dcc.Store(id="istd-intensity-pos"), 1488 dcc.Store(id="istd-intensity-neg"), 1489 dcc.Store(id="istd-mz-pos"), 1490 dcc.Store(id="istd-mz-neg"), 1491 dcc.Store(id="istd-delta-rt-pos"), 1492 dcc.Store(id="istd-delta-rt-neg"), 1493 dcc.Store(id="istd-in-run-delta-rt-pos"), 1494 dcc.Store(id="istd-in-run-delta-rt-neg"), 1495 dcc.Store(id="istd-delta-mz-pos"), 1496 dcc.Store(id="istd-delta-mz-neg"), 1497 dcc.Store(id="qc-warnings-pos"), 1498 dcc.Store(id="qc-warnings-neg"), 1499 dcc.Store(id="qc-fails-pos"), 1500 dcc.Store(id="qc-fails-neg"), 1501 dcc.Store(id="sequence"), 1502 dcc.Store(id="metadata"), 1503 dcc.Store(id="bio-rt-pos"), 1504 dcc.Store(id="bio-rt-neg"), 1505 dcc.Store(id="bio-intensity-pos"), 1506 dcc.Store(id="bio-intensity-neg"), 1507 dcc.Store(id="bio-mz-pos"), 1508 dcc.Store(id="bio-mz-neg"), 1509 dcc.Store(id="study-resources"), 1510 dcc.Store(id="samples"), 1511 dcc.Store(id="pos-internal-standards"), 1512 dcc.Store(id="neg-internal-standards"), 1513 dcc.Store(id="instruments"), 1514 dcc.Store(id="load-finished"), 1515 dcc.Store(id="close-load-modal"), 1516 1517 # Data for starting a new AutoQC job 1518 dcc.Store(id="new-sequence"), 1519 dcc.Store(id="new-metadata"), 1520 1521 # Dummy inputs for UI update callbacks 1522 dcc.Store(id="chromatography-added"), 1523 dcc.Store(id="chromatography-removed"), 1524 dcc.Store(id="chromatography-msdial-config-added"), 1525 dcc.Store(id="istd-msp-added"), 1526 dcc.Store(id="bio-standard-added"), 1527 dcc.Store(id="bio-standard-removed"), 1528 dcc.Store(id="bio-msp-added"), 1529 dcc.Store(id="bio-standard-msdial-config-added"), 1530 dcc.Store(id="qc-config-added"), 1531 dcc.Store(id="qc-config-removed"), 1532 dcc.Store(id="qc-parameters-saved"), 1533 dcc.Store(id="qc-parameters-reset"), 1534 dcc.Store(id="msdial-config-added"), 1535 dcc.Store(id="msdial-config-removed"), 1536 dcc.Store(id="msdial-parameters-saved"), 1537 dcc.Store(id="msdial-parameters-reset"), 1538 dcc.Store(id="msdial-directory-saved"), 1539 dcc.Store(id="google-drive-sync-finished"), 1540 dcc.Store(id="close-sync-modal"), 1541 dcc.Store(id="database-md5"), 1542 dcc.Store(id="selected-data-folder"), 1543 dcc.Store(id="selected-msdial-folder"), 1544 dcc.Store(id="google-drive-user-added"), 1545 dcc.Store(id="google-drive-user-deleted"), 1546 dcc.Store(id="email-added"), 1547 dcc.Store(id="email-deleted"), 1548 dcc.Store(id="gdrive-credentials-saved"), 1549 dcc.Store(id="slack-bot-token-saved"), 1550 dcc.Store(id="slack-channel-saved"), 1551 dcc.Store(id="google-drive-sync-update"), 1552 dcc.Store(id="job-marked-completed"), 1553 dcc.Store(id="job-restarted"), 1554 dcc.Store(id="job-deleted"), 1555 dcc.Store(id="job-action-failed"), 1556 1557 # Dummy inputs for Google Drive authentication 1558 dcc.Store(id="google-drive-download-database"), 1559 dcc.Store(id="workspace-has-been-setup-1"), 1560 dcc.Store(id="workspace-has-been-setup-2"), 1561 dcc.Store(id="google-drive-authenticated-1"), 1562 dcc.Store(id="gdrive-folder-id-1"), 1563 dcc.Store(id="gdrive-database-file-id-1"), 1564 dcc.Store(id="gdrive-methods-zip-id-1"), 1565 dcc.Store(id="google-drive-authenticated-2"), 1566 dcc.Store(id="gdrive-folder-id-2"), 1567 dcc.Store(id="gdrive-database-file-id-2"), 1568 dcc.Store(id="gdrive-methods-zip-id-2"), 1569 dcc.Store(id="google-drive-authenticated-3"), 1570 dcc.Store(id="gdrive-folder-id-3"), 1571 dcc.Store(id="gdrive-database-file-id-3"), 1572 dcc.Store(id="gdrive-methods-zip-id-3"), 1573 ]) 1574 ])
1610@app.callback(Output("google-drive-download-database", "data"), 1611 Input("tabs", "value"), prevent_initial_call=True) 1612def sync_with_google_drive(instrument_id): 1613 1614 """ 1615 For users signed in to MS-AutoQC from an external device, this will download the selected instrument database 1616 """ 1617 1618 # Download database on page load (or refresh) if sync is enabled 1619 if db.sync_is_enabled(): 1620 if instrument_id != db.get_device_identity(): 1621 return db.download_database(instrument_id) 1622 else: 1623 return None 1624 1625 # If Google Drive sync is not enabled, perform no action 1626 else: 1627 raise PreventUpdate
For users signed in to MS-AutoQC from an external device, this will download the selected instrument database
1630@app.callback(Output("google-drive-authenticated", "data"), 1631 Input("on-page-load", "data")) 1632def authenticate_with_google_drive(on_page_load): 1633 1634 """ 1635 Authenticates with Google Drive if the credentials file is found 1636 """ 1637 1638 # Initialize Google Drive if sync is enabled 1639 if db.sync_is_enabled(): 1640 return db.initialize_google_drive() 1641 else: 1642 raise PreventUpdate
Authenticates with Google Drive if the credentials file is found
1645@app.callback(Output("google-drive-authenticated-1", "data"), 1646 Output("google-drive-authenticated-2", "data"), 1647 Output("google-drive-authenticated-3", "data"), 1648 Input("setup-google-drive-button-1", "n_clicks"), 1649 Input("setup-google-drive-button-2", "n_clicks"), 1650 Input("google-drive-sync-button", "n_clicks"), 1651 State("gdrive-client-id-1", "value"), 1652 State("gdrive-client-id-2", "value"), 1653 State("gdrive-client-id", "value"), 1654 State("gdrive-client-secret-1", "value"), 1655 State("gdrive-client-secret-2", "value"), 1656 State("gdrive-client-secret", "value"), prevent_initial_call=True) 1657def launch_google_drive_authentication(setup_auth_button_clicks, sign_in_auth_button_clicks, settings_button_clicks, 1658 client_id_1, client_id_2, client_id_3, client_secret_1, client_secret_2, client_secret_3): 1659 1660 """ 1661 Launches Google Drive authentication window from first-time setup 1662 """ 1663 1664 # Get the correct authentication button 1665 button_id = ctx.triggered_id 1666 1667 # If user clicks a sign-in button, launch Google authentication page 1668 if button_id is not None: 1669 1670 # Create a settings.yaml file to access Drive API 1671 if button_id == "setup-google-drive-button-1": 1672 db.generate_client_settings_yaml(client_id_1, client_secret_1) 1673 elif button_id == "setup-google-drive-button-2": 1674 db.generate_client_settings_yaml(client_id_2, client_secret_2) 1675 elif button_id == "google-drive-sync-button": 1676 # Regenerate Drive settings file 1677 if not os.path.exists(drive_settings_file): 1678 db.generate_client_settings_yaml(client_id_3, client_secret_3) 1679 1680 # Authenticate, then save the credentials to a file 1681 db.launch_google_drive_authentication() 1682 1683 if button_id == "setup-google-drive-button-1": 1684 return True, None, None 1685 elif button_id == "setup-google-drive-button-2": 1686 return None, True, None 1687 elif button_id == "google-drive-sync-button": 1688 return None, None, True 1689 else: 1690 raise PreventUpdate
Launches Google Drive authentication window from first-time setup
1693@app.callback(Output("setup-google-drive-button-1", "children"), 1694 Output("setup-google-drive-button-1", "color"), 1695 Output("setup-google-drive-button-1", "outline"), 1696 Output("google-drive-button-1-popover", "children"), 1697 Output("google-drive-button-1-popover", "is_open"), 1698 Output("gdrive-folder-id-1", "data"), 1699 Output("gdrive-methods-zip-id-1", "data"), 1700 Input("google-drive-authenticated-1", "data"), prevent_initial_call=True) 1701def check_first_time_google_drive_authentication(google_drive_is_authenticated): 1702 1703 """ 1704 UI feedback for Google Drive authentication in Welcome > Setup New Instrument page 1705 """ 1706 1707 if google_drive_is_authenticated: 1708 1709 drive = db.get_drive_instance() 1710 1711 # Initial values 1712 gdrive_folder_id = None 1713 gdrive_methods_zip_id = None 1714 popover_message = [dbc.PopoverHeader("No existing workspace found."), 1715 dbc.PopoverBody("A new MS-AutoQC workspace will be created.")] 1716 1717 # Check for workspace in Google Drive 1718 for file in drive.ListFile({"q": "'root' in parents and trashed=false"}).GetList(): 1719 if file["title"] == "MS-AutoQC": 1720 gdrive_folder_id = file["id"] 1721 break 1722 1723 # If Google Drive folder is found, look for settings database next 1724 if gdrive_folder_id is not None: 1725 for file in drive.ListFile({"q": "'" + gdrive_folder_id + "' in parents and trashed=false"}).GetList(): 1726 if file["title"] == "methods.zip": 1727 os.chdir(data_directory) # Switch to data directory 1728 file.GetContentFile(file["title"]) # Download methods ZIP archive 1729 gdrive_methods_zip_id = file["id"] # Get methods ZIP file ID 1730 os.chdir(root_directory) # Switch back to root directory 1731 db.unzip_methods() # Unzip methods ZIP archive 1732 1733 if gdrive_methods_zip_id is not None: 1734 popover_message = [dbc.PopoverHeader("Workspace found!"), 1735 dbc.PopoverBody("This instrument will be added to the existing MS-AutoQC workspace.")] 1736 1737 return "You're signed in!", "success", False, popover_message, True, gdrive_folder_id, gdrive_methods_zip_id 1738 1739 else: 1740 return "Sign in to Google Drive", "primary", True, "", False, None, None
UI feedback for Google Drive authentication in Welcome > Setup New Instrument page
1743@app.callback(Output("first-time-instrument-vendor", "label"), 1744 Output("thermo-fisher-item", "n_clicks"), 1745 Output("agilent-item", "n_clicks"), 1746 Output("bruker-item", "n_clicks"), 1747 Output("sciex-item", "n_clicks"), 1748 Output("waters-item", "n_clicks"), 1749 Input("thermo-fisher-item", "n_clicks"), 1750 Input("agilent-item", "n_clicks"), 1751 Input("bruker-item", "n_clicks"), 1752 Input("sciex-item", "n_clicks"), 1753 Input("waters-item", "n_clicks"), prevent_initial_call=True) 1754def vendor_dropdown_handling(thermo_fisher_click, agilent_click, bruker_click, sciex_click, waters_click): 1755 1756 """ 1757 Why didn't Dash Bootstrap Components implement this themselves? 1758 The world may never know... 1759 """ 1760 1761 thermo_selected = "Thermo Fisher", 0, 0, 0, 0, 0 1762 agilent_selected = "Agilent", 0, 0, 0, 0, 0, 1763 bruker_selected = "Bruker", 0, 0, 0, 0, 0 1764 sciex_selected = "Sciex", 0, 0, 0, 0, 0 1765 waters_selected = "Waters", 0, 0, 0, 0, 0 1766 1767 inputs = [thermo_fisher_click, agilent_click, bruker_click, sciex_click, waters_click] 1768 outputs = [thermo_selected, agilent_selected, bruker_selected, sciex_selected, waters_selected] 1769 1770 for index, input in enumerate(inputs): 1771 if input is not None: 1772 if input > 0: 1773 return outputs[index]
Why didn't Dash Bootstrap Components implement this themselves? The world may never know...
1809@app.callback(Output("workspace-has-been-setup-1", "data"), 1810 Input("first-time-complete-setup-button", "children"), 1811 State("first-time-instrument-id", "value"), 1812 State("first-time-instrument-vendor", "label"), 1813 State("google-drive-authenticated-1", "data"), 1814 State("gdrive-folder-id-1", "data"), 1815 State("gdrive-methods-zip-id-1", "data"), prevent_initial_call=True) 1816def complete_first_time_setup(button_click, instrument_id, instrument_vendor, google_drive_authenticated, 1817 gdrive_folder_id, methods_zip_file_id): 1818 1819 """ 1820 Upon "Complete setup" button click, this callback completes the following: 1821 1. If databases DO exist in Google Drive, downloads databases 1822 2. If databases DO NOT exist in Google Drive, initializes new SQLite database 1823 3. Adds instrument to "instruments" table 1824 4. Uploads database to Google Drive folder 1825 5. Dismisses setup window 1826 """ 1827 1828 if button_click: 1829 1830 # Initialize a new database if one does not exist 1831 if not db.is_valid(instrument_id=instrument_id): 1832 if methods_zip_file_id is not None: 1833 db.create_databases(instrument_id=instrument_id, new_instrument=True) 1834 else: 1835 db.create_databases(instrument_id=instrument_id) 1836 1837 # Handle Google Drive sync 1838 if google_drive_authenticated: 1839 1840 drive = db.get_drive_instance() 1841 1842 # Create necessary folders if not found 1843 if gdrive_folder_id is None: 1844 1845 # Create MS-AutoQC folder 1846 folder_metadata = { 1847 "title": "MS-AutoQC", 1848 "mimeType": "application/vnd.google-apps.folder" 1849 } 1850 folder = drive.CreateFile(folder_metadata) 1851 folder.Upload() 1852 1853 # Get Google Drive ID of folder 1854 gdrive_folder_id = folder["id"] 1855 1856 # Add instrument to database 1857 db.insert_new_instrument(instrument_id, instrument_vendor) 1858 1859 # Download other instrument databases 1860 for file in drive.ListFile({"q": "'" + gdrive_folder_id + "' in parents and trashed=false"}).GetList(): 1861 if file["title"] != "methods.zip": 1862 os.chdir(data_directory) # Switch to data directory 1863 file.GetContentFile(file["title"]) # Download database ZIP archive 1864 os.chdir(root_directory) # Switch back to root directory 1865 db.unzip_database(filename=file["title"]) # Unzip database ZIP archive 1866 1867 # Sync newly created instrument database to Google Drive folder 1868 db.zip_database(instrument_id=instrument_id) 1869 filename = instrument_id.replace(" ", "_") + ".zip" 1870 1871 metadata = { 1872 "title": filename, 1873 "parents": [{"id": gdrive_folder_id}], 1874 } 1875 file = drive.CreateFile(metadata=metadata) 1876 file.SetContentFile(db.get_database_file(instrument_id, zip=True)) 1877 file.Upload() 1878 1879 # Grab Google Drive file ID 1880 main_db_file_id = file["id"] 1881 1882 # Create local methods directory 1883 if not os.path.exists(methods_directory): 1884 os.makedirs(methods_directory) 1885 1886 # Upload/update local methods directory to Google Drive 1887 methods_zip_file = db.zip_methods() 1888 1889 if methods_zip_file_id is not None: 1890 file = drive.CreateFile({"id": methods_zip_file_id, "title": "methods.zip"}) 1891 else: 1892 metadata = { 1893 "title": "methods.zip", 1894 "parents": [{"id": gdrive_folder_id}], 1895 } 1896 file = drive.CreateFile(metadata=metadata) 1897 1898 file.SetContentFile(methods_zip_file) 1899 file.Upload() 1900 1901 # Grab Google Drive file ID 1902 methods_zip_file_id = file["id"] 1903 1904 # Save user credentials 1905 db.save_google_drive_credentials() 1906 1907 # Save Google Drive ID's for each file 1908 db.insert_google_drive_ids(instrument_id, gdrive_folder_id, main_db_file_id, methods_zip_file_id) 1909 1910 # Sync database with Drive again to save Google Drive ID's 1911 db.upload_database(instrument_id, sync_settings=True) 1912 1913 else: 1914 # Add instrument to database 1915 db.insert_new_instrument(instrument_id, instrument_vendor) 1916 1917 # Dismiss setup window by returning True for workspace_has_been_setup boolean 1918 return db.is_valid() 1919 1920 else: 1921 raise PreventUpdate
Upon "Complete setup" button click, this callback completes the following:
- If databases DO exist in Google Drive, downloads databases
- If databases DO NOT exist in Google Drive, initializes new SQLite database
- Adds instrument to "instruments" table
- Uploads database to Google Drive folder
- Dismisses setup window
1924@app.callback(Output("setup-google-drive-button-2", "children"), 1925 Output("setup-google-drive-button-2", "color"), 1926 Output("setup-google-drive-button-2", "outline"), 1927 Output("google-drive-button-2-popover", "children"), 1928 Output("google-drive-button-2-popover", "is_open"), 1929 Output("gdrive-folder-id-2", "data"), 1930 Output("device-identity-selection", "options"), 1931 Input("google-drive-authenticated-2", "data"), prevent_initial_call=True) 1932def check_workspace_login_google_drive_authentication(google_drive_is_authenticated): 1933 1934 """ 1935 UI feedback for Google Drive authentication in Welcome > Sign In To Workspace page 1936 """ 1937 1938 if google_drive_is_authenticated: 1939 drive = db.get_drive_instance() 1940 1941 # Initial values 1942 gdrive_folder_id = None 1943 1944 # Failed popover message 1945 button_text = "Sign in to Google Drive" 1946 button_color = "danger" 1947 popover_message = [dbc.PopoverHeader("No workspace found"), 1948 dbc.PopoverBody("Double-check that your Google account has access in " + 1949 "Settings > General, or sign in from a different account.")] 1950 1951 # Check for MS-AutoQC folder in Google Drive root directory 1952 for file in drive.ListFile({"q": "'root' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false"}).GetList(): 1953 if file["title"] == "MS-AutoQC": 1954 gdrive_folder_id = file["id"] 1955 break 1956 1957 # If it's not there, check "Shared With Me" and copy it over to root directory 1958 if gdrive_folder_id is None: 1959 for file in drive.ListFile({"q": "sharedWithMe and mimeType='application/vnd.google-apps.folder' and trashed=false"}).GetList(): 1960 if file["title"] == "MS-AutoQC": 1961 gdrive_folder_id = file["id"] 1962 break 1963 1964 # If Google Drive folder is found, download methods directory and all databases next 1965 if gdrive_folder_id is not None: 1966 for file in drive.ListFile({"q": "'" + gdrive_folder_id + "' in parents and trashed=false"}).GetList(): 1967 1968 # Download and unzip instrument databases 1969 if file["title"] != "methods.zip": 1970 os.chdir(data_directory) # Switch to data directory 1971 file.GetContentFile(file["title"]) # Download database ZIP archive 1972 os.chdir(root_directory) # Switch back to root directory 1973 db.unzip_database(filename=file["title"]) # Unzip database ZIP archive 1974 1975 # Download and unzip methods directory 1976 else: 1977 os.chdir(data_directory) # Switch to data directory 1978 file.GetContentFile(file["title"]) # Download methods ZIP archive 1979 os.chdir(root_directory) # Switch back to root directory 1980 db.unzip_methods() # Unzip methods ZIP archive 1981 1982 # Popover alert 1983 button_text = "Signed in to Google Drive" 1984 button_color = "success" 1985 popover_message = [dbc.PopoverHeader("Workspace found!"), 1986 dbc.PopoverBody("Click the button below to sign in.")] 1987 1988 # Fill instrument identity dropdown 1989 instruments = db.get_instruments_list() 1990 instrument_options = [] 1991 for instrument in instruments: 1992 instrument_options.append({"label": instrument, "value": instrument}) 1993 1994 return button_text, button_color, False, popover_message, True, gdrive_folder_id, instrument_options 1995 1996 else: 1997 return "Sign in to Google Drive", "primary", True, "", False, None, []
UI feedback for Google Drive authentication in Welcome > Sign In To Workspace page
2000@app.callback(Output("device-identity-selection", "disabled"), 2001 Input("device-identity-checkbox", "value"), prevent_initial_call=True) 2002def enable_instrument_id_selection(is_instrument_computer): 2003 2004 """ 2005 In Welcome > Sign In To Workspace page, enables instrument dropdown selection if user is signing in to instrument 2006 """ 2007 2008 if is_instrument_computer: 2009 return False 2010 else: 2011 return True
In Welcome > Sign In To Workspace page, enables instrument dropdown selection if user is signing in to instrument
2073@app.callback(Output("workspace-setup-modal", "is_open"), 2074 Output("on-page-load", "data"), 2075 Input("workspace-has-been-setup-1", "data"), 2076 Input("workspace-has-been-setup-2", "data")) 2077def dismiss_setup_window(workspace_has_been_setup_1, workspace_has_been_setup_2): 2078 2079 """ 2080 Checks for a valid database on every start and dismisses setup window if found 2081 """ 2082 2083 # Check if setup is complete 2084 is_valid = db.is_valid() 2085 return not is_valid, is_valid
Checks for a valid database on every start and dismisses setup window if found
2088@app.callback(Output("google-drive-sync-button", "color"), 2089 Output("google-drive-sync-button", "children"), 2090 Output("google-drive-sync-form-text", "children"), 2091 Output("google-drive-sign-in-from-settings-alert", "is_open"), 2092 Output("gdrive-client-id", "placeholder"), 2093 Output("gdrive-client-secret", "placeholder"), 2094 Input("google-drive-authenticated-3", "data"), 2095 Input("google-drive-authenticated", "data"), 2096 Input("settings-modal", "is_open"), 2097 State("google-drive-sync-form-text", "children"), 2098 State("tabs", "value"), prevent_initial_call=True) 2099def update_google_drive_sync_status_in_settings(google_drive_authenticated, google_drive_authenticated_on_start, 2100 settings_is_open, form_text, instrument_id): 2101 2102 """ 2103 Updates Google Drive sync status in user settings on user authentication 2104 """ 2105 2106 trigger = ctx.triggered_id 2107 2108 if not settings_is_open: 2109 raise PreventUpdate 2110 2111 # Authenticated on app startup 2112 if (trigger == "google-drive-authenticated" or trigger == "settings-modal") and google_drive_authenticated_on_start is not None: 2113 form_text = "Cloud sync is enabled! You can now sign in to this MS-AutoQC workspace from any device." 2114 return "success", "Signed in to Google Drive", form_text, False, "Client ID (saved)", "Client secret (saved)" 2115 2116 # Authenticated from "Sign in to Google Drive" button in Settings > General 2117 elif trigger == "google-drive-authenticated-3" and google_drive_authenticated_on_start is None: 2118 2119 drive = db.get_drive_instance() 2120 gdrive_folder_id = None 2121 main_db_file_id = None 2122 2123 # Check for MS-AutoQC folder in Google Drive root directory 2124 for file in drive.ListFile({"q": "'root' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false"}).GetList(): 2125 if file["title"] == "MS-AutoQC": 2126 gdrive_folder_id = file["id"] 2127 break 2128 2129 # If it's not there, check "Shared With Me" and copy it over to root directory 2130 if gdrive_folder_id is None: 2131 for file in drive.ListFile({"q": "sharedWithMe and mimeType='application/vnd.google-apps.folder' and trashed=false"}).GetList(): 2132 if file["title"] == "MS-AutoQC": 2133 gdrive_folder_id = file["id"] 2134 break 2135 2136 # If Google Drive folder is found, alert user that they need to sign in with a different Google account 2137 if gdrive_folder_id is not None: 2138 os.remove(credentials_file) 2139 return "danger", "Sign in to Google Drive", form_text, True, "Client ID", "Client secret" 2140 2141 # If no workspace found, all good to create one 2142 else: 2143 # Create MS-AutoQC folder 2144 folder_metadata = { 2145 "title": "MS-AutoQC", 2146 "mimeType": "application/vnd.google-apps.folder" 2147 } 2148 folder = drive.CreateFile(folder_metadata) 2149 folder.Upload() 2150 2151 # Get Google Drive ID of folder 2152 for file in drive.ListFile({"q": "'root' in parents and trashed=false"}).GetList(): 2153 if file["title"] == "MS-AutoQC": 2154 gdrive_folder_id = file["id"] 2155 break 2156 2157 # Upload database to Google Drive folder 2158 db.zip_database(instrument_id=instrument_id) 2159 2160 metadata = { 2161 "title": instrument_id.replace(" ", "_") + ".zip", 2162 "parents": [{"id": gdrive_folder_id}], 2163 } 2164 2165 file = drive.CreateFile(metadata=metadata) 2166 file.SetContentFile(db.get_database_file(instrument_id, zip=True)) 2167 file.Upload() 2168 main_db_file_id = file["id"] 2169 2170 # Create local methods directory 2171 if not os.path.exists(methods_directory): 2172 os.makedirs(methods_directory) 2173 2174 # Upload local methods directory to Google Drive 2175 methods_zip_file = db.zip_methods() 2176 2177 metadata = { 2178 "title": "methods.zip", 2179 "parents": [{"id": gdrive_folder_id}], 2180 } 2181 2182 file = drive.CreateFile(metadata=metadata) 2183 file.SetContentFile(methods_zip_file) 2184 file.Upload() 2185 methods_zip_file_id = file["id"] 2186 2187 # Put Google Drive ID's into database 2188 db.insert_google_drive_ids(instrument_id, gdrive_folder_id, main_db_file_id, methods_zip_file_id) 2189 2190 # Sync database 2191 db.upload_database(instrument_id, sync_settings=True) 2192 2193 # Save user credentials 2194 db.save_google_drive_credentials() 2195 2196 form_text = "Cloud sync is enabled! You can now sign in to this MS-AutoQC workspace from any device." 2197 return "success", "Signed in to Google Drive", form_text, False, "Client ID (saved)", "Client secret (saved)" 2198 2199 else: 2200 raise PreventUpdate
Updates Google Drive sync status in user settings on user authentication
2203@app.callback(Output("gdrive-credentials-saved", "data"), 2204 Input("set-gdrive-credentials-button", "n_clicks"), 2205 State("gdrive-client-id", "value"), 2206 State("gdrive-client-secret", "value"), prevent_initial_call=True) 2207def regenerate_settings_yaml_file(button_click, client_id, client_secret): 2208 2209 """ 2210 Regenerates settings.yaml file with new credentials 2211 """ 2212 2213 # Ensure user has entered client ID and client secret 2214 if client_id is not None and client_secret is not None: 2215 2216 # Delete existing settings.yaml file (if it exists) 2217 if os.path.exists(drive_settings_file): 2218 os.remove(drive_settings_file) 2219 2220 # Regenerate file 2221 db.generate_client_settings_yaml(client_id, client_secret) 2222 return "Success" 2223 2224 else: 2225 return "Error"
Regenerates settings.yaml file with new credentials
2228@app.callback(Output("gdrive-credentials-saved-alert", "is_open"), 2229 Output("gdrive-credentials-saved-alert", "children"), 2230 Output("gdrive-credentials-saved-alert", "color"), 2231 Input("gdrive-credentials-saved", "data"), prevent_initial_call=True) 2232def ui_alert_on_gdrive_credential_save(credential_save_result): 2233 2234 """ 2235 Displays UI alert on Google API credential save 2236 """ 2237 2238 if credential_save_result is not None: 2239 if credential_save_result == "Success": 2240 return True, "Your Google API credentials were successfully saved.", "success" 2241 elif credential_save_result == "Error": 2242 return True, "Error: Please enter both the client ID and client secret first.", "danger" 2243 else: 2244 raise PreventUpdate
Displays UI alert on Google API credential save
2247@app.callback(Output("tabs", "children"), 2248 Output("tabs", "value"), 2249 Input("instruments", "data"), 2250 Input("workspace-setup-modal", "is_open"), 2251 Input("google-drive-sync-update", "data")) 2252def get_instrument_tabs(instruments, check_workspace_setup, sync_update): 2253 2254 """ 2255 Retrieves all instruments on a user installation of MS-AutoQC 2256 """ 2257 2258 if db.is_valid(): 2259 2260 # Get list of instruments from database 2261 instrument_list = db.get_instruments_list() 2262 2263 # Create tabs for each instrument 2264 instrument_tabs = [] 2265 for instrument in instrument_list: 2266 instrument_tabs.append(dcc.Tab(label=instrument, value=instrument)) 2267 2268 return instrument_tabs, instrument_list[0] 2269 2270 else: 2271 raise PreventUpdate
Retrieves all instruments on a user installation of MS-AutoQC
2274@app.callback(Output("instrument-run-table", "active_cell"), 2275 Output("instrument-run-table", "selected_cells"), 2276 Input("tabs", "value"), 2277 Input("job-deleted", "data"), prevent_initial_call=True) 2278def reset_instrument_table(instrument, job_deleted): 2279 2280 """ 2281 Removes selected cell highlight upon tab switch to different instrument 2282 (A case study in insane side missions during frontend development) 2283 """ 2284 2285 return None, []
Removes selected cell highlight upon tab switch to different instrument (A case study in insane side missions during frontend development)
2288@app.callback(Output("instrument-run-table", "data"), 2289 Output("table-container", "style"), 2290 Output("plot-container", "style"), 2291 Input("tabs", "value"), 2292 Input("refresh-interval", "n_intervals"), 2293 State("study-resources", "data"), 2294 Input("google-drive-sync-update", "data"), 2295 Input("start-run-monitor-modal", "is_open"), 2296 Input("job-marked-completed", "data"), 2297 Input("job-deleted", "data")) 2298def populate_instrument_runs_table(instrument_id, refresh, resources, sync_update, new_job_started, job_marked_completed, job_deleted): 2299 2300 """ 2301 Dash callback for populating tables with list of past/active instrument runs 2302 """ 2303 2304 trigger = ctx.triggered_id 2305 2306 # Ensure that refresh does not trigger data parsing if no new samples processed 2307 if trigger == "refresh-interval": 2308 resources = json.loads(resources) 2309 run_id = resources["run_id"] 2310 status = resources["status"] 2311 2312 if db.get_device_identity() != instrument_id: 2313 if db.sync_is_enabled() and status != "Complete": 2314 db.download_qc_results(instrument_id, run_id) 2315 2316 completed_count_in_cache = resources["samples_completed"] 2317 actual_completed_count, total = db.get_completed_samples_count(instrument_id, run_id, status) 2318 2319 if completed_count_in_cache == actual_completed_count: 2320 raise PreventUpdate 2321 2322 if instrument_id != "tab-1": 2323 # Get instrument runs from database 2324 df_instrument_runs = db.get_instrument_runs(instrument_id) 2325 2326 if len(df_instrument_runs) == 0: 2327 empty_table = [{"Run ID": "N/A", "Chromatography": "N/A", "Status": "N/A"}] 2328 return empty_table, {"display": "block"}, {"display": "none"} 2329 2330 # DataFrame refactoring 2331 df_instrument_runs = df_instrument_runs[["run_id", "chromatography", "status"]] 2332 df_instrument_runs = df_instrument_runs.rename( 2333 columns={"run_id": "Run ID", 2334 "chromatography": "Chromatography", 2335 "status": "Status"}) 2336 df_instrument_runs = df_instrument_runs[::-1] 2337 2338 # Convert DataFrame into a dictionary 2339 instrument_runs = df_instrument_runs.to_dict("records") 2340 return instrument_runs, {"display": "block"}, {"display": "block"} 2341 2342 else: 2343 raise PreventUpdate
Dash callback for populating tables with list of past/active instrument runs
2346@app.callback(Output("loading-modal", "is_open"), 2347 Output("loading-modal-title", "children"), 2348 Output("loading-modal-body", "children"), 2349 Input("instrument-run-table", "active_cell"), 2350 State("instrument-run-table", "data"), 2351 Input("close-load-modal", "data"), prevent_initial_call=True, suppress_callback_exceptions=True) 2352def open_loading_modal(active_cell, table_data, load_finished): 2353 2354 """ 2355 Shows loading modal on selection of an instrument run 2356 """ 2357 2358 trigger = ctx.triggered_id 2359 2360 if active_cell: 2361 run_id = table_data[active_cell["row"]]["Run ID"] 2362 2363 title = html.Div([ 2364 html.Div(children=[dbc.Spinner(color="primary"), " Loading QC results for " + run_id])]) 2365 body = "This may take a few seconds..." 2366 2367 if trigger == "instrument-run-table": 2368 modal_is_open = True 2369 elif trigger == "close-load-modal": 2370 modal_is_open = False 2371 2372 return modal_is_open, title, body 2373 2374 else: 2375 raise PreventUpdate
Shows loading modal on selection of an instrument run
2378@app.callback(Output("istd-rt-pos", "data"), 2379 Output("istd-rt-neg", "data"), 2380 Output("istd-intensity-pos", "data"), 2381 Output("istd-intensity-neg", "data"), 2382 Output("istd-mz-pos", "data"), 2383 Output("istd-mz-neg", "data"), 2384 Output("sequence", "data"), 2385 Output("metadata", "data"), 2386 Output("bio-rt-pos", "data"), 2387 Output("bio-rt-neg", "data"), 2388 Output("bio-intensity-pos", "data"), 2389 Output("bio-intensity-neg", "data"), 2390 Output("bio-mz-pos", "data"), 2391 Output("bio-mz-neg", "data"), 2392 Output("study-resources", "data"), 2393 Output("samples", "data"), 2394 Output("pos-internal-standards", "data"), 2395 Output("neg-internal-standards", "data"), 2396 Output("istd-delta-rt-pos", "data"), 2397 Output("istd-delta-rt-neg", "data"), 2398 Output("istd-in-run-delta-rt-pos", "data"), 2399 Output("istd-in-run-delta-rt-neg", "data"), 2400 Output("istd-delta-mz-pos", "data"), 2401 Output("istd-delta-mz-neg", "data"), 2402 Output("qc-warnings-pos", "data"), 2403 Output("qc-warnings-neg", "data"), 2404 Output("qc-fails-pos", "data"), 2405 Output("qc-fails-neg", "data"), 2406 Output("load-finished", "data"), 2407 Input("refresh-interval", "n_intervals"), 2408 Input("instrument-run-table", "active_cell"), 2409 State("instrument-run-table", "data"), 2410 State("study-resources", "data"), 2411 State("tabs", "value"), prevent_initial_call=True, suppress_callback_exceptions=True) 2412def load_data(refresh, active_cell, table_data, resources, instrument_id): 2413 2414 """ 2415 Updates and stores QC results in dcc.Store objects (user's browser session) 2416 """ 2417 2418 trigger = ctx.triggered_id 2419 2420 if active_cell: 2421 2422 # Get run ID and status 2423 run_id = table_data[active_cell["row"]]["Run ID"] 2424 status = table_data[active_cell["row"]]["Status"] 2425 2426 # Ensure that refresh does not trigger data parsing if no new samples processed 2427 if trigger == "refresh-interval": 2428 try: 2429 if db.get_device_identity() != instrument_id: 2430 if db.sync_is_enabled() and status != "Complete": 2431 db.download_qc_results(instrument_id, run_id) 2432 2433 completed_count_in_cache = json.loads(resources)["samples_completed"] 2434 actual_completed_count, total = db.get_completed_samples_count(instrument_id, run_id, status) 2435 2436 if completed_count_in_cache == actual_completed_count: 2437 raise PreventUpdate 2438 except: 2439 raise PreventUpdate 2440 2441 # If the acquisition listener was stopped for some reason, start a new process and pass remaining samples 2442 if status == "Active" and os.name == "nt": 2443 2444 # Check that device is the instrument that the run is on 2445 if db.get_device_identity() == instrument_id: 2446 2447 # Get listener process ID from database; if process is not running, restart it 2448 listener_id = db.get_pid(instrument_id, run_id) 2449 if not qc.subprocess_is_running(listener_id): 2450 2451 # Retrieve acquisition path 2452 acquisition_path = db.get_acquisition_path(instrument_id, run_id).replace("\\", "/") 2453 acquisition_path = acquisition_path + "/" if acquisition_path[-1] != "/" else acquisition_path 2454 2455 # Delete temporary data file directory 2456 db.delete_temp_directory(instrument_id, run_id) 2457 2458 # Restart AcquisitionListener and store process ID 2459 process = psutil.Popen(["py", "AcquisitionListener.py", acquisition_path, instrument_id, run_id]) 2460 db.store_pid(instrument_id, run_id, process.pid) 2461 2462 # If new sample, route raw data -> parsed data -> user session cache -> plots 2463 return get_qc_results(instrument_id, run_id, status) + (True,) 2464 2465 else: 2466 return (None, None, None, None, None, None, None, None, None, None, 2467 None, None, None, None, None, None, None, None, None, None, 2468 None, None, None, None, None, None, None, None, None)
Updates and stores QC results in dcc.Store objects (user's browser session)
2479@app.callback(Output("sample-table", "data"), 2480 Input("samples", "data"), prevent_initial_call=True) 2481def populate_sample_tables(samples): 2482 2483 """ 2484 Populates table with list of samples for selected run from instrument runs table 2485 """ 2486 2487 if samples is not None: 2488 df_samples = pd.DataFrame(json.loads(samples)) 2489 df_samples = df_samples[["Sample", "Position", "QC"]] 2490 return df_samples.to_dict("records") 2491 else: 2492 return None
Populates table with list of samples for selected run from instrument runs table
2495@app.callback(Output("istd-rt-dropdown", "options"), 2496 Output("istd-mz-dropdown", "options"), 2497 Output("istd-intensity-dropdown", "options"), 2498 Output("bio-standard-benchmark-dropdown", "options"), 2499 Output("rt-plot-sample-dropdown", "options"), 2500 Output("mz-plot-sample-dropdown", "options"), 2501 Output("intensity-plot-sample-dropdown", "options"), 2502 Input("polarity-options", "value"), 2503 State("sample-table", "data"), 2504 Input("samples", "data"), 2505 State("bio-intensity-pos", "data"), 2506 State("bio-intensity-neg", "data"), 2507 State("pos-internal-standards", "data"), 2508 State("neg-internal-standards", "data")) 2509def update_dropdowns_on_polarity_change(polarity, table_data, samples, bio_intensity_pos, bio_intensity_neg, 2510 pos_internal_standards, neg_internal_standards): 2511 2512 """ 2513 Updates dropdown lists with correct items for user-selected polarity 2514 """ 2515 2516 if samples is not None: 2517 df_samples = pd.DataFrame(json.loads(samples)) 2518 2519 if polarity == "Neg": 2520 istd_dropdown = json.loads(neg_internal_standards) 2521 2522 if bio_intensity_neg is not None: 2523 df = pd.DataFrame(json.loads(bio_intensity_neg)) 2524 df.drop(columns=["Name"], inplace=True) 2525 bio_dropdown = df.columns.tolist() 2526 else: 2527 bio_dropdown = [] 2528 2529 df_samples = df_samples.loc[df_samples["Sample"].str.contains("Neg")] 2530 sample_dropdown = df_samples["Sample"].tolist() 2531 2532 elif polarity == "Pos": 2533 istd_dropdown = json.loads(pos_internal_standards) 2534 2535 if bio_intensity_pos is not None: 2536 df = pd.DataFrame(json.loads(bio_intensity_pos)) 2537 df.drop(columns=["Name"], inplace=True) 2538 bio_dropdown = df.columns.tolist() 2539 else: 2540 bio_dropdown = [] 2541 2542 df_samples = df_samples.loc[(df_samples["Sample"].str.contains("Pos"))] 2543 sample_dropdown = df_samples["Sample"].tolist() 2544 2545 return istd_dropdown, istd_dropdown, istd_dropdown, bio_dropdown, sample_dropdown, sample_dropdown, sample_dropdown 2546 2547 else: 2548 return [], [], [], [], [], [], []
Updates dropdown lists with correct items for user-selected polarity
2551@app.callback(Output("rt-plot-sample-dropdown", "value"), 2552 Output("mz-plot-sample-dropdown", "value"), 2553 Output("intensity-plot-sample-dropdown", "value"), 2554 Input("sample-filtering-options", "value"), 2555 Input("polarity-options", "value"), 2556 Input("samples", "data"), 2557 State("metadata", "data"), prevent_initial_call=True) 2558def apply_sample_filter_to_plots(filter, polarity, samples, metadata): 2559 2560 """ 2561 Apply sample filter to internal standard plots, options are: 2562 1. All samples 2563 2. Filter by samples only 2564 3. Filter by treatments / classes 2565 4. Filter by pools 2566 5. Filter by blanks 2567 """ 2568 2569 # Get complete list of samples (including blanks + pools) in polarity 2570 if samples is not None: 2571 df_samples = pd.DataFrame(json.loads(samples)) 2572 df_samples = df_samples.loc[df_samples["Polarity"].str.contains(polarity)] 2573 sample_list = df_samples["Sample"].tolist() 2574 else: 2575 raise PreventUpdate 2576 2577 if filter is not None: 2578 # Return all samples, blanks, and pools 2579 if filter == "all": 2580 return [], [], [] 2581 2582 # Return samples only 2583 elif filter == "samples": 2584 df_metadata = pd.read_json(metadata, orient="split") 2585 df_metadata = df_metadata.loc[df_metadata["Filename"].isin(sample_list)] 2586 samples_only = df_metadata["Filename"].tolist() 2587 return samples_only, samples_only, samples_only 2588 2589 # Return pools only 2590 elif filter == "pools": 2591 pools = [sample for sample in sample_list if "QC" in sample] 2592 return pools, pools, pools 2593 2594 # Return blanks only 2595 elif filter == "blanks": 2596 blanks = [sample for sample in sample_list if "BK" in sample] 2597 return blanks, blanks, blanks 2598 2599 else: 2600 return [], [], []
Apply sample filter to internal standard plots, options are:
- All samples
- Filter by samples only
- Filter by treatments / classes
- Filter by pools
- Filter by blanks
2603@app.callback(Output("istd-rt-plot", "figure"), 2604 Output("rt-prev-button", "n_clicks"), 2605 Output("rt-next-button", "n_clicks"), 2606 Output("istd-rt-dropdown", "value"), 2607 Output("istd-rt-div", "style"), 2608 Input("polarity-options", "value"), 2609 Input("istd-rt-dropdown", "value"), 2610 Input("rt-plot-sample-dropdown", "value"), 2611 Input("istd-rt-pos", "data"), 2612 Input("istd-rt-neg", "data"), 2613 State("samples", "data"), 2614 State("study-resources", "data"), 2615 State("pos-internal-standards", "data"), 2616 State("neg-internal-standards", "data"), 2617 Input("rt-prev-button", "n_clicks"), 2618 Input("rt-next-button", "n_clicks"), prevent_initial_call=True) 2619def populate_istd_rt_plot(polarity, internal_standard, selected_samples, rt_pos, rt_neg, samples, resources, 2620 pos_internal_standards, neg_internal_standards, previous, next): 2621 2622 """ 2623 Populates internal standard retention time vs. sample plot 2624 """ 2625 2626 if rt_pos is None and rt_neg is None: 2627 return {}, None, None, None, {"display": "none"} 2628 2629 trigger = ctx.triggered_id 2630 2631 # Get internal standard RT data 2632 df_istd_rt_pos = pd.DataFrame() 2633 df_istd_rt_neg = pd.DataFrame() 2634 2635 if rt_pos is not None: 2636 df_istd_rt_pos = pd.DataFrame(json.loads(rt_pos)) 2637 2638 if rt_neg is not None: 2639 df_istd_rt_neg = pd.DataFrame(json.loads(rt_neg)) 2640 2641 # Get samples 2642 df_samples = pd.DataFrame(json.loads(samples)) 2643 samples = df_samples.loc[df_samples["Polarity"] == polarity]["Sample"].astype(str).tolist() 2644 2645 # Filter out biological standards 2646 identifiers = db.get_biological_standard_identifiers() 2647 for identifier in identifiers: 2648 samples = [x for x in samples if identifier not in x] 2649 2650 # Filter samples and internal standards by polarity 2651 if polarity == "Pos": 2652 internal_standards = json.loads(pos_internal_standards) 2653 df_istd_rt = df_istd_rt_pos 2654 elif polarity == "Neg": 2655 internal_standards = json.loads(neg_internal_standards) 2656 df_istd_rt = df_istd_rt_neg 2657 2658 # Get retention times 2659 retention_times = json.loads(resources)["retention_times_dict"] 2660 2661 # Set initial dropdown values when none are selected 2662 if not internal_standard or trigger == "polarity-options": 2663 internal_standard = internal_standards[0] 2664 2665 if not selected_samples: 2666 selected_samples = samples 2667 2668 # Calculate index of internal standard from button clicks 2669 if trigger == "rt-prev-button" or trigger == "rt-next-button": 2670 index = get_internal_standard_index(previous, next, len(internal_standards)) 2671 internal_standard = internal_standards[index] 2672 else: 2673 index = next 2674 2675 try: 2676 # Generate internal standard RT vs. sample plot 2677 return load_istd_rt_plot(dataframe=df_istd_rt, samples=selected_samples, 2678 internal_standard=internal_standard, retention_times=retention_times), \ 2679 None, index, internal_standard, {"display": "block"} 2680 2681 except Exception as error: 2682 print("Error in loading RT vs. sample plot:", error) 2683 return {}, None, None, None, {"display": "none"}
Populates internal standard retention time vs. sample plot
2686@app.callback(Output("istd-intensity-plot", "figure"), 2687 Output("intensity-prev-button", "n_clicks"), 2688 Output("intensity-next-button", "n_clicks"), 2689 Output("istd-intensity-dropdown", "value"), 2690 Output("istd-intensity-div", "style"), 2691 Input("polarity-options", "value"), 2692 Input("istd-intensity-dropdown", "value"), 2693 Input("intensity-plot-sample-dropdown", "value"), 2694 Input("istd-intensity-pos", "data"), 2695 Input("istd-intensity-neg", "data"), 2696 State("samples", "data"), 2697 State("metadata", "data"), 2698 State("pos-internal-standards", "data"), 2699 State("neg-internal-standards", "data"), 2700 Input("intensity-prev-button", "n_clicks"), 2701 Input("intensity-next-button", "n_clicks"), prevent_initial_call=True) 2702def populate_istd_intensity_plot(polarity, internal_standard, selected_samples, intensity_pos, intensity_neg, samples, metadata, 2703 pos_internal_standards, neg_internal_standards, previous, next): 2704 2705 """ 2706 Populates internal standard intensity vs. sample plot 2707 """ 2708 2709 if intensity_pos is None and intensity_neg is None: 2710 return {}, None, None, None, {"display": "none"} 2711 2712 trigger = ctx.triggered_id 2713 2714 # Get internal standard intensity data 2715 df_istd_intensity_pos = pd.DataFrame() 2716 df_istd_intensity_neg = pd.DataFrame() 2717 2718 if intensity_pos is not None: 2719 df_istd_intensity_pos = pd.DataFrame(json.loads(intensity_pos)) 2720 2721 if intensity_neg is not None: 2722 df_istd_intensity_neg = pd.DataFrame(json.loads(intensity_neg)) 2723 2724 # Get samples 2725 df_samples = pd.DataFrame(json.loads(samples)) 2726 samples = df_samples.loc[df_samples["Polarity"] == polarity]["Sample"].astype(str).tolist() 2727 2728 identifiers = db.get_biological_standard_identifiers() 2729 for identifier in identifiers: 2730 samples = [x for x in samples if identifier not in x] 2731 2732 # Get sample metadata 2733 df_metadata = pd.read_json(metadata, orient="split") 2734 2735 # Filter samples and internal standards by polarity 2736 if polarity == "Pos": 2737 internal_standards = json.loads(pos_internal_standards) 2738 df_istd_intensity = df_istd_intensity_pos 2739 elif polarity == "Neg": 2740 internal_standards = json.loads(neg_internal_standards) 2741 df_istd_intensity = df_istd_intensity_neg 2742 2743 # Set initial internal standard dropdown value when none are selected 2744 if not internal_standard or trigger == "polarity-options": 2745 internal_standard = internal_standards[0] 2746 2747 # Set initial sample dropdown value when none are selected 2748 if not selected_samples: 2749 selected_samples = samples 2750 treatments = pd.DataFrame() 2751 else: 2752 df_metadata = df_metadata.loc[df_metadata["Filename"].isin(selected_samples)] 2753 treatments = df_metadata[["Filename", "Treatment"]] 2754 if len(df_metadata) == len(selected_samples): 2755 selected_samples = df_metadata["Filename"].tolist() 2756 2757 # Calculate index of internal standard from button clicks 2758 if trigger == "intensity-prev-button" or trigger == "intensity-next-button": 2759 index = get_internal_standard_index(previous, next, len(internal_standards)) 2760 internal_standard = internal_standards[index] 2761 else: 2762 index = next 2763 2764 try: 2765 # Generate internal standard intensity vs. sample plot 2766 return load_istd_intensity_plot(dataframe=df_istd_intensity, samples=selected_samples, 2767 internal_standard=internal_standard, treatments=treatments), \ 2768 None, index, internal_standard, {"display": "block"} 2769 2770 except Exception as error: 2771 print("Error in loading intensity vs. sample plot:", error) 2772 return {}, None, None, None, {"display": "none"}
Populates internal standard intensity vs. sample plot
2775@app.callback(Output("istd-mz-plot", "figure"), 2776 Output("mz-prev-button", "n_clicks"), 2777 Output("mz-next-button", "n_clicks"), 2778 Output("istd-mz-dropdown", "value"), 2779 Output("istd-mz-div", "style"), 2780 Input("polarity-options", "value"), 2781 Input("istd-mz-dropdown", "value"), 2782 Input("mz-plot-sample-dropdown", "value"), 2783 Input("istd-delta-mz-pos", "data"), 2784 Input("istd-delta-mz-neg", "data"), 2785 State("samples", "data"), 2786 State("pos-internal-standards", "data"), 2787 State("neg-internal-standards", "data"), 2788 State("study-resources", "data"), 2789 Input("mz-prev-button", "n_clicks"), 2790 Input("mz-next-button", "n_clicks"), prevent_initial_call=True) 2791def populate_istd_mz_plot(polarity, internal_standard, selected_samples, delta_mz_pos, delta_mz_neg, samples, 2792 pos_internal_standards, neg_internal_standards, resources, previous, next): 2793 2794 """ 2795 Populates internal standard delta m/z vs. sample plot 2796 """ 2797 2798 if delta_mz_pos is None and delta_mz_neg is None: 2799 return {}, None, None, None, {"display": "none"} 2800 2801 trigger = ctx.triggered_id 2802 2803 # Get internal standard RT data 2804 df_istd_mz_pos = pd.DataFrame() 2805 df_istd_mz_neg = pd.DataFrame() 2806 2807 if delta_mz_pos is not None: 2808 df_istd_mz_pos = pd.DataFrame(json.loads(delta_mz_pos)) 2809 2810 if delta_mz_neg is not None: 2811 df_istd_mz_neg = pd.DataFrame(json.loads(delta_mz_neg)) 2812 2813 # Get samples (and filter out biological standards) 2814 df_samples = pd.DataFrame(json.loads(samples)) 2815 samples = df_samples.loc[df_samples["Polarity"] == polarity]["Sample"].astype(str).tolist() 2816 2817 identifiers = db.get_biological_standard_identifiers() 2818 for identifier in identifiers: 2819 samples = [x for x in samples if identifier not in x] 2820 2821 # Filter samples and internal standards by polarity 2822 if polarity == "Pos": 2823 internal_standards = json.loads(pos_internal_standards) 2824 df_istd_mz = df_istd_mz_pos 2825 2826 elif polarity == "Neg": 2827 internal_standards = json.loads(neg_internal_standards) 2828 df_istd_mz = df_istd_mz_neg 2829 2830 # Set initial dropdown values when none are selected 2831 if not internal_standard or trigger == "polarity-options": 2832 internal_standard = internal_standards[0] 2833 if not selected_samples: 2834 selected_samples = samples 2835 2836 # Calculate index of internal standard from button clicks 2837 if trigger == "mz-prev-button" or trigger == "mz-next-button": 2838 index = get_internal_standard_index(previous, next, len(internal_standards)) 2839 internal_standard = internal_standards[index] 2840 else: 2841 index = next 2842 2843 try: 2844 # Generate internal standard delta m/z vs. sample plot 2845 return load_istd_delta_mz_plot(dataframe=df_istd_mz, samples=selected_samples, internal_standard=internal_standard), \ 2846 None, index, internal_standard, {"display": "block"} 2847 2848 except Exception as error: 2849 print("Error in loading delta m/z vs. sample plot:", error) 2850 return {}, None, None, None, {"display": "none"}
Populates internal standard delta m/z vs. sample plot
2853@app.callback(Output("bio-standards-plot-dropdown", "options"), 2854 Input("study-resources", "data"), prevent_initial_call=True) 2855def populate_biological_standards_dropdown(resources): 2856 2857 """ 2858 Retrieves list of biological standards included in run 2859 """ 2860 2861 try: 2862 return ast.literal_eval(ast.literal_eval(resources)["biological_standards"]) 2863 except: 2864 return []
Retrieves list of biological standards included in run
2867@app.callback(Output("bio-standard-mz-rt-plot", "figure"), 2868 Output("bio-standard-benchmark-dropdown", "value"), 2869 Output("bio-standard-mz-rt-plot", "clickData"), 2870 Output("bio-standard-mz-rt-div", "style"), 2871 Input("polarity-options", "value"), 2872 Input("bio-rt-pos", "data"), 2873 Input("bio-rt-neg", "data"), 2874 State("bio-intensity-pos", "data"), 2875 State("bio-intensity-neg", "data"), 2876 State("bio-mz-pos", "data"), 2877 State("bio-mz-neg", "data"), 2878 State("study-resources", "data"), 2879 Input("bio-standard-mz-rt-plot", "clickData"), 2880 Input("bio-standards-plot-dropdown", "value"), prevent_initial_call=True) 2881def populate_bio_standard_mz_rt_plot(polarity, rt_pos, rt_neg, intensity_pos, intensity_neg, mz_pos, mz_neg, 2882 resources, click_data, selected_bio_standard): 2883 2884 """ 2885 Populates biological standard m/z vs. RT plot 2886 """ 2887 2888 if rt_pos is None and rt_neg is None: 2889 if mz_pos is None and mz_neg is None: 2890 return {}, None, None, {"display": "none"} 2891 2892 # Get run ID and status 2893 resources = json.loads(resources) 2894 instrument_id = resources["instrument"] 2895 run_id = resources["run_id"] 2896 status = resources["status"] 2897 2898 # Get Google Drive instance 2899 drive = None 2900 if status == "Active" and db.sync_is_enabled(): 2901 drive = db.get_drive_instance() 2902 2903 # Toggle a different biological standard 2904 if selected_bio_standard is not None: 2905 rt_pos, rt_neg, intensity_pos, intensity_neg, mz_pos, mz_neg = get_qc_results(instrument_id=instrument_id, 2906 run_id=run_id, status=status, biological_standard=selected_bio_standard, biological_standards_only=True) 2907 2908 # Get biological standard m/z, RT, and intensity data 2909 if polarity == "Pos": 2910 if rt_pos is not None and intensity_pos is not None and mz_pos is not None: 2911 df_bio_rt = pd.DataFrame(json.loads(rt_pos)) 2912 df_bio_intensity = pd.DataFrame(json.loads(intensity_pos)) 2913 df_bio_mz = pd.DataFrame(json.loads(mz_pos)) 2914 2915 elif polarity == "Neg": 2916 if rt_neg is not None and intensity_neg is not None and mz_neg is not None: 2917 df_bio_rt = pd.DataFrame(json.loads(rt_neg)) 2918 df_bio_intensity = pd.DataFrame(json.loads(intensity_neg)) 2919 df_bio_mz = pd.DataFrame(json.loads(mz_neg)) 2920 2921 if click_data is not None: 2922 selected_feature = click_data["points"][0]["hovertext"] 2923 else: 2924 selected_feature = None 2925 2926 try: 2927 # Biological standard metabolites – m/z vs. retention time 2928 return load_bio_feature_plot(run_id=run_id, df_rt=df_bio_rt, df_mz=df_bio_mz, df_intensity=df_bio_intensity), \ 2929 selected_feature, None, {"display": "block"} 2930 except Exception as error: 2931 print("Error in loading biological standard m/z-RT plot:", error) 2932 return {}, None, None, {"display": "none"}
Populates biological standard m/z vs. RT plot
2935@app.callback(Output("bio-standard-benchmark-plot", "figure"), 2936 Output("bio-standard-benchmark-div", "style"), 2937 Input("polarity-options", "value"), 2938 Input("bio-standard-benchmark-dropdown", "value"), 2939 Input("bio-intensity-pos", "data"), 2940 Input("bio-intensity-neg", "data"), 2941 Input("bio-standards-plot-dropdown", "value"), 2942 State("study-resources", "data"), prevent_initial_call=True) 2943def populate_bio_standard_benchmark_plot(polarity, selected_feature, intensity_pos, intensity_neg, selected_bio_standard, resources): 2944 2945 """ 2946 Populates biological standard benchmark plot 2947 """ 2948 2949 if intensity_pos is None and intensity_neg is None: 2950 return {}, {"display": "none"} 2951 2952 # Get run ID and status 2953 resources = json.loads(resources) 2954 instrument_id = resources["instrument"] 2955 run_id = resources["run_id"] 2956 status = resources["status"] 2957 2958 # Get Google Drive instance 2959 drive = None 2960 if status == "Active" and db.sync_is_enabled(): 2961 drive = db.get_drive_instance() 2962 2963 # Toggle a different biological standard 2964 if selected_bio_standard is not None: 2965 intensity_pos, intensity_neg = get_qc_results(instrument_id=instrument_id, run_id=run_id, 2966 status=status, drive=drive, biological_standard=selected_bio_standard, for_benchmark_plot=True) 2967 2968 # Get intensity data 2969 if polarity == "Pos": 2970 if intensity_pos is not None: 2971 df_bio_intensity = pd.DataFrame(json.loads(intensity_pos)) 2972 2973 elif polarity == "Neg": 2974 if intensity_neg is not None: 2975 df_bio_intensity = pd.DataFrame(json.loads(intensity_neg)) 2976 2977 # Get clicked or selected feature from biological standard m/z-RT plot 2978 if not selected_feature: 2979 selected_feature = df_bio_intensity.columns[1] 2980 2981 try: 2982 # Generate biological standard metabolite intensity vs. instrument run plot 2983 return load_bio_benchmark_plot(dataframe=df_bio_intensity, 2984 metabolite_name=selected_feature), {"display": "block"} 2985 2986 except Exception as error: 2987 print("Error loading biological standard intensity plot:", error) 2988 return {}, {"display": "none"}
Populates biological standard benchmark plot
2991@app.callback(Output("sample-info-modal", "is_open"), 2992 Output("sample-modal-title", "children"), 2993 Output("sample-modal-body", "children"), 2994 Output("sample-table", "selected_cells"), 2995 Output("sample-table", "active_cell"), 2996 Output("istd-rt-plot", "clickData"), 2997 Output("istd-intensity-plot", "clickData"), 2998 Output("istd-mz-plot", "clickData"), 2999 State("sample-info-modal", "is_open"), 3000 Input("sample-table", "active_cell"), 3001 State("sample-table", "data"), 3002 Input("istd-rt-plot", "clickData"), 3003 Input("istd-intensity-plot", "clickData"), 3004 Input("istd-mz-plot", "clickData"), 3005 State("istd-rt-pos", "data"), 3006 State("istd-rt-neg", "data"), 3007 State("istd-intensity-pos", "data"), 3008 State("istd-intensity-neg", "data"), 3009 State("istd-mz-pos", "data"), 3010 State("istd-mz-neg", "data"), 3011 State("istd-delta-rt-pos", "data"), 3012 State("istd-delta-rt-neg", "data"), 3013 State("istd-in-run-delta-rt-pos", "data"), 3014 State("istd-in-run-delta-rt-neg", "data"), 3015 State("istd-delta-mz-pos", "data"), 3016 State("istd-delta-mz-neg", "data"), 3017 State("qc-warnings-pos", "data"), 3018 State("qc-warnings-neg", "data"), 3019 State("qc-fails-pos", "data"), 3020 State("qc-fails-neg", "data"), 3021 State("bio-rt-pos", "data"), 3022 State("bio-rt-neg", "data"), 3023 State("bio-intensity-pos", "data"), 3024 State("bio-intensity-neg", "data"), 3025 State("bio-mz-pos", "data"), 3026 State("bio-mz-neg", "data"), 3027 State("sequence", "data"), 3028 State("metadata", "data"), 3029 State("study-resources", "data"), prevent_initial_call=True) 3030def toggle_sample_card(is_open, active_cell, table_data, rt_click, intensity_click, mz_click, rt_pos, rt_neg, intensity_pos, 3031 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, 3032 qc_warnings_pos, qc_warnings_neg, qc_fails_pos, qc_fails_neg, bio_rt_pos, bio_rt_neg, bio_intensity_pos, bio_intensity_neg, 3033 bio_mz_pos, bio_mz_neg, sequence, metadata, resources): 3034 3035 """ 3036 Opens information modal when a sample is clicked from the sample table 3037 """ 3038 3039 # Get selected sample from table 3040 if active_cell: 3041 clicked_sample = table_data[active_cell["row"]][active_cell["column_id"]] 3042 3043 # Get selected sample from plots 3044 if rt_click: 3045 clicked_sample = rt_click["points"][0]["x"] 3046 clicked_sample = clicked_sample.replace(": RT Info", "") 3047 3048 if intensity_click: 3049 clicked_sample = intensity_click["points"][0]["x"] 3050 clicked_sample = clicked_sample.replace(": Height", "") 3051 3052 if mz_click: 3053 clicked_sample = mz_click["points"][0]["x"] 3054 clicked_sample = clicked_sample.replace(": Precursor m/z Info", "") 3055 3056 # Get instrument ID and run ID 3057 resources = json.loads(resources) 3058 instrument_id = resources["instrument"] 3059 run_id = resources["run_id"] 3060 status = resources["status"] 3061 3062 # Get sequence and metadata 3063 df_sequence = pd.read_json(sequence, orient="split") 3064 try: 3065 df_metadata = pd.read_json(metadata, orient="split") 3066 except: 3067 df_metadata = pd.DataFrame() 3068 3069 # Check whether sample is a biological standard or not 3070 is_bio_standard = False 3071 identifiers = db.get_biological_standard_identifiers() 3072 3073 for identifier in identifiers.keys(): 3074 if identifier in clicked_sample: 3075 is_bio_standard = True 3076 break 3077 3078 # Get polarity 3079 polarity = db.get_polarity_for_sample(instrument_id, run_id, clicked_sample, status) 3080 3081 # Generate DataFrames with quantified features and metadata for selected sample 3082 if not is_bio_standard: 3083 3084 if polarity == "Pos": 3085 df_rt = pd.DataFrame(json.loads(rt_pos)) 3086 df_intensity = pd.DataFrame(json.loads(intensity_pos)) 3087 df_mz = pd.DataFrame(json.loads(mz_pos)) 3088 df_delta_rt = pd.DataFrame(json.loads(delta_rt_pos)) 3089 df_in_run_delta_rt = pd.DataFrame(json.loads(in_run_delta_rt_pos)) 3090 df_delta_mz = pd.DataFrame(json.loads(delta_mz_pos)) 3091 df_warnings = pd.DataFrame(json.loads(qc_warnings_pos)) 3092 df_fails = pd.DataFrame(json.loads(qc_fails_pos)) 3093 3094 elif polarity == "Neg": 3095 df_rt = pd.DataFrame(json.loads(rt_neg)) 3096 df_intensity = pd.DataFrame(json.loads(intensity_neg)) 3097 df_mz = pd.DataFrame(json.loads(mz_neg)) 3098 df_delta_rt = pd.DataFrame(json.loads(delta_rt_neg)) 3099 df_in_run_delta_rt = pd.DataFrame(json.loads(in_run_delta_rt_neg)) 3100 df_delta_mz = pd.DataFrame(json.loads(delta_mz_neg)) 3101 df_warnings = pd.DataFrame(json.loads(qc_warnings_neg)) 3102 df_fails = pd.DataFrame(json.loads(qc_fails_neg)) 3103 3104 df_sample_features, df_sample_info = generate_sample_metadata_dataframe(clicked_sample, df_rt, df_mz, df_intensity, 3105 df_delta_rt, df_in_run_delta_rt, df_delta_mz, df_warnings, df_fails, df_sequence, df_metadata) 3106 3107 elif is_bio_standard: 3108 3109 if polarity == "Pos": 3110 df_rt = pd.DataFrame(json.loads(bio_rt_pos)) 3111 df_intensity = pd.DataFrame(json.loads(bio_intensity_pos)) 3112 df_mz = pd.DataFrame(json.loads(bio_mz_pos)) 3113 3114 elif polarity == "Neg": 3115 df_rt = pd.DataFrame(json.loads(bio_rt_neg)) 3116 df_intensity = pd.DataFrame(json.loads(bio_intensity_neg)) 3117 df_mz = pd.DataFrame(json.loads(bio_mz_neg)) 3118 3119 df_sample_features, df_sample_info = generate_bio_standard_dataframe(clicked_sample, instrument_id, run_id, df_rt, df_mz, df_intensity) 3120 3121 # Create tables from DataFrames 3122 metadata_table = dbc.Table.from_dataframe(df_sample_info, striped=True, bordered=True, hover=True) 3123 feature_table = dbc.Table.from_dataframe(df_sample_features, striped=True, bordered=True, hover=True) 3124 3125 # Add tables to sample information modal 3126 title = clicked_sample 3127 body = html.Div(children=[metadata_table, feature_table]) 3128 3129 # Toggle modal 3130 if is_open: 3131 return False, title, body, [], None, None, None, None 3132 else: 3133 return True, title, body, [], None, None, None, None
Opens information modal when a sample is clicked from the sample table
3136@app.callback(Output("settings-modal", "is_open"), 3137 Input("settings-button", "n_clicks"), prevent_initial_call=True) 3138def toggle_settings_modal(button_click): 3139 3140 """ 3141 Toggles global settings modal 3142 """ 3143 3144 if db.sync_is_enabled(): 3145 db.download_methods() 3146 3147 return True
Toggles global settings modal
3150@app.callback(Output("google-drive-sync-modal", "is_open"), 3151 Output("database-md5", "data"), 3152 Input("settings-modal", "is_open"), 3153 State("google-drive-authenticated", "data"), 3154 State("google-drive-sync-modal", "is_open"), 3155 Input("close-sync-modal", "data"), 3156 State("database-md5", "data"), prevent_initial_call=True) 3157def show_sync_modal(settings_is_open, google_drive_authenticated, sync_modal_is_open, sync_finished, md5_checksum): 3158 3159 """ 3160 Launches progress modal, which syncs database and methods directory to Google Drive 3161 """ 3162 3163 # If sync modal is open 3164 if sync_modal_is_open: 3165 # If sync is finished 3166 if sync_finished: 3167 # Close the modal 3168 return False, None 3169 3170 # Check if settings modal has been closed 3171 if settings_is_open: 3172 return False, db.get_md5_for_settings_db() 3173 3174 elif not settings_is_open: 3175 3176 # Check if user is logged into Google Drive 3177 if google_drive_authenticated: 3178 3179 # Get MD5 checksum after use closes settings 3180 new_md5_checksum = db.get_md5_for_settings_db() 3181 3182 # Compare new MD5 checksum to old MD5 checksum 3183 if md5_checksum != new_md5_checksum: 3184 return True, new_md5_checksum 3185 else: 3186 return False, new_md5_checksum 3187 3188 else: 3189 return False, None
Launches progress modal, which syncs database and methods directory to Google Drive
3192@app.callback(Output("google-drive-sync-finished", "data"), 3193 Input("settings-modal", "is_open"), 3194 State("google-drive-authenticated", "data"), 3195 State("google-drive-authenticated-3", "data"), 3196 State("database-md5", "data"), prevent_initial_call=True) 3197def sync_settings_to_google_drive(settings_modal_is_open, google_drive_authenticated, auth_in_app, md5_checksum): 3198 3199 """ 3200 Syncs settings and methods files to Google Drive 3201 """ 3202 3203 if not settings_modal_is_open: 3204 if google_drive_authenticated or auth_in_app: 3205 if db.settings_were_modified(md5_checksum): 3206 db.upload_methods() 3207 return True 3208 3209 return False
Syncs settings and methods files to Google Drive
3221@app.callback(Output("workspace-users-table", "children"), 3222 Input("on-page-load", "data"), 3223 Input("google-drive-user-added", "data"), 3224 Input("google-drive-user-deleted", "data"), 3225 Input("google-drive-sync-update", "data")) 3226def get_users_with_workspace_access(on_page_load, user_added, user_deleted, sync_update): 3227 3228 """ 3229 Returns table of users that have access to the MS-AutoQC workspace 3230 """ 3231 3232 # Get users from database 3233 if db.is_valid(): 3234 df_gdrive_users = db.get_table("Settings", "gdrive_users") 3235 df_gdrive_users = df_gdrive_users.rename( 3236 columns={"id": "User", 3237 "name": "Name", 3238 "email_address": "Google Account Email Address"}) 3239 df_gdrive_users.drop(["permission_id"], inplace=True, axis=1) 3240 3241 # Generate and return table 3242 if len(df_gdrive_users) > 0: 3243 table = dbc.Table.from_dataframe(df_gdrive_users, striped=True, hover=True) 3244 return table 3245 else: 3246 return None 3247 else: 3248 raise PreventUpdate
Returns table of users that have access to the MS-AutoQC workspace
3251@app.callback(Output("google-drive-user-added", "data"), 3252 Input("add-user-button", "n_clicks"), 3253 State("add-user-text-field", "value"), 3254 State("google-drive-authenticated", "data"), prevent_initial_call=True) 3255def add_user_to_workspace(button_click, user_email_address, google_drive_is_authenticated): 3256 3257 """ 3258 Grants user permission to MS-AutoQC workspace in Google Drive 3259 """ 3260 3261 if user_email_address in db.get_workspace_users_list(): 3262 return "User already exists" 3263 3264 if db.sync_is_enabled(): 3265 db.add_user_to_workspace(user_email_address) 3266 3267 if user_email_address in db.get_workspace_users_list(): 3268 return user_email_address 3269 else: 3270 return "Error"
Grants user permission to MS-AutoQC workspace in Google Drive
3273@app.callback(Output("google-drive-user-deleted", "data"), 3274 Input("delete-user-button", "n_clicks"), 3275 State("add-user-text-field", "value"), 3276 State("google-drive-authenticated", "data"), prevent_initial_call=True) 3277def delete_user_from_workspace(button_click, user_email_address, google_drive_is_authenticated): 3278 3279 """ 3280 Revokes user permission to MS-AutoQC workspace in Google Drive 3281 """ 3282 3283 if user_email_address not in db.get_workspace_users_list(): 3284 return "User does not exist" 3285 3286 if db.sync_is_enabled(): 3287 db.delete_user_from_workspace(user_email_address) 3288 3289 if user_email_address in db.get_workspace_users_list(): 3290 return "Error" 3291 else: 3292 return user_email_address
Revokes user permission to MS-AutoQC workspace in Google Drive
3295@app.callback(Output("user-addition-alert", "is_open"), 3296 Output("user-addition-alert", "children"), 3297 Output("user-addition-alert", "color"), 3298 Input("google-drive-user-added", "data"), prevent_initial_call=True) 3299def ui_feedback_for_adding_gdrive_user(user_added_result): 3300 3301 """ 3302 UI alert upon adding a new user to MS-AutoQC workspace 3303 """ 3304 3305 if user_added_result is not None: 3306 if user_added_result != "Error" and user_added_result != "User already exists": 3307 return True, user_added_result + " has been granted access to the workspace.", "success" 3308 elif user_added_result == "User already exists": 3309 return True, "Error: This user already has access to the workspace.", "danger" 3310 else: 3311 return True, "Error: Could not grant access.", "danger"
UI alert upon adding a new user to MS-AutoQC workspace
3314@app.callback(Output("user-deletion-alert", "is_open"), 3315 Output("user-deletion-alert", "children"), 3316 Output("user-deletion-alert", "color"), 3317 Input("google-drive-user-deleted", "data"), prevent_initial_call=True) 3318def ui_feedback_for_deleting_gdrive_user(user_deleted_result): 3319 3320 """ 3321 UI alert upon deleting a user from the MS-AutoQC workspace 3322 """ 3323 3324 if user_deleted_result is not None: 3325 if user_deleted_result != "Error" and user_deleted_result != "User does not exist": 3326 return True, "Revoked workspace access for " + user_deleted_result + ".", "primary" 3327 elif user_deleted_result == "User does not exist": 3328 return True, "Error: this user cannot be deleted because they are not in the workspace.", "danger" 3329 else: 3330 return True, "Error: Could not revoke access.", "danger"
UI alert upon deleting a user from the MS-AutoQC workspace
3333@app.callback(Output("slack-bot-token", "placeholder"), 3334 Input("slack-bot-token-saved", "data"), 3335 Input("google-drive-sync-update", "data")) 3336def get_slack_bot_token(token_save_result, sync_update): 3337 3338 """ 3339 Get Slack bot token saved in database 3340 """ 3341 3342 if db.is_valid(): 3343 if db.get_slack_bot_token() != "None": 3344 return "Slack bot user OAuth token (saved)" 3345 else: 3346 raise PreventUpdate 3347 else: 3348 raise PreventUpdate
Get Slack bot token saved in database
3351@app.callback(Output("slack-bot-token-saved", "data"), 3352 Input("save-slack-token-button", "n_clicks"), 3353 State("slack-bot-token", "value"), prevent_initial_call=True) 3354def save_slack_bot_token(button_click, slack_bot_token): 3355 3356 """ 3357 Saves Slack bot user OAuth token in database 3358 """ 3359 3360 if slack_bot_token is not None: 3361 db.update_slack_bot_token(slack_bot_token) 3362 return "Success" 3363 else: 3364 return "Error"
Saves Slack bot user OAuth token in database
3367@app.callback(Output("slack-token-save-alert", "is_open"), 3368 Output("slack-token-save-alert", "children"), 3369 Output("slack-token-save-alert", "color"), 3370 Input("slack-bot-token-saved", "data"), prevent_initial_call=True) 3371def ui_alert_on_slack_token_save(token_save_result): 3372 3373 """ 3374 Displays UI alert on Slack bot token save 3375 """ 3376 3377 if token_save_result is not None: 3378 if token_save_result == "Success": 3379 return True, "Your Slack bot token was successfully saved.", "success" 3380 elif token_save_result == "Error": 3381 return True, "Error: Please enter your Slack bot token first.", "danger" 3382 else: 3383 raise PreventUpdate
Displays UI alert on Slack bot token save
3386@app.callback(Output("slack-channel", "value"), 3387 Output("slack-notifications-enabled", "value"), 3388 Input("slack-channel-saved", "data"), 3389 Input("google-drive-sync-update", "data")) 3390def get_slack_channel(result, sync_update): 3391 3392 """ 3393 Gets Slack channel and notification toggle setting from database 3394 """ 3395 3396 if db.is_valid(): 3397 slack_channel = db.get_slack_channel() 3398 slack_notifications_enabled = db.get_slack_notifications_toggled() 3399 3400 if slack_notifications_enabled == 1: 3401 return "#" + slack_channel, slack_notifications_enabled 3402 else: 3403 raise PreventUpdate 3404 else: 3405 raise PreventUpdate
Gets Slack channel and notification toggle setting from database
3408@app.callback(Output("slack-channel-saved", "data"), 3409 Input("slack-notifications-enabled", "value"), 3410 State("slack-channel", "value"), prevent_initial_call=True) 3411def save_slack_channel(notifications_enabled, slack_channel): 3412 3413 """ 3414 1. Registers Slack channel for MS-AutoQC notifications 3415 2. Sends a Slack message to confirm registration 3416 """ 3417 3418 if slack_channel is not None: 3419 if notifications_enabled == 1: 3420 if db.get_slack_bot_token() != "None": 3421 db.update_slack_channel(slack_channel, notifications_enabled) 3422 return "Enabled" 3423 else: 3424 return "No token" 3425 elif notifications_enabled == 0: 3426 db.update_slack_channel(slack_channel, notifications_enabled) 3427 return "Disabled" 3428 else: 3429 raise PreventUpdate 3430 else: 3431 raise PreventUpdate
- Registers Slack channel for MS-AutoQC notifications
- Sends a Slack message to confirm registration
3434@app.callback(Output("slack-notifications-toggle-alert", "is_open"), 3435 Output("slack-notifications-toggle-alert", "children"), 3436 Output("slack-notifications-toggle-alert", "color"), 3437 Input("slack-channel-saved", "data"), prevent_initial_call=True) 3438def ui_alert_on_slack_notifications_toggle(result): 3439 3440 """ 3441 UI alert on setting Slack channel and toggling Slack notifications 3442 """ 3443 3444 if result is not None: 3445 if result == "Enabled": 3446 return True, "Success! Slack notifications have been enabled.", "success" 3447 elif result == "Disabled": 3448 return True, "Slack notifications have been disabled.", "primary" 3449 elif result == "No token": 3450 return True, "Error: Please save your Slack bot token first.", "danger" 3451 else: 3452 raise PreventUpdate
UI alert on setting Slack channel and toggling Slack notifications
3455@app.callback(Output("email-notifications-table", "children"), 3456 Input("on-page-load", "data"), 3457 Input("email-added", "data"), 3458 Input("email-deleted", "data"), 3459 Input("google-drive-sync-update", "data")) 3460def get_emails_registered_for_notifications(on_page_load, email_added, email_deleted, sync_update): 3461 3462 """ 3463 Returns table of emails that are registered for email notifications 3464 """ 3465 3466 # Get emails from database 3467 if db.is_valid(): 3468 df_emails = pd.DataFrame() 3469 df_emails["Registered Email Addresses"] = db.get_email_notifications_list() 3470 3471 # Generate and return table 3472 if len(df_emails) > 0: 3473 table = dbc.Table.from_dataframe(df_emails, striped=True, hover=True) 3474 return table 3475 else: 3476 return None 3477 else: 3478 raise PreventUpdate
Returns table of emails that are registered for email notifications
3481@app.callback(Output("email-added", "data"), 3482 Input("add-email-button", "n_clicks"), 3483 State("email-notifications-text-field", "value"), prevent_initial_call=True) 3484def register_email_for_notifications(button_click, user_email_address): 3485 3486 """ 3487 Registers email address for MS-AutoQC notifications 3488 """ 3489 3490 if user_email_address in db.get_email_notifications_list(): 3491 return "Email already exists" 3492 3493 db.register_email_for_notifications(user_email_address) 3494 3495 if user_email_address in db.get_email_notifications_list(): 3496 return user_email_address 3497 else: 3498 return "Error"
Registers email address for MS-AutoQC notifications
3501@app.callback(Output("email-deleted", "data"), 3502 Input("delete-email-button", "n_clicks"), 3503 State("email-notifications-text-field", "value"), prevent_initial_call=True) 3504def delete_email_from_notifications(button_click, user_email_address): 3505 3506 """ 3507 Unsubscribes email address from MS-AutoQC notifications 3508 """ 3509 3510 if user_email_address not in db.get_email_notifications_list(): 3511 return "Email does not exist" 3512 3513 db.delete_email_from_notifications(user_email_address) 3514 3515 if user_email_address in db.get_email_notifications_list(): 3516 return "Error" 3517 else: 3518 return user_email_address
Unsubscribes email address from MS-AutoQC notifications
3521@app.callback(Output("email-addition-alert", "is_open"), 3522 Output("email-addition-alert", "children"), 3523 Output("email-addition-alert", "color"), 3524 Input("email-added", "data"), prevent_initial_call=True) 3525def ui_feedback_for_registering_email(email_added_result): 3526 3527 """ 3528 UI alert upon registering email for email notifications 3529 """ 3530 3531 if email_added_result is not None: 3532 if email_added_result != "Error" and email_added_result != "Email already exists": 3533 return True, email_added_result + " has been registered for MS-AutoQC notifications.", "success" 3534 elif email_added_result == "Email already exists": 3535 return True, "Error: This email is already registered for MS-AutoQC notifications.", "danger" 3536 else: 3537 return True, "Error: Could not register email for MS-AutoQC notifications.", "danger"
UI alert upon registering email for email notifications
3540@app.callback(Output("email-deletion-alert", "is_open"), 3541 Output("email-deletion-alert", "children"), 3542 Output("email-deletion-alert", "color"), 3543 Input("email-deleted", "data"), prevent_initial_call=True) 3544def ui_feedback_for_deleting_email(email_deleted_result): 3545 3546 """ 3547 UI alert upon deleting email from email notifications list 3548 """ 3549 3550 if email_deleted_result is not None: 3551 if email_deleted_result != "Error" and email_deleted_result != "Email does not exist": 3552 return True, "Unsubscribed " + email_deleted_result + " from email notifications.", "primary" 3553 elif email_deleted_result == "Email does not exist": 3554 message = "Error: Email cannot be deleted because it isn't registered for notifications." 3555 return True, message, "danger" 3556 else: 3557 return True, "Error: Could not unsubscribe email from MS-AutoQC notifications.", "danger"
UI alert upon deleting email from email notifications list
3560@app.callback(Output("chromatography-methods-table", "children"), 3561 Output("select-istd-chromatography-dropdown", "options"), 3562 Output("select-bio-chromatography-dropdown", "options"), 3563 Output("add-chromatography-text-field", "value"), 3564 Output("chromatography-added", "data"), 3565 Input("on-page-load", "data"), 3566 Input("add-chromatography-button", "n_clicks"), 3567 State("add-chromatography-text-field", "value"), 3568 Input("istd-msp-added", "data"), 3569 Input("chromatography-removed", "data"), 3570 Input("chromatography-msdial-config-added", "data"), 3571 Input("google-drive-sync-update", "data")) 3572def add_chromatography_method(on_page_load, button_click, chromatography_method, msp_added, method_removed, config_added, sync_update): 3573 3574 """ 3575 Add chromatography method to database 3576 """ 3577 3578 if db.is_valid(): 3579 3580 # Add chromatography method to database 3581 method_added = "" 3582 if chromatography_method is not None: 3583 db.insert_chromatography_method(chromatography_method) 3584 method_added = "Added" 3585 3586 # Update table 3587 df_methods = db.get_chromatography_methods() 3588 3589 df_methods = df_methods.rename( 3590 columns={"method_id": "Method ID", 3591 "num_pos_standards": "Pos (+) Standards", 3592 "num_neg_standards": "Neg (–) Standards", 3593 "msdial_config_id": "MS-DIAL Config"}) 3594 3595 df_methods = df_methods[["Method ID", "Pos (+) Standards", "Neg (–) Standards", "MS-DIAL Config"]] 3596 3597 methods_table = dbc.Table.from_dataframe(df_methods, striped=True, hover=True) 3598 3599 # Update dropdown 3600 dropdown_options = [] 3601 for method in df_methods["Method ID"].astype(str).tolist(): 3602 dropdown_options.append({"label": method, "value": method}) 3603 3604 return methods_table, dropdown_options, dropdown_options, None, method_added 3605 3606 else: 3607 raise PreventUpdate
Add chromatography method to database
3610@app.callback(Output("chromatography-removed", "data"), 3611 Input("remove-chromatography-method-button", "n_clicks"), 3612 State("select-istd-chromatography-dropdown", "value"), prevent_initial_call=True) 3613def remove_chromatography_method(button_click, chromatography): 3614 3615 """ 3616 Remove chromatography method from database 3617 """ 3618 3619 if chromatography is not None: 3620 db.remove_chromatography_method(chromatography) 3621 return "Removed" 3622 3623 else: 3624 return ""
Remove chromatography method from database
3643@app.callback(Output("chromatography-removal-alert", "is_open"), 3644 Output("chromatography-removal-alert", "children"), 3645 Input("chromatography-removed", "data")) 3646def show_alert_on_chromatography_addition(chromatography_removed): 3647 3648 """ 3649 UI feedback for removing a chromatography method 3650 """ 3651 3652 if chromatography_removed is not None: 3653 if chromatography_removed == "Removed": 3654 return True, "The selected chromatography method was removed." 3655 3656 return False, None
UI feedback for removing a chromatography method
3674@app.callback(Output("add-istd-msp-text-field", "value"), 3675 Input("add-istd-msp-button", "filename"), prevent_intitial_call=True) 3676def bio_standard_msp_text_field_feedback(filename): 3677 3678 """ 3679 UI feedback for selecting an MSP to save for a chromatography method 3680 """ 3681 3682 return filename
UI feedback for selecting an MSP to save for a chromatography method
3685@app.callback(Output("istd-msp-added", "data"), 3686 Input("msp-save-changes-button", "n_clicks"), 3687 State("add-istd-msp-button", "contents"), 3688 State("add-istd-msp-button", "filename"), 3689 State("select-istd-chromatography-dropdown", "value"), 3690 State("select-istd-polarity-dropdown", "value"), prevent_initial_call=True) 3691def capture_uploaded_istd_msp(button_click, contents, filename, chromatography, polarity): 3692 3693 """ 3694 In Settings > Internal Standards, captures contents of uploaded MSP file and calls add_msp_to_database() 3695 """ 3696 3697 if contents is not None and chromatography is not None and polarity is not None: 3698 3699 # Decode file contents 3700 content_type, content_string = contents.split(",") 3701 decoded = base64.b64decode(content_string) 3702 file = io.StringIO(decoded.decode("utf-8")) 3703 3704 # Add identification file to database 3705 if button_click is not None and chromatography is not None and polarity is not None: 3706 if filename.endswith(".msp"): 3707 db.add_msp_to_database(file, chromatography, polarity) # Parse MSP files 3708 elif filename.endswith(".csv") or filename.endswith(".txt"): 3709 db.add_csv_to_database(file, chromatography, polarity) # Parse CSV files 3710 return "Success! " + filename + " has been added to " + chromatography + " " + polarity + "." 3711 else: 3712 return "Error" 3713 3714 return "Ready" 3715 3716 # Update dummy dcc.Store object to update chromatography methods table 3717 return None
In Settings > Internal Standards, captures contents of uploaded MSP file and calls add_msp_to_database()
3720@app.callback(Output("chromatography-msp-success-alert", "is_open"), 3721 Output("chromatography-msp-success-alert", "children"), 3722 Output("chromatography-msp-error-alert", "is_open"), 3723 Output("chromatography-msp-error-alert", "children"), 3724 Input("istd-msp-added", "data"), prevent_initial_call=True) 3725def ui_feedback_for_adding_msp_to_chromatography(msp_added): 3726 3727 """ 3728 UI feedback for adding an MSP to a chromatography method 3729 """ 3730 3731 if msp_added is not None: 3732 if "Success" in msp_added: 3733 return True, msp_added, False, "" 3734 elif msp_added == "Error": 3735 return False, "", True, "Error: Please select a chromatography and polarity." 3736 else: 3737 return False, "", False, ""
UI feedback for adding an MSP to a chromatography method
3740@app.callback(Output("msdial-directory", "value"), 3741 Input("file-explorer-select-button", "n_clicks"), 3742 Input("settings-modal", "is_open"), 3743 State("selected-msdial-folder", "data"), 3744 Input("google-drive-sync-update", "data")) 3745def get_msdial_directory(select_folder_button, settings_modal_is_open, selected_folder, sync_update): 3746 3747 """ 3748 Returns (previously inputted by user) location of MS-DIAL directory 3749 """ 3750 3751 selected_component = ctx.triggered_id 3752 3753 if selected_component == "file-explorer-select-button": 3754 return selected_folder 3755 3756 if db.is_valid(): 3757 return db.get_msdial_directory() 3758 else: 3759 raise PreventUpdate
Returns (previously inputted by user) location of MS-DIAL directory
3762@app.callback(Output("msdial-directory-saved", "data"), 3763 Input("msdial-folder-save-button", "n_clicks"), 3764 State("msdial-directory", "value"), prevent_initial_call=True) 3765def update_msdial_directory(button_click, msdial_directory): 3766 3767 """ 3768 Updates MS-DIAL directory 3769 """ 3770 3771 if msdial_directory is not None: 3772 if os.path.exists(msdial_directory): 3773 db.update_msdial_directory(msdial_directory) 3774 return "Success" 3775 else: 3776 return "Does not exist" 3777 else: 3778 return "Error"
Updates MS-DIAL directory
3781@app.callback(Output("msdial-directory-saved-alert", "is_open"), 3782 Output("msdial-directory-saved-alert", "children"), 3783 Output("msdial-directory-saved-alert", "color"), 3784 Input("msdial-directory-saved", "data"), prevent_initial_call=True) 3785def ui_alert_for_msdial_directory_save(msdial_folder_save_result): 3786 3787 """ 3788 Displays alert on MS-DIAL directory update 3789 """ 3790 3791 if msdial_folder_save_result is not None: 3792 if msdial_folder_save_result == "Success": 3793 return True, "The MS-DIAL location was successfully saved.", "success" 3794 elif msdial_folder_save_result == "Does not exist": 3795 return True, "Error: This directory does not exist on your computer.", "danger" 3796 else: 3797 return True, "Error: Could not set MS-DIAL directory.", "danger"
Displays alert on MS-DIAL directory update
3800@app.callback(Output("msdial-config-added", "data"), 3801 Output("add-msdial-configuration-text-field", "value"), 3802 Input("add-msdial-configuration-button", "n_clicks"), 3803 State("add-msdial-configuration-text-field", "value"), prevent_initial_call=True) 3804def add_msdial_configuration(button_click, msdial_config_id): 3805 3806 """ 3807 Adds new MS-DIAL configuration to the database 3808 """ 3809 3810 if msdial_config_id is not None: 3811 db.add_msdial_configuration(msdial_config_id) 3812 return "Added", None 3813 else: 3814 return "", None
Adds new MS-DIAL configuration to the database
3817@app.callback(Output("msdial-config-removed", "data"), 3818 Input("remove-config-button", "n_clicks"), 3819 State("msdial-configs-dropdown", "value"), prevent_initial_call=True) 3820def delete_msdial_configuration(button_click, msdial_config_id): 3821 3822 """ 3823 Removes dropdown-selected MS-DIAL configuration from database 3824 """ 3825 3826 if msdial_config_id is not None: 3827 if msdial_config_id != "Default": 3828 db.remove_msdial_configuration(msdial_config_id) 3829 return "Removed" 3830 else: 3831 return "Cannot remove" 3832 else: 3833 return ""
Removes dropdown-selected MS-DIAL configuration from database
3836@app.callback(Output("msdial-configs-dropdown", "options"), 3837 Output("msdial-configs-dropdown", "value"), 3838 Input("on-page-load", "data"), 3839 Input("msdial-config-added", "data"), 3840 Input("msdial-config-removed", "data"), 3841 Input("google-drive-sync-update", "data")) 3842def get_msdial_configs_for_dropdown(on_page_load, on_config_added, on_config_removed, sync_update): 3843 3844 """ 3845 Retrieves list of user-created configurations of MS-DIAL parameters from database 3846 """ 3847 3848 if db.is_valid(): 3849 3850 # Get MS-DIAL configurations from database 3851 msdial_configurations = db.get_msdial_configurations() 3852 3853 # Create and return options for dropdown 3854 config_options = [] 3855 3856 for config in msdial_configurations: 3857 config_options.append({"label": config, "value": config}) 3858 3859 return config_options, "Default" 3860 3861 else: 3862 raise PreventUpdate
Retrieves list of user-created configurations of MS-DIAL parameters from database
3865@app.callback(Output("msdial-config-addition-alert", "is_open"), 3866 Output("msdial-config-addition-alert", "children"), 3867 Output("msdial-config-addition-alert", "color"), 3868 Input("msdial-config-added", "data"), prevent_initial_call=True) 3869def show_alert_on_msdial_config_addition(config_added): 3870 3871 """ 3872 UI feedback on MS-DIAL configuration addition 3873 """ 3874 3875 if config_added is not None: 3876 if config_added == "Added": 3877 return True, "Success! New MS-DIAL configuration added.", "success" 3878 3879 return False, None, "success"
UI feedback on MS-DIAL configuration addition
3882@app.callback(Output("msdial-config-removal-alert", "is_open"), 3883 Output("msdial-config-removal-alert", "children"), 3884 Output("msdial-config-removal-alert", "color"), 3885 Input("msdial-config-removed", "data"), 3886 State("msdial-configs-dropdown", "value"), prevent_initial_call=True) 3887def show_alert_on_msdial_config_removal(config_removed, selected_config): 3888 3889 """ 3890 UI feedback on MS-DIAL configuration removal 3891 """ 3892 3893 if config_removed is not None: 3894 if config_removed == "Removed": 3895 message = "The selected MS-DIAL configuration was deleted." 3896 color = "primary" 3897 if selected_config == "Default": 3898 message = "Error: The default configuration cannot be deleted." 3899 color = "danger" 3900 return True, message, color 3901 else: 3902 return False, "", "danger"
UI feedback on MS-DIAL configuration removal
3905@app.callback(Output("retention-time-begin", "value"), 3906 Output("retention-time-end", "value"), 3907 Output("mass-range-begin", "value"), 3908 Output("mass-range-end", "value"), 3909 Output("ms1-centroid-tolerance", "value"), 3910 Output("ms2-centroid-tolerance", "value"), 3911 Output("select-smoothing-dropdown", "value"), 3912 Output("smoothing-level", "value"), 3913 Output("min-peak-width", "value"), 3914 Output("min-peak-height", "value"), 3915 Output("mass-slice-width", "value"), 3916 Output("post-id-rt-tolerance", "value"), 3917 Output("post-id-mz-tolerance", "value"), 3918 Output("post-id-score-cutoff", "value"), 3919 Output("alignment-rt-tolerance", "value"), 3920 Output("alignment-mz-tolerance", "value"), 3921 Output("alignment-rt-factor", "value"), 3922 Output("alignment-mz-factor", "value"), 3923 Output("peak-count-filter", "value"), 3924 Output("qc-at-least-filter-dropdown", "value"), 3925 Input("msdial-configs-dropdown", "value"), 3926 Input("msdial-parameters-saved", "data"), 3927 Input("msdial-parameters-reset", "data"), prevent_initial_call=True) 3928def get_msdial_parameters_for_config(msdial_config_id, on_parameters_saved, on_parameters_reset): 3929 3930 """ 3931 In Settings > MS-DIAL parameters, fills text fields with placeholders 3932 of current parameter values stored in the database. 3933 """ 3934 3935 return db.get_msdial_configuration_parameters(msdial_config_id)
In Settings > MS-DIAL parameters, fills text fields with placeholders of current parameter values stored in the database.
3938@app.callback(Output("msdial-parameters-saved", "data"), 3939 Input("save-changes-msdial-parameters-button", "n_clicks"), 3940 State("msdial-configs-dropdown", "value"), 3941 State("retention-time-begin", "value"), 3942 State("retention-time-end", "value"), 3943 State("mass-range-begin", "value"), 3944 State("mass-range-end", "value"), 3945 State("ms1-centroid-tolerance", "value"), 3946 State("ms2-centroid-tolerance", "value"), 3947 State("select-smoothing-dropdown", "value"), 3948 State("smoothing-level", "value"), 3949 State("mass-slice-width", "value"), 3950 State("min-peak-width", "value"), 3951 State("min-peak-height", "value"), 3952 State("post-id-rt-tolerance", "value"), 3953 State("post-id-mz-tolerance", "value"), 3954 State("post-id-score-cutoff", "value"), 3955 State("alignment-rt-tolerance", "value"), 3956 State("alignment-mz-tolerance", "value"), 3957 State("alignment-rt-factor", "value"), 3958 State("alignment-mz-factor", "value"), 3959 State("peak-count-filter", "value"), 3960 State("qc-at-least-filter-dropdown", "value"), prevent_initial_call=True) 3961def write_msdial_parameters_to_database(button_clicks, config_name, rt_begin, rt_end, mz_begin, mz_end, 3962 ms1_centroid_tolerance, ms2_centroid_tolerance, smoothing_method, smoothing_level, mass_slice_width, min_peak_width, 3963 min_peak_height, post_id_rt_tolerance, post_id_mz_tolerance, post_id_score_cutoff, alignment_rt_tolerance, 3964 alignment_mz_tolerance, alignment_rt_factor, alignment_mz_factor, peak_count_filter, qc_at_least_filter): 3965 3966 """ 3967 Saves MS-DIAL parameters to respective configuration in database 3968 """ 3969 3970 db.update_msdial_configuration(config_name, rt_begin, rt_end, mz_begin, mz_end, ms1_centroid_tolerance, 3971 ms2_centroid_tolerance, smoothing_method, smoothing_level, mass_slice_width, min_peak_width, min_peak_height, 3972 post_id_rt_tolerance, post_id_mz_tolerance, post_id_score_cutoff, alignment_rt_tolerance, alignment_mz_tolerance, 3973 alignment_rt_factor, alignment_mz_factor, peak_count_filter, qc_at_least_filter) 3974 3975 return "Saved"
Saves MS-DIAL parameters to respective configuration in database
4171@app.callback(Output("qc-parameters-reset", "data"), 4172 Input("reset-default-qc-parameters-button", "n_clicks"), 4173 State("qc-configs-dropdown", "value"), prevent_initial_call=True) 4174def reset_msdial_parameters_to_default(button_clicks, qc_config_name): 4175 4176 """ 4177 Resets parameters for selected QC configuration to default settings 4178 """ 4179 4180 db.update_qc_configuration(config_name=qc_config_name, intensity_dropouts_cutoff=4, 4181 library_rt_shift_cutoff=0.1, in_run_rt_shift_cutoff=0.05, library_mz_shift_cutoff=0.005, 4182 intensity_enabled=True, library_rt_enabled=True, in_run_rt_enabled=True, library_mz_enabled=True) 4183 return "Reset"
Resets parameters for selected QC configuration to default settings
3993@app.callback(Output("msdial-parameters-success-alert", "is_open"), 3994 Output("msdial-parameters-success-alert", "children"), 3995 Input("msdial-parameters-saved", "data"), prevent_initial_call=True) 3996def show_alert_on_parameter_save(parameters_saved): 3997 3998 """ 3999 UI feedback for saving changes to MS-DIAL parameters 4000 """ 4001 4002 if parameters_saved is not None: 4003 if parameters_saved == "Saved": 4004 return True, "Your changes were successfully saved."
UI feedback for saving changes to MS-DIAL parameters
4007@app.callback(Output("msdial-parameters-reset-alert", "is_open"), 4008 Output("msdial-parameters-reset-alert", "children"), 4009 Input("msdial-parameters-reset", "data"), prevent_initial_call=True) 4010def show_alert_on_parameter_reset(parameters_reset): 4011 4012 """ 4013 UI feedback for resetting MS-DIAL parameters in a configuration 4014 """ 4015 4016 if parameters_reset is not None: 4017 if parameters_reset == "Reset": 4018 return True, "Your configuration has been reset to its default settings."
UI feedback for resetting MS-DIAL parameters in a configuration
4021@app.callback(Output("qc-config-added", "data"), 4022 Output("add-qc-configuration-text-field", "value"), 4023 Input("add-qc-configuration-button", "n_clicks"), 4024 State("add-qc-configuration-text-field", "value"), prevent_initial_call=True) 4025def add_qc_configuration(button_click, qc_config_id): 4026 4027 """ 4028 Adds new QC configuration to the database 4029 """ 4030 4031 if qc_config_id is not None: 4032 db.add_qc_configuration(qc_config_id) 4033 return "Added", None 4034 else: 4035 return "", None
Adds new QC configuration to the database
4038@app.callback(Output("qc-config-removed", "data"), 4039 Input("remove-qc-config-button", "n_clicks"), 4040 State("qc-configs-dropdown", "value"), prevent_initial_call=True) 4041def delete_qc_configuration(button_click, qc_config_id): 4042 4043 """ 4044 Removes dropdown-selected QC configuration from database 4045 """ 4046 4047 if qc_config_id is not None: 4048 if qc_config_id != "Default": 4049 db.remove_qc_configuration(qc_config_id) 4050 return "Removed" 4051 else: 4052 return "Cannot remove" 4053 else: 4054 return ""
Removes dropdown-selected QC configuration from database
4057@app.callback(Output("qc-configs-dropdown", "options"), 4058 Output("qc-configs-dropdown", "value"), 4059 Input("on-page-load", "data"), 4060 Input("qc-config-added", "data"), 4061 Input("qc-config-removed", "data"), 4062 Input("google-drive-sync-update", "data")) 4063def get_qc_configs_for_dropdown(on_page_load, qc_config_added, qc_config_removed, sync_update): 4064 4065 """ 4066 Retrieves list of user-created configurations of QC parameters from database 4067 """ 4068 4069 if db.is_valid(): 4070 4071 # Get QC configurations from database 4072 qc_configurations = db.get_qc_configurations_list() 4073 4074 # Create and return options for dropdown 4075 config_options = [] 4076 4077 for config in qc_configurations: 4078 config_options.append({"label": config, "value": config}) 4079 4080 return config_options, "Default" 4081 4082 else: 4083 raise PreventUpdate
Retrieves list of user-created configurations of QC parameters from database
4086@app.callback(Output("qc-config-addition-alert", "is_open"), 4087 Output("qc-config-addition-alert", "children"), 4088 Output("qc-config-addition-alert", "color"), 4089 Input("qc-config-added", "data"), prevent_initial_call=True) 4090def show_alert_on_qc_config_addition(config_added): 4091 4092 """ 4093 UI feedback on QC configuration addition 4094 """ 4095 4096 if config_added is not None: 4097 if config_added == "Added": 4098 return True, "Success! New QC configuration added.", "success" 4099 4100 return False, None, "success"
UI feedback on QC configuration addition
4103@app.callback(Output("qc-config-removal-alert", "is_open"), 4104 Output("qc-config-removal-alert", "children"), 4105 Output("qc-config-removal-alert", "color"), 4106 Input("qc-config-removed", "data"), 4107 State("qc-configs-dropdown", "value"), prevent_initial_call=True) 4108def show_alert_on_qc_config_removal(config_removed, selected_config): 4109 4110 """ 4111 UI feedback on QC configuration removal 4112 """ 4113 4114 if config_removed is not None: 4115 if config_removed == "Removed": 4116 message = "The selected QC configuration was deleted." 4117 color = "primary" 4118 if selected_config == "Default": 4119 message = "Error: The default configuration cannot be deleted." 4120 color = "danger" 4121 return True, message, color 4122 else: 4123 return False, "", "danger"
UI feedback on QC configuration removal
4126@app.callback(Output("intensity-dropouts-cutoff", "value"), 4127 Output("library-rt-shift-cutoff", "value"), 4128 Output("in-run-rt-shift-cutoff", "value"), 4129 Output("library-mz-shift-cutoff", "value"), 4130 Output("intensity-cutoff-enabled", "value"), 4131 Output("library-rt-shift-cutoff-enabled", "value"), 4132 Output("in-run-rt-shift-cutoff-enabled", "value"), 4133 Output("library-mz-shift-cutoff-enabled", "value"), 4134 Input("qc-configs-dropdown", "value"), 4135 Input("qc-parameters-saved", "data"), 4136 Input("qc-parameters-reset", "data"), prevent_initial_call=True) 4137def get_qc_parameters_for_config(qc_config_name, on_parameters_saved, on_parameters_reset): 4138 4139 """ 4140 In Settings > QC Configurations, fills text fields with placeholders 4141 of current parameter values stored in the database. 4142 """ 4143 4144 selected_config = db.get_qc_configuration_parameters(config_name=qc_config_name) 4145 return tuple(selected_config.to_records(index=False)[0])
In Settings > QC Configurations, fills text fields with placeholders of current parameter values stored in the database.
4148@app.callback(Output("qc-parameters-saved", "data"), 4149 Input("save-changes-qc-parameters-button", "n_clicks"), 4150 State("qc-configs-dropdown", "value"), 4151 State("intensity-dropouts-cutoff", "value"), 4152 State("library-rt-shift-cutoff", "value"), 4153 State("in-run-rt-shift-cutoff", "value"), 4154 State("library-mz-shift-cutoff", "value"), 4155 State("intensity-cutoff-enabled", "value"), 4156 State("library-rt-shift-cutoff-enabled", "value"), 4157 State("in-run-rt-shift-cutoff-enabled", "value"), 4158 State("library-mz-shift-cutoff-enabled", "value"), prevent_initial_call=True) 4159def write_qc_parameters_to_database(button_clicks, qc_config_name, intensity_dropouts_cutoff, library_rt_shift_cutoff, 4160 in_run_rt_shift_cutoff, library_mz_shift_cutoff, intensity_enabled, library_rt_enabled, in_run_rt_enabled, library_mz_enabled): 4161 4162 """ 4163 Saves QC parameters to respective configuration in database 4164 """ 4165 4166 db.update_qc_configuration(qc_config_name, intensity_dropouts_cutoff, library_rt_shift_cutoff, in_run_rt_shift_cutoff, 4167 library_mz_shift_cutoff, intensity_enabled, library_rt_enabled, in_run_rt_enabled, library_mz_enabled) 4168 return "Saved"
Saves QC parameters to respective configuration in database
4186@app.callback(Output("qc-parameters-success-alert", "is_open"), 4187 Output("qc-parameters-success-alert", "children"), 4188 Input("qc-parameters-saved", "data"), prevent_initial_call=True) 4189def show_alert_on_qc_parameter_save(parameters_saved): 4190 4191 """ 4192 UI feedback for saving changes to QC parameters 4193 """ 4194 4195 if parameters_saved is not None: 4196 if parameters_saved == "Saved": 4197 return True, "Your changes were successfully saved."
UI feedback for saving changes to QC parameters
4200@app.callback(Output("qc-parameters-reset-alert", "is_open"), 4201 Output("qc-parameters-reset-alert", "children"), 4202 Input("qc-parameters-reset", "data"), prevent_initial_call=True) 4203def show_alert_on_qc_parameter_reset(parameters_reset): 4204 4205 """ 4206 UI feedback for resetting QC parameters in a configuration 4207 """ 4208 4209 if parameters_reset is not None: 4210 if parameters_reset == "Reset": 4211 return True, "Your QC configuration has been reset to its default settings."
UI feedback for resetting QC parameters in a configuration
4214@app.callback(Output("select-bio-standard-dropdown", "options"), 4215 Output("biological-standards-table", "children"), 4216 Input("on-page-load", "data"), 4217 Input("bio-standard-added", "data"), 4218 Input("bio-standard-removed", "data"), 4219 Input("chromatography-added", "data"), 4220 Input("chromatography-removed", "data"), 4221 Input("bio-msp-added", "data"), 4222 Input("bio-standard-msdial-config-added", "data"), 4223 Input("google-drive-sync-update", "data")) 4224def get_biological_standards(on_page_load, on_standard_added, on_standard_removed, on_method_added, on_method_removed, 4225 on_msp_added, on_bio_standard_msdial_config_added, sync_update): 4226 4227 """ 4228 Populates dropdown and table of biological standards 4229 """ 4230 4231 if db.is_valid(): 4232 4233 # Populate dropdown 4234 dropdown_options = [] 4235 for biological_standard in db.get_biological_standards_list(): 4236 dropdown_options.append({"label": biological_standard, "value": biological_standard}) 4237 4238 # Populate table 4239 df_biological_standards = db.get_biological_standards() 4240 4241 # DataFrame refactoring 4242 df_biological_standards = df_biological_standards.rename( 4243 columns={"name": "Name", 4244 "identifier": "Identifier", 4245 "chromatography": "Method ID", 4246 "num_pos_features": "Pos (+) Metabolites", 4247 "num_neg_features": "Neg (–) Metabolites", 4248 "msdial_config_id": "MS-DIAL Config"}) 4249 4250 df_biological_standards = df_biological_standards[ 4251 ["Name", "Identifier", "Method ID", "Pos (+) Metabolites", "Neg (–) Metabolites", "MS-DIAL Config"]] 4252 4253 biological_standards_table = dbc.Table.from_dataframe(df_biological_standards, striped=True, hover=True) 4254 4255 return dropdown_options, biological_standards_table 4256 4257 else: 4258 raise PreventUpdate
Populates dropdown and table of biological standards
4261@app.callback(Output("bio-standard-added", "data"), 4262 Output("add-bio-standard-text-field", "value"), 4263 Output("add-bio-standard-identifier-text-field", "value"), 4264 Input("add-bio-standard-button", "n_clicks"), 4265 State("add-bio-standard-text-field", "value"), 4266 State("add-bio-standard-identifier-text-field", "value"), prevent_initial_call=True) 4267def add_biological_standard(button_click, name, identifier): 4268 4269 """ 4270 Adds biological standard to database 4271 """ 4272 4273 if name is not None and identifier is not None: 4274 4275 if len(db.get_chromatography_methods()) == 0: 4276 return "Error 2", name, identifier 4277 else: 4278 db.add_biological_standard(name, identifier) 4279 4280 return "Added", None, None 4281 4282 else: 4283 return "Error 1", name, identifier
Adds biological standard to database
4286@app.callback(Output("bio-standard-removed", "data"), 4287 Input("remove-bio-standard-button", "n_clicks"), 4288 State("select-bio-standard-dropdown", "value"), prevent_initial_call=True) 4289def remove_biological_standard(button_click, biological_standard_name): 4290 4291 """ 4292 Removes biological standard (and all corresponding MSPs) in the database 4293 """ 4294 4295 if biological_standard_name is not None: 4296 db.remove_biological_standard(biological_standard_name) 4297 return "Deleted " + biological_standard_name + " and all corresponding MSP files." 4298 else: 4299 return "Error"
Removes biological standard (and all corresponding MSPs) in the database
4302@app.callback(Output("add-bio-msp-text-field", "value"), 4303 Input("add-bio-msp-button", "filename"), prevent_intitial_call=True) 4304def bio_standard_msp_text_field_ui_callback(filename): 4305 4306 """ 4307 UI feedback for selecting an MSP to save for a biological standard 4308 """ 4309 4310 return filename
UI feedback for selecting an MSP to save for a biological standard
4313@app.callback(Output("bio-msp-added", "data"), 4314 Input("bio-standard-save-changes-button", "n_clicks"), 4315 State("add-bio-msp-button", "contents"), 4316 State("add-bio-msp-button", "filename"), 4317 State("select-bio-chromatography-dropdown", "value"), 4318 State("select-bio-polarity-dropdown", "value"), 4319 State("select-bio-standard-dropdown", "value"), prevent_initial_call=True) 4320def capture_uploaded_bio_msp(button_click, contents, filename, chromatography, polarity, bio_standard): 4321 4322 """ 4323 In Settings > Biological Standards, captures contents of uploaded MSP file and calls add_msp_to_database(). 4324 """ 4325 4326 if contents is not None and chromatography is not None and polarity is not None: 4327 4328 # Decode file contents 4329 content_type, content_string = contents.split(",") 4330 decoded = base64.b64decode(content_string) 4331 file = io.StringIO(decoded.decode("utf-8")) 4332 4333 # Add MSP file to database 4334 if button_click is not None and chromatography is not None and polarity is not None and bio_standard is not None: 4335 if filename.endswith(".msp"): 4336 db.add_msp_to_database(file, chromatography, polarity, bio_standard=bio_standard) 4337 4338 # Check whether MSP was added successfully 4339 if bio_standard in db.get_biological_standards_list(): 4340 return "Success! Added " + filename + " to " + bio_standard + " in " + chromatography + " " + polarity + "." 4341 else: 4342 return "Error 1" 4343 else: 4344 return "Error 2" 4345 4346 return "Ready" 4347 4348 # Update dummy dcc.Store object to update chromatography methods table 4349 return ""
In Settings > Biological Standards, captures contents of uploaded MSP file and calls add_msp_to_database().
4352@app.callback(Output("bio-standard-addition-alert", "is_open"), 4353 Output("bio-standard-addition-alert", "children"), 4354 Output("bio-standard-addition-alert", "color"), 4355 Input("bio-standard-added", "data"), prevent_initial_call=True) 4356def show_alert_on_bio_standard_addition(bio_standard_added): 4357 4358 """ 4359 UI feedback for adding a biological standard 4360 """ 4361 4362 if bio_standard_added is not None: 4363 if bio_standard_added == "Added": 4364 return True, "Success! New biological standard added.", "success" 4365 elif bio_standard_added == "Error 2": 4366 return True, "Error: Please add a chromatography method first.", "danger" 4367 4368 return False, None, None
UI feedback for adding a biological standard
4371@app.callback(Output("bio-standard-removal-alert", "is_open"), 4372 Output("bio-standard-removal-alert", "children"), 4373 Input("bio-standard-removed", "data"), prevent_initial_call=True) 4374def show_alert_on_bio_standard_removal(bio_standard_removed): 4375 4376 """ 4377 UI feedback for removing a biological standard 4378 """ 4379 4380 if bio_standard_removed is not None: 4381 if "Deleted" in bio_standard_removed: 4382 return True, bio_standard_removed 4383 4384 return False, None
UI feedback for removing a biological standard
4387@app.callback(Output("bio-msp-success-alert", "is_open"), 4388 Output("bio-msp-success-alert", "children"), 4389 Output("bio-msp-error-alert", "is_open"), 4390 Output("bio-msp-error-alert", "children"), 4391 Input("bio-msp-added", "data"), prevent_initial_call=True) 4392def ui_feedback_for_adding_msp_to_bio_standard(bio_standard_msp_added): 4393 4394 """ 4395 UI feedback for adding an MSP to a biological standard 4396 """ 4397 4398 if bio_standard_msp_added is not None: 4399 if "Success" in bio_standard_msp_added: 4400 return True, bio_standard_msp_added, False, "" 4401 elif bio_standard_msp_added == "Error 1": 4402 return False, "", True, "Error: Unable to add MSP to biological standard." 4403 elif bio_standard_msp_added == "Error 2": 4404 return False, "", True, "Error: Please select a biological standard, chromatography, and polarity first." 4405 else: 4406 return False, "", False, ""
UI feedback for adding an MSP to a biological standard
4427@app.callback(Output("bio-standard-msdial-configs-dropdown", "options"), 4428 Output("istd-msdial-configs-dropdown", "options"), 4429 Input("msdial-config-added", "data"), 4430 Input("msdial-config-removed", "data"), 4431 Input("google-drive-sync-update", "data")) 4432def populate_msdial_configs_for_biological_standard(msdial_config_added, msdial_config_removed, sync_update): 4433 4434 """ 4435 In Settings > Biological Standards, populates the MS-DIAL configurations dropdown 4436 """ 4437 4438 if db.is_valid(): 4439 4440 options = [] 4441 4442 for config in db.get_msdial_configurations(): 4443 options.append({"label": config, "value": config}) 4444 4445 return options, options 4446 4447 else: 4448 raise PreventUpdate
In Settings > Biological Standards, populates the MS-DIAL configurations dropdown
4451@app.callback(Output("bio-standard-msdial-config-added", "data"), 4452 Input("bio-standard-msdial-configs-button", "n_clicks"), 4453 State("select-bio-standard-dropdown", "value"), 4454 State("select-bio-chromatography-dropdown", "value"), 4455 State("bio-standard-msdial-configs-dropdown", "value"), prevent_initial_call=True) 4456def add_msdial_config_for_bio_standard(button_click, biological_standard, chromatography, config_id): 4457 4458 """ 4459 In Settings > Biological Standards, sets the MS-DIAL configuration to be used for chromatography 4460 """ 4461 4462 if biological_standard is not None and chromatography is not None and config_id is not None: 4463 db.update_msdial_config_for_bio_standard(biological_standard, chromatography, config_id) 4464 return "Added" 4465 else: 4466 return ""
In Settings > Biological Standards, sets the MS-DIAL configuration to be used for chromatography
4469@app.callback(Output("bio-config-success-alert", "is_open"), 4470 Output("bio-config-success-alert", "children"), 4471 Input("bio-standard-msdial-config-added", "data"), 4472 State("select-bio-standard-dropdown", "value"), 4473 State("select-bio-chromatography-dropdown", "value"), prevent_initial_call=True) 4474def ui_feedback_for_setting_msdial_config_for_bio_standard(config_added, bio_standard, chromatography): 4475 4476 """ 4477 In Settings > Biological Standards, provides an alert when MS-DIAL config is successfully set for biological standard 4478 """ 4479 4480 if config_added is not None: 4481 if config_added == "Added": 4482 message = "MS-DIAL parameter configuration saved successfully for " + bio_standard + " (" + chromatography + " method)." 4483 return True, message 4484 4485 return False, ""
In Settings > Biological Standards, provides an alert when MS-DIAL config is successfully set for biological standard
4488@app.callback(Output("chromatography-msdial-config-added", "data"), 4489 Input("istd-msdial-configs-button", "n_clicks"), 4490 State("select-istd-chromatography-dropdown", "value"), 4491 State("istd-msdial-configs-dropdown", "value"), prevent_initial_call=True) 4492def add_msdial_config_for_chromatography(button_click, chromatography, config_id): 4493 4494 """ 4495 In Settings > Internal Standards, sets the MS-DIAL configuration to be used for processing samples 4496 """ 4497 4498 if chromatography is not None and config_id is not None: 4499 db.update_msdial_config_for_internal_standards(chromatography, config_id) 4500 return "Added" 4501 else: 4502 return ""
In Settings > Internal Standards, sets the MS-DIAL configuration to be used for processing samples
4505@app.callback(Output("istd-config-success-alert", "is_open"), 4506 Output("istd-config-success-alert", "children"), 4507 Input("chromatography-msdial-config-added", "data"), 4508 State("select-istd-chromatography-dropdown", "value"), prevent_initial_call=True) 4509def ui_feedback_for_setting_msdial_config_for_chromatography(config_added, chromatography): 4510 4511 """ 4512 In Settings > Internal Standards, provides an alert when MS-DIAL config is successfully set for a chromatography 4513 """ 4514 4515 if config_added is not None: 4516 if config_added == "Added": 4517 message = "MS-DIAL parameter configuration saved successfully for " + chromatography + "." 4518 return True, message 4519 4520 return False, ""
In Settings > Internal Standards, provides an alert when MS-DIAL config is successfully set for a chromatography
4523@app.callback(Output("setup-new-run-modal", "is_open"), 4524 Output("setup-new-run-button", "n_clicks"), 4525 Output("setup-new-run-modal-title", "children"), 4526 Input("setup-new-run-button", "n_clicks"), 4527 Input("start-run-monitor-modal", "is_open"), 4528 State("tabs", "value"), 4529 Input("data-acquisition-folder-button", "n_clicks"), 4530 Input("file-explorer-select-button", "n_clicks"), 4531 State("settings-modal", "is_open"), prevent_initial_call=True) 4532def toggle_new_run_modal(button_clicks, success, instrument_name, browse_folder_button, file_explorer_button, settings_modal_is_open): 4533 4534 """ 4535 Toggles modal for setting up AutoQC monitoring for a new instrument run 4536 """ 4537 4538 button = ctx.triggered_id 4539 4540 modal_title = "New QC Job – " + instrument_name 4541 4542 open_modal = True, 1, modal_title 4543 close_modal = False, 0, modal_title 4544 4545 if button == "data-acquisition-folder-button": 4546 return close_modal 4547 4548 elif button == "file-explorer-select-button": 4549 if settings_modal_is_open: 4550 return close_modal 4551 else: 4552 return open_modal 4553 4554 if not success and button_clicks != 0: 4555 return open_modal 4556 else: 4557 return close_modal
Toggles modal for setting up AutoQC monitoring for a new instrument run
4560@app.callback(Output("start-run-chromatography-dropdown", "options"), 4561 Output("start-run-bio-standards-dropdown", "options"), 4562 Output("start-run-qc-configs-dropdown", "options"), 4563 Input("setup-new-run-button", "n_clicks"), prevent_initial_call=True) 4564def populate_options_for_new_run(button_click): 4565 4566 """ 4567 Populates dropdowns and checklists for Setup New MS-AutoQC Job page 4568 """ 4569 4570 chromatography_methods = [] 4571 biological_standards = [] 4572 qc_configurations = [] 4573 4574 for method in db.get_chromatography_methods_list(): 4575 chromatography_methods.append({"value": method, "label": method}) 4576 4577 for bio_standard in db.get_biological_standards_list(): 4578 biological_standards.append({"value": bio_standard, "label": bio_standard}) 4579 4580 for qc_configuration in db.get_qc_configurations_list(): 4581 qc_configurations.append({"value": qc_configuration, "label": qc_configuration}) 4582 4583 return chromatography_methods, biological_standards, qc_configurations
Populates dropdowns and checklists for Setup New MS-AutoQC Job page
4586@app.callback(Output("sequence-path", "value"), 4587 Output("new-sequence", "data"), 4588 Input("sequence-upload-button", "contents"), 4589 State("sequence-upload-button", "filename"), prevent_initial_call=True) 4590def capture_uploaded_sequence(contents, filename): 4591 4592 """ 4593 Converts sequence CSV file to JSON string and stores in dcc.Store object 4594 """ 4595 4596 # Decode sequence file contents 4597 content_type, content_string = contents.split(",") 4598 decoded = base64.b64decode(content_string) 4599 sequence_file_contents = io.StringIO(decoded.decode("utf-8")) 4600 4601 # Get sequence file as JSON string 4602 sequence = qc.convert_sequence_to_json(sequence_file_contents) 4603 4604 # Update UI and store sequence JSON string 4605 return filename, sequence
Converts sequence CSV file to JSON string and stores in dcc.Store object
4608@app.callback(Output("metadata-path", "value"), 4609 Output("new-metadata", "data"), 4610 Input("metadata-upload-button", "contents"), 4611 State("metadata-upload-button", "filename"), prevent_initial_call=True) 4612def capture_uploaded_metadata(contents, filename): 4613 4614 """ 4615 Converts metadata CSV file to JSON string and stores in dcc.Store object 4616 """ 4617 4618 # Decode metadata file contents 4619 content_type, content_string = contents.split(",") 4620 decoded = base64.b64decode(content_string) 4621 metadata_file_contents = io.StringIO(decoded.decode("utf-8")) 4622 4623 # Get metadata file as JSON string 4624 metadata = qc.convert_metadata_to_json(metadata_file_contents) 4625 4626 # Update UI and store metadata JSON string 4627 return filename, metadata
Converts metadata CSV file to JSON string and stores in dcc.Store object
4662@app.callback(Output("instrument-run-id", "valid"), 4663 Output("instrument-run-id", "invalid"), 4664 Output("start-run-chromatography-dropdown", "valid"), 4665 Output("start-run-chromatography-dropdown", "invalid"), 4666 Output("start-run-qc-configs-dropdown", "valid"), 4667 Output("start-run-qc-configs-dropdown", "invalid"), 4668 Output("sequence-path", "valid"), 4669 Output("sequence-path", "invalid"), 4670 Output("metadata-path", "valid"), 4671 Output("metadata-path", "invalid"), 4672 Output("data-acquisition-folder-path", "valid"), 4673 Output("data-acquisition-folder-path", "invalid"), 4674 Input("instrument-run-id", "value"), 4675 Input("start-run-chromatography-dropdown", "value"), 4676 Input("start-run-bio-standards-dropdown", "value"), 4677 Input("start-run-qc-configs-dropdown", "value"), 4678 Input("sequence-upload-button", "contents"), 4679 State("sequence-upload-button", "filename"), 4680 Input("metadata-upload-button", "contents"), 4681 State("metadata-upload-button", "filename"), 4682 Input("data-acquisition-folder-path", "value"), 4683 State("instrument-run-id", "valid"), 4684 State("instrument-run-id", "invalid"), 4685 State("start-run-chromatography-dropdown", "valid"), 4686 State("start-run-chromatography-dropdown", "invalid"), 4687 State("start-run-qc-configs-dropdown", "valid"), 4688 State("start-run-qc-configs-dropdown", "invalid"), 4689 State("sequence-path", "valid"), 4690 State("sequence-path", "invalid"), 4691 State("metadata-path", "valid"), 4692 State("metadata-path", "invalid"), 4693 State("data-acquisition-folder-path", "valid"), 4694 State("data-acquisition-folder-path", "invalid"), 4695 State("tabs", "value"), prevent_initial_call=True) 4696def validation_feedback_for_new_run_setup_form(run_id, chromatography, bio_standards, qc_config, sequence_contents, 4697 sequence_filename, metadata_contents, metadata_filename, data_acquisition_path, run_id_valid, run_id_invalid, 4698 chromatography_valid, chromatography_invalid, qc_config_valid, qc_config_invalid, sequence_valid, sequence_invalid, 4699 metadata_valid, metadata_invalid, path_valid, path_invalid, instrument): 4700 4701 """ 4702 Extensive form validation and feedback for setting up a new MS-AutoQC job 4703 """ 4704 4705 # Instrument run ID validation 4706 if run_id is not None: 4707 4708 # Get run ID's for instrument 4709 run_ids = db.get_instrument_runs(instrument)["run_id"].astype(str).tolist() 4710 4711 # Check if run ID is unique 4712 if run_id not in run_ids: 4713 run_id_valid, run_id_invalid = True, False 4714 else: 4715 run_id_valid, run_id_invalid = False, True 4716 4717 # Chromatography validation 4718 if chromatography is not None: 4719 if qc.chromatography_valid(chromatography): 4720 chromatography_valid, chromatography_invalid = True, False 4721 else: 4722 chromatography_valid, chromatography_invalid = False, True 4723 4724 # Biological standard validation 4725 if bio_standards is not None: 4726 if qc.biological_standards_valid(chromatography, bio_standards): 4727 chromatography_valid, chromatography_invalid = True, False 4728 else: 4729 chromatography_valid, chromatography_invalid = False, True 4730 elif chromatography is not None: 4731 if qc.chromatography_valid(chromatography): 4732 chromatography_valid, chromatography_invalid = True, False 4733 else: 4734 chromatography_valid, chromatography_invalid = False, True 4735 4736 # QC configuration validation 4737 if qc_config is not None: 4738 qc_config_valid = True 4739 4740 # Instrument sequence file validation 4741 if sequence_contents is not None: 4742 4743 content_type, content_string = sequence_contents.split(",") 4744 decoded = base64.b64decode(content_string) 4745 sequence_contents = io.StringIO(decoded.decode("utf-8")) 4746 4747 if qc.sequence_is_valid(sequence_filename, sequence_contents): 4748 sequence_valid, sequence_invalid = True, False 4749 else: 4750 sequence_valid, sequence_invalid = False, True 4751 4752 # Metadata file validation 4753 if metadata_contents is not None: 4754 4755 content_type, content_string = metadata_contents.split(",") 4756 decoded = base64.b64decode(content_string) 4757 metadata_contents = io.StringIO(decoded.decode("utf-8")) 4758 4759 if qc.metadata_is_valid(metadata_filename, metadata_contents): 4760 metadata_valid, metadata_invalid = True, False 4761 else: 4762 metadata_valid, metadata_invalid = False, True 4763 4764 # Validate that data acquisition path exists 4765 if data_acquisition_path is not None: 4766 if os.path.exists(data_acquisition_path): 4767 path_valid, path_invalid = True, False 4768 else: 4769 path_valid, path_invalid = False, True 4770 4771 return run_id_valid, run_id_invalid, chromatography_valid, chromatography_invalid, qc_config_valid, qc_config_invalid, \ 4772 sequence_valid, sequence_invalid, metadata_valid, metadata_invalid, path_valid, path_invalid
Extensive form validation and feedback for setting up a new MS-AutoQC job
4793@app.callback(Output("start-run-monitor-modal", "is_open"), 4794 Output("new-job-error-modal", "is_open"), 4795 Input("monitor-new-run-button", "n_clicks"), 4796 State("instrument-run-id", "value"), 4797 State("tabs", "value"), 4798 State("start-run-chromatography-dropdown", "value"), 4799 State("start-run-bio-standards-dropdown", "value"), 4800 State("new-sequence", "data"), 4801 State("new-metadata", "data"), 4802 State("data-acquisition-folder-path", "value"), 4803 State("start-run-qc-configs-dropdown", "value"), 4804 State("ms_autoqc-job-type", "value"), prevent_initial_call=True) 4805def new_autoqc_job_setup(button_clicks, run_id, instrument_id, chromatography, bio_standards, sequence, metadata, 4806 acquisition_path, qc_config_id, job_type): 4807 4808 """ 4809 This callback initiates the following: 4810 1. Writing a new instrument run to the database 4811 2. Generate parameters files for MS-DIAL processing 4812 3a. Initializing run monitoring at the given directory for an active run, or 4813 3b. Iterating through and processing data files for a completed run 4814 """ 4815 4816 if run_id not in db.get_instrument_runs(instrument_id, as_list=True): 4817 4818 # Write a new instrument run to the database 4819 db.insert_new_run(run_id, instrument_id, chromatography, bio_standards, acquisition_path, sequence, metadata, qc_config_id, job_type) 4820 4821 # Get MSPs and generate parameters files for MS-DIAL processing 4822 for polarity in ["Positive", "Negative"]: 4823 4824 # Generate parameters files for processing samples 4825 msp_file_path = db.get_msp_file_path(chromatography, polarity) 4826 db.generate_msdial_parameters_file(chromatography, polarity, msp_file_path) 4827 4828 # Generate parameters files for processing each biological standard 4829 if bio_standards is not None: 4830 for bio_standard in bio_standards: 4831 msp_file_path = db.get_msp_file_path(chromatography, polarity, bio_standard) 4832 db.generate_msdial_parameters_file(chromatography, polarity, msp_file_path, bio_standard) 4833 4834 # Start AcquisitionListener process in the background 4835 process = psutil.Popen(["py", "AcquisitionListener.py", acquisition_path, instrument_id, run_id]) 4836 db.store_pid(instrument_id, run_id, process.pid) 4837 4838 # Upload database to Google Drive 4839 if db.is_instrument_computer() and db.sync_is_enabled(): 4840 db.upload_database(instrument_id) 4841 4842 return True, False
This callback initiates the following:
- Writing a new instrument run to the database
- Generate parameters files for MS-DIAL processing 3a. Initializing run monitoring at the given directory for an active run, or 3b. Iterating through and processing data files for a completed run
4845@app.callback(Output("file-explorer-modal", "is_open"), 4846 Input("data-acquisition-folder-button", "n_clicks"), 4847 Input("file-explorer-select-button", "n_clicks"), 4848 State("setup-new-run-modal", "is_open"), 4849 Input("msdial-folder-button", "n_clicks"), prevent_initial_call=True) 4850def open_file_explorer(new_job_browse_folder_button, select_folder_button, new_run_modal_is_open, msdial_select_folder_button): 4851 4852 """ 4853 Opens custom file explorer modal 4854 """ 4855 4856 button = ctx.triggered_id 4857 4858 if button == "msdial-folder-button" or button == "data-acquisition-folder-button": 4859 return True 4860 elif button == "file-explorer-select-button": 4861 return False 4862 else: 4863 raise PreventUpdate
Opens custom file explorer modal
4866@app.callback(Output("file-explorer-modal-body", "children"), 4867 Input("file-explorer-modal", "is_open"), 4868 Input("selected-data-folder", "data"), 4869 Input("selected-msdial-folder", "data"), 4870 State("settings-modal", "is_open"), prevent_initial_call=True) 4871def list_directories_in_file_explorer(file_explorer_is_open, selected_data_folder, selected_msdial_folder, settings_is_open): 4872 4873 """ 4874 Lists directories for a user to select in the file explorer modal 4875 """ 4876 4877 if file_explorer_is_open: 4878 4879 link_components = [] 4880 start_folder = None 4881 4882 if not settings_is_open and selected_data_folder is not None: 4883 start_folder = selected_data_folder 4884 elif settings_is_open and selected_msdial_folder is not None: 4885 start_folder = selected_msdial_folder 4886 4887 if start_folder is None: 4888 if sys.platform == "win32": 4889 start_folder = "C:/" 4890 elif sys.platform == "darwin": 4891 start_folder = "/Users/" 4892 4893 folders = [f.path for f in os.scandir(start_folder) if f.is_dir()] 4894 4895 if len(folders) > 0: 4896 for index, folder in enumerate(folders): 4897 link = html.A(folder, href="#", id="dir-" + str(index + 1)) 4898 link_components.append(link) 4899 link_components.append(html.Br()) 4900 4901 for index in range(len(folders), 30): 4902 link_components.append(html.A("", id="dir-" + str(index + 1))) 4903 else: 4904 link_components = [] 4905 4906 return link_components 4907 4908 else: 4909 raise PreventUpdate
Lists directories for a user to select in the file explorer modal
4912@app.callback(Output("selected-data-folder", "data"), 4913 Output("selected-msdial-folder", "data"), 4914 Input("dir-1", "n_clicks"), Input("dir-2", "n_clicks"), Input("dir-3", "n_clicks"), 4915 Input("dir-4", "n_clicks"), Input("dir-5", "n_clicks"), Input("dir-6", "n_clicks"), 4916 Input("dir-7", "n_clicks"), Input("dir-8", "n_clicks"), Input("dir-9", "n_clicks"), 4917 Input("dir-10", "n_clicks"), Input("dir-11", "n_clicks"), Input("dir-12", "n_clicks"), 4918 Input("dir-13", "n_clicks"), Input("dir-14", "n_clicks"), Input("dir-15", "n_clicks"), 4919 Input("dir-16", "n_clicks"), Input("dir-17", "n_clicks"), Input("dir-18", "n_clicks"), 4920 Input("dir-19", "n_clicks"), Input("dir-20", "n_clicks"), Input("dir-21", "n_clicks"), 4921 Input("dir-22", "n_clicks"), Input("dir-23", "n_clicks"), Input("dir-24", "n_clicks"), 4922 Input("dir-25", "n_clicks"), Input("dir-26", "n_clicks"), Input("dir-27", "n_clicks"), 4923 Input("dir-28", "n_clicks"), Input("dir-29", "n_clicks"), Input("dir-30", "n_clicks"), 4924 State("dir-1", "children"), State("dir-2", "children"), State("dir-3", "children"), 4925 State("dir-4", "children"), State("dir-5", "children"), State("dir-6", "children"), 4926 State("dir-7", "children"), State("dir-8", "children"), State("dir-9", "children"), 4927 State("dir-10", "children"), State("dir-11", "children"), State("dir-12", "children"), 4928 State("dir-13", "children"), State("dir-14", "children"), State("dir-15", "children"), 4929 State("dir-16", "children"), State("dir-17", "children"), State("dir-18", "children"), 4930 State("dir-19", "children"), State("dir-20", "children"), State("dir-21", "children"), 4931 State("dir-22", "children"), State("dir-23", "children"), State("dir-24", "children"), 4932 State("dir-25", "children"), State("dir-26", "children"), State("dir-27", "children"), 4933 State("dir-28", "children"), State("dir-29", "children"), State("dir-30", "children"), 4934 Input("selected-data-folder", "data"), 4935 Input("selected-msdial-folder", "data"), 4936 Input("file-explorer-back-button", "n_clicks"), 4937 State("settings-modal", "is_open"), prevent_initial_call=True) 4938def the_most_inefficient_callback_in_history(com_1, com_2, com_3, com_4, com_5, com_6, com_7, com_8, com_9, com_10, 4939 com_11, com_12, com_13, com_14, com_15, com_16, com_17, com_18, com_19, com_20, com_21, com_22, com_23, com_24, 4940 com_25, com_26, com_27, com_28, com_29, com_30, dir_1, dir_2, dir_3, dir_4, dir_5, dir_6, dir_7, dir_8, dir_9, dir_10, 4941 dir_11, dir_12, dir_13, dir_14, dir_15, dir_16, dir_17, dir_18, dir_19, dir_20, dir_21, dir_22, dir_23, dir_24, 4942 dir_25, dir_26, dir_27, dir_28, dir_29, dir_30, selected_data_folder, selected_msdial_folder, back_button, settings_is_open): 4943 4944 """ 4945 Handles user selection of folder in the file explorer modal (I'm sorry) 4946 """ 4947 4948 if settings_is_open: 4949 selected_folder = selected_msdial_folder 4950 else: 4951 selected_folder = selected_data_folder 4952 4953 if selected_folder is None: 4954 4955 if sys.platform == "win32": 4956 start = "C:/Users/" 4957 elif sys.platform == "darwin": 4958 start = "/Users/" 4959 4960 if settings_is_open: 4961 return None, start 4962 else: 4963 return start, None 4964 4965 # Get <a> component that triggered callback 4966 selected_component = ctx.triggered_id 4967 4968 if selected_component == "file-explorer-back-button": 4969 last_folder = "/" + selected_folder.split("/")[-1] 4970 previous = selected_folder.replace(last_folder, "") 4971 4972 if settings_is_open: 4973 return None, previous 4974 else: 4975 return previous, None 4976 4977 # Create a dictionary with all link components and their values 4978 components = ("dir-1", "dir-2", "dir-3", "dir-4", "dir-5", "dir-6", "dir-7", "dir-8", "dir-9", "dir-10", "dir-11", "dir-12", 4979 "dir-13", "dir-14", "dir-15", "dir-16", "dir-17", "dir-18", "dir-19", "dir-20", "dir-21", "dir-22", "dir-23", "dir-24", 4980 "dir-25", "dir-26", "dir-27", "dir-28", "dir-29", "dir-30") 4981 values = (dir_1, dir_2, dir_3, dir_4, dir_5, dir_6, dir_7, dir_8, dir_9, dir_10, dir_11, dir_12, dir_13, dir_14, dir_15, 4982 dir_16, dir_17, dir_18, dir_19, dir_20, dir_21, dir_22, dir_23, dir_24, dir_25, dir_26, dir_27, dir_28, dir_29, dir_30) 4983 folders = {components[i]: values[i] for i in range(len(components))} 4984 4985 # Append to selected folder path by indexing set 4986 selected_folder = folders[selected_component] 4987 4988 # Return selected folder and all folder values 4989 if settings_is_open: 4990 return None, selected_folder.replace("\\", "/") 4991 else: 4992 return selected_folder.replace("\\", "/"), None
Handles user selection of folder in the file explorer modal (I'm sorry)
4995@app.callback(Output("file-explorer-modal-title", "children"), 4996 Input("selected-data-folder", "data"), 4997 Input("selected-msdial-folder", "data"), 4998 State("settings-modal", "is_open"), prevent_initial_call=True) 4999def update_file_explorer_title(selected_data_folder, selected_msdial_folder, settings_is_open): 5000 5001 """ 5002 Populates data acquisition path text field with user selection 5003 """ 5004 5005 if not settings_is_open: 5006 return selected_data_folder 5007 elif settings_is_open: 5008 return selected_msdial_folder 5009 else: 5010 raise PreventUpdate
Populates data acquisition path text field with user selection
5013@app.callback(Output("data-acquisition-folder-path", "value"), 5014 Input("file-explorer-select-button", "n_clicks"), 5015 State("selected-data-folder", "data"), 5016 State("settings-modal", "is_open"), prevent_initial_call=True) 5017def update_folder_path_text_field(select_folder_button, selected_folder, settings_is_open): 5018 5019 """ 5020 Populates data acquisition path text field with user selection 5021 """ 5022 5023 if not settings_is_open: 5024 return selected_folder
Populates data acquisition path text field with user selection
5027@app.callback(Output("active-run-progress-card", "style"), 5028 Output("active-run-progress-header", "children"), 5029 Output("active-run-progress-bar", "value"), 5030 Output("active-run-progress-bar", "label"), 5031 Output("refresh-interval", "disabled"), 5032 Output("job-controller-panel", "style"), 5033 Input("instrument-run-table", "active_cell"), 5034 State("instrument-run-table", "data"), 5035 Input("refresh-interval", "n_intervals"), 5036 Input("tabs", "value"), 5037 Input("start-run-monitor-modal", "is_open"), prevent_initial_call=True) 5038def update_progress_bar_during_active_instrument_run(active_cell, table_data, refresh, instrument_id, new_job_started): 5039 5040 """ 5041 Displays and updates progress bar if an active instrument run was selected from the table 5042 """ 5043 5044 if active_cell: 5045 5046 # Get run ID 5047 run_id = table_data[active_cell["row"]]["Run ID"] 5048 status = table_data[active_cell["row"]]["Status"] 5049 5050 # Construct values for progress bar 5051 completed, total = db.get_completed_samples_count(instrument_id, run_id, status) 5052 percent_complete = db.get_run_progress(instrument_id, run_id, status) 5053 progress_label = str(percent_complete) + "%" 5054 header_text = run_id + " – " + str(completed) + " out of " + str(total) + " samples processed" 5055 5056 if status == "Complete": 5057 refresh_interval_disabled = True 5058 else: 5059 refresh_interval_disabled = False 5060 5061 if db.get_device_identity() == instrument_id: 5062 controller_panel_visibility = {"display": "block"} 5063 else: 5064 controller_panel_visibility = {"display": "none"} 5065 5066 return {"display": "block"}, header_text, percent_complete, progress_label, refresh_interval_disabled, controller_panel_visibility 5067 5068 else: 5069 return {"display": "none"}, None, None, None, True, {"display": "none"}
Displays and updates progress bar if an active instrument run was selected from the table
5072@app.callback(Output("setup-new-run-button", "style"), 5073 Input("tabs", "value"), prevent_initial_call=True) 5074def hide_elements_for_non_instrument_devices(instrument_id): 5075 5076 """ 5077 Hides job setup button for shared users 5078 """ 5079 5080 if db.is_valid(): 5081 if db.get_device_identity() != instrument_id: 5082 return {"display": "none"} 5083 else: 5084 return {"display": "block", "margin-top": "15px", "line-height": "1.75"} 5085 else: 5086 raise PreventUpdate
Hides job setup button for shared users
5089@app.callback(Output("job-controller-modal", "is_open"), 5090 Output("job-controller-modal-title", "children"), 5091 Output("job-controller-modal-body", "children"), 5092 Output("job-controller-confirm-button", "children"), 5093 Output("job-controller-confirm-button", "color"), 5094 Input("mark-as-completed-button", "n_clicks"), 5095 Input("job-marked-completed", "data"), 5096 Input("restart-job-button", "n_clicks"), 5097 Input("job-restarted", "data"), 5098 Input("delete-job-button", "n_clicks"), 5099 Input("job-deleted", "data"), 5100 State("study-resources", "data"), prevent_initial_call=True) 5101def confirm_action_on_job(mark_job_as_completed, job_completed, restart_job, job_restarted, delete_job, job_deleted, resources): 5102 5103 """ 5104 Shows an alert confirming that the user wants to perform an action on the selected MS-AutoQC job 5105 """ 5106 5107 trigger = ctx.triggered_id 5108 resources = json.loads(resources) 5109 instrument_id = resources["instrument"] 5110 run_id = resources["run_id"] 5111 5112 if trigger == "mark-as-completed-button": 5113 title = "Mark " + run_id + " as completed?" 5114 body = dbc.Label("This will save your QC results as-is and end the current job. Continue?") 5115 return True, title, body, "Mark Job as Completed", "success" 5116 5117 elif trigger == "restart-job-button": 5118 title = "Restart " + run_id + "?" 5119 body = dbc.Label("This will restart the acquisition listener process for " + run_id + ". Continue?") 5120 return True, title, body, "Restart Job", "warning" 5121 5122 elif trigger == "delete-job-button": 5123 title = "Delete " + run_id + " on " + instrument_id + "?" 5124 body = dbc.Label("This will delete all QC results for " + run_id + " on " + instrument_id + 5125 ". This process cannot be undone. Continue?") 5126 return True, title, body, "Delete Job", "danger" 5127 5128 elif trigger == "job-marked-completed" or trigger == "job-restarted" or trigger == "job-deleted" or trigger == "job-action-failed": 5129 return False, None, None, None, None 5130 5131 else: 5132 raise PreventUpdate
Shows an alert confirming that the user wants to perform an action on the selected MS-AutoQC job
5135@app.callback(Output("job-marked-completed", "data"), 5136 Output("job-restarted", "data"), 5137 Output("job-deleted", "data"), 5138 Output("job-action-failed", "data"), 5139 Input("job-controller-confirm-button", "n_clicks"), 5140 State("job-controller-modal-title", "children"), 5141 State("study-resources", "data"), prevent_initial_call=True) 5142def perform_action_on_job(confirm_button, modal_title, resources): 5143 5144 """ 5145 Performs the selected action on the selected MS-AutoQC job 5146 """ 5147 5148 resources = json.loads(resources) 5149 instrument_id = resources["instrument"] 5150 run_id = resources["run_id"] 5151 acquisition_path = db.get_acquisition_path(instrument_id, run_id) 5152 5153 if "Mark" in modal_title: 5154 5155 try: 5156 # Mark instrument run as completed 5157 db.mark_run_as_completed(instrument_id, run_id) 5158 5159 # Sync database on run completion 5160 if db.sync_is_enabled(): 5161 db.sync_on_run_completion(instrument_id, run_id) 5162 5163 # Delete temporary data file directory 5164 db.delete_temp_directory(instrument_id, run_id) 5165 5166 # Kill acquisition listener 5167 pid = db.get_pid(instrument_id, run_id) 5168 qc.kill_subprocess(pid) 5169 return True, None, None, None 5170 5171 except: 5172 print("Could not mark instrument run as completed.") 5173 traceback.print_exc() 5174 return None, None, None, True 5175 5176 elif "Restart" in modal_title: 5177 5178 try: 5179 # Kill current acquisition listener (acquisition listener will be restarted automatically) 5180 pid = db.get_pid(instrument_id, run_id) 5181 qc.kill_subprocess(pid) 5182 5183 # Delete temporary data file directory 5184 db.delete_temp_directory(instrument_id, run_id) 5185 5186 # Restart AcquisitionListener and store process ID 5187 process = psutil.Popen(["py", "AcquisitionListener.py", acquisition_path, instrument_id, run_id]) 5188 db.store_pid(instrument_id, run_id, process.pid) 5189 return None, True, None, None 5190 5191 except: 5192 print("Could not restart listener.") 5193 traceback.print_exc() 5194 return None, None, None, True 5195 5196 elif "Delete" in modal_title: 5197 5198 try: 5199 # Delete instrument run from database 5200 db.delete_instrument_run(instrument_id, run_id) 5201 5202 # Sync with Google Drive 5203 if db.sync_is_enabled(): 5204 db.upload_database(instrument_id) 5205 db.delete_active_run_csv_files(instrument_id, run_id) 5206 5207 # Delete temporary data file directory 5208 db.delete_temp_directory(instrument_id, run_id) 5209 return None, None, True, None 5210 5211 except: 5212 print("Could not delete instrument run.") 5213 traceback.print_exc() 5214 return None, None, None, True 5215 5216 else: 5217 raise PreventUpdate
Performs the selected action on the selected MS-AutoQC job