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
local_stylesheet = {'href': 'https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap', 'rel': 'stylesheet'}

Dash app layout

def serve_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    ])
@app.callback(Output('google-drive-download-database', 'data'), Input('tabs', 'value'), prevent_initial_call=True)
def sync_with_google_drive(instrument_id):
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

@app.callback(Output('google-drive-authenticated', 'data'), Input('on-page-load', 'data'))
def authenticate_with_google_drive(on_page_load):
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

@app.callback(Output('google-drive-authenticated-1', 'data'), Output('google-drive-authenticated-2', 'data'), Output('google-drive-authenticated-3', 'data'), Input('setup-google-drive-button-1', 'n_clicks'), Input('setup-google-drive-button-2', 'n_clicks'), Input('google-drive-sync-button', 'n_clicks'), State('gdrive-client-id-1', 'value'), State('gdrive-client-id-2', 'value'), State('gdrive-client-id', 'value'), State('gdrive-client-secret-1', 'value'), State('gdrive-client-secret-2', 'value'), State('gdrive-client-secret', 'value'), prevent_initial_call=True)
def launch_google_drive_authentication( setup_auth_button_clicks, sign_in_auth_button_clicks, settings_button_clicks, client_id_1, client_id_2, client_id_3, client_secret_1, client_secret_2, client_secret_3):
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

@app.callback(Output('setup-google-drive-button-1', 'children'), Output('setup-google-drive-button-1', 'color'), Output('setup-google-drive-button-1', 'outline'), Output('google-drive-button-1-popover', 'children'), Output('google-drive-button-1-popover', 'is_open'), Output('gdrive-folder-id-1', 'data'), Output('gdrive-methods-zip-id-1', 'data'), Input('google-drive-authenticated-1', 'data'), prevent_initial_call=True)
def check_first_time_google_drive_authentication(google_drive_is_authenticated):
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

@app.callback(Output('first-time-instrument-vendor', 'label'), Output('thermo-fisher-item', 'n_clicks'), Output('agilent-item', 'n_clicks'), Output('bruker-item', 'n_clicks'), Output('sciex-item', 'n_clicks'), Output('waters-item', 'n_clicks'), Input('thermo-fisher-item', 'n_clicks'), Input('agilent-item', 'n_clicks'), Input('bruker-item', 'n_clicks'), Input('sciex-item', 'n_clicks'), Input('waters-item', 'n_clicks'), prevent_initial_call=True)
def vendor_dropdown_handling( thermo_fisher_click, agilent_click, bruker_click, sciex_click, waters_click):
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...

@app.callback(Output('first-time-complete-setup-button', 'disabled'), Output('first-time-instrument-id', 'valid'), Input('first-time-instrument-id', 'value'), Input('first-time-instrument-vendor', 'label'), prevent_initial_call=True)
def enable_complete_setup_button(instrument_name, instrument_vendor):
1776@app.callback(Output("first-time-complete-setup-button", "disabled"),
1777              Output("first-time-instrument-id", "valid"),
1778              Input("first-time-instrument-id", "value"),
1779              Input("first-time-instrument-vendor", "label"), prevent_initial_call=True)
1780def enable_complete_setup_button(instrument_name, instrument_vendor):
1781
1782    """
1783    Enables "Complete setup" button upon form completion in Welcome > Setup New Instrument page
1784    """
1785
1786    valid = False, True
1787    invalid = True, False
1788
1789    if instrument_name is not None:
1790        if len(instrument_name) > 3 and instrument_vendor != "Choose Vendor":
1791            return valid
1792        else:
1793            return invalid
1794    else:
1795        return invalid

Enables "Complete setup" button upon form completion in Welcome > Setup New Instrument page

@app.callback(Output('first-time-complete-setup-button', 'children'), Input('first-time-complete-setup-button', 'n_clicks'), prevent_initial_call=True)
def ui_feedback_for_complete_setup_button(button_click):
1798@app.callback(Output("first-time-complete-setup-button", "children"),
1799              Input("first-time-complete-setup-button", "n_clicks"), prevent_initial_call=True)
1800def ui_feedback_for_complete_setup_button(button_click):
1801
1802    """
1803    Returns loading feedback on complete setup button
1804    """
1805
1806    return [dbc.Spinner(size="sm"), " Finishing up, please wait..."]

Returns loading feedback on complete setup button

@app.callback(Output('workspace-has-been-setup-1', 'data'), Input('first-time-complete-setup-button', 'children'), State('first-time-instrument-id', 'value'), State('first-time-instrument-vendor', 'label'), State('google-drive-authenticated-1', 'data'), State('gdrive-folder-id-1', 'data'), State('gdrive-methods-zip-id-1', 'data'), prevent_initial_call=True)
def complete_first_time_setup( button_click, instrument_id, instrument_vendor, google_drive_authenticated, gdrive_folder_id, methods_zip_file_id):
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:

  1. If databases DO exist in Google Drive, downloads databases
  2. If databases DO NOT exist in Google Drive, initializes new SQLite database
  3. Adds instrument to "instruments" table
  4. Uploads database to Google Drive folder
  5. Dismisses setup window
@app.callback(Output('setup-google-drive-button-2', 'children'), Output('setup-google-drive-button-2', 'color'), Output('setup-google-drive-button-2', 'outline'), Output('google-drive-button-2-popover', 'children'), Output('google-drive-button-2-popover', 'is_open'), Output('gdrive-folder-id-2', 'data'), Output('device-identity-selection', 'options'), Input('google-drive-authenticated-2', 'data'), prevent_initial_call=True)
def check_workspace_login_google_drive_authentication(google_drive_is_authenticated):
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

@app.callback(Output('device-identity-selection', 'disabled'), Input('device-identity-checkbox', 'value'), prevent_initial_call=True)
def enable_instrument_id_selection(is_instrument_computer):
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

@app.callback(Output('first-time-sign-in-button', 'disabled'), Input('setup-google-drive-button-2', 'children'), Input('device-identity-checkbox', 'value'), Input('device-identity-selection', 'value'), prevent_initial_call=True)
def enable_workspace_login_button(button_text, is_instrument_computer, instrument_id):
2014@app.callback(Output("first-time-sign-in-button", "disabled"),
2015              Input("setup-google-drive-button-2", "children"),
2016              Input("device-identity-checkbox", "value"),
2017              Input("device-identity-selection", "value"), prevent_initial_call=True)
2018def enable_workspace_login_button(button_text, is_instrument_computer, instrument_id):
2019
2020    """
2021    Enables "Sign in to workspace" button upon form completion in Welcome > Sign In To Workspace page
2022    """
2023
2024    if button_text is not None:
2025        if button_text == "Signed in to Google Drive":
2026            if is_instrument_computer:
2027                if instrument_id is not None:
2028                    return False
2029                else:
2030                    return True
2031            else:
2032                return False
2033        else:
2034            return True
2035    else:
2036        return True

Enables "Sign in to workspace" button upon form completion in Welcome > Sign In To Workspace page

@app.callback(Output('first-time-sign-in-button', 'children'), Input('first-time-sign-in-button', 'n_clicks'), prevent_initial_call=True)
def ui_feedback_for_workspace_login_button(button_click):
2039@app.callback(Output("first-time-sign-in-button", "children"),
2040              Input("first-time-sign-in-button", "n_clicks"), prevent_initial_call=True)
2041def ui_feedback_for_workspace_login_button(button_click):
2042
2043    """
2044    UI feedback for workspace sign in button in Setup > Login To Workspace
2045    """
2046
2047    return [dbc.Spinner(size="sm"), " Signing in, this may take a moment..."]

UI feedback for workspace sign in button in Setup > Login To Workspace

@app.callback(Output('workspace-has-been-setup-2', 'data'), Input('first-time-sign-in-button', 'children'), State('device-identity-checkbox', 'value'), State('device-identity-selection', 'value'), prevent_initial_call=True)
def ui_feedback_for_login_button(button_click, is_instrument_computer, instrument_id):
2050@app.callback(Output("workspace-has-been-setup-2", "data"),
2051              Input("first-time-sign-in-button", "children"),
2052              State("device-identity-checkbox", "value"),
2053              State("device-identity-selection", "value"), prevent_initial_call=True)
2054def ui_feedback_for_login_button(button_click, is_instrument_computer, instrument_id):
2055
2056    """
2057    Dismisses setup window and signs in to MS-AutoQC workspace
2058    """
2059
2060    if button_click:
2061
2062        # Set device identity and proceed
2063        db.set_device_identity(is_instrument_computer, instrument_id)
2064
2065        # Save Google Drive credentials
2066        db.save_google_drive_credentials()
2067        return True
2068
2069    else:
2070        raise PreventUpdate

Dismisses setup window and signs in to MS-AutoQC workspace

@app.callback(Output('workspace-setup-modal', 'is_open'), Output('on-page-load', 'data'), Input('workspace-has-been-setup-1', 'data'), Input('workspace-has-been-setup-2', 'data'))
def dismiss_setup_window(workspace_has_been_setup_1, workspace_has_been_setup_2):
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

@app.callback(Output('google-drive-sync-button', 'color'), Output('google-drive-sync-button', 'children'), Output('google-drive-sync-form-text', 'children'), Output('google-drive-sign-in-from-settings-alert', 'is_open'), Output('gdrive-client-id', 'placeholder'), Output('gdrive-client-secret', 'placeholder'), Input('google-drive-authenticated-3', 'data'), Input('google-drive-authenticated', 'data'), Input('settings-modal', 'is_open'), State('google-drive-sync-form-text', 'children'), State('tabs', 'value'), prevent_initial_call=True)
def update_google_drive_sync_status_in_settings( google_drive_authenticated, google_drive_authenticated_on_start, settings_is_open, form_text, instrument_id):
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

@app.callback(Output('gdrive-credentials-saved', 'data'), Input('set-gdrive-credentials-button', 'n_clicks'), State('gdrive-client-id', 'value'), State('gdrive-client-secret', 'value'), prevent_initial_call=True)
def regenerate_settings_yaml_file(button_click, client_id, client_secret):
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

@app.callback(Output('gdrive-credentials-saved-alert', 'is_open'), Output('gdrive-credentials-saved-alert', 'children'), Output('gdrive-credentials-saved-alert', 'color'), Input('gdrive-credentials-saved', 'data'), prevent_initial_call=True)
def ui_alert_on_gdrive_credential_save(credential_save_result):
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

@app.callback(Output('tabs', 'children'), Output('tabs', 'value'), Input('instruments', 'data'), Input('workspace-setup-modal', 'is_open'), Input('google-drive-sync-update', 'data'))
def get_instrument_tabs(instruments, check_workspace_setup, sync_update):
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

@app.callback(Output('instrument-run-table', 'active_cell'), Output('instrument-run-table', 'selected_cells'), Input('tabs', 'value'), Input('job-deleted', 'data'), prevent_initial_call=True)
def reset_instrument_table(instrument, job_deleted):
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)

@app.callback(Output('instrument-run-table', 'data'), Output('table-container', 'style'), Output('plot-container', 'style'), Input('tabs', 'value'), Input('refresh-interval', 'n_intervals'), State('study-resources', 'data'), Input('google-drive-sync-update', 'data'), Input('start-run-monitor-modal', 'is_open'), Input('job-marked-completed', 'data'), Input('job-deleted', 'data'))
def populate_instrument_runs_table( instrument_id, refresh, resources, sync_update, new_job_started, job_marked_completed, job_deleted):
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

@app.callback(Output('loading-modal', 'is_open'), Output('loading-modal-title', 'children'), Output('loading-modal-body', 'children'), Input('instrument-run-table', 'active_cell'), State('instrument-run-table', 'data'), Input('close-load-modal', 'data'), prevent_initial_call=True, suppress_callback_exceptions=True)
def open_loading_modal(active_cell, table_data, load_finished):
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

@app.callback(Output('istd-rt-pos', 'data'), Output('istd-rt-neg', 'data'), Output('istd-intensity-pos', 'data'), Output('istd-intensity-neg', 'data'), Output('istd-mz-pos', 'data'), Output('istd-mz-neg', 'data'), Output('sequence', 'data'), Output('metadata', 'data'), Output('bio-rt-pos', 'data'), Output('bio-rt-neg', 'data'), Output('bio-intensity-pos', 'data'), Output('bio-intensity-neg', 'data'), Output('bio-mz-pos', 'data'), Output('bio-mz-neg', 'data'), Output('study-resources', 'data'), Output('samples', 'data'), Output('pos-internal-standards', 'data'), Output('neg-internal-standards', 'data'), Output('istd-delta-rt-pos', 'data'), Output('istd-delta-rt-neg', 'data'), Output('istd-in-run-delta-rt-pos', 'data'), Output('istd-in-run-delta-rt-neg', 'data'), Output('istd-delta-mz-pos', 'data'), Output('istd-delta-mz-neg', 'data'), Output('qc-warnings-pos', 'data'), Output('qc-warnings-neg', 'data'), Output('qc-fails-pos', 'data'), Output('qc-fails-neg', 'data'), Output('load-finished', 'data'), Input('refresh-interval', 'n_intervals'), Input('instrument-run-table', 'active_cell'), State('instrument-run-table', 'data'), State('study-resources', 'data'), State('tabs', 'value'), prevent_initial_call=True, suppress_callback_exceptions=True)
def load_data(refresh, active_cell, table_data, resources, instrument_id):
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)

@app.callback(Output('close-load-modal', 'data'), Input('load-finished', 'data'), prevent_initial_call=True)
def signal_load_finished(load_finished):
2471@app.callback(Output("close-load-modal", "data"),
2472              Input("load-finished", "data"), prevent_initial_call=True)
2473def signal_load_finished(load_finished):
2474
2475    # Welcome to Dash callback hell :D
2476    return True
@app.callback(Output('sample-table', 'data'), Input('samples', 'data'), prevent_initial_call=True)
def populate_sample_tables(samples):
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

@app.callback(Output('istd-rt-dropdown', 'options'), Output('istd-mz-dropdown', 'options'), Output('istd-intensity-dropdown', 'options'), Output('bio-standard-benchmark-dropdown', 'options'), Output('rt-plot-sample-dropdown', 'options'), Output('mz-plot-sample-dropdown', 'options'), Output('intensity-plot-sample-dropdown', 'options'), Input('polarity-options', 'value'), State('sample-table', 'data'), Input('samples', 'data'), State('bio-intensity-pos', 'data'), State('bio-intensity-neg', 'data'), State('pos-internal-standards', 'data'), State('neg-internal-standards', 'data'))
def update_dropdowns_on_polarity_change( polarity, table_data, samples, bio_intensity_pos, bio_intensity_neg, pos_internal_standards, neg_internal_standards):
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

@app.callback(Output('rt-plot-sample-dropdown', 'value'), Output('mz-plot-sample-dropdown', 'value'), Output('intensity-plot-sample-dropdown', 'value'), Input('sample-filtering-options', 'value'), Input('polarity-options', 'value'), Input('samples', 'data'), State('metadata', 'data'), prevent_initial_call=True)
def apply_sample_filter_to_plots(filter, polarity, samples, metadata):
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:

  1. All samples
  2. Filter by samples only
  3. Filter by treatments / classes
  4. Filter by pools
  5. Filter by blanks
@app.callback(Output('istd-rt-plot', 'figure'), Output('rt-prev-button', 'n_clicks'), Output('rt-next-button', 'n_clicks'), Output('istd-rt-dropdown', 'value'), Output('istd-rt-div', 'style'), Input('polarity-options', 'value'), Input('istd-rt-dropdown', 'value'), Input('rt-plot-sample-dropdown', 'value'), Input('istd-rt-pos', 'data'), Input('istd-rt-neg', 'data'), State('samples', 'data'), State('study-resources', 'data'), State('pos-internal-standards', 'data'), State('neg-internal-standards', 'data'), Input('rt-prev-button', 'n_clicks'), Input('rt-next-button', 'n_clicks'), prevent_initial_call=True)
def populate_istd_rt_plot( polarity, internal_standard, selected_samples, rt_pos, rt_neg, samples, resources, pos_internal_standards, neg_internal_standards, previous, next):
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

@app.callback(Output('istd-intensity-plot', 'figure'), Output('intensity-prev-button', 'n_clicks'), Output('intensity-next-button', 'n_clicks'), Output('istd-intensity-dropdown', 'value'), Output('istd-intensity-div', 'style'), Input('polarity-options', 'value'), Input('istd-intensity-dropdown', 'value'), Input('intensity-plot-sample-dropdown', 'value'), Input('istd-intensity-pos', 'data'), Input('istd-intensity-neg', 'data'), State('samples', 'data'), State('metadata', 'data'), State('pos-internal-standards', 'data'), State('neg-internal-standards', 'data'), Input('intensity-prev-button', 'n_clicks'), Input('intensity-next-button', 'n_clicks'), prevent_initial_call=True)
def populate_istd_intensity_plot( polarity, internal_standard, selected_samples, intensity_pos, intensity_neg, samples, metadata, pos_internal_standards, neg_internal_standards, previous, next):
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

@app.callback(Output('istd-mz-plot', 'figure'), Output('mz-prev-button', 'n_clicks'), Output('mz-next-button', 'n_clicks'), Output('istd-mz-dropdown', 'value'), Output('istd-mz-div', 'style'), Input('polarity-options', 'value'), Input('istd-mz-dropdown', 'value'), Input('mz-plot-sample-dropdown', 'value'), Input('istd-delta-mz-pos', 'data'), Input('istd-delta-mz-neg', 'data'), State('samples', 'data'), State('pos-internal-standards', 'data'), State('neg-internal-standards', 'data'), State('study-resources', 'data'), Input('mz-prev-button', 'n_clicks'), Input('mz-next-button', 'n_clicks'), prevent_initial_call=True)
def populate_istd_mz_plot( polarity, internal_standard, selected_samples, delta_mz_pos, delta_mz_neg, samples, pos_internal_standards, neg_internal_standards, resources, previous, next):
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

@app.callback(Output('bio-standards-plot-dropdown', 'options'), Input('study-resources', 'data'), prevent_initial_call=True)
def populate_biological_standards_dropdown(resources):
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

@app.callback(Output('bio-standard-mz-rt-plot', 'figure'), Output('bio-standard-benchmark-dropdown', 'value'), Output('bio-standard-mz-rt-plot', 'clickData'), Output('bio-standard-mz-rt-div', 'style'), Input('polarity-options', 'value'), Input('bio-rt-pos', 'data'), Input('bio-rt-neg', 'data'), State('bio-intensity-pos', 'data'), State('bio-intensity-neg', 'data'), State('bio-mz-pos', 'data'), State('bio-mz-neg', 'data'), State('study-resources', 'data'), Input('bio-standard-mz-rt-plot', 'clickData'), Input('bio-standards-plot-dropdown', 'value'), prevent_initial_call=True)
def populate_bio_standard_mz_rt_plot( polarity, rt_pos, rt_neg, intensity_pos, intensity_neg, mz_pos, mz_neg, resources, click_data, selected_bio_standard):
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

@app.callback(Output('bio-standard-benchmark-plot', 'figure'), Output('bio-standard-benchmark-div', 'style'), Input('polarity-options', 'value'), Input('bio-standard-benchmark-dropdown', 'value'), Input('bio-intensity-pos', 'data'), Input('bio-intensity-neg', 'data'), Input('bio-standards-plot-dropdown', 'value'), State('study-resources', 'data'), prevent_initial_call=True)
def populate_bio_standard_benchmark_plot( polarity, selected_feature, intensity_pos, intensity_neg, selected_bio_standard, resources):
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

@app.callback(Output('sample-info-modal', 'is_open'), Output('sample-modal-title', 'children'), Output('sample-modal-body', 'children'), Output('sample-table', 'selected_cells'), Output('sample-table', 'active_cell'), Output('istd-rt-plot', 'clickData'), Output('istd-intensity-plot', 'clickData'), Output('istd-mz-plot', 'clickData'), State('sample-info-modal', 'is_open'), Input('sample-table', 'active_cell'), State('sample-table', 'data'), Input('istd-rt-plot', 'clickData'), Input('istd-intensity-plot', 'clickData'), Input('istd-mz-plot', 'clickData'), State('istd-rt-pos', 'data'), State('istd-rt-neg', 'data'), State('istd-intensity-pos', 'data'), State('istd-intensity-neg', 'data'), State('istd-mz-pos', 'data'), State('istd-mz-neg', 'data'), State('istd-delta-rt-pos', 'data'), State('istd-delta-rt-neg', 'data'), State('istd-in-run-delta-rt-pos', 'data'), State('istd-in-run-delta-rt-neg', 'data'), State('istd-delta-mz-pos', 'data'), State('istd-delta-mz-neg', 'data'), State('qc-warnings-pos', 'data'), State('qc-warnings-neg', 'data'), State('qc-fails-pos', 'data'), State('qc-fails-neg', 'data'), State('bio-rt-pos', 'data'), State('bio-rt-neg', 'data'), State('bio-intensity-pos', 'data'), State('bio-intensity-neg', 'data'), State('bio-mz-pos', 'data'), State('bio-mz-neg', 'data'), State('sequence', 'data'), State('metadata', 'data'), State('study-resources', 'data'), prevent_initial_call=True)
def toggle_sample_card( is_open, active_cell, table_data, rt_click, intensity_click, mz_click, rt_pos, rt_neg, intensity_pos, 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, qc_warnings_pos, qc_warnings_neg, qc_fails_pos, qc_fails_neg, bio_rt_pos, bio_rt_neg, bio_intensity_pos, bio_intensity_neg, bio_mz_pos, bio_mz_neg, sequence, metadata, resources):
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

@app.callback(Output('settings-modal', 'is_open'), Input('settings-button', 'n_clicks'), prevent_initial_call=True)
def toggle_settings_modal(button_click):
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

@app.callback(Output('google-drive-sync-modal', 'is_open'), Output('database-md5', 'data'), Input('settings-modal', 'is_open'), State('google-drive-authenticated', 'data'), State('google-drive-sync-modal', 'is_open'), Input('close-sync-modal', 'data'), State('database-md5', 'data'), prevent_initial_call=True)
def show_sync_modal( settings_is_open, google_drive_authenticated, sync_modal_is_open, sync_finished, md5_checksum):
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

@app.callback(Output('google-drive-sync-finished', 'data'), Input('settings-modal', 'is_open'), State('google-drive-authenticated', 'data'), State('google-drive-authenticated-3', 'data'), State('database-md5', 'data'), prevent_initial_call=True)
def sync_settings_to_google_drive( settings_modal_is_open, google_drive_authenticated, auth_in_app, md5_checksum):
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

@app.callback(Output('close-sync-modal', 'data'), Input('google-drive-sync-finished', 'data'), prevent_initial_call=True)
def close_sync_modal(sync_finished):
3212@app.callback(Output("close-sync-modal", "data"),
3213              Input("google-drive-sync-finished", "data"), prevent_initial_call=True)
3214def close_sync_modal(sync_finished):
3215
3216    # You've reached Dash callback purgatory :/
3217    if sync_finished:
3218        return True
@app.callback(Output('workspace-users-table', 'children'), Input('on-page-load', 'data'), Input('google-drive-user-added', 'data'), Input('google-drive-user-deleted', 'data'), Input('google-drive-sync-update', 'data'))
def get_users_with_workspace_access(on_page_load, user_added, user_deleted, sync_update):
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

@app.callback(Output('google-drive-user-added', 'data'), Input('add-user-button', 'n_clicks'), State('add-user-text-field', 'value'), State('google-drive-authenticated', 'data'), prevent_initial_call=True)
def add_user_to_workspace(button_click, user_email_address, google_drive_is_authenticated):
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

@app.callback(Output('google-drive-user-deleted', 'data'), Input('delete-user-button', 'n_clicks'), State('add-user-text-field', 'value'), State('google-drive-authenticated', 'data'), prevent_initial_call=True)
def delete_user_from_workspace(button_click, user_email_address, google_drive_is_authenticated):
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

@app.callback(Output('user-addition-alert', 'is_open'), Output('user-addition-alert', 'children'), Output('user-addition-alert', 'color'), Input('google-drive-user-added', 'data'), prevent_initial_call=True)
def ui_feedback_for_adding_gdrive_user(user_added_result):
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

@app.callback(Output('user-deletion-alert', 'is_open'), Output('user-deletion-alert', 'children'), Output('user-deletion-alert', 'color'), Input('google-drive-user-deleted', 'data'), prevent_initial_call=True)
def ui_feedback_for_deleting_gdrive_user(user_deleted_result):
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

@app.callback(Output('slack-bot-token', 'placeholder'), Input('slack-bot-token-saved', 'data'), Input('google-drive-sync-update', 'data'))
def get_slack_bot_token(token_save_result, sync_update):
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

@app.callback(Output('slack-bot-token-saved', 'data'), Input('save-slack-token-button', 'n_clicks'), State('slack-bot-token', 'value'), prevent_initial_call=True)
def save_slack_bot_token(button_click, slack_bot_token):
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

@app.callback(Output('slack-token-save-alert', 'is_open'), Output('slack-token-save-alert', 'children'), Output('slack-token-save-alert', 'color'), Input('slack-bot-token-saved', 'data'), prevent_initial_call=True)
def ui_alert_on_slack_token_save(token_save_result):
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

@app.callback(Output('slack-channel', 'value'), Output('slack-notifications-enabled', 'value'), Input('slack-channel-saved', 'data'), Input('google-drive-sync-update', 'data'))
def get_slack_channel(result, sync_update):
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

@app.callback(Output('slack-channel-saved', 'data'), Input('slack-notifications-enabled', 'value'), State('slack-channel', 'value'), prevent_initial_call=True)
def save_slack_channel(notifications_enabled, slack_channel):
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
  1. Registers Slack channel for MS-AutoQC notifications
  2. Sends a Slack message to confirm registration
@app.callback(Output('slack-notifications-toggle-alert', 'is_open'), Output('slack-notifications-toggle-alert', 'children'), Output('slack-notifications-toggle-alert', 'color'), Input('slack-channel-saved', 'data'), prevent_initial_call=True)
def ui_alert_on_slack_notifications_toggle(result):
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

@app.callback(Output('email-notifications-table', 'children'), Input('on-page-load', 'data'), Input('email-added', 'data'), Input('email-deleted', 'data'), Input('google-drive-sync-update', 'data'))
def get_emails_registered_for_notifications(on_page_load, email_added, email_deleted, sync_update):
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

@app.callback(Output('email-added', 'data'), Input('add-email-button', 'n_clicks'), State('email-notifications-text-field', 'value'), prevent_initial_call=True)
def register_email_for_notifications(button_click, user_email_address):
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

@app.callback(Output('email-deleted', 'data'), Input('delete-email-button', 'n_clicks'), State('email-notifications-text-field', 'value'), prevent_initial_call=True)
def delete_email_from_notifications(button_click, user_email_address):
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

@app.callback(Output('email-addition-alert', 'is_open'), Output('email-addition-alert', 'children'), Output('email-addition-alert', 'color'), Input('email-added', 'data'), prevent_initial_call=True)
def ui_feedback_for_registering_email(email_added_result):
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

@app.callback(Output('email-deletion-alert', 'is_open'), Output('email-deletion-alert', 'children'), Output('email-deletion-alert', 'color'), Input('email-deleted', 'data'), prevent_initial_call=True)
def ui_feedback_for_deleting_email(email_deleted_result):
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

@app.callback(Output('chromatography-methods-table', 'children'), Output('select-istd-chromatography-dropdown', 'options'), Output('select-bio-chromatography-dropdown', 'options'), Output('add-chromatography-text-field', 'value'), Output('chromatography-added', 'data'), Input('on-page-load', 'data'), Input('add-chromatography-button', 'n_clicks'), State('add-chromatography-text-field', 'value'), Input('istd-msp-added', 'data'), Input('chromatography-removed', 'data'), Input('chromatography-msdial-config-added', 'data'), Input('google-drive-sync-update', 'data'))
def add_chromatography_method( on_page_load, button_click, chromatography_method, msp_added, method_removed, config_added, sync_update):
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

@app.callback(Output('chromatography-removed', 'data'), Input('remove-chromatography-method-button', 'n_clicks'), State('select-istd-chromatography-dropdown', 'value'), prevent_initial_call=True)
def remove_chromatography_method(button_click, chromatography):
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

@app.callback(Output('chromatography-removal-alert', 'is_open'), Output('chromatography-removal-alert', 'children'), Input('chromatography-removed', 'data'))
def show_alert_on_chromatography_addition(chromatography_removed):
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

@app.callback(Output('msp-save-changes-button', 'children'), Input('select-istd-chromatography-dropdown', 'value'), Input('select-istd-polarity-dropdown', 'value'))
def add_msp_to_chromatography_button_feedback(chromatography, polarity):
3659@app.callback(Output("msp-save-changes-button", "children"),
3660              Input("select-istd-chromatography-dropdown", "value"),
3661              Input("select-istd-polarity-dropdown", "value"))
3662def add_msp_to_chromatography_button_feedback(chromatography, polarity):
3663
3664    """
3665    "Save changes" button UI feedback for Settings > Internal Standards
3666    """
3667
3668    if chromatography is not None and polarity is not None:
3669        return "Add MSP to " + chromatography + " " + polarity
3670    else:
3671        return "Add MSP"

"Save changes" button UI feedback for Settings > Internal Standards

@app.callback(Output('add-istd-msp-text-field', 'value'), Input('add-istd-msp-button', 'filename'), prevent_intitial_call=True)
def bio_standard_msp_text_field_feedback(filename):
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

@app.callback(Output('istd-msp-added', 'data'), Input('msp-save-changes-button', 'n_clicks'), State('add-istd-msp-button', 'contents'), State('add-istd-msp-button', 'filename'), State('select-istd-chromatography-dropdown', 'value'), State('select-istd-polarity-dropdown', 'value'), prevent_initial_call=True)
def capture_uploaded_istd_msp(button_click, contents, filename, chromatography, polarity):
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()

@app.callback(Output('chromatography-msp-success-alert', 'is_open'), Output('chromatography-msp-success-alert', 'children'), Output('chromatography-msp-error-alert', 'is_open'), Output('chromatography-msp-error-alert', 'children'), Input('istd-msp-added', 'data'), prevent_initial_call=True)
def ui_feedback_for_adding_msp_to_chromatography(msp_added):
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

@app.callback(Output('msdial-directory', 'value'), Input('file-explorer-select-button', 'n_clicks'), Input('settings-modal', 'is_open'), State('selected-msdial-folder', 'data'), Input('google-drive-sync-update', 'data'))
def get_msdial_directory( select_folder_button, settings_modal_is_open, selected_folder, sync_update):
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

@app.callback(Output('msdial-directory-saved', 'data'), Input('msdial-folder-save-button', 'n_clicks'), State('msdial-directory', 'value'), prevent_initial_call=True)
def update_msdial_directory(button_click, msdial_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

@app.callback(Output('msdial-directory-saved-alert', 'is_open'), Output('msdial-directory-saved-alert', 'children'), Output('msdial-directory-saved-alert', 'color'), Input('msdial-directory-saved', 'data'), prevent_initial_call=True)
def ui_alert_for_msdial_directory_save(msdial_folder_save_result):
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

@app.callback(Output('msdial-config-added', 'data'), Output('add-msdial-configuration-text-field', 'value'), Input('add-msdial-configuration-button', 'n_clicks'), State('add-msdial-configuration-text-field', 'value'), prevent_initial_call=True)
def add_msdial_configuration(button_click, msdial_config_id):
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

@app.callback(Output('msdial-config-removed', 'data'), Input('remove-config-button', 'n_clicks'), State('msdial-configs-dropdown', 'value'), prevent_initial_call=True)
def delete_msdial_configuration(button_click, msdial_config_id):
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

@app.callback(Output('msdial-configs-dropdown', 'options'), Output('msdial-configs-dropdown', 'value'), Input('on-page-load', 'data'), Input('msdial-config-added', 'data'), Input('msdial-config-removed', 'data'), Input('google-drive-sync-update', 'data'))
def get_msdial_configs_for_dropdown(on_page_load, on_config_added, on_config_removed, sync_update):
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

@app.callback(Output('msdial-config-addition-alert', 'is_open'), Output('msdial-config-addition-alert', 'children'), Output('msdial-config-addition-alert', 'color'), Input('msdial-config-added', 'data'), prevent_initial_call=True)
def show_alert_on_msdial_config_addition(config_added):
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

@app.callback(Output('msdial-config-removal-alert', 'is_open'), Output('msdial-config-removal-alert', 'children'), Output('msdial-config-removal-alert', 'color'), Input('msdial-config-removed', 'data'), State('msdial-configs-dropdown', 'value'), prevent_initial_call=True)
def show_alert_on_msdial_config_removal(config_removed, selected_config):
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

@app.callback(Output('retention-time-begin', 'value'), Output('retention-time-end', 'value'), Output('mass-range-begin', 'value'), Output('mass-range-end', 'value'), Output('ms1-centroid-tolerance', 'value'), Output('ms2-centroid-tolerance', 'value'), Output('select-smoothing-dropdown', 'value'), Output('smoothing-level', 'value'), Output('min-peak-width', 'value'), Output('min-peak-height', 'value'), Output('mass-slice-width', 'value'), Output('post-id-rt-tolerance', 'value'), Output('post-id-mz-tolerance', 'value'), Output('post-id-score-cutoff', 'value'), Output('alignment-rt-tolerance', 'value'), Output('alignment-mz-tolerance', 'value'), Output('alignment-rt-factor', 'value'), Output('alignment-mz-factor', 'value'), Output('peak-count-filter', 'value'), Output('qc-at-least-filter-dropdown', 'value'), Input('msdial-configs-dropdown', 'value'), Input('msdial-parameters-saved', 'data'), Input('msdial-parameters-reset', 'data'), prevent_initial_call=True)
def get_msdial_parameters_for_config(msdial_config_id, on_parameters_saved, on_parameters_reset):
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.

@app.callback(Output('msdial-parameters-saved', 'data'), Input('save-changes-msdial-parameters-button', 'n_clicks'), State('msdial-configs-dropdown', 'value'), State('retention-time-begin', 'value'), State('retention-time-end', 'value'), State('mass-range-begin', 'value'), State('mass-range-end', 'value'), State('ms1-centroid-tolerance', 'value'), State('ms2-centroid-tolerance', 'value'), State('select-smoothing-dropdown', 'value'), State('smoothing-level', 'value'), State('mass-slice-width', 'value'), State('min-peak-width', 'value'), State('min-peak-height', 'value'), State('post-id-rt-tolerance', 'value'), State('post-id-mz-tolerance', 'value'), State('post-id-score-cutoff', 'value'), State('alignment-rt-tolerance', 'value'), State('alignment-mz-tolerance', 'value'), State('alignment-rt-factor', 'value'), State('alignment-mz-factor', 'value'), State('peak-count-filter', 'value'), State('qc-at-least-filter-dropdown', 'value'), prevent_initial_call=True)
def write_msdial_parameters_to_database( button_clicks, config_name, rt_begin, rt_end, mz_begin, mz_end, ms1_centroid_tolerance, ms2_centroid_tolerance, smoothing_method, smoothing_level, mass_slice_width, min_peak_width, min_peak_height, post_id_rt_tolerance, post_id_mz_tolerance, post_id_score_cutoff, alignment_rt_tolerance, alignment_mz_tolerance, alignment_rt_factor, alignment_mz_factor, peak_count_filter, qc_at_least_filter):
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

@app.callback(Output('qc-parameters-reset', 'data'), Input('reset-default-qc-parameters-button', 'n_clicks'), State('qc-configs-dropdown', 'value'), prevent_initial_call=True)
def reset_msdial_parameters_to_default(button_clicks, qc_config_name):
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

@app.callback(Output('msdial-parameters-success-alert', 'is_open'), Output('msdial-parameters-success-alert', 'children'), Input('msdial-parameters-saved', 'data'), prevent_initial_call=True)
def show_alert_on_parameter_save(parameters_saved):
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

@app.callback(Output('msdial-parameters-reset-alert', 'is_open'), Output('msdial-parameters-reset-alert', 'children'), Input('msdial-parameters-reset', 'data'), prevent_initial_call=True)
def show_alert_on_parameter_reset(parameters_reset):
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

@app.callback(Output('qc-config-added', 'data'), Output('add-qc-configuration-text-field', 'value'), Input('add-qc-configuration-button', 'n_clicks'), State('add-qc-configuration-text-field', 'value'), prevent_initial_call=True)
def add_qc_configuration(button_click, qc_config_id):
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

@app.callback(Output('qc-config-removed', 'data'), Input('remove-qc-config-button', 'n_clicks'), State('qc-configs-dropdown', 'value'), prevent_initial_call=True)
def delete_qc_configuration(button_click, qc_config_id):
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

@app.callback(Output('qc-configs-dropdown', 'options'), Output('qc-configs-dropdown', 'value'), Input('on-page-load', 'data'), Input('qc-config-added', 'data'), Input('qc-config-removed', 'data'), Input('google-drive-sync-update', 'data'))
def get_qc_configs_for_dropdown(on_page_load, qc_config_added, qc_config_removed, sync_update):
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

@app.callback(Output('qc-config-addition-alert', 'is_open'), Output('qc-config-addition-alert', 'children'), Output('qc-config-addition-alert', 'color'), Input('qc-config-added', 'data'), prevent_initial_call=True)
def show_alert_on_qc_config_addition(config_added):
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

@app.callback(Output('qc-config-removal-alert', 'is_open'), Output('qc-config-removal-alert', 'children'), Output('qc-config-removal-alert', 'color'), Input('qc-config-removed', 'data'), State('qc-configs-dropdown', 'value'), prevent_initial_call=True)
def show_alert_on_qc_config_removal(config_removed, selected_config):
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

@app.callback(Output('intensity-dropouts-cutoff', 'value'), Output('library-rt-shift-cutoff', 'value'), Output('in-run-rt-shift-cutoff', 'value'), Output('library-mz-shift-cutoff', 'value'), Output('intensity-cutoff-enabled', 'value'), Output('library-rt-shift-cutoff-enabled', 'value'), Output('in-run-rt-shift-cutoff-enabled', 'value'), Output('library-mz-shift-cutoff-enabled', 'value'), Input('qc-configs-dropdown', 'value'), Input('qc-parameters-saved', 'data'), Input('qc-parameters-reset', 'data'), prevent_initial_call=True)
def get_qc_parameters_for_config(qc_config_name, on_parameters_saved, on_parameters_reset):
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.

@app.callback(Output('qc-parameters-saved', 'data'), Input('save-changes-qc-parameters-button', 'n_clicks'), State('qc-configs-dropdown', 'value'), State('intensity-dropouts-cutoff', 'value'), State('library-rt-shift-cutoff', 'value'), State('in-run-rt-shift-cutoff', 'value'), State('library-mz-shift-cutoff', 'value'), State('intensity-cutoff-enabled', 'value'), State('library-rt-shift-cutoff-enabled', 'value'), State('in-run-rt-shift-cutoff-enabled', 'value'), State('library-mz-shift-cutoff-enabled', 'value'), prevent_initial_call=True)
def write_qc_parameters_to_database( button_clicks, qc_config_name, intensity_dropouts_cutoff, library_rt_shift_cutoff, in_run_rt_shift_cutoff, library_mz_shift_cutoff, intensity_enabled, library_rt_enabled, in_run_rt_enabled, library_mz_enabled):
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

@app.callback(Output('qc-parameters-success-alert', 'is_open'), Output('qc-parameters-success-alert', 'children'), Input('qc-parameters-saved', 'data'), prevent_initial_call=True)
def show_alert_on_qc_parameter_save(parameters_saved):
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

@app.callback(Output('qc-parameters-reset-alert', 'is_open'), Output('qc-parameters-reset-alert', 'children'), Input('qc-parameters-reset', 'data'), prevent_initial_call=True)
def show_alert_on_qc_parameter_reset(parameters_reset):
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

@app.callback(Output('select-bio-standard-dropdown', 'options'), Output('biological-standards-table', 'children'), Input('on-page-load', 'data'), Input('bio-standard-added', 'data'), Input('bio-standard-removed', 'data'), Input('chromatography-added', 'data'), Input('chromatography-removed', 'data'), Input('bio-msp-added', 'data'), Input('bio-standard-msdial-config-added', 'data'), Input('google-drive-sync-update', 'data'))
def get_biological_standards( on_page_load, on_standard_added, on_standard_removed, on_method_added, on_method_removed, on_msp_added, on_bio_standard_msdial_config_added, sync_update):
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

@app.callback(Output('bio-standard-added', 'data'), Output('add-bio-standard-text-field', 'value'), Output('add-bio-standard-identifier-text-field', 'value'), Input('add-bio-standard-button', 'n_clicks'), State('add-bio-standard-text-field', 'value'), State('add-bio-standard-identifier-text-field', 'value'), prevent_initial_call=True)
def add_biological_standard(button_click, name, identifier):
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

@app.callback(Output('bio-standard-removed', 'data'), Input('remove-bio-standard-button', 'n_clicks'), State('select-bio-standard-dropdown', 'value'), prevent_initial_call=True)
def remove_biological_standard(button_click, biological_standard_name):
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

@app.callback(Output('add-bio-msp-text-field', 'value'), Input('add-bio-msp-button', 'filename'), prevent_intitial_call=True)
def bio_standard_msp_text_field_ui_callback(filename):
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

@app.callback(Output('bio-msp-added', 'data'), Input('bio-standard-save-changes-button', 'n_clicks'), State('add-bio-msp-button', 'contents'), State('add-bio-msp-button', 'filename'), State('select-bio-chromatography-dropdown', 'value'), State('select-bio-polarity-dropdown', 'value'), State('select-bio-standard-dropdown', 'value'), prevent_initial_call=True)
def capture_uploaded_bio_msp( button_click, contents, filename, chromatography, polarity, bio_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().

@app.callback(Output('bio-standard-addition-alert', 'is_open'), Output('bio-standard-addition-alert', 'children'), Output('bio-standard-addition-alert', 'color'), Input('bio-standard-added', 'data'), prevent_initial_call=True)
def show_alert_on_bio_standard_addition(bio_standard_added):
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

@app.callback(Output('bio-standard-removal-alert', 'is_open'), Output('bio-standard-removal-alert', 'children'), Input('bio-standard-removed', 'data'), prevent_initial_call=True)
def show_alert_on_bio_standard_removal(bio_standard_removed):
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

@app.callback(Output('bio-msp-success-alert', 'is_open'), Output('bio-msp-success-alert', 'children'), Output('bio-msp-error-alert', 'is_open'), Output('bio-msp-error-alert', 'children'), Input('bio-msp-added', 'data'), prevent_initial_call=True)
def ui_feedback_for_adding_msp_to_bio_standard(bio_standard_msp_added):
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

@app.callback(Output('bio-standard-save-changes-button', 'children'), Input('select-bio-chromatography-dropdown', 'value'), Input('select-bio-polarity-dropdown', 'value'), Input('select-bio-standard-dropdown', 'value'))
def add_msp_to_bio_standard_button_feedback(chromatography, polarity, bio_standard):
4409@app.callback(Output("bio-standard-save-changes-button", "children"),
4410              Input("select-bio-chromatography-dropdown", "value"),
4411              Input("select-bio-polarity-dropdown", "value"),
4412              Input("select-bio-standard-dropdown", "value"))
4413def add_msp_to_bio_standard_button_feedback(chromatography, polarity, bio_standard):
4414
4415    """
4416    "Save changes" button UI feedback for Settings > Biological Standards
4417    """
4418
4419    if bio_standard is not None and chromatography is not None and polarity is not None:
4420        return "Add MSP to " + bio_standard + " in " + chromatography + " " + polarity
4421    elif bio_standard is not None:
4422        return "Added MSP to " + bio_standard
4423    else:
4424        return "Add MSP"

"Save changes" button UI feedback for Settings > Biological Standards

@app.callback(Output('bio-standard-msdial-configs-dropdown', 'options'), Output('istd-msdial-configs-dropdown', 'options'), Input('msdial-config-added', 'data'), Input('msdial-config-removed', 'data'), Input('google-drive-sync-update', 'data'))
def populate_msdial_configs_for_biological_standard(msdial_config_added, msdial_config_removed, sync_update):
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

@app.callback(Output('bio-standard-msdial-config-added', 'data'), Input('bio-standard-msdial-configs-button', 'n_clicks'), State('select-bio-standard-dropdown', 'value'), State('select-bio-chromatography-dropdown', 'value'), State('bio-standard-msdial-configs-dropdown', 'value'), prevent_initial_call=True)
def add_msdial_config_for_bio_standard(button_click, biological_standard, chromatography, config_id):
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

@app.callback(Output('bio-config-success-alert', 'is_open'), Output('bio-config-success-alert', 'children'), Input('bio-standard-msdial-config-added', 'data'), State('select-bio-standard-dropdown', 'value'), State('select-bio-chromatography-dropdown', 'value'), prevent_initial_call=True)
def ui_feedback_for_setting_msdial_config_for_bio_standard(config_added, bio_standard, 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

@app.callback(Output('chromatography-msdial-config-added', 'data'), Input('istd-msdial-configs-button', 'n_clicks'), State('select-istd-chromatography-dropdown', 'value'), State('istd-msdial-configs-dropdown', 'value'), prevent_initial_call=True)
def add_msdial_config_for_chromatography(button_click, chromatography, config_id):
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

@app.callback(Output('istd-config-success-alert', 'is_open'), Output('istd-config-success-alert', 'children'), Input('chromatography-msdial-config-added', 'data'), State('select-istd-chromatography-dropdown', 'value'), prevent_initial_call=True)
def ui_feedback_for_setting_msdial_config_for_chromatography(config_added, chromatography):
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

@app.callback(Output('setup-new-run-modal', 'is_open'), Output('setup-new-run-button', 'n_clicks'), Output('setup-new-run-modal-title', 'children'), Input('setup-new-run-button', 'n_clicks'), Input('start-run-monitor-modal', 'is_open'), State('tabs', 'value'), Input('data-acquisition-folder-button', 'n_clicks'), Input('file-explorer-select-button', 'n_clicks'), State('settings-modal', 'is_open'), prevent_initial_call=True)
def toggle_new_run_modal( button_clicks, success, instrument_name, browse_folder_button, file_explorer_button, settings_modal_is_open):
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

@app.callback(Output('start-run-chromatography-dropdown', 'options'), Output('start-run-bio-standards-dropdown', 'options'), Output('start-run-qc-configs-dropdown', 'options'), Input('setup-new-run-button', 'n_clicks'), prevent_initial_call=True)
def populate_options_for_new_run(button_click):
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

@app.callback(Output('sequence-path', 'value'), Output('new-sequence', 'data'), Input('sequence-upload-button', 'contents'), State('sequence-upload-button', 'filename'), prevent_initial_call=True)
def capture_uploaded_sequence(contents, filename):
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

@app.callback(Output('metadata-path', 'value'), Output('new-metadata', 'data'), Input('metadata-upload-button', 'contents'), State('metadata-upload-button', 'filename'), prevent_initial_call=True)
def capture_uploaded_metadata(contents, filename):
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

@app.callback(Output('monitor-new-run-button', 'children'), Output('data-acquisition-path-title', 'children'), Output('data-acquisition-path-form-text', 'children'), Input('ms_autoqc-job-type', 'value'))
def update_new_job_button_text(job_type):
4630@app.callback(Output("monitor-new-run-button", "children"),
4631              Output("data-acquisition-path-title", "children"),
4632              Output("data-acquisition-path-form-text", "children"),
4633              Input("ms_autoqc-job-type", "value"))
4634def update_new_job_button_text(job_type):
4635
4636    """
4637    Updates New MS-AutoQC Job form submit button based on job type
4638    """
4639
4640    if job_type == "active":
4641        button_text = "Start monitoring instrument run"
4642        text_field_title = "Data acquisition path"
4643        form_text = "Please enter the folder path to which incoming raw data files will be saved."
4644    elif job_type == "completed":
4645        button_text = "Start QC processing data files"
4646        text_field_title = "Data file path"
4647        form_text = "Please enter the folder path where your data files are saved."
4648
4649    msconvert_valid = db.pipeline_valid(module="msconvert")
4650    msdial_valid = db.pipeline_valid(module="msdial")
4651
4652    if not msconvert_valid and not msdial_valid:
4653        button_text = "Error: MSConvert and MS-DIAL installations not found"
4654    if not msdial_valid:
4655        button_text = "Error: Could not locate MS-DIAL console app"
4656    if not msconvert_valid:
4657        button_text = "Error: Could not locate MSConvert installation"
4658
4659    return button_text, text_field_title, form_text

Updates New MS-AutoQC Job form submit button based on job type

@app.callback(Output('instrument-run-id', 'valid'), Output('instrument-run-id', 'invalid'), Output('start-run-chromatography-dropdown', 'valid'), Output('start-run-chromatography-dropdown', 'invalid'), Output('start-run-qc-configs-dropdown', 'valid'), Output('start-run-qc-configs-dropdown', 'invalid'), Output('sequence-path', 'valid'), Output('sequence-path', 'invalid'), Output('metadata-path', 'valid'), Output('metadata-path', 'invalid'), Output('data-acquisition-folder-path', 'valid'), Output('data-acquisition-folder-path', 'invalid'), Input('instrument-run-id', 'value'), Input('start-run-chromatography-dropdown', 'value'), Input('start-run-bio-standards-dropdown', 'value'), Input('start-run-qc-configs-dropdown', 'value'), Input('sequence-upload-button', 'contents'), State('sequence-upload-button', 'filename'), Input('metadata-upload-button', 'contents'), State('metadata-upload-button', 'filename'), Input('data-acquisition-folder-path', 'value'), State('instrument-run-id', 'valid'), State('instrument-run-id', 'invalid'), State('start-run-chromatography-dropdown', 'valid'), State('start-run-chromatography-dropdown', 'invalid'), State('start-run-qc-configs-dropdown', 'valid'), State('start-run-qc-configs-dropdown', 'invalid'), State('sequence-path', 'valid'), State('sequence-path', 'invalid'), State('metadata-path', 'valid'), State('metadata-path', 'invalid'), State('data-acquisition-folder-path', 'valid'), State('data-acquisition-folder-path', 'invalid'), State('tabs', 'value'), prevent_initial_call=True)
def validation_feedback_for_new_run_setup_form( run_id, chromatography, bio_standards, qc_config, sequence_contents, sequence_filename, metadata_contents, metadata_filename, data_acquisition_path, run_id_valid, run_id_invalid, chromatography_valid, chromatography_invalid, qc_config_valid, qc_config_invalid, sequence_valid, sequence_invalid, metadata_valid, metadata_invalid, path_valid, path_invalid, instrument):
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

@app.callback(Output('monitor-new-run-button', 'disabled'), Input('instrument-run-id', 'valid'), Input('start-run-chromatography-dropdown', 'valid'), Input('start-run-qc-configs-dropdown', 'valid'), Input('sequence-path', 'valid'), Input('data-acquisition-folder-path', 'valid'), prevent_initial_call=True)
def enable_new_autoqc_job_button( run_id_valid, chromatography_valid, qc_config_valid, sequence_valid, path_valid):
4775@app.callback(Output("monitor-new-run-button", "disabled"),
4776              Input("instrument-run-id", "valid"),
4777              Input("start-run-chromatography-dropdown", "valid"),
4778              Input("start-run-qc-configs-dropdown", "valid"),
4779              Input("sequence-path", "valid"),
4780              Input("data-acquisition-folder-path", "valid"), prevent_initial_call=True)
4781def enable_new_autoqc_job_button(run_id_valid, chromatography_valid, qc_config_valid, sequence_valid, path_valid):
4782
4783    """
4784    Enables "submit" button for New MS-AutoQC Job form
4785    """
4786
4787    if run_id_valid and chromatography_valid and qc_config_valid and sequence_valid and path_valid and db.pipeline_valid():
4788        return False
4789    else:
4790        return True

Enables "submit" button for New MS-AutoQC Job form

@app.callback(Output('start-run-monitor-modal', 'is_open'), Output('new-job-error-modal', 'is_open'), Input('monitor-new-run-button', 'n_clicks'), State('instrument-run-id', 'value'), State('tabs', 'value'), State('start-run-chromatography-dropdown', 'value'), State('start-run-bio-standards-dropdown', 'value'), State('new-sequence', 'data'), State('new-metadata', 'data'), State('data-acquisition-folder-path', 'value'), State('start-run-qc-configs-dropdown', 'value'), State('ms_autoqc-job-type', 'value'), prevent_initial_call=True)
def new_autoqc_job_setup( button_clicks, run_id, instrument_id, chromatography, bio_standards, sequence, metadata, acquisition_path, qc_config_id, job_type):
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:

  1. Writing a new instrument run to the database
  2. 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
@app.callback(Output('file-explorer-modal', 'is_open'), Input('data-acquisition-folder-button', 'n_clicks'), Input('file-explorer-select-button', 'n_clicks'), State('setup-new-run-modal', 'is_open'), Input('msdial-folder-button', 'n_clicks'), prevent_initial_call=True)
def open_file_explorer( new_job_browse_folder_button, select_folder_button, new_run_modal_is_open, msdial_select_folder_button):
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

@app.callback(Output('file-explorer-modal-body', 'children'), Input('file-explorer-modal', 'is_open'), Input('selected-data-folder', 'data'), Input('selected-msdial-folder', 'data'), State('settings-modal', 'is_open'), prevent_initial_call=True)
def list_directories_in_file_explorer( file_explorer_is_open, selected_data_folder, selected_msdial_folder, settings_is_open):
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

@app.callback(Output('selected-data-folder', 'data'), Output('selected-msdial-folder', 'data'), Input('dir-1', 'n_clicks'), Input('dir-2', 'n_clicks'), Input('dir-3', 'n_clicks'), Input('dir-4', 'n_clicks'), Input('dir-5', 'n_clicks'), Input('dir-6', 'n_clicks'), Input('dir-7', 'n_clicks'), Input('dir-8', 'n_clicks'), Input('dir-9', 'n_clicks'), Input('dir-10', 'n_clicks'), Input('dir-11', 'n_clicks'), Input('dir-12', 'n_clicks'), Input('dir-13', 'n_clicks'), Input('dir-14', 'n_clicks'), Input('dir-15', 'n_clicks'), Input('dir-16', 'n_clicks'), Input('dir-17', 'n_clicks'), Input('dir-18', 'n_clicks'), Input('dir-19', 'n_clicks'), Input('dir-20', 'n_clicks'), Input('dir-21', 'n_clicks'), Input('dir-22', 'n_clicks'), Input('dir-23', 'n_clicks'), Input('dir-24', 'n_clicks'), Input('dir-25', 'n_clicks'), Input('dir-26', 'n_clicks'), Input('dir-27', 'n_clicks'), Input('dir-28', 'n_clicks'), Input('dir-29', 'n_clicks'), Input('dir-30', 'n_clicks'), State('dir-1', 'children'), State('dir-2', 'children'), State('dir-3', 'children'), State('dir-4', 'children'), State('dir-5', 'children'), State('dir-6', 'children'), State('dir-7', 'children'), State('dir-8', 'children'), State('dir-9', 'children'), State('dir-10', 'children'), State('dir-11', 'children'), State('dir-12', 'children'), State('dir-13', 'children'), State('dir-14', 'children'), State('dir-15', 'children'), State('dir-16', 'children'), State('dir-17', 'children'), State('dir-18', 'children'), State('dir-19', 'children'), State('dir-20', 'children'), State('dir-21', 'children'), State('dir-22', 'children'), State('dir-23', 'children'), State('dir-24', 'children'), State('dir-25', 'children'), State('dir-26', 'children'), State('dir-27', 'children'), State('dir-28', 'children'), State('dir-29', 'children'), State('dir-30', 'children'), Input('selected-data-folder', 'data'), Input('selected-msdial-folder', 'data'), Input('file-explorer-back-button', 'n_clicks'), State('settings-modal', 'is_open'), prevent_initial_call=True)
def 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, 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, 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, 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, dir_25, dir_26, dir_27, dir_28, dir_29, dir_30, selected_data_folder, selected_msdial_folder, back_button, settings_is_open):
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)

@app.callback(Output('file-explorer-modal-title', 'children'), Input('selected-data-folder', 'data'), Input('selected-msdial-folder', 'data'), State('settings-modal', 'is_open'), prevent_initial_call=True)
def update_file_explorer_title(selected_data_folder, selected_msdial_folder, settings_is_open):
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

@app.callback(Output('data-acquisition-folder-path', 'value'), Input('file-explorer-select-button', 'n_clicks'), State('selected-data-folder', 'data'), State('settings-modal', 'is_open'), prevent_initial_call=True)
def update_folder_path_text_field(select_folder_button, selected_folder, settings_is_open):
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

@app.callback(Output('active-run-progress-card', 'style'), Output('active-run-progress-header', 'children'), Output('active-run-progress-bar', 'value'), Output('active-run-progress-bar', 'label'), Output('refresh-interval', 'disabled'), Output('job-controller-panel', 'style'), Input('instrument-run-table', 'active_cell'), State('instrument-run-table', 'data'), Input('refresh-interval', 'n_intervals'), Input('tabs', 'value'), Input('start-run-monitor-modal', 'is_open'), prevent_initial_call=True)
def update_progress_bar_during_active_instrument_run(active_cell, table_data, refresh, instrument_id, new_job_started):
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

@app.callback(Output('setup-new-run-button', 'style'), Input('tabs', 'value'), prevent_initial_call=True)
def hide_elements_for_non_instrument_devices(instrument_id):
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

@app.callback(Output('job-controller-modal', 'is_open'), Output('job-controller-modal-title', 'children'), Output('job-controller-modal-body', 'children'), Output('job-controller-confirm-button', 'children'), Output('job-controller-confirm-button', 'color'), Input('mark-as-completed-button', 'n_clicks'), Input('job-marked-completed', 'data'), Input('restart-job-button', 'n_clicks'), Input('job-restarted', 'data'), Input('delete-job-button', 'n_clicks'), Input('job-deleted', 'data'), State('study-resources', 'data'), prevent_initial_call=True)
def confirm_action_on_job( mark_job_as_completed, job_completed, restart_job, job_restarted, delete_job, job_deleted, resources):
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

@app.callback(Output('job-marked-completed', 'data'), Output('job-restarted', 'data'), Output('job-deleted', 'data'), Output('job-action-failed', 'data'), Input('job-controller-confirm-button', 'n_clicks'), State('job-controller-modal-title', 'children'), State('study-resources', 'data'), prevent_initial_call=True)
def perform_action_on_job(confirm_button, modal_title, resources):
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