Explore Jobs Interactively#
by Silvia Mazzoni, DesignSafe, 2025
Here’s a simple interactive Jupyter widget so you can select a job UUID from a dropdown, then pull its metadata, status, history, and outputs.
This is super handy in a notebook so you don’t have to modify code each time.
Using local utilities library
Connect to Tapis#
t=OpsUtils.connect_tapis()
-- Checking Tapis token --
Token loaded from file. Token is still valid!
Token expires at: 2025-08-26T20:38:09+00:00
Token expires in: 0:05:45.217479
-- LOG IN SUCCESSFUL! --
Get all your Jobs Metadata in a dataframe#
JobsData_df = OpsUtils.get_tapis_jobs_df(t, displayIt=False)
interactive_tapis_jobs_explorer#
Use a utility function that builds an interactive dashboard to explore Tapis jobs inside a Jupyter notebook.
interactive_tapis_job_explorer.py
# ../OpsUtils/OpsUtils/Tapis/interactive_tapis_job_explorer.py
def interactive_tapis_job_explorer(t,JobsData_df):
"""
Launches an interactive visual explorer for Tapis jobs in a Jupyter Notebook environment.
This tool presents a rich, widget-based interface that allows users to:
- Filter Tapis jobs by status, system, date range, and app ID.
- Sort and browse available jobs, with optional full display.
- Select a job and view its:
• Metadata
• Execution history
• File outputs (with structure)
- Download all output files or select and download/view individual outputs.
The explorer dynamically updates available jobs and outputs based on filters,
and provides a smooth, visual workflow for reviewing and managing simulation results.
Parameters
----------
t : Tapis
An authenticated Tapis client instance (from `connect_tapis()`).
JobsData_df : pandas.DataFrame
A DataFrame containing job information with at least the following columns:
- 'uuid' : Tapis job UUID
- 'name' : job name
- 'created' : job creation timestamp (UTC ISO format)
- 'status' : job status string
- 'execSystemId' : system the job ran on
- 'appId' : ID of the application used
Optional columns like 'index_column' can improve labeling of jobs.
Notes
-----
- This tool is intended to be run in a Jupyter Notebook environment.
- Uses `ipywidgets` for UI and assumes IPython display context.
- Internally calls OpsUtils functions:
• `connect_tapis()`
• `get_tapis_job_metadata()`
• `get_tapis_job_history()`
• `process_tacc_job_history()`
• `get_tapis_job_all_files()`
Widgets and Features
--------------------
• Search Controls:
- Filter jobs by creation date, status, execution system, and app ID
- Sort jobs by any column; reverse sorting; show all rows
• Selection & Metadata:
- Dropdown menu to select a job
- Displays job metadata, history, and job step durations
• File Management:
- Shows all output files (with optional download)
- Individual file viewer for small text-based outputs
- Bulk download option with overwrite toggle
Returns
-------
None
The function does not return data, but displays a widget-based interface in the notebook.
Example
-------
>>> from TapisExplorer import interactive_tapis_job_explorer
>>> t = OpsUtils.connect_tapis()
>>> df = OpsUtils.get_jobs_dataframe(t)
>>> interactive_tapis_job_explorer(t, df)
"""
# Silvia Mazzoni, 2025
import pandas as pd
import ipywidgets as widgets
# from datetime import datetime
from IPython.display import display, clear_output
import os
from OpsUtils import OpsUtils
if JobsData_df.empty:
print("⚠️ No jobs found.")
return
if not 'created_dt' in JobsData_df.keys():
JobsData_df['created_dt'] = pd.to_datetime(JobsData_df['created'], utc=True)
connect_out = widgets.Output()
display(connect_out)
with connect_out:
t=OpsUtils.connect_tapis()
with connect_out:
clear_output()
JobsData_df_keys = list(JobsData_df.keys())
# filtered = JobsData_df.copy()
# -------------------------------
# Format widgets
# -------------------------------
borderedLayout=widgets.Layout(
border='2px solid gray',
padding='10px',
margin='5px'
)
borderedLayoutRed=widgets.Layout(
border='2px solid red',
padding='10px',
margin='5px'
)
# -------------------------------
# Build filter widgets
# -------------------------------
status_dropdown = widgets.Dropdown(
options=['(any)'] + sorted(JobsData_df['status'].dropna().unique()),
value='(any)',
description='Status:',
layout=widgets.Layout(width='50%'),
style={'description_width': 'initial'},
custom_id = 'status_dropdown'
)
execSystemId_dropdown = widgets.Dropdown(
options=['(any)'] + sorted(JobsData_df['execSystemId'].dropna().unique()),
value='(any)',
description='Execution System:',
layout=widgets.Layout(width='50%'),
style={'description_width': 'initial'},
custom_id = 'execSystemId_dropdown'
)
min_date = JobsData_df['created_dt'].min().date()
max_date = JobsData_df['created_dt'].max().date()
start_date_picker = widgets.DatePicker(
description=f'Start Date ({min_date}-{max_date})', value=min_date,
layout=widgets.Layout(width='50%'),
style={'description_width': 'initial'},
custom_id = 'start_date_picker'
)
end_date_picker = widgets.DatePicker(
description=f'End Date ({min_date}-{max_date})', value=max_date,
layout=widgets.Layout(width='50%'),
style={'description_width': 'initial'},
custom_id = 'end_date_picker'
)
app_dropdown = widgets.Dropdown(
options=['(any)'] + sorted(JobsData_df['appId'].dropna().unique()),
value='(any)',
description='App ID:',
layout=widgets.Layout(width='60%'),
style={'description_width': 'initial'},
custom_id = 'app_dropdown'
)
uuid_dropdown = widgets.Dropdown(
options=[],
description='Select Job:',
layout=widgets.Layout(width='80%'),
style={'description_width': 'initial'},
custom_id = 'uuid_dropdown'
)
outputs_dropdown = widgets.Dropdown(
options=[],
description='Select Output:',
layout=widgets.Layout(width='80%'),
style={'description_width': 'initial'},
custom_id = 'outputs_dropdown'
)
sorts_dropdown = widgets.Dropdown(
options=JobsData_df_keys,
value = 'created_dt',
description='Sort Jobs By:',
layout=widgets.Layout(width='80%'),
style={'description_width': 'initial'},
custom_id = 'sorts_dropdown'
)
# Checkbox to reverse order
reverse_checkbox = widgets.Checkbox(
value=False,
description='Descending',
custom_id = 'reverse_checkbox'
)
show_all_checkbox = widgets.Checkbox(
value=False,
description='Show all rows',
custom_id = 'show_all_checkbox'
)
download_all_overwrite_checkbox = widgets.Checkbox(
value=False,
description='Overwrite',
custom_id = 'download_all_overwrite_checkbox'
)
download_select_overwrite_checkbox = widgets.Checkbox(
value=False,
description='Overwrite',
custom_id = 'download_select_overwrite_checkbox'
)
run_button = widgets.Button(description="Explore Selected Job", button_style='success')
download_all_button = widgets.Button(description="Download All", button_style='info')
viewfile_button = widgets.Button(description="View Selected", button_style='info')
download_select_button = widgets.Button(description="Download Selected", button_style='info')
# -------------------------------
# Output boxes
# -------------------------------
## search
count_box = widgets.Output(layout=widgets.Layout(
border='0px solid gray',
padding='10px',
margin='5px'
))
## jobs
dataframe_box = widgets.Output()
jobsWidget = widgets.VBox([
widgets.HBox([sorts_dropdown,reverse_checkbox,show_all_checkbox],layout=widgets.Layout(width='75%')),
dataframe_box
])
jobs_accordion = widgets.Accordion(children=[jobsWidget])
jobs_accordion.set_title(0, 'JOBS')
main_search = widgets.VBox([
widgets.Label(value='SEARCH PARAMETERS:'),
widgets.HBox([start_date_picker, end_date_picker]),
widgets.HBox([status_dropdown, execSystemId_dropdown, app_dropdown]),
count_box,
jobs_accordion
],layout=borderedLayout)
## select job
run_button_status = widgets.Output()
main_select = widgets.VBox([
widgets.Label(value='SELECT JOB:'),
uuid_dropdown,
widgets.Label(value='Options are update automatically as you change the search parameters.'),
run_button,
run_button_status
],layout=borderedLayout)
## selected-job metadata
metadata_box_base = widgets.Output()
main_metadata = widgets.VBox([
# widgets.Label(value='SELECTED-JOB METADATA:'),
metadata_box_base
])
## download all
download_all_base = widgets.Output()
main_download_all = widgets.VBox([
# widgets.Label(value='DOWNLOAD ALL:'),
download_all_base
])
## visualize & download individual files
download_select_base = widgets.Output()
main_download_select = widgets.VBox([
# widgets.Label(value='VISUALIZE & DOWNLOAD INDIVIDUAL FILES:'),
download_select_base
])
## combined all:
main_box_in = widgets.VBox([
widgets.HTML(value='JOB-SEARCH INPUT
'),
main_search,
main_select,
],layout=borderedLayoutRed)
main_box_out = widgets.VBox([
widgets.HTML(value='SELECTED-JOB DATA
'),
main_metadata,
main_download_all,
main_download_select,
],layout=borderedLayoutRed)
main_box_out_base = widgets.Output()
main_box = widgets.VBox([
widgets.HTML(value='-- EXPLORE TAPIS JOBS --
'),
main_box_in,
main_box_out_base
])
files_box = widgets.Output()
metadata_selected_job = widgets.Output()
#
outputs_box_metadata = widgets.Output()
outputs_box_history = widgets.Output()
outputs_box_historyPro = widgets.Output()
outputs_box_contents = widgets.Output()
# separate accordions so you can keep all open.
metadata_accordion_metadata = widgets.Accordion(children=[outputs_box_metadata])
metadata_accordion_metadata.set_title(0, 'Metadata')
metadata_accordion_history = widgets.Accordion(children=[outputs_box_history])
metadata_accordion_history.set_title(0, 'History')
# metadata_accordion_Details = widgets.Accordion(children=[outputs_box_historyPro])
# metadata_accordion_Details.set_title(0, 'Details')
metadata_accordion_contents = widgets.Accordion(children=[outputs_box_contents])
metadata_accordion_contents.set_title(0, 'Contents')
# metadata_box = widgets.VBox([
# widgets.Label(value='SELECTED-JOB METADATA:'),
# metadata_selected_job,
# metadata_accordion_metadata,
# metadata_accordion_history,
# metadata_accordion_contents
# ],layout=borderedLayout)
metadata_box = widgets.VBox([
widgets.Label(value='SELECTED-JOB METADATA:'),
metadata_selected_job,
outputs_box_metadata,
outputs_box_history,
outputs_box_contents
],layout=borderedLayout)
outputs_box_files_ctrl = widgets.Output()
outputs_box_files = widgets.Output()
files_accordion = widgets.Accordion(children=[outputs_box_files])
files_accordion.set_title(0, 'Files')
download_all_out_base = widgets.Output()
download_all_box = widgets.VBox([
widgets.Label(value='DOWNLOAD ALL:'),
widgets.HBox([download_all_button, download_all_overwrite_checkbox]),
download_all_out_base
],layout=borderedLayout)
download_select_out_base = widgets.Output()
download_select_box = widgets.VBox([
widgets.Label(value='VISUALIZE AND/OR DOWNLOAD SELECTED FILE:'),
outputs_dropdown,
widgets.HBox([download_select_button, download_select_overwrite_checkbox]),
viewfile_button,
download_select_out_base
],layout=borderedLayout)
outputs_box = widgets.Output()
with outputs_box:
display(outputs_box_files_ctrl)
display(files_accordion)
# -------------------------------
# Download logic
# -------------------------------
def on_viewfile_clicked(b):
selected_path = outputs_dropdown.value
view_select_out = widgets.Output()
view_select_out_acc = widgets.Accordion(children=[view_select_out])
view_select_out_acc.set_title(0, f" View: {selected_path}")
view_select_out_acc.selected_index = 0
with download_select_out_base:
# clear_output()
display(view_select_out_acc)
with view_select_out:
clear_output()
if selected_path:
local_file = selected_path.split('/')[-1]
# print('selected_path',selected_path)
jobUuid = uuid_dropdown.value
data = t.jobs.getJobOutputDownload(jobUuid=jobUuid, outputPath=selected_path)
print(f" Viewing: {selected_path}")
textarea = widgets.Textarea(
value=data,
placeholder='',
description='',
disabled=False,
layout=widgets.Layout(width='100%', height='500px')
)
display(textarea)
else:
print(" No output file selected to download.")
viewfile_button.on_click(on_viewfile_clicked)
# -------------------------------
# Download logic
# -------------------------------
def on_download_select_clicked(b):
selected_path = outputs_dropdown.value
overwrite = download_select_overwrite_checkbox.value
download_select_out = widgets.Output()
download_select_out_acc = widgets.Accordion(children=[download_select_out])
download_select_out_acc.set_title(0, f'Download: {selected_path}')
download_select_out_acc.selected_index = 0
with download_select_out_base:
# clear_output()
display(download_select_out_acc)
with download_select_out:
clear_output()
if selected_path:
local_file = selected_path.split('/')[-1]
homePath = os.path.expanduser('~')
local_file = os.path.join(homePath, local_file)
# print('local_file:',local_file)
if os.path.exists(local_file) and not overwrite:
print(f" [SKIP] {local_file} (already exists)")
return
else:
jobUuid = uuid_dropdown.value
data = t.jobs.getJobOutputDownload(jobUuid=jobUuid, outputPath=selected_path)
with open(local_file, "wb") as f:
f.write(data)
print(f" Downloaded: {selected_path} to {local_file}")
else:
print(" No output file selected to download.")
download_select_button.on_click(on_download_select_clicked)
# -------------------------------
# Download All logic
# -------------------------------
def on_download_all_clicked(b):
overwrite = download_all_overwrite_checkbox.value
download_all_out = widgets.Output()
download_all_out_acc = widgets.Accordion(children=[download_all_out])
download_all_out_acc.set_title(0, 'DOWNLOAD INFO')
download_all_out_acc.selected_index = 0
with download_all_out_base:
clear_output()
display(download_all_out_acc)
with download_all_out:
clear_output()
jobUuid = uuid_dropdown.value
returnedData = OpsUtils.get_tapis_job_all_files(t, jobUuid, displayIt=10, target_dir=f"outputs_{jobUuid}", overwrite=overwrite)
print(f"File Download DONE!")
download_all_button.on_click(on_download_all_clicked)
# -------------------------------
# Explore selected job
# -------------------------------
def explore_job(jobUuid):
with run_button_status:
clear_output()
print('..... processing ....')
with main_box_out_base:
display(main_box_out)
with outputs_box_files:
clear_output()
with metadata_selected_job:
clear_output()
print(f'*** JOB: {jobUuid} ***')
with outputs_box_metadata:
clear_output()
OpsUtils.get_tapis_job_metadata(t, jobUuid)
# get_tapis_job_history(t, jobUuid,print_all=False)
# get_tapis_job_metadata(t, jobUuid)
with outputs_box_history:
clear_output()
OpsUtils.get_tapis_job_history_data(t, jobUuid)
# with outputs_box_historyPro:
# clear_output()
# OpsUtils.get_tapis_job_history_durations(t, jobUuid,getMetadata=False)
# # process_tapis_job_history(t, jobUuid,getMetadata=False)
with outputs_box_contents:
clear_output()
AllFilesDict = OpsUtils.get_tapis_job_all_files(t, jobUuid, displayIt=10, target_dir=False, overwrite=False)
outputs_dropdown.options = []
output_files = []
for thisLocalPath in AllFilesDict['LocalPath']:
output_files.append((thisLocalPath, thisLocalPath))
if len(output_files)>0:
outputs_dropdown.options = output_files
outputs_dropdown.value = output_files[0][1]
with download_all_base:
clear_output()
display(download_all_box)
with download_select_base:
clear_output()
display(download_select_box)
with metadata_box_base:
clear_output()
display(metadata_box)
with run_button_status:
clear_output()
# -------------------------------
# Update jobs on filter change
# -------------------------------
def update_jobs(change):
status_selected = status_dropdown.value
execSystemId_selected = execSystemId_dropdown.value
start = pd.to_datetime(start_date_picker.value).tz_localize('UTC')
end = pd.to_datetime(end_date_picker.value).tz_localize('UTC')
app_selected = app_dropdown.value
filtered = JobsData_df[
(JobsData_df['appId'] != 'opensees-interactive') &
(JobsData_df['created_dt'] >= start) &
(JobsData_df['created_dt'] <= end)
]
if status_selected != '(any)':
filtered = filtered[filtered['status'] == status_selected]
if execSystemId_selected != '(any)':
filtered = filtered[filtered['execSystemId'] == execSystemId_selected]
if app_selected != '(any)':
filtered = filtered[filtered['appId'] == app_selected]
with count_box:
clear_output()
print(f" {len(filtered)} jobs found matching filters.")
if filtered.empty:
uuid_dropdown.options = []
with outputs_box_history:
clear_output()
print(f" No jobs found matching these filters.")
else:
# Sort so latest job is first
sort_selected = sorts_dropdown.value
show_all_selected = show_all_checkbox.value
filtered = filtered.sort_values(by=sort_selected, ascending=not reverse_checkbox.value)
with dataframe_box:
clear_output()
if show_all_selected:
display(filtered.style.hide(axis="index"))
else:
display(filtered.copy().reset_index(drop=True))
job_options = [(f"{row['index_column']} | {row['name']} | {str(row['created_dt']).split('.')[0]} | {row['status']} | {row['appId']} | {row['uuid'][:8]}...", row['uuid'])
for _, row in filtered.iterrows()] # | {row['execSystemId']}
uuid_dropdown.options = job_options
uuid_dropdown.value = job_options[0][1] # auto-select most recent
# -------------------------------
# Update jobs on filter change
# -------------------------------
def update_sorts(change):
update_jobs(change)
# Connect triggers
status_dropdown.observe(update_jobs, names='value')
execSystemId_dropdown.observe(update_jobs, names='value')
start_date_picker.observe(update_jobs, names='value')
end_date_picker.observe(update_jobs, names='value')
app_dropdown.observe(update_jobs, names='value')
sorts_dropdown.observe(update_sorts, names='value')
reverse_checkbox.observe(update_sorts, names='value')
show_all_checkbox.observe(update_sorts, names='value')
run_button.on_click(lambda b: explore_job(uuid_dropdown.value))
update_jobs(None) # initial load
display(main_box)
OpsUtils.interactive_tapis_job_explorer(t,JobsData_df)