Customizing Streamlit UIs

firestone generates functional Streamlit UIs out of the box, but you can customize them for your specific needs.

Column Mappings

Control which columns display and their order using --col-mappings.

Basic Column Selection

firestone generate \
  --resources addressbook.yaml \
  streamlit \
  --col-mappings '{"addressbook": ["city", "state", "country"]}'

Result: Only city, state, and country columns appear in the table.

Column Ordering

Columns appear in the order specified:

--col-mappings '{"addressbook": ["country", "state", "city", "street"]}'

Displays: countrystatecitystreet

Multiple Resources

Specify mappings for each resource:

--col-mappings '{
  "addressbook": ["street", "city", "state"],
  "persons": ["first_name", "last_name", "age"]
}'

All Columns (Default)

Omit --col-mappings to display all properties:

firestone generate --resources addressbook.yaml streamlit
# Shows all columns: address_key, person, addrtype, street, city, state, country, people, is_valid

Custom Templates

Replace the default Streamlit template with your own.

Using Custom Templates

firestone generate \
  --resources tasks.yaml \
  streamlit \
  --template ./my-streamlit-template.j2

Template Structure

firestone uses Jinja2 templates. Here's the structure:

import streamlit as st
import requests
import json

BACKEND_URL = "{{ backend_url }}"

st.title("{{ title }}")

{% for resource in resources %}
# {{ resource.kind }} resource
st.header("{{ resource.kind }}")

# Fetch data
response = requests.get(f"{BACKEND_URL}/{{ resource.kind }}")
data = response.json()

# Display table
st.dataframe(data)

# CRUD operations
# ... your custom logic here ...
{% endfor %}

Template Variables

Available variables in templates:

VariableDescriptionExample
titleApp title"Addressbook Manager"
descriptionApp description"Manage addresses"
versionAPI version"1.0"
backend_urlBackend API URL"http://localhost:8000"
resourcesList of resource objects[{kind: "addressbook", ...}]
col_mappingsColumn mappings dict{"addressbook": ["city"]}

Resource Object Properties

Each resource in resources has:

{
  "kind": "addressbook",
  "apiVersion": "v1",
  "metadata": {
    "description": "An addressbook resource"
  },
  "schema": {
    "type": "array",
    "key": {...},
    "items": {...}
  },
  "methods": {
    "resource": ["get", "post"],
    "instance": ["get", "put", "delete"]
  }
}

Styling and Layout

Custom Page Configuration

Add to your template:

import streamlit as st

# Page config
st.set_page_config(
    page_title="My Admin Panel",
    page_icon="🗂️",
    layout="wide",  # Use full width
    initial_sidebar_state="expanded"
)

Custom CSS

Inject custom styles:

st.markdown("""
<style>
.stDataFrame {
    font-size: 12px;
}
.stButton>button {
    background-color: #4CAF50;
    color: white;
}
</style>
""", unsafe_allow_html=True)

Add sidebar for multi-resource apps:

with st.sidebar:
    st.title("Navigation")
    resource = st.selectbox(
        "Select Resource",
        ["addressbook", "persons", "postal_codes"]
    )

Authentication

Add basic authentication to the UI:

import streamlit as st

def check_password():
    """Returns True if user has correct password."""
    def password_entered():
        if st.session_state["password"] == "mypassword":
            st.session_state["password_correct"] = True
            del st.session_state["password"]
        else:
            st.session_state["password_correct"] = False

    if "password_correct" not in st.session_state:
        st.text_input(
            "Password",
            type="password",
            on_change=password_entered,
            key="password"
        )
        return False
    elif not st.session_state["password_correct"]:
        st.text_input(
            "Password",
            type="password",
            on_change=password_entered,
            key="password"
        )
        st.error("😕 Password incorrect")
        return False
    else:
        return True

if check_password():
    # Show main app
    st.title("Admin Panel")
    # ... rest of app ...

API Token Support

Add Bearer token authentication:

import os

API_TOKEN = os.getenv("API_TOKEN", "")

def api_request(method, url, **kwargs):
    headers = kwargs.get("headers", {})
    if API_TOKEN:
        headers["Authorization"] = f"Bearer {API_TOKEN}"
    kwargs["headers"] = headers
    return requests.request(method, url, **kwargs)

# Use it
response = api_request("GET", f"{BACKEND_URL}/addressbook")

Run with token:

API_TOKEN=your-secret-token streamlit run app.py

Advanced Features

Pagination

Add pagination for large datasets:

# Fetch with pagination
page = st.number_input("Page", min_value=1, value=1)
limit = 20
offset = (page - 1) * limit

response = requests.get(
    f"{BACKEND_URL}/addressbook",
    params={"limit": limit, "offset": offset}
)

Search and Filtering

Add search box:

search = st.text_input("Search city")
if search:
    params = {"city": search}
else:
    params = {}

response = requests.get(f"{BACKEND_URL}/addressbook", params=params)

Data Refresh

Auto-refresh data:

import time

# Refresh every 30 seconds
if st.button("Enable Auto-Refresh"):
    while True:
        # Fetch and display data
        response = requests.get(f"{BACKEND_URL}/addressbook")
        st.dataframe(response.json())
        time.sleep(30)
        st.rerun()

Export to CSV

Add export button:

import pandas as pd

response = requests.get(f"{BACKEND_URL}/addressbook")
df = pd.DataFrame(response.json())

csv = df.to_csv(index=False)
st.download_button(
    label="Download CSV",
    data=csv,
    file_name="addressbook.csv",
    mime="text/csv"
)

Real-World Customization Example

Custom template with auth, search, and export:

import streamlit as st
import requests
import pandas as pd
import os

st.set_page_config(page_title="Admin Panel", layout="wide")

# Auth
API_TOKEN = os.getenv("API_TOKEN")
if not API_TOKEN:
    st.error("API_TOKEN environment variable required")
    st.stop()

headers = {"Authorization": f"Bearer {API_TOKEN}"}

# Title
st.title("📋 Addressbook Manager")

# Search
search = st.text_input("🔍 Search by city")

# Fetch data
params = {"city": search} if search else {}
response = requests.get(
    "http://api.example.com/addressbook",
    headers=headers,
    params=params
)

if response.status_code == 200:
    data = response.json()
    df = pd.DataFrame(data)

    # Display selected columns only
    display_cols = ["street", "city", "state", "country"]
    st.dataframe(df[display_cols])

    # Export
    csv = df.to_csv(index=False)
    st.download_button("Download CSV", csv, "addresses.csv")
else:
    st.error(f"API error: {response.status_code}")

Best Practices

Keep It Simple

# ✅ Good - Clear, focused UI
st.title("Address Manager")
st.dataframe(data)

# ❌ Too complex
st.balloons()
st.snow()
with st.expander("Advanced settings"):
    with st.container():
        col1, col2, col3, col4 = st.columns(4)
        # Too many options confuses users

Error Handling

# ✅ Good - Clear error messages
try:
    response = requests.get(url)
    response.raise_for_status()
except requests.exceptions.RequestException as e:
    st.error(f"Cannot connect to API: {e}")
    st.stop()

# ❌ Silent failures
response = requests.get(url)
# No error check - app breaks silently

Performance

# ✅ Good - Cache API calls
@st.cache_data(ttl=60)
def fetch_data():
    return requests.get(f"{BACKEND_URL}/addressbook").json()

# ❌ Slow - Re-fetch on every interaction
data = requests.get(f"{BACKEND_URL}/addressbook").json()

Next Steps