Welcome to the community.
index.py
"""
Panel App Gallery
A Panel-based index page that serves as a gallery of Panel applications with search and pagination functionality.
This demonstrates how to use `panel serve *.py --index index.py` to create a custom Python-based index page
instead of relying on HTML/Jinja templates.
Features:
- Real-time search functionality that filters apps by name and description
- Responsive thumbnails and app metadata display
- Pagination support when there are more than APPS_PER_PAGE results
- Modern Design styling with hover effects
- Auto-discovery of Panel apps in the current directory
Usage:
panel serve *.py --index index.py --dev --show
This app provides an interactive way to browse and search through a collection of Panel applications,
making it easy to find specific apps in larger collections. Each app displays its name, description,
and thumbnail image with direct links to launch the application.
"""
import math
import panel as pn
import param
from pathlib import Path
from typing import List
# Configuration
APPS_PER_PAGE = 6
ACCENT_COLOR = "#4099da"
ACCENT_FILL_HOVER = "#0072b5"
# Initialize Panel with required extensions
pn.extension("tabulator", sizing_mode="stretch_width")
class AppInfo:
"""Data class to hold information about a Panel app"""
def __init__(self, name: str, filename: str, description: str = "", thumbnail: str = ""):
self.name = name
self.filename = filename
self.description = description or f"Panel application: {name}"
self.thumbnail = thumbnail or self._get_default_thumbnail()
def _get_default_thumbnail(self) -> str:
"""Generate a default thumbnail placeholder"""
return "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE1MCIgdmlld0JveD0iMCAwIDIwMCAxNTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTUwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik04NyA2NUg5M1Y1OUg4N1Y2NVoiIGZpbGw9IiM5Q0E4QjciLz4KPHA5YXRoIGQ9Ik05NyA2MUgxMTNWODdIOTdWNjFaIiBmaWxsPSIjOUNBOEI3Ii8+CjxwYXRoIGQ9Ik04MyA3NEgxMTdWNzlIODNWNzRaIiBmaWxsPSIjOUNBOEI3Ii8+CjxwYXRoIGQ9Ik04MyA4Mkg5OFY4N0g4M1Y4MloiIGZpbGw9IiM5Q0E4QjciLz4KPC9zdmc+Cg=="
def url(self) -> str:
"""Generate the URL to launch the app"""
return f"./{self.filename.replace('.py', '')}"
class AppGallery(pn.viewable.Viewer):
"""
A Panel-based gallery for browsing and searching Panel applications.
This component provides:
- Search functionality with real-time filtering
- Pagination for large app collections
- Thumbnail previews and app metadata
- Direct links to launch applications
"""
# Parameters for state management
search_term = param.String(default="", doc="Current search term")
current_page = param.Integer(default=1, bounds=(1, None), doc="Current page number")
apps_per_page = param.Integer(default=APPS_PER_PAGE, bounds=(1, 50), doc="Number of apps per page")
def __init__(self, **params):
super().__init__(**params)
# Discover available Panel apps
self.all_apps = self._discover_apps()
# Create UI components
self._create_ui()
def _discover_apps(self) -> List[AppInfo]:
"""Discover Panel apps in the current directory"""
apps = []
# Add some demo apps for illustration
demo_apps = [
AppInfo("Environmental Dashboard", "panel_environmental_dashboard.py",
"Interactive dashboard for environmental data analysis with real-time charts and filters"),
AppInfo("Sales Analytics", "sales_dashboard.py",
"Comprehensive sales performance dashboard with KPI tracking and trend analysis"),
AppInfo("Material UI Demo", "material_ui_button_demo.py",
"Showcase of Panel's Material UI components and styling options"),
AppInfo("Data Explorer", "data_explorer.py",
"Interactive data exploration tool with filtering, sorting, and visualization capabilities"),
AppInfo("ML Model Comparison", "ml_comparison.py",
"Compare machine learning model performance with interactive metrics and plots"),
AppInfo("Time Series Analysis", "timeseries_analyzer.py",
"Advanced time series analysis with forecasting and anomaly detection"),
AppInfo("Portfolio Tracker", "portfolio_tracker.py",
"Track investment portfolio performance with real-time updates and analytics"),
AppInfo("Weather Forecast", "weather_app.py",
"Interactive weather forecast application with maps and detailed predictions")
]
apps.extend(demo_apps)
return sorted(apps, key=lambda x: x.name)
def _create_ui(self):
"""Create the user interface components"""
with pn.config.set(sizing_mode="stretch_width"):
# Header
self._header = pn.pane.HTML("""
<div style="text-align: center; padding: 20px 0;">
<h1 style="color: #2E86AB; margin-bottom: 10px;">App Gallery</h1>
<p style="color: #666; font-size: 18px;">Discover and explore applications</p>
</div>
""")
# Search bar
self._search_input = pn.widgets.TextInput.from_param(
self.param.search_term,
placeholder="Search apps by name or description...",
margin=(10, 20),
sizing_mode="stretch_width",
)
# Apps grid container
self._apps_container = pn.FlexBox(
sizing_mode="stretch_width",
align="center",
)
# Pagination controls
self._pagination = pn.Row(
sizing_mode="stretch_width",
styles = {"margin": "0 auto"}
)
# Main layout
self._layout = pn.Column(
self._header,
self._search_input,
self._apps_container,
pn.Spacer(sizing_mode="stretch_height"),
self._pagination,
max_width=975,
styles={"margin": "0 auto"},
align="center",
sizing_mode="stretch_both"
)
# Initial update will be triggered automatically by parameter reactivity
self._update_display()
@param.depends('search_term', watch=True)
def _on_search_change(self):
"""Handle search term changes by resetting to first page"""
self.current_page = 1
def _get_filtered_apps(self) -> List[AppInfo]:
"""Get filtered apps based on search term"""
# Filter apps based on search term
search_term = str(self.search_term or "")
if search_term:
filtered_apps = [
app for app in self.all_apps
if (search_term.lower() in app.name.lower() or
search_term.lower() in app.description.lower())
]
else:
filtered_apps = self.all_apps
return filtered_apps
def _get_paginated_apps(self, apps: List[AppInfo]) -> List[AppInfo]:
"""Get apps for current page"""
# Safely get parameter values with defaults
current_page = max(1, getattr(self, 'current_page', 1) or 1)
apps_per_page = max(1, getattr(self, 'apps_per_page', APPS_PER_PAGE) or APPS_PER_PAGE)
start_idx = (current_page - 1) * apps_per_page
end_idx = start_idx + apps_per_page
return apps[start_idx:end_idx]
def _create_app_card(self, app: AppInfo) -> pn.pane.HTML:
"""Create a card for a single app"""
card_html = f"""
<div style="
border: 1px solid var(--neutral-stroke-rest);
border-radius: 12px;
padding: 20px;
margin: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: all 0.3s ease;
height: 320px;
display: flex;
flex-direction: column;
" onmouseover="this.style.transform='translateY(-5px)'; this.style.boxShadow='0 8px 25px rgba(0,0,0,0.15)'"
onmouseout="this.style.transform='translateY(0px)'; this.style.boxShadow='0 2px 4px rgba(0,0,0,0.1)'">
<div style="text-align: center; margin-bottom: 15px;">
<img src="{app.thumbnail}"
style="width: 100%; height: 120px; object-fit: cover; border-radius: 8px; background: #f5f5f5;"
alt="App thumbnail"/>
</div>
<h3 style="color: {ACCENT_COLOR}; margin: 0 0 10px 0; font-size: 18px; text-align: center;">
{app.name}
</h3>
<p style="color: #666; margin: 0 0 15px 0; font-size: 14px; line-height: 1.4; flex-grow: 1; overflow: hidden;">
{app.description[:120]}{'...' if len(app.description) > 120 else ''}
</p>
<div style="text-align: center; margin-top: auto;">
<a href="{app.url()}"
style="
background: {ACCENT_COLOR};
color: white;
padding: 8px 16px;
text-decoration: none;
border-radius: 6px;
font-weight: 500;
transition: background-color 0.2s;
"
onmouseover="this.style.backgroundColor='{ACCENT_FILL_HOVER}'"
onmouseout="this.style.backgroundColor='{ACCENT_COLOR}'">
Open App →
</a>
</div>
</div>
"""
return pn.pane.HTML(card_html, width=300, height=320)
def _create_pagination_controls(self, total_apps: int) -> pn.Row:
"""Create pagination controls"""
# Safely get parameter values with defaults
current_page = max(1, getattr(self, 'current_page', 1) or 1)
apps_per_page = max(1, getattr(self, 'apps_per_page', APPS_PER_PAGE) or APPS_PER_PAGE)
total_pages = math.ceil(total_apps / apps_per_page)
if total_pages <= 1:
return pn.Row()
controls = []
# Previous button
if current_page > 1:
prev_btn = pn.widgets.Button(
name="← Previous",
button_style="outline",
)
prev_btn.on_click(lambda event: self._go_to_page(current_page - 1))
controls.append(prev_btn)
# Page numbers
start_page = max(1, current_page - 2)
end_page = min(total_pages, current_page + 2)
for page_num in range(start_page, end_page + 1):
if page_num == current_page:
# Current page (highlighted)
page_btn = pn.widgets.Button(
name=str(page_num),
button_type="primary",
margin=(5, 5),
width=40
)
else:
# Other pages
page_btn = pn.widgets.Button(
name=str(page_num),
button_style="outline",
margin=(5, 5),
width=40
)
page_btn.on_click(lambda event, p=page_num: self._go_to_page(p))
controls.append(page_btn)
# Next button
if current_page < total_pages:
next_btn = pn.widgets.Button(
name="Next →",
button_style="outline",
margin=(5, 10)
)
next_btn.on_click(lambda event: self._go_to_page(current_page + 1))
controls.append(next_btn)
return pn.Row(*controls)
def _go_to_page(self, page_num: int):
"""Navigate to a specific page"""
self.current_page = page_num # Reactivity will handle the update automatically
@param.depends('search_term', 'current_page', watch=True)
def _update_display(self):
"""Update the apps display based on current filters and page"""
# Get filtered apps
filtered_apps = self._get_filtered_apps()
# Get paginated apps
paginated_apps = self._get_paginated_apps(filtered_apps)
# Clear current display
self._apps_container.clear()
self._pagination.clear()
if not paginated_apps:
# No apps found
no_apps_msg = pn.pane.HTML(
"<div style='text-align: center; padding: 40px; color: #666;'>"
"<h3>No apps found</h3>"
"<p>Try adjusting your search terms.</p>"
"</div>",
sizing_mode="stretch_width"
)
self._apps_container.append(no_apps_msg)
else:
# Create grid of app cards
self._apps_container[:] = [self._create_app_card(app) for app in paginated_apps]
# Update pagination
pagination_controls = self._create_pagination_controls(len(filtered_apps))
if len(pagination_controls):
self._pagination[:] = pagination_controls
def __panel__(self):
"""Return the panel for notebook display"""
return self._layout
@classmethod
def create_app(cls, **params):
"""Create a servable app instance"""
instance = cls(**params)
return pn.template.FastListTemplate(
title="Holoviz Panel",
sidebar_width=0, # No sidebar needed
header_background=ACCENT_COLOR,
main_layout=None,
main = instance
)
# Serve the app
if __name__ == "__main__":
# Run with: python index.py
AppGallery.create_app().show(port=5007, autoreload=True, open=True)
elif pn.state.served:
# Run with: panel serve *.py --index index.py --dev --show
AppGallery.create_app().servable()