Skip to content

Commit 28ab2ab

Browse files
committed
Refactor Device_List page to multi-fragment for UI Boost
1 parent 087b14d commit 28ab2ab

File tree

2 files changed

+134
-98
lines changed

2 files changed

+134
-98
lines changed

src/libinspector/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ def get_human_readable_time(timestamp=None):
352352
"""
353353
if timestamp is None:
354354
timestamp = time.time()
355-
return datetime.datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S')
355+
return datetime.datetime.fromtimestamp(timestamp).strftime("%b %d,%Y %I:%M:%S%p")
356356

357357

358358
def initialize_config_dict():

src/libinspector/device_list_page.py

Lines changed: 133 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import functools
88
import os
99
import requests
10-
import pandas as pd
1110

1211

1312
logger = logging.getLogger("client")
@@ -28,7 +27,7 @@ def api_worker_thread():
2827
logger.info("[Device ID API] Starting worker thread to periodically call the API for each device.")
2928
while True:
3029
time.sleep(15)
31-
device_list = get_devices()
30+
device_list = get_all_devices()
3231
logger.info("[Device ID API] 15 seconds passed, will start calling API for each device if needed.")
3332

3433
# Getting inputs and calling API
@@ -50,8 +49,10 @@ def api_worker_thread():
5049
finally:
5150
# If API is down, just try using OUI vendor if no custom name is set in config.json
5251
if common.config_get(custom_name_key, default='') == '':
53-
common.config_set(custom_name_key,
54-
meta_data.get('oui_vendor', 'Unknown Device, likely a Mobile Phone'))
52+
vendor = (meta_data.get('oui_vendor') or '').strip()
53+
if not vendor:
54+
vendor = 'Unknown Device, likely a Mobile Phone'
55+
common.config_set(custom_name_key, vendor)
5556

5657

5758
@functools.cache
@@ -122,7 +123,7 @@ def call_predict_api(meta_data_string: str, remote_hostnames: str,
122123
return result
123124

124125

125-
def get_devices() -> list[dict]:
126+
def get_all_devices() -> list[dict]:
126127
"""
127128
Get the list of devices from the database.
128129
@@ -143,23 +144,48 @@ def get_devices() -> list[dict]:
143144
return device_list
144145

145146

146-
@st.fragment(run_every=1)
147+
def get_device_data(mac_address: str) -> dict:
148+
"""
149+
Get the device details for a specific device by MAC address.
150+
151+
Returns:
152+
mac_address (str): The MAC address of the device.
153+
"""
154+
sql = """
155+
SELECT * FROM devices
156+
WHERE mac_address = ?
157+
"""
158+
db_conn, rwlock = libinspector.global_state.db_conn_and_lock
159+
with rwlock:
160+
row = db_conn.execute(sql, (mac_address,)).fetchone()
161+
if row:
162+
return dict(row)
163+
else:
164+
return dict()
165+
166+
147167
def show_list(toast_obj: st.toast):
148168
"""
149169
The main page that creates a "card" for each device that was found via IoT Inspector
150170
"""
151171
human_readable_time = common.get_human_readable_time()
152-
st.markdown(f'Updated: {human_readable_time}')
172+
st.markdown(f'Last Page Refresh: {human_readable_time}')
153173

154-
device_list = get_devices()
174+
device_list = get_all_devices()
155175
if not device_list:
156176
st.warning('We are still scanning the network for devices. Please wait a moment. This page will refresh automatically.')
177+
time.sleep(5)
178+
st.rerun()
157179

158-
device_upload, device_download = common.bar_graph_data_frame(int(time.time()))
159180
# Create a card entry for the discovered device
160181
for device_dict in device_list:
161182
st.markdown('---')
162-
show_device_card(device_dict, device_upload, device_download)
183+
c1, c2 = st.columns([6, 4], gap='small')
184+
with c1:
185+
show_device_bar_graph(device_dict)
186+
# Set whether a device is to be inspected, favorite, or blocked
187+
with c2:
188+
toggle_device_fragment(device_dict["mac_address"])
163189

164190
# Create a pop-up showing if any new devices were found
165191
prev_device_count = st.session_state.get('prev_device_count', 0)
@@ -169,21 +195,20 @@ def show_list(toast_obj: st.toast):
169195
time.sleep(1.5) # Give the user a moment to read the toast
170196

171197

172-
def show_device_card(device_dict: dict, device_upload: pd.DataFrame, device_download: pd.DataFrame):
198+
def update_device_inspected_status(device_dict: dict):
173199
"""
174-
Process the data for a discovered device into a list of cards.
175-
200+
Update the 'is_inspected' status of a device in the database based on user interaction.
176201
Args:
177202
device_dict (dict): information about the device, from the 'devices' table
178-
device_upload (pd.DataFrame): DataFrame containing upload traffic data
179-
device_download (pd.DataFrame): DataFrame containing download traffic data
180203
"""
181204
# Check if the user has previously inspected this device
182205
device_inspected_config_key = f'device_is_inspected_{device_dict["mac_address"]}'
183206
is_inspected = common.config_get(device_inspected_config_key, False)
184207

185208
# Update the inspected status in the database if it is different
186209
if is_inspected != (device_dict['is_inspected'] == 1):
210+
# Note, on start time, the is_inspected sets to 0 for all devices, then changes to 1 once the user clicks the Inspect button
211+
logger.info(f"[Device List Page] Updating 'is_inspected' status for device to {is_inspected}")
187212
db_conn, rwlock = libinspector.global_state.db_conn_and_lock
188213
with rwlock:
189214
sql = """
@@ -193,89 +218,100 @@ def show_device_card(device_dict: dict, device_upload: pd.DataFrame, device_down
193218
"""
194219
db_conn.execute(sql, (is_inspected, device_dict['mac_address']))
195220

196-
# Extra information on the device's metadata, e.g., OUI
197-
metadata_dict = json.loads(device_dict['metadata_json'])
198221

199-
# Get the device's custom name as set by the user
222+
@st.fragment(run_every=1)
223+
def show_device_bar_graph(device_dict: dict):
224+
"""
225+
Show the upload/download bar graph for a device.
226+
227+
Args:
228+
device_dict (dict): information about the device, from the 'devices' table
229+
"""
230+
# Get the Bar Graph data for upload/download
231+
device_upload, device_download = common.bar_graph_data_frame(int(time.time()))
232+
233+
# Extra information on the device's metadata, e.g., OUI
234+
metadata_dict = json.loads(device_dict['metadata_json']) # Get the device's custom name as set by the user
200235
device_custom_name = common.get_device_custom_name(device_dict['mac_address'])
236+
device_detail_url = f"/device_details?device_mac_address={device_dict['mac_address']}"
237+
title_text = f'**[{device_custom_name}]({device_detail_url})**'
238+
st.markdown(title_text)
239+
caption = f'{device_dict["ip_address"]} | {device_dict["mac_address"]}'
240+
if "oui_vendor" in metadata_dict:
241+
caption += f' | {metadata_dict["oui_vendor"]}'
242+
243+
api_output = common.config_get(f'device_details@{device_dict["mac_address"]}', default={})
244+
if "Vendor" in api_output or "Explanation" in api_output:
245+
vendor = api_output.get("Vendor", "")
246+
explanation = api_output.get("Explanation", "")
247+
if vendor or explanation:
248+
caption += f"| Vendor: {vendor} | Explanation: {explanation}"
249+
st.caption(caption, help='IP address, MAC address, manufacturer OUI, and Device Identification API output')
250+
251+
# --- Add bar charts for upload/download ---
252+
chart_col_upload, chart_col_download = st.columns(2)
253+
with chart_col_upload:
254+
device_upload_graph = device_upload[device_upload['mac_address'] == device_dict["mac_address"]]
255+
common.plot_traffic_volume(device_upload_graph,
256+
"Upload Traffic (sent by device) in the last 60 seconds")
257+
with chart_col_download:
258+
device_download_graph = device_download[device_download['mac_address'] == device_dict["mac_address"]]
259+
common.plot_traffic_volume(device_download_graph,
260+
"Download Traffic (sent by device) in the last 60 seconds")
261+
262+
263+
@st.fragment(run_every=3)
264+
def toggle_device_fragment(mac_address: str):
265+
# Maps short options to long options for display
266+
option_dict = {
267+
'inspected': ':material/troubleshoot: Inspected',
268+
'favorite': ':material/favorite: Favorite',
269+
'blocked': ':material/block: Blocked'
270+
}
201271

202-
c1, c2 = st.columns([6, 4], gap='small')
203-
204-
# show high level information, IP, Mac, OUI
205-
with c1:
206-
device_detail_url = f"/device_details?device_mac_address={device_dict['mac_address']}"
207-
title_text = f'**[{device_custom_name}]({device_detail_url})**'
208-
st.markdown(title_text)
209-
caption = f'{device_dict["ip_address"]} | {device_dict["mac_address"]}'
210-
if "oui_vendor" in metadata_dict:
211-
caption += f' | {metadata_dict["oui_vendor"]}'
212-
213-
api_output = common.config_get(f'device_details@{device_dict["mac_address"]}', default={})
214-
if "Vendor" in api_output or "Explanation" in api_output:
215-
vendor = api_output.get("Vendor", "")
216-
explanation = api_output.get("Explanation", "")
217-
if vendor or explanation:
218-
caption += f"| Vendor: {vendor} | Explanation: {explanation}"
219-
st.caption(caption, help='IP address, MAC address, manufacturer OUI, and Device Identification API output')
220-
221-
# --- Add bar charts for upload/download ---
222-
chart_col_upload, chart_col_download = st.columns(2)
223-
with chart_col_upload:
224-
device_upload_graph = device_upload[device_upload['mac_address'] == device_dict["mac_address"]]
225-
common.plot_traffic_volume(device_upload_graph,
226-
"Upload Traffic (sent by device) in the last 60 seconds")
227-
with chart_col_download:
228-
device_download_graph = device_download[device_download['mac_address'] == device_dict["mac_address"]]
229-
common.plot_traffic_volume(device_download_graph,
230-
"Download Traffic (sent by device) in the last 60 seconds")
231-
# Set whether a device is to be inspected, favorite, or blocked
232-
with c2:
233-
# Maps short options to long options for display
234-
option_dict = {
235-
'inspected': ':material/troubleshoot: Inspected',
236-
'favorite': ':material/favorite: Favorite',
237-
'blocked': ':material/block: Blocked'
238-
}
272+
device_data = get_device_data(mac_address)
273+
update_device_inspected_status(device_data)
274+
275+
# Reverse the option_dict to map long options back to short options
276+
option_reversed_dict = {v: k for k, v in option_dict.items()}
239277

240-
# Reverse the option_dict to map long options back to short options
241-
option_reversed_dict = {v: k for k, v in option_dict.items()}
242-
243-
# Read the device's favorite and blocked status from the config
244-
device_is_favorite_config_key = f'device_is_favorite_{device_dict["mac_address"]}'
245-
device_is_favorite = common.config_get(device_is_favorite_config_key, False)
246-
247-
device_is_blocked_config_key = f'device_is_blocked_{device_dict["mac_address"]}'
248-
device_is_blocked = common.config_get(device_is_blocked_config_key, False)
249-
250-
# Create a list of default options based on the device's status
251-
default_option_list = []
252-
if is_inspected:
253-
default_option_list.append(option_dict['inspected'])
254-
if device_is_favorite:
255-
default_option_list.append(option_dict['favorite'])
256-
if device_is_blocked:
257-
default_option_list.append(option_dict['blocked'])
258-
259-
def _device_options_changed_callback():
260-
261-
# The long options selected by the user
262-
selected_list = st.session_state[f'device_options_{device_dict["mac_address"]}']
263-
264-
# Transform the long options back to short options
265-
selected_list = [option_reversed_dict[option] for option in selected_list]
266-
267-
# Reset the device's status based on the selected options
268-
common.config_set(device_is_favorite_config_key, 'favorite' in selected_list)
269-
common.config_set(device_is_blocked_config_key, 'blocked' in selected_list)
270-
common.config_set(device_inspected_config_key, 'inspected' in selected_list)
271-
272-
# Create a list of options to display
273-
st.pills(
274-
'Options',
275-
options=option_dict.values(),
276-
selection_mode='multi',
277-
default=default_option_list,
278-
label_visibility='collapsed',
279-
key=f"device_options_{device_dict['mac_address']}",
280-
on_change=_device_options_changed_callback
281-
)
278+
# Read the device's favorite and blocked status from the config
279+
device_is_favorite_config_key = f'device_is_favorite_{mac_address}'
280+
device_is_favorite = common.config_get(device_is_favorite_config_key, False)
281+
282+
device_is_blocked_config_key = f'device_is_blocked_{mac_address}'
283+
device_is_blocked = common.config_get(device_is_blocked_config_key, False)
284+
285+
# Create a list of default options based on the device's status
286+
default_option_list = []
287+
device_inspected_config_key = f'device_is_inspected_{mac_address}'
288+
is_inspected = common.config_get(device_inspected_config_key, False)
289+
if is_inspected:
290+
default_option_list.append(option_dict['inspected'])
291+
if device_is_favorite:
292+
default_option_list.append(option_dict['favorite'])
293+
if device_is_blocked:
294+
default_option_list.append(option_dict['blocked'])
295+
296+
def _device_options_changed_callback():
297+
# The long options selected by the user
298+
selected_list = st.session_state[f'device_options_{mac_address}']
299+
300+
# Transform the long options back to short options
301+
selected_list = [option_reversed_dict[option] for option in selected_list]
302+
303+
# Reset the device's status based on the selected options
304+
common.config_set(device_is_favorite_config_key, 'favorite' in selected_list)
305+
common.config_set(device_is_blocked_config_key, 'blocked' in selected_list)
306+
common.config_set(device_inspected_config_key, 'inspected' in selected_list)
307+
308+
# Create a list of options to display
309+
st.pills(
310+
'Options',
311+
options=option_dict.values(),
312+
selection_mode='multi',
313+
default=default_option_list,
314+
label_visibility='collapsed',
315+
key=f"device_options_{mac_address}",
316+
on_change=_device_options_changed_callback
317+
)

0 commit comments

Comments
 (0)