A reusable PyShiny table editor widget with PostgreSQL database support for data management applications.
- Database-Backed Table Editor: Edit data directly connected to PostgreSQL
- Primary Key Based Tracking: Track modifications using composite primary keys
- Session Book Management: Efficient pagination with in-memory caching
- Modification History: Full audit trail of all changes with undo support
- Column Presets: Save and load custom column configurations per user
- Status Workflow: Built-in approval/rejection workflow with configurable status values
- Real-time Filtering & Sorting: Database-level query optimization
- Faceted Filters: Checkbox+count sidebar panels for any column
- Theming: Built-in themes (modern, classic, eye-protection, dark) with runtime switching
- Read-Only & Clean Slate Modes: Display-only mode and minimal UI mode
- Row Selection Toggle: Configurable row checkboxes for approve/reject/export flows
- Review Detail: Emit selected row PKs via commute layer for parent-app integration
- Approval Assignment: Column-to-column value copy on row approval
- Tracker Mode: Performance profiling with per-query and per-render elapsed times
- Batch Upload: Bulk data import support
- Timezone Display Control: Strip timezone from datetime values in table display
pip install dmapTableEditorOr install from source:
git clone https://github.com/yourusername/dmapTableEditor.git
cd dmapTableEditor
pip install -e .Copy the template and customize for your database:
cp app_config.template.json app_config.jsonEdit app_config.json:
{
"app_title": "My Data Editor",
"database": {
"enabled": true,
"connection_string": "postgresql://user:pass@localhost/mydb",
"data_table": "my_table",
"mods_table": "my_modifications",
"state_table": "my_ui_state"
},
"table": {
"primary_key": ["id"],
"editable_columns": ["status", "comments"],
"display_columns": ["id", "name", "status", "comments"]
}
}from shiny import App, ui
from dmapTableEditor import table_editor_ui, table_editor_server
from dmapTableEditor.config import load_app_config
# Load configuration
config = load_app_config("app_config.json")
app_ui = ui.page_fluid(
ui.h1(config.app_title),
table_editor_ui("editor")
)
def server(input, output, session):
table_editor_server("editor", config=config)
app = App(app_ui, server)shiny run app.pySee template/app_config.template.json for a fully annotated example and docs/REFERENCE.md for every key with defaults.
| Setting | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Enable PostgreSQL mode |
mode |
string | "direct" |
direct (SQLAlchemy) or datum (proxy) |
connection_string |
string | โ | PostgreSQL connection URL (direct mode) |
source_table |
string | "" |
Source table to copy from (optional) |
data_table |
string | "epitopes_data" |
Working data table |
mods_table |
string | "epitopes_modifications" |
Modifications tracking table |
state_table |
string | "epitopes_ui_state" |
UI state persistence table |
status_column |
string | "Status" |
Column holding row status |
auto_detect_pk |
bool | true |
Auto-detect primary key from DB schema |
pool_size |
int | 5 |
SQLAlchemy connection pool size |
max_overflow |
int | 10 |
Max connections above pool_size |
pool_timeout |
int | 30 |
Seconds to wait for a connection from pool |
lazy_loading |
bool | false |
DB-level pagination (fetch only current page) |
page_buffer_size |
int | 300 |
Rows per query when lazy loading |
max_rows |
int|null | null |
Max rows to load (null = all) |
max_rows_per_page |
int | 100 |
Hard upper limit per page |
default_rows_per_page |
int | 25 |
Default rows shown per page |
shared_cache_key |
string|null | null |
Share loaded data across sessions using this key |
shared_cache_ttl |
int | 300 |
Seconds to keep shared cache alive (5 min default) |
datum_base_url |
string | "" |
Datum proxy base URL (mode=datum only) |
datum_token |
string | "" |
Datum API token (mode=datum only) |
datum_database |
string | "" |
Target database name (mode=datum only) |
datum_schema |
string | "public" |
Database schema (mode=datum only) |
datum_service_name |
string | "postgres_sql" |
Service name (mode=datum only) |
| Setting | Type | Default | Description |
|---|---|---|---|
primary_key |
list | ["id"] |
Primary key column(s) |
default_columns |
list | [] |
Columns visible by default (empty = all) |
editable_columns |
list | [] |
Columns users can edit (empty = all) |
readonly_columns |
list | [] |
Columns that cannot be edited |
column_masks |
object | {} |
Display-name overrides: {"real_name": "Display Name"} |
default_column_width |
int | 130 |
Default width (px) for columns not in default_column_widths |
default_column_widths |
object | {} |
Per-column width overrides: {"col_name": 180} |
default_sort_column |
string | null |
Initial sort column |
default_sort_ascending |
bool | true |
Initial sort direction |
default_rows_per_page |
int | 25 |
Default rows shown per page |
rows_per_page_options |
list | [10, 25, 50, 100, "all"] |
Available rows-per-page choices |
presets_enabled |
bool | true |
Enable column presets. When false, hides preset UI and skips all preset DB calls |
default_preset |
string | "Default" |
Name of the preset loaded on startup |
no_tz_display |
bool | false |
Strip timezone from datetime/timestamp values in table display |
| Setting | Type | Default | Description |
|---|---|---|---|
persist_state |
bool | true |
When false, skips all UI state DB calls (table creation, load, save). Use for sub-table / detail-tab mode where the DB may be unreachable |
state_storage |
string | "local" |
State storage type |
persist_filters |
bool | true |
Persist active filters across sessions |
persist_sort |
bool | true |
Persist sort column/direction |
persist_page |
bool | true |
Persist current page |
persist_column_selection |
bool | true |
Persist selected columns |
persist_column_widths |
bool | true |
Persist column widths |
| Setting | Type | Default | Description |
|---|---|---|---|
searchable_columns |
list | [] |
Columns for text search (empty = all) |
filter_columns |
object | {} |
Column filter types: "select", "text", "numeric", "date" |
default_filters |
object | {} |
Filters applied on load (see Filter Operators below) |
status_filter_enabled |
bool | true |
Show status filter in sidebar |
enable_search |
bool | true |
Show the search input |
search_case_sensitive |
bool | false |
Case-sensitive search |
search_regex_enabled |
bool | false |
Allow regex in search input |
facet_columns |
list | [] |
Columns shown as faceted checkbox+count panels in sidebar |
facet_max_values |
int | 5 |
Max distinct values to show per facet panel |
| Setting | Type | Default | Description |
|---|---|---|---|
enable_approval_workflow |
bool | true |
Show approve/reject buttons and status column |
enable_save_button |
bool | true |
Show the save button |
enable_export |
bool | true |
Show the export button |
enable_undo |
bool | true |
Show the undo button |
enable_copy_column |
bool | true |
Allow copying column values |
enable_status_filter |
bool | true |
Show status distribution in sidebar |
enable_synthesis |
bool | false |
Enable the synthesis transform feature |
enable_review_detail |
bool | false |
Show Review Detail button (emits event via commute layer) |
review_detail_multi_select |
bool | false |
When true, Review Detail emits all selected rows (pk is a list) |
enable_row_select |
bool | true |
Show row selection checkboxes. When false, disables approve/reject/export-selected |
read_only |
bool | false |
All columns read-only, editing controls hidden for all users |
tracker_mode |
bool | false |
Force-flush logs every SQL query and UI render with elapsed time (ms) |
theme |
string | "modern" |
Color scheme: modern | classic | eye-protection | dark. Switchable at runtime |
clean_slate |
bool | false |
Display only data table and pagination. Hides sidebar, toolbar, and modals |
fix_filter |
bool | false |
Lock filters to only default_filters |
| Setting | Type | Default | Description |
|---|---|---|---|
query |
string | "" |
SQL SELECT to materialize |
result_table_prefix |
string | "_synthesis_result" |
Table name for cached result |
ttl_minutes |
int | 10 |
Cache lifetime (0 = always regenerate) |
label |
string | "Synthesis" |
Button and modal title label |
{
"status_labels": {
"unprocessed": "Unreviewed",
"edited": "Edited",
"approved": "Accepted",
"rejected": "Rejected"
}
}Internal keys (unprocessed, edited, approved, rejected) must not change. Values are displayed in the UI and also recognized in the database status_column.
Controls what value is written to the database status column when Approve/Reject buttons are clicked:
{
"status_values": {
"approved": "approved",
"rejected": "rejected"
}
}Change to match your column's convention, e.g. {"approved": "Accepted", "rejected": "Declined"}.
Column-to-column copy on approval. For each {source: target} pair, the source column value is written to the target column when a row is approved:
{
"approval_assignment": {
"Draft_Value": "Final_Value",
"Notes": "Approved_Notes"
}
}| Setting | Type | Default | Description |
|---|---|---|---|
default_role |
string | "viewer" |
Role for users not in user_roles |
user_roles |
object | {} |
Map of username โ role ("editor" or "viewer") |
The default_filters object supports plain values ("Clinical") or operator objects:
| Operator | Value Type | Description |
|---|---|---|
in |
array | Match any value in list |
not_in |
array | Exclude values in list |
contains |
string | Case-insensitive substring |
not_contains |
string | Exclude substring matches |
between |
[min, max] |
Inclusive range |
gt, gte, lt, lte |
number/string | Comparison |
last_n_days |
integer | Date column within last N days |
not_empty |
(none) | Non-null, non-blank |
regex |
string | PostgreSQL regex (~*) |
Example:
{
"default_filters": {
"sequencing_date": {"op": "last_n_days", "value": 7},
"status": {"op": "in", "value": ["Completed", "Others"]},
"clinical_or_research": "Clinical"
}
}When the editor is embedded as a detail tab or sub-table (e.g., opened from a parent app via commute), the Datum proxy may be unreachable from the child process. To prevent DNS/connection errors on state and preset DB calls, disable persistence:
{
"state": { "persist_state": false },
"table": { "presets_enabled": false }
}This skips all state table creation/load/save and preset table creation/load/save. The app uses config defaults for sort, filters, pagination, and columns. Preset dropdown and Save Layout button are hidden from the toolbar.
All configuration can be overridden via environment variables, which is useful for production deployments (e.g., RStudio Connect) where you don't want secrets in config files.
| Environment Variable | Config Field | Description |
|---|---|---|
APP_DATABASE_ENABLED |
database.enabled |
Enable database mode (true/false) |
APP_DATABASE_MODE |
database.mode |
datum (Datum proxy; only supported mode) |
APP_DB_CONNECTION_STRING |
database.connection_string |
PostgreSQL connection URL |
APP_DB_DATA_TABLE |
database.data_table |
Main data table name |
APP_DB_MODS_TABLE |
database.mods_table |
Modifications table name |
APP_DB_STATE_TABLE |
database.state_table |
UI state table name |
For deployments using the Datum proxy service (e.g., RStudio Connect without direct DB access):
| Environment Variable | Config Field | Description |
|---|---|---|
DATUM_BASE_URL |
database.datum_base_url |
Datum proxy base URL |
DATUM_API_TOKEN |
database.datum_token |
API authentication token |
DATUM_DATABASE |
database.datum_database |
Target database name |
DATUM_SCHEMA |
database.datum_schema |
Database schema (default: public) |
DATUM_SERVICE_NAME |
database.datum_service_name |
Service name (default: postgres_sql) |
| Environment Variable | Config Field | Description |
|---|---|---|
APP_DATA_SOURCE_TYPE |
data_source.source_type |
Source type (csv, json, api, database) |
APP_DATA_FILE_PATH |
data_source.file_path |
Path to data file |
APP_DATA_API_URL |
data_source.api_url |
API endpoint URL |
| Environment Variable | Config Field | Description |
|---|---|---|
APP_PERSISTENCE_TYPE |
persistence.persistence_type |
local, api, or database |
APP_MODIFICATIONS_LOG_PATH |
persistence.modifications_log_path |
Path to modifications log |
APP_SAVE_API_URL |
persistence.api_save_url |
API endpoint for saving |
Set these environment variables in RStudio Connect:
APP_DATABASE_MODE=datum
DATUM_BASE_URL=https://datum-proxy.yourcompany.com
DATUM_API_TOKEN=your-secret-token
DATUM_DATABASE=epitopes_db
DATUM_SCHEMA=publicdmapTableEditor/
โโโ config/ # Configuration schema and loading
โโโ data/ # Data loading utilities
โโโ db/ # Database operations
โ โโโ db_operations.py # High-level DB interface
โ โโโ db_schema.py # Schema introspection
โ โโโ query_builder.py # SQL query construction
โ โโโ session_book.py # Session data management
โ โโโ user_presets.py # User preferences storage
โโโ processing/ # Data processing utilities
โโโ utils/ # Helper utilities
โ โโโ column_utils.py # Column handling
โ โโโ data_operations.py # Edit/undo operations
โ โโโ filter_utils.py # Filter handling
โ โโโ modal_utils.py # Modal dialogs
โ โโโ pagination_utils.py
โ โโโ table_utils.py # Table rendering
โโโ widgets/ # Shiny widget modules
โโโ css/ # Stylesheets
โ โโโ themes/ # Theme system
โ โโโ variables.css # Base CSS variable defaults (classic)
โ โโโ classic/theme.css # Classic overrides
โ โโโ modern/theme.css # Modern overrides
โ โโโ dark/theme.css # Dark overrides
โ โโโ eye-protection/theme.css # Eye-protection overrides
โโโ js/ # JavaScript utilities
โโโ commute/ # Event emitter for parent-app integration
The package auto-creates required tables if they don't exist. Tables are typically created under a schema (e.g., epitopes.modifications, epitopes.ui_state):
CREATE TABLE <schema>.modifications (
id SERIAL PRIMARY KEY,
row_pk JSONB NOT NULL,
column_name VARCHAR(255) NOT NULL,
old_value TEXT,
new_value TEXT,
mod_type VARCHAR(50) NOT NULL,
undone BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW(),
created_by VARCHAR(255)
);
CREATE INDEX idx_modifications_row_pk ON <schema>.modifications USING GIN (row_pk);
CREATE INDEX idx_modifications_created_at ON <schema>.modifications (created_at DESC);UI state tables are created per-user. The table name is constructed as <base_state_table>.<username>.
For example, if state_table is configured as epitopes.ui_state and the username is testuser, the actual table will be epitopes.ui_state.testuser.
CREATE TABLE <schema>.ui_state.<username> (
id SERIAL PRIMARY KEY,
user_id VARCHAR(255),
session_id VARCHAR(255),
filters JSONB,
sort_column VARCHAR(255),
sort_ascending BOOLEAN DEFAULT TRUE,
current_page INT DEFAULT 1,
rows_per_page INT DEFAULT 25,
column_preset VARCHAR(255),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(user_id, session_id)
);
CREATE INDEX idx_ui_state_user_session ON <schema>.ui_state.<username> (user_id, session_id);Every user-facing interaction mapped from trigger โ server โ data flow.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ User types query โโโบ Picks column (or "All") โ
โ Clicks [Search] โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ _handle_search() โ
โ read input.search_input โ
โ read input.search_columnโ
โโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ
โ
โโโโโโโโโดโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโ โโโโโโโโโโโโโโโ
โ search โ โ current_pageโ
โ _state โ โ .set(1) โ
โ .set() โ โโโโโโโโโโโโโโโ
โโโโโโฌโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Lazy mode? โ
โ YES โโบ _build_query_params() โโบ SQL WHERE ILIKE โ
โ NO โโบ _get_filtered_rows() โโบ pandas str.containsโ
โโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโ
โ table_container โ
โ re-renders โ
โโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ User clicks status bar in histogram โ
โ (e.g. uncheck "Unreviewed") โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JS: initHistogramCheckboxesโ
โ syncs .status-checkbox โ
โ โโโบ input.status_filter_ โ
โ multi (hidden select) โ
โโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ _reset_page_on_filter... โ
โ current_page.set(1) โ
โโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโดโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ
โ Lazy: SQL โ โ In-memory: โ
โ WHERE status โ โ _get_filtered_ โ
โ IN (...) โ โ rows() filters โ
โโโโโโโโฌโโโโโโโโ โ by _get_row_ โ
โ โ status() โ
โ โโโโโโโโโโฌโโโโโโโโโโ
โโโโโโโโโโฌโโโโโโโโโโโ
โผ
โโโโโโโโโโโโโโโโโโโโ
โ table_container โ
โ + stats_histogramโ
โ re-render โ
โโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ User checks/unchecks value in facet panel โ
โ e.g. โ "RNA_Access" under NGS_TEST โ
โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JS: initFacetCheckboxes() โ
โ collect checked values per col โ
โ โโโบ setShinyInput( โ
โ "facet_filter_change", โ
โ {col: [values]} ) โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ _handle_facet_filter() โ
โ merge into active_filters โ
โ (null = clear that column) โ
โ current_page.set(1) โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโดโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ
โ table re- โ โ facet_panels โ
โ renders โ โ _ui re-rendersโ
โ with new โ โ with updated โ
โ filters โ โ checkmarks โ
โโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Click [+] โ โ Existing filter actions: โ
โ Add Filter โ โ โ remove โ โ edit โ op dropdown โ
โโโโโโโโฌโโโโโโโโ โโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโโ
โ โ โ โ
โผ โผ โผ โผ
โโโโโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ JS: open โ โ_remove_ โ โ_apply_ โ โ_set_filter_ โ
โ AddFilter โ โfilter() โ โfilter_ โ โoperator() โ
โ Modal โ โ โ โvalue() โ โ{col, op} โ
โโโโโโโโฌโโโโโโโ โโโโโโฌโโโโโโ โโโโโฌโโโโโ โโโโโโโโโโฌโโโโโโโโโ
โ โ โ โ
โผ โ โ โ
โโโโโโโโโโโโโโโโโโโ โ โ โ
โ Pick column โ โ โ โ
โ โโโบ JS addFilterโ โ โ โ
โ โโโบ _add_filter โ โ โ โ
โโโโโโโโโโฌโโโโโโโโโ โ โ โ
โ โ โ โ
โผ โผ โผ โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ active_filters.set(...) โ
โ current_page.set(1) โ
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโดโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ table re- โ โ dynamic_filters โ
โ renders โ โ panel re-renders โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Click column header โฎ โโโบ Sort Asc / Desc โ
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JS sends input.sort_ โ
โ column = {col, direction}โ
โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ _sort_column() โ
โ โ
โ Lazy? โ
โ YES โโบ current_sort.set()โ
โ (SQL ORDER BY) โ
โ NO โโบ sort_dataframe() โ
โ on data reactive โ
โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ
โ
โโโโโโโโโดโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ
โ current_page โ โ save_ui_state โ
โ .set(1) โ โ (persist sort) โ
โโโโโโโโฌโโโโโโโโโ โโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโ
โ table_container โ
โ re-renders โ
โโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โฎ First โ Prev [3/12] Next โถ Last โญ โ
โ Go to: [__] [Go] โ
โ Rows/page: [25 โพ] โ
โโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโ
โ โ โ โ โ
โผ โผ โผ โผ โผ
set(1) set(p-1) set(p+1) set(max) set(input)
โ โ โ โ โ
โโโโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโ
โ current_page.set(N) โ
โ save_ui_state() โ
โโโโโโโโโโโโฌโโโโโโโโโโโโ
โ
โโโโโโโโโดโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ
โ Lazy: SQL โ โ In-memory: โ
โ LIMIT/OFFSET โ โ DataFrame slice โ
โ new page โ โ [start:end] โ
โโโโโโโโฌโโโโโโโโ โโโโโโโโโโฌโโโโโโโโโโ
โโโโโโโโโโฌโโโโโโโโโโโ
โผ
โโโโโโโโโโโโโโโโโโโโ
โ table_container โ
โ + pagination_ โ
โ controls render โ
โโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Click editable cell โ
โ (double-click/single) โ
โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JS: openCellPopup() โ
โ Shows popup with: โ
โ Current value โ Original val โ
โ [Save] [Copy] [Cancel] โ
โโโโฌโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโ
โ โ โ
โผ โผ โผ
โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโโโโโ
โ Save โ โ Copy toโ โ Cancel: โ
โ sends โ โ clip- โ โ close popupโ
โ cell_ โ โ board โ โ (no server โ
โ edit โ โ (JS) โ โ call) โ
โโโโโฌโโโโโ โโโโโโโโโโ โโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ _handle_cell_edit() โ
โ perform_cell_edit() โ
โ {row, col, old, new} โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโผโโโโโโโโโโโ
โ โ โ
โผ โผ โผ
โโโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโโโ
โ data โ โmods_logโ โ edited_ โ
โ .set() โ โ.set() โ โ cells โ
โ update โ โappend โ โ track โ
โ cell โ โentry โ โ {r,c} โ
โโโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ auto-save to DB / file โ
โ table re-renders with โ
โ highlighted edited cell โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ Select rows (click/shift-click/select-all) โ
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโดโโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ
โ [Approve] โ โ [Reject] โ
โโโโโโโโฌโโโโโโโโ โโโโโโโโโฌโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ _approve_data() / _reject_data() โ
โ 1. get_selected_row_indices() โ
โ 2. _get_selected_pks(indices, df) โ
โ 3. create_approval/rejection_entry โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโผโโโโโโโโโโโ
โ โ โ
โผ โผ โผ
โโโโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโโโโโโโโโ
โ mods_log โ โ save โ โ _save_status_ โ
โ .set() โ โ log to โ โ to_db() per PK โ
โ append โ โ file โ โ INSERT INTO โ
โ entries โ โ โ โ mods table โ
โโโโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโ
โ table re-renders โ
โ with status colorโ
โ histogram updatesโ
โโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Click [Export Selected] or โ
โ [Export All] โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JS: openExportConfirmModalโ
โ PHI/PII warning dialog โ
โ [I Understand] [Cancel] โ
โโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ _prepare_export() โ
โ export_state = "preparing" โ
โ โ
โ type == "selected"? โ
โ YES โโบ get selected row PKs โ
โ filter df to those PKs โ
โ NO โโบ use full filtered/ โ
โ sorted df โ
โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ df.to_csv() โโโบ export_csv โ
โ export_state = "ready" โ
โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ export_download_ui renders โ
โ [Download CSV] button โ
โ โโโบ browser downloads file โ
โ export_state = "idle" โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Click [Manage Layout] โ
โโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Add Column Modal โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Active: [col1] [col2] [col3] [ร] โ โ
โ โ (drag to reorder) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ
โ โ Available: [col4] [col5] [col6] โ โ
โ โ (click to add) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ
โ โ [Add All] [Remove All] [Update] โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโผโโโโโโโโฌโโโโโโโโฌโโโโโโโโ
โ โ โ โ โ
โผ โผ โผ โผ โผ
drag click click click click
reorder add remove add remove
cols col col ร all all
โ โ โ โ โ
โโโโโโโโโดโโโโโโโโดโโโโโโโโดโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโ
โ active_columns.set() โ
โ + column_order input โ
โโโโโโโโโโโโโฌโโโโโโโโโโโโ
โ
โโโโโโโโโดโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ
โ table re- โ โ modal content โ
โ renders with โ โ re-renders โ
โ new columns โ โ available list โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Table Header Row โ
โ โโโโโโโโฌโโโโโโโฌโโโโโโโฌโโโโโโโ โ
โ โ ColA โ ColB โ ColC โ ColD โ โ drag to reorderโ
โ โ โ โ โ โ โ โ drag โ to resizeโ
โ โโโโโโโโดโโโโโโโดโโโโโโโดโโโโโโโ โ
โโโโโโโโโโโโโฌโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ
โ Drag header โ โ Drag resize โ
โ to reorder โ โ handle โ โ
โโโโโโโโฌโโโโโโโโ โโโโโโโโโฌโโโโโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ
โ JS: updateHeader โ โ JS: saveColumn โ
โ Order() sends โ โ Widths() sends โ
โ input.column_ โ โ input.column_ โ
โ order = [...] โ โ widths = {...} โ
โโโโโโโโโโฌโโโโโโโโโโ โโโโโโโโโโฌโโโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ
โ active_columns โ โ column_widths โ
โ .set(new order) โ โ .set(new widths) โ
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ
โ โ
โโโโโโโโโโโโฌโโโโโโโโโโโ
โผ
โโโโโโโโโโโโโโโโโโโโ
โ table re-renders โ
โ save_ui_state() โ
โโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Click [Preset โพ] dropdown โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ Default โ
โ โ My Variant View [ร] โ
โ โ Clinical Review [ร] โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ Save as: [________] [Save] โ
โ [Save Layout] [Reset to Default] [โณ] โ
โโโโฌโโโโโโโฌโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโโโ
โ โ โ โ
โผ โผ โผ โผ
load save new save layout delete
preset preset to current preset
โ โ โ โ
โผ โผ โผ โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ column_presets reactive updated โ
โ active_columns.set(preset cols) โ
โ column_widths.set(preset widths) โ
โ active_preset.set(name) โ
โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโดโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ
โ table re- โ โ _save_presets โ
โ renders with โ โ to file / DB โ
โ new layout โ โ โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Modifications Log Modal โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ #1 ColA: "old" โ "new" [Undo] โ โ
โ โ #2 ColB: "x" โ "y" [Undo] โ โ
โ โ #3 Approved row PK={..} [Undo] โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ _handle_undo() โ
โ process_undo_action() โ
โ perform_undo() โ
โโโโโโโโโโโโโโโฌโโโโโโโโโโโโ
โ
โโโโโโโโโโโโผโโโโโโโโโโโ
โ โ โ
โผ โผ โผ
โโโโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโโโ
โ data โ โmods_logโ โ auto-saveโ
โ .set() โ โ.set() โ โ log + โ
โ revert โ โremove โ โ data โ
โ cell โ โentry โ โ state โ
โโโโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโ
โ table re-renders โ
โ cell un-highlightedโ
โโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโ
โ Click [Save] โ
โโโโโโโโโโฌโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ _save_modifications() โ
โ save_modifications_to_file(data, log) โ
โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโผโโโโโโโโโโโ
โ โ โ
โผ โผ โผ
โโโโโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโโโโ
โ Write โ โ Write โ โ DB mode: โ
โ mods log โ โ data โ โ already โ
โ JSON file โ โ state โ โ persisted โ
โ โ โ JSON โ โ per-edit โ
โโโโโโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโ
โ notification_show() โ
โ "Saved successfully"โ
โโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Synthesis Modal โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ SQL Query (read-only preview) โ โ
โ โ SELECT ... FROM ... WHERE ... GROUP BY ... โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ
โ โ Status: โ Ready โ 1,234 rows (cached 3m ago, TTL 10m) โ โ
โ โ [Run Transform] [Regenerate] [Exit Synthesis] โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโฌโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโ
โ โ โ
โผ โผ โผ
โโโโโโโโโโโโ โโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ Run โ โ Regenerateโ โ Exit โ
โ (use โ โ (force โ โ (deactivate โ
โ cache if โ โ refresh โ โ synthesis โ
โ valid) โ โ matview) โ โ fetcher) โ
โโโโโโฌโโโโโโ โโโโโโโฌโโโโโโ โโโโโโโโฌโโโโโโโโ
โ โ โ
โผ โผ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ config.run_synthesis() โ โ
โ CREATE MATERIALIZED VIEW โ โ
โ AS (user query) โ โ
โ or REFRESH MATERIALIZED โ โ
โ VIEW (force=True) โ โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโ โ
โ โ
โผ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ activate_synthesis_fetcher โ โ
โ DataFetcher._table_override โ โ
โ = matview table name โ โ
โ active_columns synced โ โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ โ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ data.set(result_df) โ
โ _table_reload_trigger + 1 โ
โ table re-renders with synthesis data โ
โ all filters/sort/pagination work on โ
โ the materialized view via SQL โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโ
โ Click [Reload] โ
โโโโโโโโโโฌโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ _reload_data() โ
โ โ
โ Lazy mode? โ
โ YES โโบ refresh total_rows โ
โ from DB COUNT(*) โ
โ NO โโบ load_data_from_ โ
โ source() full reload โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ mods_log reloaded from DB โ
โ current_page.set(1) โ
โ table_container re-renders โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ Sidebar โ Table Content โ
โ โโโโโโโโโโ โ โ
โ โFilters โ โ โ
โ โSearch โ โ โ
โ โFacets โ โ โ
โ โโโโโโโโโโ โ โ
โโโโโโโโฌโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Click โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โถ โ Table Content โ
โ โ (full width now) โ
โโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Click โถ
โผ
(toggles back to expanded sidebar)
JS: toggleLeftPanel()
Scoped to containing widget via closest()
โโโโโโโโโโโโโโโโโโโโ
โ Click [Copy] โ
โ in toolbar โ
โโโโโโโโโโฌโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Copy Column Modal โ
โ โโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ [Column A] โ โ
โ โ [Column B] โ โ
โ โ [Column C] โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโ
โ click column name
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ _handle_copy_request() โ
โ process_copy_request() โ
โ extract values for selected โ
โ rows (or all on page) โ
โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JS: navigator.clipboard โ
โ .writeText(values) โ
โ notification: "Copied!" โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Any reactive trigger fires: โ
โ โข search_state โ
โ โข active_filters โ
โ โข status_filter_multi โ
โ โข current_sort โ
โ โข current_page โ
โ โข rows_per_page_value โ
โ โข mods_log โ
โ โข data โ
โ โข active_columns โ
โ โข _table_reload_trigger โ
โโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโ
โ is_lazy_loading()? โ
โโโโฌโโโโโโโโโโโโโโโฌโโโโ
โ โ
YES โผ NO โผ
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ
โ _build_query_ โ โ _get_filtered_rows() โ
โ params() โ โ pandas filtering โ
โ โโโบ DataFetcher โ โ + sort + slice โ
โ .fetch_page() โ โ โ
โ SQL query with: โ โ โ
โ WHERE + ORDER โ โ โ
โ + LIMIT/OFFSET โ โ โ
โโโโโโโโโโฌโโโโโโโโโโ โโโโโโโโโโโโฌโโโโโโโโโโโโ
โ โ
โโโโโโโโโโโโโฌโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ build_table_container() โ
โ HTML table with: โ
โ โข header (โฎ menus) โ
โ โข rows (editable cells) โ
โ โข selection checkboxes โ
โ โข status highlighting โ
โ โข edit highlighting โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JS post-render inits: โ
โ โข initRowSelection() โ
โ โข initHeaderDrag() โ
โ โข initColumnResize() โ
โ โข initHistogramCBs() โ
โ โข initFacetCBs() โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The tooling/ directory contains quality-control scripts that run in CI:
# Generate function mapping & type analysis report (outputs to qcmetric/)
python tooling/src/qc/generate_qc.py
# Full QC SOP (lint + mapping + golden-SQL checks)
bash tooling/sop_qc.sh# Install dev dependencies
pip install -e ".[dev]"
# Run tests with coverage
pytest tests/ --cov=src --cov-report=term-missing- 1482+ tests passing (~1.5s runtime)
- 96.5% public function coverage (222/230 functions)
- Database modules: 90โ100% coverage
- Utility modules: 93โ100% coverage
- Security: 5-layer SQL injection defense, golden-SQL regression tests
- Python 3.10+
- shiny >= 1.0.0
- pandas >= 2.0.0
- sqlalchemy >= 2.0.0
- psycopg2-binary >= 2.9.0
MIT License