Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
# transcribe-ui
User interface for the SUNET transcription service

## Features

### Admin Dashboard
The admin dashboard provides comprehensive management capabilities including:
- **Group Management**: Create and manage user groups with transcription quotas
- **User Management**: Enable/disable users and assign admin roles
- **Customer Management**: View and manage customer accounts
- **Price Plan Display**: View current price plan and remaining blocks (for fixed plans)
- Shows plan name and type
- Displays blocks remaining with visual progress indicator
- Color-coded warnings when blocks are running low
- **Statistics**: View detailed transcription statistics per group and user
- **Health Monitoring**: Monitor system health and worker status (BOFH users only)

## Development environment

1. Edit the environment settings, should be in a file named `.env`. The following settings should be sufficient for most cases:
Expand Down
113 changes: 93 additions & 20 deletions pages/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,22 @@ def user_statistics_get(group_id: str) -> dict:
print(f"Error fetching user statistics: {e}")
return {}

def priceplan_get() -> dict:
"""
Fetch price plan information from backend.
"""

try:
res = requests.get(
settings.API_URL + "/api/v1/admin/priceplan", headers=get_auth_header()
)
res.raise_for_status()

return res.json()
except requests.RequestException as e:
print(f"Error fetching price plan: {e}")
return {}

def create_group_dialog(page: callable) -> None:
with ui.dialog() as create_group_dialog:
with ui.card().style("width: 500px; max-width: 90vw;"):
Expand Down Expand Up @@ -581,6 +597,58 @@ def statistics(group_id: str) -> None:
ui.icon("search")


def create_priceplan_card() -> None:
"""
Create a card to display price plan information for admins.
"""
priceplan_data = priceplan_get()

if not priceplan_data:
return

try:
result = priceplan_data.get("result", {})
plan_type = result.get("plan_type", "Unknown")
plan_name = result.get("plan_name", "Unknown")
blocks_remaining = result.get("blocks_remaining")
total_blocks = result.get("total_blocks")

with ui.card().classes("my-2").style("width: 100%; box-shadow: none; border: 2px solid #082954; padding: 16px; background-color: #f8f9fa;"):
with ui.row().style("justify-content: space-between; align-items: center; width: 100%;"):
with ui.column().style("flex: 1;"):
ui.label("Current Price Plan").classes("text-h5 font-bold").style("color: #082954;")
ui.label(f"Plan: {plan_name}").classes("text-lg font-medium")
ui.label(f"Type: {plan_type}").classes("text-md")

# Display blocks remaining if it's a fixed plan
if plan_type.lower() == "fixed" and blocks_remaining is not None:
with ui.row().classes("items-center gap-2 mt-2"):
ui.icon("inventory_2").classes("text-2xl").style("color: #082954;")
if total_blocks is not None:
ui.label(f"Blocks remaining: {blocks_remaining} / {total_blocks}").classes("text-lg font-semibold").style("color: #082954;")
else:
ui.label(f"Blocks remaining: {blocks_remaining}").classes("text-lg font-semibold").style("color: #082954;")

# Add a progress bar for visual representation
if total_blocks is not None and total_blocks > 0:
percentage = (blocks_remaining / total_blocks) * 100
with ui.column().style("width: 100%; margin-top: 8px;"):
ui.linear_progress(value=percentage/100).props(f"color={'positive' if percentage > 50 else 'warning' if percentage > 20 else 'negative'}")

# Show warning if blocks are running low
if percentage <= 20 and blocks_remaining > 0:
with ui.row().classes("items-center gap-2 mt-2"):
ui.icon("warning").classes("text-xl").style("color: #f57c00;")
ui.label("Warning: Blocks running low!").classes("text-sm font-medium").style("color: #f57c00;")
elif blocks_remaining == 0:
with ui.row().classes("items-center gap-2 mt-2"):
ui.icon("error").classes("text-xl").style("color: #d32f2f;")
ui.label("No blocks remaining!").classes("text-sm font-medium").style("color: #d32f2f;")

except (KeyError, TypeError) as e:
print(f"Error displaying price plan: {e}")


def create() -> None:
@ui.refreshable
@ui.page("/admin")
Expand Down Expand Up @@ -630,28 +698,33 @@ def admin() -> None:
.props("color=white flat")
)
customers.on("click", lambda: ui.navigate.to("/admin/customers"))
groups = groups_get()

if not groups:
ui.label("No groups found. Create a new group to get started.").classes("text-lg")
return
with ui.scroll_area().style("height: calc(100vh - 160px); width: 100%;"):
groups = sorted(
groups_get()["result"],
key=lambda x: (x["name"].lower() != "all users", x["name"].lower())
# Display price plan information
create_priceplan_card()

groups = groups_get()

if not groups:
ui.label("No groups found. Create a new group to get started.").classes("text-lg")
return

with ui.scroll_area().style("height: calc(100vh - 160px); width: 100%;"):
groups = sorted(
groups_get()["result"],
key=lambda x: (x["name"].lower() != "all users", x["name"].lower())
)
for group in groups:
g = Group(
group_id=group["id"],
name=group["name"],
description=group["description"],
created_at=group["created_at"],
users=group["users"],
nr_users=group["nr_users"],
stats=group["stats"],
quota_seconds=group["quota_seconds"],
)
for group in groups:
g = Group(
group_id=group["id"],
name=group["name"],
description=group["description"],
created_at=group["created_at"],
users=group["users"],
nr_users=group["nr_users"],
stats=group["stats"],
quota_seconds=group["quota_seconds"],
)
g.create_card()
g.create_card()

@ui.page("/admin/users")
def users() -> None:
Expand Down