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_upda