/
dmukhin
/
SpherePlatform
Обзор
Документация
Войти
/
dmukhin
/
SpherePlatform
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
app/app.py
14 159 строк
591 KB
OIS\sabogdan
Update for release
23 июн 2026, 07:26
23 июн 2026, 07:26
38e41eb
Код
Авторство
О чём код?
import os import json import re import uuid import warnings from typing import Any from urllib import error as url_error from urllib import parse as url_parse from urllib import request as url_request warnings.filterwarnings( "ignore", message=r".*rx\.Base has been deprecated.*", category=DeprecationWarning, ) import reflex as rx from dotenv import load_dotenv from pydantic import BaseModel, Field from starlette.requests import Request from starlette.responses import JSONResponse from supabase import Client, create_client try: import psycopg # type: ignore except Exception: # pragma: no cover psycopg = None try: import psycopg2 # type: ignore except Exception: # pragma: no cover psycopg2 = None try: import pandas as pd import io except Exception: # pragma: no cover pd = None io = None try: import matplotlib.pyplot as plt import matplotlib matplotlib.use('Agg') # Use non-interactive backend except Exception: # pragma: no cover plt = None matplotlib = None load_dotenv() from app.i18n import UI_STR_EN, UI_STR_RU, ui_str def ui_txt(key: str): """Reactive label from ``BrowserState.ui_locale`` (BrowserState defined below).""" return rx.cond( BrowserState.ui_locale == "ru", UI_STR_RU[key], UI_STR_EN[key], ) def _normalize_supabase_env() -> None: """Map common local Supabase Docker env names to app env names.""" if not os.getenv("SUPABASE_ANON_KEY"): docker_anon_key = os.getenv("ANON_KEY") if docker_anon_key: os.environ["SUPABASE_ANON_KEY"] = docker_anon_key if not os.getenv("SUPABASE_SERVICE_ROLE_KEY"): docker_service_key = os.getenv("SERVICE_ROLE_KEY") if docker_service_key: os.environ["SUPABASE_SERVICE_ROLE_KEY"] = docker_service_key if not os.getenv("SUPABASE_URL"): docker_url = os.getenv("SUPABASE_PUBLIC_URL") or os.getenv("API_EXTERNAL_URL") kong_http_port = str(os.getenv("KONG_HTTP_PORT", "")).strip() if kong_http_port.isdigit(): # Some local Docker setups keep API_EXTERNAL_URL=...:8000 while # actually publishing Kong to a different host port. if not docker_url or docker_url.rstrip("/").endswith(":8000"): docker_url = f"http://127.0.0.1:{kong_http_port}" if not docker_url: docker_url = "http://127.0.0.1:8000" os.environ["SUPABASE_URL"] = docker_url _normalize_supabase_env() def _sphere_supabase_client_for_api() -> Client: url = os.getenv("SUPABASE_URL", "http://127.0.0.1:8000") key = os.getenv("SUPABASE_ANON_KEY") if not key: raise ValueError("SUPABASE_ANON_KEY is not set.") return create_client(url, key) def _application_group_row_sort_key(row: dict[str, Any]) -> tuple[str, str]: """Text sort_order (lexical); empty sort_order sorts last.""" name = str(row.get("name") or "").strip().lower() or "" raw = str(row.get("sort_order") or "").strip() if not raw: return ("\uffff", name) return (raw, name) def _sorted_application_group_rows(rows: list[Any]) -> list[dict[str, Any]]: ordered = [r for r in rows if isinstance(r, dict)] ordered.sort(key=_application_group_row_sort_key) return ordered COLOR_CARD = "#121a2e" COLOR_CARD_ALT = "#0f172a" COLOR_TEXT = "#e5e7eb" COLOR_MUTED = "#9ca3af" COLOR_PLACEHOLDER = "#cbd5e1" COLOR_ACCENT = "#3b82f6" COLOR_FOCUS = "#60a5fa" VISUAL_SERIES_COLORS = [ "#60a5fa", "#34d399", "#f59e0b", "#a78bfa", "#f472b6", "#22d3ee", "#fb7185", "#84cc16", ] COLOR_BORDER = "#2a3655" COLOR_ERROR = "#f87171" MOCK_LOGIN_EMAIL = "sts@sts.com" MOCK_LOGIN_PASSWORD = "sts" INPUT_STYLE = { "color": COLOR_TEXT, "font_size": "0.84rem", "white_space": "nowrap", "size": "1", "variant": "surface", "background": COLOR_CARD_ALT, "border": f"1px solid {COLOR_BORDER}", "border_radius": "8px", "_hover": {"border_color": COLOR_ACCENT}, "_focus": { "border_color": COLOR_FOCUS, "box_shadow": f"0 0 0 1px {COLOR_FOCUS}", }, } INLINE_EDIT_INPUT_STYLE = { **INPUT_STYLE, "background": "#17223d", "border": "1px solid transparent", "_hover": {"border_color": "transparent"}, "_focus": { "border_color": "transparent", "box_shadow": "none", }, } SELECT_STYLE = { "color": COLOR_TEXT, "background": COLOR_CARD_ALT, "border": f"1px solid {COLOR_BORDER}", "border_radius": "8px", "size": "1", "variant": "surface", "color_scheme": "gray", "_hover": {"border_color": COLOR_ACCENT}, "_focus": { "border_color": COLOR_FOCUS, "box_shadow": f"0 0 0 1px {COLOR_FOCUS}", }, } SERVICE_DATA_EDITOR_THEME = { "accent_color": COLOR_ACCENT, "accent_fg": COLOR_TEXT, "accent_light": "#17223d", "bg_cell": COLOR_CARD_ALT, "bg_cell_medium": COLOR_CARD, "bg_header": COLOR_CARD, "bg_header_hovered": "#17223d", "bg_header_has_focus": "#1b2a47", "border_color": COLOR_BORDER, "horizontal_border_color": COLOR_BORDER, "header_bottom_border_color": COLOR_BORDER, "text_dark": COLOR_TEXT, "text_medium": COLOR_MUTED, "text_light": COLOR_MUTED, "text_header": COLOR_TEXT, "text_header_selected": COLOR_TEXT, "link_color": COLOR_ACCENT, # Keep text crisp and slightly larger without CSS zoom scaling. "editor_font_size": "1rem", "base_font_style": "1rem Inter, system-ui, sans-serif", "header_font_style": "700 1rem Inter, system-ui, sans-serif", "line_height": 24, "cell_vertical_padding": 8, "cell_horizontal_padding": 10, } # Keep a small vertical buffer to avoid an extra internal scrollbar # when the horizontal scrollbar/gutter is present. DATA_EDITOR_ROW_HEIGHT = 42 DATA_EDITOR_HEADER_HEIGHT = 42 DATA_EDITOR_SCROLLBAR_GUTTER = 16 # Glide DataEditor breaks inside ``rx.foreach``; Run all uses fixed indexed slots. EXPAND_ALL_MAX_SLOTS = 24 def _build_row_grid_selection( row_idx: int, col_idx: int, total_rows: int, total_cols: int ) -> dict[str, Any]: """Build a row-wide grid selection for reflex data editor.""" if row_idx < 0 or row_idx >= total_rows: return {} normalized_cols = total_cols if total_cols > 0 else 1 normalized_col_idx = col_idx if 0 <= col_idx < normalized_cols else 0 return { "current": { "cell": (normalized_col_idx, row_idx), "range": {"x": 0, "y": row_idx, "width": normalized_cols, "height": 1}, "rangeStack": [], }, "columns": {"items": []}, "rows": {"items": [[row_idx, row_idx + 1]]}, } def _extract_cell_from_grid_selection(selection: Any) -> tuple[int, int] | None: """Extract active cell coordinates from data editor grid selection payload.""" if not isinstance(selection, dict): return None current = selection.get("current") if not isinstance(current, dict): return None cell = current.get("cell") if not isinstance(cell, (list, tuple)) or len(cell) < 2: return None try: return int(cell[0]), int(cell[1]) except (TypeError, ValueError): return None def _extract_column_index_from_header_position(position: Any) -> int | None: """Extract clicked header column index from data editor payload.""" candidate: Any = None if isinstance(position, dict): for key in ("col", "column", "x", "column_idx"): if key in position: candidate = position.get(key) break if candidate is None: cell = position.get("cell") if isinstance(cell, dict): for key in ("col", "column", "x"): if key in cell: candidate = cell.get(key) break elif isinstance(cell, (list, tuple)) and len(cell) > 0: candidate = cell[0] elif isinstance(position, (list, tuple)) and len(position) > 0: candidate = position[0] else: for attr in ("col", "column", "x"): if hasattr(position, attr): candidate = getattr(position, attr) break try: return int(candidate) except (TypeError, ValueError): return None class TreeNode(rx.Base): id: str label: str children: list[str] class TableRow(rx.Base): cells: list[str] class ApplicationTile(rx.Base): id: str name: str app_type: str # Executable payload (SQL, URL, master/detail text, custom table name, …). content: str color: str # Dashboard tile preview from DB ``description`` (not ``content``). description: str = "" group_id: str = "" group_name: str = "" class ApplicationGroupTiles(rx.Base): id: str name: str tiles: list[ApplicationTile] class ApplicationNavGroup(rx.Base): id: str name: str class NavBreadcrumbItem(rx.Base): label: str idx: int is_current: bool class DashboardMdExpandSession(rx.Base): """Interactive master/detail block for Applications «Run all».""" master_table: str = "" detail_table: str = "" master_key: str = "" detail_key: str = "" master_grid_columns: list[dict[str, Any]] = [] master_rows: list[list[str]] = [] master_key_values: list[str] = [] selected_master_idx: int = -1 master_grid_selection: dict[str, Any] = {} detail_grid_columns: list[dict[str, Any]] = [] detail_rows: list[list[str]] = [] detail_columns: list[str] = [] detail_row_ids: list[str] = [] detail_selected_row_idx: int = -1 detail_grid_selection: dict[str, Any] = {} detail_adding_row: bool = False detail_editing_idx: int = -1 detail_edit_original_row: list[str] = [] caption: str = "" class DashboardExpandedApp(rx.Base): """Single app block in Applications «Run all» stack view.""" tile_id: str title: str type_label: str kind: str # grid | iframe | chart | md | text | error message: str = "" grid_columns: list[dict[str, Any]] = [] grid_rows: list[list[str]] = [] iframe_src: str = "" chart_src: str = "" # Interactive chart (Run all); same shape as visuals_chart_data / visuals_data_series chart_points: list[dict[str, Any]] = [] chart_series: list[dict[str, str]] = [] chart_axis_label: str = "" chart_is_histogram: bool = False md_session: DashboardMdExpandSession | None = None grid_table_name: str = "" grid_row_ids: list[str] = [] grid_editable: bool = False def _row_label(row: dict[str, Any]) -> str: for key in ("name", "title", "display_name", "slug", "code", "field_name"): value = row.get(key) if value: return str(value) return f"id:{row.get('id', 'unknown')}" def _pick_fk(row: dict[str, Any], candidates: list[str]) -> Any: for key in candidates: if key in row and row.get(key) is not None: return row.get(key) return None def _build_group_nodes( *, groups: list[dict[str, Any]], children: list[dict[str, Any]], node_prefix: str, child_fk_candidates: list[str], child_table_name: str, ) -> tuple[list[TreeNode], dict[str, str]]: nodes: list[TreeNode] = [] leaf_meta: dict[str, str] = {} for group in groups: group_id = str(group.get("id", "unknown")) parent_node_id = f"{node_prefix}-{group_id}" group_child_rows = [ child for child in children if str(_pick_fk(child, child_fk_candidates)) == group_id ] group_children = [_row_label(child) for child in group_child_rows] for child_idx, child in enumerate(group_child_rows): child_id = str(child.get("id", "")) if child_id: leaf_meta[f"{parent_node_id}::{child_idx}"] = ( f"{child_table_name}|id|{child_id}" ) nodes.append( TreeNode( id=parent_node_id, label=_row_label(group), children=group_children, ) ) return nodes, leaf_meta def _build_user_group_nodes( *, groups: list[dict[str, Any]], members: list[dict[str, Any]] ) -> tuple[list[TreeNode], dict[str, str]]: nodes: list[TreeNode] = [] leaf_meta: dict[str, str] = {} for group in groups: group_id = str(group.get("id", "unknown")) parent_node_id = f"user-group-{group_id}" group_member_rows = [ member for member in members if str(member.get("user_group_id")) == group_id and member.get("user_id") ] group_members = [ f"user:{str(member.get('user_id'))[:8]}" for member in group_member_rows ] for child_idx, member in enumerate(group_member_rows): user_id = str(member.get("user_id", "")) if user_id: leaf_meta[f"{parent_node_id}::{child_idx}"] = ( f"user_group_members|composite|{group_id}|{user_id}" ) nodes.append( TreeNode( id=parent_node_id, label=_row_label(group), children=group_members, ) ) return nodes, leaf_meta def _build_class_nodes( *, classes: list[dict[str, Any]], class_fields: list[dict[str, Any]], objects: list[dict[str, Any]], ) -> tuple[list[TreeNode], dict[str, str]]: nodes: list[TreeNode] = [] leaf_meta: dict[str, str] = {} for class_row in classes: class_id = str(class_row.get("id", "unknown")) parent_node_id = f"class-{class_id}" class_name = class_row.get("name") or _row_label(class_row) object_rows = [row for row in objects if str(row.get("class_id")) == class_id] children_labels: list[str] = [] for child_idx, obj in enumerate(object_rows): object_id = str(obj.get("id", "")) short_id = object_id[:8] if object_id else "unknown" children_labels.append(f"object:{short_id}") if object_id: leaf_meta[f"{parent_node_id}::{child_idx}"] = f"object|id|{object_id}" nodes.append( TreeNode( id=parent_node_id, label=str(class_name), children=children_labels, ) ) return nodes, leaf_meta def _build_object_history_nodes( *, objects: list[dict[str, Any]], object_history: list[dict[str, Any]] ) -> tuple[list[TreeNode], dict[str, str]]: nodes: list[TreeNode] = [] leaf_meta: dict[str, str] = {} for obj in objects: object_id = str(obj.get("id", "unknown")) parent_node_id = f"object-{object_id}" object_short = object_id[:8] history_rows = [ row for row in object_history if str(row.get("object_id")) == object_id ] children_labels: list[str] = [] for child_idx, hist in enumerate(history_rows): field_name = str(hist.get("field_name", "field")) raw_value = str(hist.get("value", "")) value_preview = raw_value if len(raw_value) <= 36 else f"{raw_value[:33]}..." children_labels.append(f"{field_name}: {value_preview}") history_id = str(hist.get("id", "")) if history_id: leaf_meta[f"{parent_node_id}::{child_idx}"] = ( f"object_history|id|{history_id}" ) nodes.append( TreeNode( id=parent_node_id, label=f"object:{object_short}", children=children_labels, ) ) return nodes, leaf_meta class AppState(rx.State): expanded_nodes: list[str] = [] application_nodes: list[TreeNode] = [] service_nodes: list[TreeNode] = [] user_group_nodes: list[TreeNode] = [] is_loading: bool = False load_error: str = "" selected_table_title: str = "Select a group in panel1" selected_table_columns: list[str] = [] grid_columns: list[dict[str, Any]] = [] selected_table_rows: list[list[str]] = [] selected_row_ids: list[str] = [] selected_table_name: str = "" grid_selected_row_idx: int = -1 grid_selection: dict[str, Any] = {} is_table_loading: bool = False sort_column_idx: int = -1 sort_desc: bool = False leaf_meta: dict[str, str] = {} node_titles: dict[str, str] = {} is_vertical_detail: bool = False detail_pk_mode: str = "" detail_pk_1: str = "" detail_pk_2: str = "" new_class_name: str = "" new_class_description: str = "" selected_class_id: str = "" new_field_name: str = "" new_field_type: str = "string" new_object_class_id: str = "" new_object_data: str = "{}" def _sync_grid_columns(self): max_cols = ( min(len(row) for row in self.selected_table_rows) if self.selected_table_rows else len(self.selected_table_columns) ) self.grid_columns = [ {"title": col, "id": col, "type": "str"} for col in self.selected_table_columns if (col or "").strip() ][:max_cols] cols_len = len(self.grid_columns) if cols_len >= 0: self.selected_table_rows = [ ([str(cell) for cell in row[:cols_len]] + [""] * max(0, cols_len - len(row))) for row in self.selected_table_rows ] def refresh_data(self): self.load_tree() def create_class(self): class_name = self.new_class_name.strip() if not class_name: self.load_error = "Class name is required." return self.load_error = "" try: client = self._create_client() payload = { "name": class_name, "description": self.new_class_description.strip() or None, } client.table("class").insert(payload).execute() self.new_class_name = "" self.new_class_description = "" self._reload_tree_nodes(client) if self.selected_table_name == "class": rows = client.table("class").select("*").execute().data or [] self._set_selected_table(title="class", table_name="class", rows=rows) except Exception as exc: self.load_error = f"Supabase create failed: {exc}" def delete_class_row(self, row_idx: int): if self.selected_table_name != "class": return if row_idx < 0 or row_idx >= len(self.selected_row_ids): return row_id = self.selected_row_ids[row_idx] if not row_id: return self.load_error = "" try: client = self._create_client() client.table("class").delete().eq("id", row_id).execute() self._reload_tree_nodes(client) rows = client.table("class").select("*").execute().data or [] self._set_selected_table(title="class", table_name="class", rows=rows) except Exception as exc: self.load_error = f"Supabase delete failed: {exc}" def create_class_field(self): if self.selected_table_name != "class_field" or not self.selected_class_id: self.load_error = "Select a class node first." return field_name = self.new_field_name.strip() field_type = self.new_field_type.strip() if not field_name: self.load_error = "Field name is required." return if not field_type: self.load_error = "Field type is required." return self.load_error = "" try: client = self._create_client() payload = { "class_id": self.selected_class_id, "name": field_name, "type": field_type, "is_required": False, } client.table("class_field").insert(payload).execute() self.new_field_name = "" self._reload_tree_nodes(client) rows = ( client.table("class_field") .select("*") .eq("class_id", self.selected_class_id) .execute() .data or [] ) self._set_selected_table( title=self.selected_table_title, table_name="class_field", rows=rows ) except Exception as exc: self.load_error = f"Supabase create failed: {exc}" def create_object(self): class_id = self.selected_class_id or self.new_object_class_id.strip() if not class_id: self.load_error = "Class ID is required to create object." return raw_data = self.new_object_data.strip() or "{}" try: parsed_data = json.loads(raw_data) except Exception: self.load_error = "Object data must be valid JSON." return self.load_error = "" try: client = self._create_client() client.table("object").insert( {"class_id": class_id, "data": parsed_data} ).execute() self.new_object_data = "{}" if not self.selected_class_id: self.new_object_class_id = "" self._reload_tree_nodes(client) if self.selected_table_name == "object": rows = client.table("object").select("*").execute().data or [] self._set_selected_table(title="object", table_name="object", rows=rows) except Exception as exc: self.load_error = f"Supabase create failed: {exc}" def delete_object_row(self, row_idx: int): if self.selected_table_name != "object": return if row_idx < 0 or row_idx >= len(self.selected_row_ids): return row_id = self.selected_row_ids[row_idx] if not row_id: return self.load_error = "" try: client = self._create_client() client.table("object").delete().eq("id", row_id).execute() self._reload_tree_nodes(client) rows = client.table("object").select("*").execute().data or [] self._set_selected_table(title="object", table_name="object", rows=rows) except Exception as exc: self.load_error = f"Supabase delete failed: {exc}" def _reload_tree_nodes(self, client: Client): classes = client.table("class").select("*").execute().data or [] class_fields = client.table("class_field").select("*").execute().data or [] objects = client.table("object").select("*").execute().data or [] object_history = client.table("object_history").select("*").execute().data or [] self.application_nodes, class_leaf_meta = _build_class_nodes( classes=classes, class_fields=class_fields, objects=objects, ) self.service_nodes, history_leaf_meta = _build_object_history_nodes( objects=objects, object_history=object_history, ) self.user_group_nodes = [] self.leaf_meta = {**class_leaf_meta, **history_leaf_meta} self.node_titles = { **{node.id: node.label for node in self.application_nodes}, **{node.id: node.label for node in self.service_nodes}, } def toggle_node(self, node_id: str): if node_id in self.expanded_nodes: self.expanded_nodes = [ current_id for current_id in self.expanded_nodes if current_id != node_id ] else: self.expanded_nodes = [*self.expanded_nodes, node_id] def load_tree(self): self.is_loading = True self.load_error = "" try: url = os.getenv("SUPABASE_URL", "http://127.0.0.1:8000") key = os.getenv("SUPABASE_ANON_KEY") if not key: raise ValueError( "Set SUPABASE_ANON_KEY in environment." ) client: Client = create_client(url, key) self._reload_tree_nodes(client) self.expanded_nodes = [] self.selected_table_title = "Select a node in panel1" self.selected_table_columns = [] self._sync_grid_columns() self.selected_table_rows = [] self.selected_row_ids = [] self.selected_table_name = "" self.sort_column_idx = -1 self.sort_desc = False self.is_vertical_detail = False self.detail_pk_mode = "" self.detail_pk_1 = "" self.detail_pk_2 = "" self.selected_class_id = "" self.new_field_name = "" self.new_field_type = "string" self.new_object_class_id = "" self.new_object_data = "{}" except Exception as exc: self.load_error = f"Supabase load failed: {exc}" finally: self.is_loading = False def _create_client(self) -> Client: url = os.getenv("SUPABASE_URL", "http://127.0.0.1:8000") key = os.getenv("SUPABASE_ANON_KEY") if not key: raise ValueError("Set SUPABASE_ANON_KEY in environment.") return create_client(url, key) def _normalize_table_rows( self, rows: list[dict[str, Any]] ) -> tuple[list[str], list[str], list[list[str]]]: if not rows: return [], [], [] preferred_order = [ "id", "name", "type", "code", "description", "class_id", "is_required", "object_id", "field_name", "value", "data", "user_id", "user_group_id", "application_id", "application_group_id", "service_group_id", "endpoint_url", "created_at", "updated_at", "changed_at", ] row_keys = {key for row in rows for key in row.keys()} ordered_columns = [key for key in preferred_order if key in row_keys] ordered_columns.extend(sorted(row_keys - set(ordered_columns))) row_ids = [str(row.get("id", "")) for row in rows] table_rows = [ [str(row.get(column, "")) for column in ordered_columns] for row in rows ] return ordered_columns, row_ids, table_rows def _set_selected_table( self, title: str, table_name: str, rows: list[dict[str, Any]] ): columns, row_ids, table_rows = self._normalize_table_rows(rows) self.selected_table_title = title self.selected_table_name = table_name self.selected_table_columns = columns self._sync_grid_columns() self.selected_row_ids = row_ids self.selected_table_rows = table_rows self.is_vertical_detail = False self.detail_pk_mode = "" self.detail_pk_1 = "" self.detail_pk_2 = "" self.grid_selected_row_idx = -1 self.grid_selection = {} self._apply_default_grid_sort() def _apply_default_grid_sort(self): if not self.selected_table_rows or not self.grid_columns: self.sort_column_idx = -1 self.sort_desc = False return def sort_value(raw: str): value = (raw or "").strip() if value == "": return (2, "") try: return (0, float(value)) except ValueError: return (1, value.casefold()) column_idx = 0 if ( not self.is_vertical_detail and len(self.selected_row_ids) == len(self.selected_table_rows) and self.selected_row_ids ): pairs = list(zip(self.selected_row_ids, self.selected_table_rows)) else: pairs = [("", row[:]) for row in self.selected_table_rows] pairs.sort( key=lambda pair: sort_value(pair[1][column_idx]) if column_idx < len(pair[1]) else (2, ""), reverse=True, ) if self.is_vertical_detail or not self.selected_row_ids: self.selected_row_ids = [] else: self.selected_row_ids = [pair[0] for pair in pairs] self.selected_table_rows = [pair[1] for pair in pairs] self.sort_column_idx = 0 self.sort_desc = True @staticmethod def _extract_editor_value(cell: Any) -> str: if isinstance(cell, dict): if "data" in cell: return "" if cell["data"] is None else str(cell["data"]) if "displayData" in cell: return "" if cell["displayData"] is None else str(cell["displayData"]) return "" if cell is None else str(cell) def select_grid_cell(self, position: tuple[int, int]): if not isinstance(position, (list, tuple)) or len(position) < 2: return try: col_idx = int(position[0]) row_idx = int(position[1]) except (TypeError, ValueError): return if 0 <= row_idx < len(self.selected_table_rows): self.grid_selected_row_idx = row_idx self.grid_selection = _build_row_grid_selection( row_idx=row_idx, col_idx=col_idx, total_rows=len(self.selected_table_rows), total_cols=len(self.grid_columns), ) else: self.grid_selected_row_idx = -1 self.grid_selection = {} def handle_grid_selection_change(self, selection: Any): position = _extract_cell_from_grid_selection(selection) if position is None: self.grid_selected_row_idx = -1 self.grid_selection = {} return self.select_grid_cell(position) def handle_grid_cell_edited(self, position: tuple[int, int], cell: Any): if not isinstance(position, (list, tuple)) or len(position) < 2: return try: col_idx = int(position[0]) row_idx = int(position[1]) except (TypeError, ValueError): return value = self._extract_editor_value(cell) self.grid_selected_row_idx = row_idx self.grid_selection = _build_row_grid_selection( row_idx=row_idx, col_idx=col_idx, total_rows=len(self.selected_table_rows), total_cols=len(self.grid_columns), ) if self.is_vertical_detail: if col_idx == 1: self.update_detail_value(row_idx, value) return self.update_cell_value(row_idx, col_idx, value) def delete_selected_grid_row(self): if self.grid_selected_row_idx < 0: return if self.selected_table_name == "class": self.delete_class_row(self.grid_selected_row_idx) elif self.selected_table_name == "object": self.delete_object_row(self.grid_selected_row_idx) def sort_table_by_column(self, column_idx: int): if column_idx < 0 or column_idx >= len(self.selected_table_columns): return def sort_value(raw: str): value = (raw or "").strip() if value == "": return (2, "") try: return (0, float(value)) except ValueError: return (1, value.casefold()) desc = self.sort_column_idx == column_idx and not self.sort_desc pairs = list(zip(self.selected_row_ids, self.selected_table_rows)) pairs.sort( key=lambda pair: sort_value(pair[1][column_idx]) if column_idx < len(pair[1]) else (2, ""), reverse=desc, ) self.selected_row_ids = [pair[0] for pair in pairs] self.selected_table_rows = [pair[1] for pair in pairs] self.sort_column_idx = column_idx self.sort_desc = desc def handle_grid_header_clicked(self, position: Any): column_idx = _extract_column_index_from_header_position(position) if column_idx is None: return self.sort_table_by_column(column_idx) def update_cell_value(self, row_idx: int, column_idx: int, value: str): if ( row_idx < 0 or row_idx >= len(self.selected_table_rows) or column_idx < 0 or column_idx >= len(self.selected_table_columns) ): return updated_rows = [row[:] for row in self.selected_table_rows] updated_rows[row_idx][column_idx] = value self.selected_table_rows = updated_rows if row_idx >= len(self.selected_row_ids): return row_id = self.selected_row_ids[row_idx] column_name = self.selected_table_columns[column_idx] if not row_id or not self.selected_table_name: return if column_name in ("id", "created_at"): return try: client = self._create_client() client.table(self.selected_table_name).update({column_name: value}).eq( "id", row_id ).execute() if column_name == "name": self._reload_tree_nodes(client) except Exception as exc: self.load_error = f"Supabase update failed: {exc}" def select_leaf_record(self, parent_id: str, child_idx: int): meta_key = f"{parent_id}::{child_idx}" encoded = self.leaf_meta.get(meta_key) if not encoded: return parts = encoded.split("|") if len(parts) < 3: return table_name = parts[0] pk_mode = parts[1] self.is_table_loading = True self.load_error = "" try: client = self._create_client() if pk_mode == "id": pk_1 = parts[2] rows = ( client.table(table_name) .select("*") .eq("id", pk_1) .limit(1) .execute() .data or [] ) pk_2 = "" else: return record = rows[0] if rows else {} if not record: self.selected_table_title = f"{table_name} record" self.selected_table_columns = [] self._sync_grid_columns() self.selected_table_rows = [] self.selected_row_ids = [] self.selected_table_name = table_name self.is_vertical_detail = True self.detail_pk_mode = pk_mode self.detail_pk_1 = pk_1 self.detail_pk_2 = pk_2 return preferred_order = [ "id", "name", "type", "code", "description", "class_id", "is_required", "object_id", "field_name", "value", "data", "user_id", "user_group_id", "application_id", "application_group_id", "service_group_id", "endpoint_url", "created_at", "updated_at", "changed_at", ] fields = [k for k in preferred_order if k in record] fields.extend(sorted(set(record.keys()) - set(fields))) self.selected_table_title = f"{table_name} record" self.selected_table_name = table_name self.selected_table_columns = ["field", "value"] self._sync_grid_columns() self.selected_row_ids = [] self.selected_table_rows = [[field, str(record.get(field, ""))] for field in fields] self.is_vertical_detail = True self.detail_pk_mode = pk_mode self.detail_pk_1 = pk_1 self.detail_pk_2 = pk_2 self._apply_default_grid_sort() except Exception as exc: self.load_error = f"Supabase load failed: {exc}" finally: self.is_table_loading = False def update_detail_value(self, row_idx: int, value: str): if not self.is_vertical_detail: return if row_idx < 0 or row_idx >= len(self.selected_table_rows): return field_name = self.selected_table_rows[row_idx][0] if field_name in ("id", "created_at"): return updated_rows = [row[:] for row in self.selected_table_rows] updated_rows[row_idx][1] = value self.selected_table_rows = updated_rows if not self.selected_table_name or not self.detail_pk_mode: return try: client = self._create_client() query = client.table(self.selected_table_name).update({field_name: value}) query = query.eq("id", self.detail_pk_1) query.execute() if field_name == "name": self._reload_tree_nodes(client) except Exception as exc: self.load_error = f"Supabase update failed: {exc}" def select_group_table(self, node_id: str): mapping: list[tuple[str, str, str, str]] = [ ("class-", "class_field", "class_id", "class_field"), ("object-", "object_history", "object_id", "object_history"), ] selected = next((item for item in mapping if node_id.startswith(item[0])), None) if not selected: return prefix, table_name, fk_name, title = selected group_id = node_id.removeprefix(prefix) display_title = self.node_titles.get(node_id, title) self.selected_class_id = group_id if prefix == "class-" else "" if prefix == "class-": self.new_object_class_id = group_id self.is_table_loading = True self.load_error = "" try: client = self._create_client() rows = ( client.table(table_name) .select("*") .eq(fk_name, group_id) .execute() .data or [] ) self._set_selected_table( title=display_title, table_name=table_name, rows=rows ) except Exception as exc: self.load_error = f"Supabase load failed: {exc}" self.selected_table_title = display_title self.selected_table_columns = [] self._sync_grid_columns() self.selected_table_rows = [] self.selected_row_ids = [] self.selected_table_name = "" finally: self.is_table_loading = False def select_section_table(self, section_key: str): mapping: dict[str, tuple[str, str]] = { "class": ("class", "class"), "object": ("object", "object"), } selected = mapping.get(section_key) if not selected: return table_name, title = selected self.selected_class_id = "" if table_name != "object": self.new_object_class_id = "" self.is_table_loading = True self.load_error = "" try: client = self._create_client() rows = client.table(table_name).select("*").execute().data or [] self._set_selected_table(title=title, table_name=table_name, rows=rows) except Exception as exc: self.load_error = f"Supabase load failed: {exc}" self.selected_table_title = title self.selected_table_columns = [] self._sync_grid_columns() self.selected_table_rows = [] self.selected_row_ids = [] self.selected_table_name = "" finally: self.is_table_loading = False class AdminState(rx.State): expanded_nodes: list[str] = [] application_nodes: list[TreeNode] = [] service_nodes: list[TreeNode] = [] user_group_nodes: list[TreeNode] = [] is_loading: bool = False load_error: str = "" selected_table_title: str = "Select a group in panel1" selected_table_columns: list[str] = [] grid_columns: list[dict[str, Any]] = [] selected_table_rows: list[list[str]] = [] selected_row_ids: list[str] = [] selected_table_name: str = "" grid_selected_row_idx: int = -1 grid_selection: dict[str, Any] = {} is_table_loading: bool = False sort_column_idx: int = -1 sort_desc: bool = False leaf_meta: dict[str, str] = {} is_vertical_detail: bool = False detail_pk_mode: str = "" detail_pk_1: str = "" detail_pk_2: str = "" def _sync_grid_columns(self): max_cols = ( min(len(row) for row in self.selected_table_rows) if self.selected_table_rows else len(self.selected_table_columns) ) self.grid_columns = [ {"title": col, "id": col, "type": "str"} for col in self.selected_table_columns if (col or "").strip() ][:max_cols] def refresh_data(self): self.load_tree() def _create_client(self) -> Client: url = os.getenv("SUPABASE_URL", "http://127.0.0.1:8000") key = os.getenv("SUPABASE_ANON_KEY") if not key: raise ValueError("Set SUPABASE_ANON_KEY in environment.") return create_client(url, key) def _reload_tree_nodes(self, client: Client): application_groups = ( client.table("application_groups").select("*").execute().data or [] ) applications = client.table("applications").select("*").execute().data or [] service_groups = client.table("service_groups").select("*").execute().data or [] services = client.table("services").select("*").execute().data or [] user_groups = client.table("user_groups").select("*").execute().data or [] user_group_members = ( client.table("user_group_members").select("*").execute().data or [] ) self.application_nodes, app_leaf_meta = _build_group_nodes( groups=application_groups, children=applications, node_prefix="application-group", child_fk_candidates=["application_group_id", "group_id"], child_table_name="applications", ) self.service_nodes, service_leaf_meta = _build_group_nodes( groups=service_groups, children=services, node_prefix="service-group", child_fk_candidates=["service_group_id", "group_id"], child_table_name="services", ) self.user_group_nodes, user_leaf_meta = _build_user_group_nodes( groups=user_groups, members=user_group_members, ) self.leaf_meta = {**app_leaf_meta, **service_leaf_meta, **user_leaf_meta} def load_tree(self): self.is_loading = True self.load_error = "" try: client = self._create_client() self._reload_tree_nodes(client) self.expanded_nodes = [] self.selected_table_title = "Select a group in panel1" self.selected_table_columns = [] self._sync_grid_columns() self.selected_table_rows = [] self.selected_row_ids = [] self.selected_table_name = "" self.sort_column_idx = -1 self.sort_desc = False self.is_vertical_detail = False self.detail_pk_mode = "" self.detail_pk_1 = "" self.detail_pk_2 = "" except Exception as exc: self.load_error = f"Supabase load failed: {exc}" finally: self.is_loading = False def toggle_node(self, node_id: str): if node_id in self.expanded_nodes: self.expanded_nodes = [ current_id for current_id in self.expanded_nodes if current_id != node_id ] else: self.expanded_nodes = [*self.expanded_nodes, node_id] def _normalize_table_rows( self, rows: list[dict[str, Any]] ) -> tuple[list[str], list[str], list[list[str]]]: if not rows: return [], [], [] row_keys = {key for row in rows for key in row.keys()} ordered_columns = sorted(row_keys) row_ids = [str(row.get("id", "")) for row in rows] table_rows = [ [str(row.get(column, "")) for column in ordered_columns] for row in rows ] return ordered_columns, row_ids, table_rows def _set_selected_table( self, title: str, table_name: str, rows: list[dict[str, Any]] ): columns, row_ids, table_rows = self._normalize_table_rows(rows) self.selected_table_title = title self.selected_table_name = table_name self.selected_table_columns = columns self._sync_grid_columns() self.selected_row_ids = row_ids self.selected_table_rows = table_rows self.is_vertical_detail = False self.detail_pk_mode = "" self.detail_pk_1 = "" self.detail_pk_2 = "" self.grid_selected_row_idx = -1 self.grid_selection = {} self._apply_default_grid_sort() def _apply_default_grid_sort(self): if not self.selected_table_rows or not self.grid_columns: self.sort_column_idx = -1 self.sort_desc = False return def sort_value(raw: str): value = (raw or "").strip() if value == "": return (2, "") try: return (0, float(value)) except ValueError: return (1, value.casefold()) column_idx = 0 if ( not self.is_vertical_detail and len(self.selected_row_ids) == len(self.selected_table_rows) and self.selected_row_ids ): pairs = list(zip(self.selected_row_ids, self.selected_table_rows)) else: pairs = [("", row[:]) for row in self.selected_table_rows] pairs.sort( key=lambda pair: sort_value(pair[1][column_idx]) if column_idx < len(pair[1]) else (2, ""), reverse=True, ) if self.is_vertical_detail or not self.selected_row_ids: self.selected_row_ids = [] else: self.selected_row_ids = [pair[0] for pair in pairs] self.selected_table_rows = [pair[1] for pair in pairs] self.sort_column_idx = 0 self.sort_desc = True @staticmethod def _extract_editor_value(cell: Any) -> str: if isinstance(cell, dict): if "data" in cell: return "" if cell["data"] is None else str(cell["data"]) if "displayData" in cell: return "" if cell["displayData"] is None else str(cell["displayData"]) return "" if cell is None else str(cell) def select_grid_cell(self, position: tuple[int, int]): if not isinstance(position, (list, tuple)) or len(position) < 2: return try: col_idx = int(position[0]) row_idx = int(position[1]) except (TypeError, ValueError): return if 0 <= row_idx < len(self.selected_table_rows): self.grid_selected_row_idx = row_idx self.grid_selection = _build_row_grid_selection( row_idx=row_idx, col_idx=col_idx, total_rows=len(self.selected_table_rows), total_cols=len(self.grid_columns), ) else: self.grid_selected_row_idx = -1 self.grid_selection = {} def handle_grid_selection_change(self, selection: Any): position = _extract_cell_from_grid_selection(selection) if position is None: self.grid_selected_row_idx = -1 self.grid_selection = {} return self.select_grid_cell(position) def handle_grid_cell_edited(self, position: tuple[int, int], cell: Any): if not isinstance(position, (list, tuple)) or len(position) < 2: return try: col_idx = int(position[0]) row_idx = int(position[1]) except (TypeError, ValueError): return value = self._extract_editor_value(cell) self.grid_selected_row_idx = row_idx self.grid_selection = _build_row_grid_selection( row_idx=row_idx, col_idx=col_idx, total_rows=len(self.selected_table_rows), total_cols=len(self.grid_columns), ) if self.is_vertical_detail: if col_idx == 1: self.update_detail_value(row_idx, value) return self.update_cell_value(row_idx, col_idx, value) def sort_table_by_column(self, column_idx: int): if column_idx < 0 or column_idx >= len(self.selected_table_columns): return def sort_value(raw: str): value = (raw or "").strip() if value == "": return (2, "") try: return (0, float(value)) except ValueError: return (1, value.casefold()) desc = self.sort_column_idx == column_idx and not self.sort_desc pairs = list(zip(self.selected_row_ids, self.selected_table_rows)) pairs.sort( key=lambda pair: sort_value(pair[1][column_idx]) if column_idx < len(pair[1]) else (2, ""), reverse=desc, ) self.selected_row_ids = [pair[0] for pair in pairs] self.selected_table_rows = [pair[1] for pair in pairs] self.sort_column_idx = column_idx self.sort_desc = desc def handle_grid_header_clicked(self, position: Any): column_idx = _extract_column_index_from_header_position(position) if column_idx is None: return self.sort_table_by_column(column_idx) def update_cell_value(self, row_idx: int, column_idx: int, value: str): if ( row_idx < 0 or row_idx >= len(self.selected_table_rows) or column_idx < 0 or column_idx >= len(self.selected_table_columns) ): return updated_rows = [row[:] for row in self.selected_table_rows] updated_rows[row_idx][column_idx] = value self.selected_table_rows = updated_rows if row_idx >= len(self.selected_row_ids): return row_id = self.selected_row_ids[row_idx] column_name = self.selected_table_columns[column_idx] if not row_id or not self.selected_table_name: return if column_name in ("id", "created_at"): return try: client = self._create_client() client.table(self.selected_table_name).update({column_name: value}).eq( "id", row_id ).execute() if column_name == "name": self._reload_tree_nodes(client) except Exception as exc: self.load_error = f"Supabase update failed: {exc}" def select_leaf_record(self, parent_id: str, child_idx: int): meta_key = f"{parent_id}::{child_idx}" encoded = self.leaf_meta.get(meta_key) if not encoded: return parts = encoded.split("|") if len(parts) < 3: return table_name = parts[0] pk_mode = parts[1] self.is_table_loading = True self.load_error = "" try: client = self._create_client() if pk_mode == "id": pk_1 = parts[2] rows = ( client.table(table_name) .select("*") .eq("id", pk_1) .limit(1) .execute() .data or [] ) pk_2 = "" elif pk_mode == "composite" and len(parts) >= 4: pk_1 = parts[2] pk_2 = parts[3] rows = ( client.table(table_name) .select("*") .eq("user_group_id", pk_1) .eq("user_id", pk_2) .limit(1) .execute() .data or [] ) else: return record = rows[0] if rows else {} if not record: self.selected_table_title = f"{table_name} record" self.selected_table_columns = [] self._sync_grid_columns() self.selected_table_rows = [] self.selected_row_ids = [] self.selected_table_name = table_name self.is_vertical_detail = True self.detail_pk_mode = pk_mode self.detail_pk_1 = pk_1 self.detail_pk_2 = pk_2 return fields = sorted(record.keys()) self.selected_table_title = f"{table_name} record" self.selected_table_name = table_name self.selected_table_columns = ["field", "value"] self._sync_grid_columns() self.selected_row_ids = [] self.selected_table_rows = [[field, str(record.get(field, ""))] for field in fields] self.is_vertical_detail = True self.detail_pk_mode = pk_mode self.detail_pk_1 = pk_1 self.detail_pk_2 = pk_2 self._apply_default_grid_sort() except Exception as exc: self.load_error = f"Supabase load failed: {exc}" finally: self.is_table_loading = False def update_detail_value(self, row_idx: int, value: str): if not self.is_vertical_detail: return if row_idx < 0 or row_idx >= len(self.selected_table_rows): return field_name = self.selected_table_rows[row_idx][0] if field_name in ("id", "created_at"): return updated_rows = [row[:] for row in self.selected_table_rows] updated_rows[row_idx][1] = value self.selected_table_rows = updated_rows if not self.selected_table_name or not self.detail_pk_mode: return try: client = self._create_client() query = client.table(self.selected_table_name).update({field_name: value}) if self.detail_pk_mode == "id": query = query.eq("id", self.detail_pk_1) else: query = query.eq("user_group_id", self.detail_pk_1).eq( "user_id", self.detail_pk_2 ) query.execute() if field_name == "name": self._reload_tree_nodes(client) except Exception as exc: self.load_error = f"Supabase update failed: {exc}" def select_group_table(self, node_id: str): mapping: list[tuple[str, str, str, str]] = [ ("application-group-", "applications", "application_group_id", "applications"), ("service-group-", "services", "service_group_id", "services"), ("user-group-", "user_group_members", "user_group_id", "user_group_members"), ] selected = next((item for item in mapping if node_id.startswith(item[0])), None) if not selected: return prefix, table_name, fk_name, title = selected group_id = node_id.removeprefix(prefix) self.is_table_loading = True self.load_error = "" try: client = self._create_client() rows = ( client.table(table_name) .select("*") .eq(fk_name, group_id) .execute() .data or [] ) self._set_selected_table(title=title, table_name=table_name, rows=rows) except Exception as exc: self.load_error = f"Supabase load failed: {exc}" self.selected_table_title = title self.selected_table_columns = [] self._sync_grid_columns() self.selected_table_rows = [] self.selected_row_ids = [] self.selected_table_name = "" finally: self.is_table_loading = False def select_section_table(self, section_key: str): mapping: dict[str, tuple[str, str]] = { "application_groups": ("application_groups", "dashboards"), "service_groups": ("service_groups", "service_groups"), "user_groups": ("user_groups", "user_groups"), } selected = mapping.get(section_key) if not selected: return table_name, title = selected self.is_table_loading = True self.load_error = "" try: client = self._create_client() rows = client.table(table_name).select("*").execute().data or [] self._set_selected_table(title=title, table_name=table_name, rows=rows) except Exception as exc: self.load_error = f"Supabase load failed: {exc}" self.selected_table_title = title self.selected_table_columns = [] self._sync_grid_columns() self.selected_table_rows = [] self.selected_row_ids = [] self.selected_table_name = "" finally: self.is_table_loading = False def render_tree_node(node: TreeNode) -> rx.Component: expanded = AppState.expanded_nodes.contains(node.id) return rx.vstack( rx.button( rx.hstack( rx.cond( expanded, rx.text("▾", color=COLOR_ACCENT), rx.text("▸", color=COLOR_ACCENT), ), rx.text(node.label, color=COLOR_TEXT, font_weight="medium"), spacing="2", align="center", width="100%", ), on_click=[AppState.toggle_node(node.id), AppState.select_group_table(node.id)], width="100%", justify_content="start", padding="0.35rem 0.5rem", background="transparent", border=f"1px solid {COLOR_BORDER}", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), rx.cond( expanded, rx.vstack( rx.foreach( node.children, lambda child, child_idx: render_second_level_node( node.id, child, child_idx ), ), spacing="1", width="100%", padding_left="1.05rem", margin_bottom="0.35rem", ), rx.fragment(), ), width="100%", spacing="0", align="start", ) def render_second_level_node( parent_id: rx.Var[str], child_label: rx.Var[str], child_idx: rx.Var[int] ) -> rx.Component: return rx.button( rx.hstack( rx.text("•", color=COLOR_ACCENT), rx.text(child_label, color=COLOR_MUTED, font_size="0.9rem"), spacing="2", align="center", width="100%", ), on_click=AppState.select_leaf_record(parent_id, child_idx), width="100%", justify_content="start", padding="0.2rem 0.35rem", background="transparent", border=f"1px solid {COLOR_BORDER}", border_radius="7px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ) def render_tree_section( title: str, section_key: str, nodes: rx.Var[list[TreeNode]] ) -> rx.Component: return rx.vstack( rx.button( rx.text(title, color=COLOR_TEXT, font_weight="bold"), on_click=AppState.select_section_table(section_key), width="100%", justify_content="start", background="transparent", border=f"1px solid {COLOR_BORDER}", padding="0.2rem 0.1rem", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, margin_top="0.35rem", ), rx.vstack( rx.foreach(nodes, render_tree_node), width="100%", spacing="1", align="start", ), width="100%", spacing="1", align="start", ) def render_tree() -> rx.Component: return rx.vstack( rx.cond( AppState.is_loading, rx.text("Loading Supabase tree...", color=COLOR_ACCENT), rx.fragment(), ), rx.cond( AppState.load_error != "", rx.text(AppState.load_error, color=COLOR_ERROR, font_size="0.9rem"), rx.fragment(), ), render_tree_section( "class", "class", AppState.application_nodes ), render_tree_section("object", "object", AppState.service_nodes), width="100%", spacing="2", align="start", ) def render_selected_table() -> rx.Component: return rx.vstack( rx.text( AppState.selected_table_title, font_weight="bold", color=COLOR_TEXT, margin_bottom="0.5rem", ), rx.cond( AppState.is_table_loading, rx.text("Loading table...", color=COLOR_ACCENT), rx.fragment(), ), rx.cond( AppState.selected_table_name == "class", rx.hstack( rx.input( placeholder="Class name", value=AppState.new_class_name, on_change=AppState.set_new_class_name, width="16rem", _placeholder={"color": COLOR_MUTED}, **INPUT_STYLE, ), rx.input( placeholder="Description", value=AppState.new_class_description, on_change=AppState.set_new_class_description, width="20rem", _placeholder={"color": COLOR_MUTED}, **INPUT_STYLE, ), rx.button( "Add class", on_click=AppState.create_class, background=COLOR_ACCENT, color=COLOR_TEXT, border=f"1px solid {COLOR_ACCENT}", border_radius="8px", _hover={"background": "#2563eb", "border_color": "#2563eb"}, ), spacing="2", align="center", width="100%", ), rx.fragment(), ), rx.cond( (AppState.selected_table_name == "class_field") & (AppState.selected_class_id != ""), rx.hstack( rx.input( placeholder="Field name", value=AppState.new_field_name, on_change=AppState.set_new_field_name, width="16rem", _placeholder={"color": COLOR_MUTED}, **INPUT_STYLE, ), rx.input( placeholder="Field type (string, date, reference...)", value=AppState.new_field_type, on_change=AppState.set_new_field_type, width="20rem", _placeholder={"color": COLOR_MUTED}, **INPUT_STYLE, ), rx.button( "Add field", on_click=AppState.create_class_field, background=COLOR_ACCENT, color=COLOR_TEXT, border=f"1px solid {COLOR_ACCENT}", border_radius="8px", _hover={"background": "#2563eb", "border_color": "#2563eb"}, ), spacing="2", align="center", width="100%", ), rx.fragment(), ), rx.cond( AppState.selected_table_name == "object", rx.hstack( rx.input( placeholder="Class ID", value=AppState.new_object_class_id, on_change=AppState.set_new_object_class_id, width="20rem", _placeholder={"color": COLOR_PLACEHOLDER}, **INPUT_STYLE, ), rx.input( placeholder='Object data JSON, e.g. {"brand":"Toyota"}', value=AppState.new_object_data, on_change=AppState.set_new_object_data, width="28rem", _placeholder={"color": COLOR_PLACEHOLDER}, **INPUT_STYLE, ), rx.button( "Add object", on_click=AppState.create_object, background=COLOR_ACCENT, color=COLOR_TEXT, border=f"1px solid {COLOR_ACCENT}", border_radius="8px", _hover={"background": "#2563eb", "border_color": "#2563eb"}, ), spacing="2", align="center", width="100%", ), rx.fragment(), ), rx.cond( (AppState.selected_table_name == "class_field") & (AppState.selected_class_id != ""), rx.hstack( rx.text("Class ID:", color=COLOR_MUTED), rx.text(AppState.selected_class_id, color=COLOR_TEXT), rx.input( placeholder='Object data JSON, e.g. {"brand":"Toyota"}', value=AppState.new_object_data, on_change=AppState.set_new_object_data, width="28rem", _placeholder={"color": COLOR_PLACEHOLDER}, **INPUT_STYLE, ), rx.button( "Add object", on_click=AppState.create_object, background=COLOR_ACCENT, color=COLOR_TEXT, border=f"1px solid {COLOR_ACCENT}", border_radius="8px", _hover={"background": "#2563eb", "border_color": "#2563eb"}, ), spacing="2", align="center", width="100%", ), rx.fragment(), ), rx.cond( AppState.selected_table_columns.length() > 0, rx.vstack( rx.cond( (AppState.selected_table_name == "class") | (AppState.selected_table_name == "object"), rx.hstack( rx.text( "Selected row:", color=COLOR_MUTED, font_size="0.85rem", ), rx.text( rx.cond( AppState.grid_selected_row_idx >= 0, AppState.grid_selected_row_idx + 1, "-", ), color=COLOR_TEXT, font_size="0.85rem", ), rx.button( "Delete selected", on_click=AppState.delete_selected_grid_row, disabled=AppState.grid_selected_row_idx < 0, background="transparent", color=COLOR_ERROR, border=f"1px solid {COLOR_ERROR}", border_radius="8px", size="1", _hover={"background": "rgba(248, 113, 113, 0.12)"}, ), spacing="2", align="center", width="100%", ), rx.fragment(), ), rx.box( rx.data_editor( columns=AppState.grid_columns, data=AppState.selected_table_rows, theme=SERVICE_DATA_EDITOR_THEME, row_markers="none", scale_to_rem=True, smooth_scroll_x=True, smooth_scroll_y=True, row_height=DATA_EDITOR_ROW_HEIGHT, header_height=DATA_EDITOR_HEADER_HEIGHT, min_column_width=90, max_column_width=720, on_cell_activated=AppState.select_grid_cell, on_cell_clicked=AppState.select_grid_cell, on_header_clicked=AppState.handle_grid_header_clicked, on_grid_selection_change=AppState.handle_grid_selection_change, on_cell_edited=AppState.handle_grid_cell_edited, grid_selection=AppState.grid_selection, width="100%", height=( AppState.selected_table_rows.length() * DATA_EDITOR_ROW_HEIGHT ) + DATA_EDITOR_HEADER_HEIGHT + DATA_EDITOR_SCROLLBAR_GUTTER, ), width="100%", min_width="0", overflow="hidden", background=COLOR_CARD_ALT, border=f"1px solid {COLOR_BORDER}", border_radius="10px", padding="0", ), width="100%", spacing="2", align="start", ), rx.text("No child objects for selected node", color=COLOR_MUTED), ), width="100%", height="100%", align="start", spacing="2", ) def render_admin_tree_node(node: TreeNode) -> rx.Component: expanded = AdminState.expanded_nodes.contains(node.id) return rx.vstack( rx.button( rx.hstack( rx.cond( expanded, rx.text("▾", color=COLOR_ACCENT), rx.text("▸", color=COLOR_ACCENT), ), rx.text(node.label, color=COLOR_TEXT, font_weight="medium"), spacing="2", align="center", width="100%", ), on_click=[ AdminState.toggle_node(node.id), AdminState.select_group_table(node.id), ], width="100%", justify_content="start", padding="0.35rem 0.5rem", background="transparent", border=f"1px solid {COLOR_BORDER}", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), rx.cond( expanded, rx.vstack( rx.foreach( node.children, lambda child, child_idx: rx.button( rx.hstack( rx.text("•", color=COLOR_ACCENT), rx.text(child, color=COLOR_MUTED, font_size="0.9rem"), spacing="2", align="center", width="100%", ), on_click=AdminState.select_leaf_record(node.id, child_idx), width="100%", justify_content="start", padding="0.2rem 0.35rem", background="transparent", border=f"1px solid {COLOR_BORDER}", border_radius="7px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), ), spacing="1", width="100%", padding_left="1.05rem", margin_bottom="0.35rem", ), rx.fragment(), ), width="100%", spacing="0", align="start", ) def render_admin_tree_section( title: str, section_key: str, nodes: rx.Var[list[TreeNode]] ) -> rx.Component: return rx.vstack( rx.button( rx.text(title, color=COLOR_TEXT, font_weight="bold"), on_click=AdminState.select_section_table(section_key), width="100%", justify_content="start", background="transparent", border=f"1px solid {COLOR_BORDER}", padding="0.2rem 0.1rem", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, margin_top="0.35rem", ), rx.vstack( rx.foreach(nodes, render_admin_tree_node), width="100%", spacing="1", align="start", ), width="100%", spacing="1", align="start", ) def render_admin_tree() -> rx.Component: return rx.vstack( rx.cond( AdminState.is_loading, rx.text("Loading Supabase tree...", color=COLOR_ACCENT), rx.fragment(), ), rx.cond( AdminState.load_error != "", rx.text(AdminState.load_error, color=COLOR_ERROR, font_size="0.9rem"), rx.fragment(), ), render_admin_tree_section( "application_groups", "dashboards", AdminState.application_nodes ), render_admin_tree_section( "service_groups", "service_groups", AdminState.service_nodes ), render_admin_tree_section("user_groups", "user_groups", AdminState.user_group_nodes), width="100%", spacing="2", align="start", ) def render_admin_selected_table() -> rx.Component: return rx.vstack( rx.text( AdminState.selected_table_title, font_weight="bold", color=COLOR_TEXT, margin_bottom="0.5rem", ), rx.cond( AdminState.is_table_loading, rx.text("Loading table...", color=COLOR_ACCENT), rx.fragment(), ), rx.cond( AdminState.selected_table_columns.length() > 0, rx.box( rx.data_editor( columns=AdminState.grid_columns, data=AdminState.selected_table_rows, theme=SERVICE_DATA_EDITOR_THEME, row_markers="none", scale_to_rem=True, smooth_scroll_x=True, smooth_scroll_y=True, row_height=DATA_EDITOR_ROW_HEIGHT, header_height=DATA_EDITOR_HEADER_HEIGHT, min_column_width=90, max_column_width=720, on_cell_activated=AdminState.select_grid_cell, on_cell_clicked=AdminState.select_grid_cell, on_header_clicked=AdminState.handle_grid_header_clicked, on_grid_selection_change=AdminState.handle_grid_selection_change, on_cell_edited=AdminState.handle_grid_cell_edited, grid_selection=AdminState.grid_selection, width="100%", height=( AdminState.selected_table_rows.length() * DATA_EDITOR_ROW_HEIGHT ) + DATA_EDITOR_HEADER_HEIGHT + DATA_EDITOR_SCROLLBAR_GUTTER, ), width="100%", min_width="0", overflow="hidden", background=COLOR_CARD_ALT, border=f"1px solid {COLOR_BORDER}", border_radius="10px", padding="0", ), rx.text("No child objects for selected node", color=COLOR_MUTED), ), width="100%", height="100%", align="start", spacing="2", ) def _generate_breadcrumbs() -> rx.Component: """Generate breadcrumbs based on current navigation state.""" return rx.cond( BrowserState.selected_table == "", rx.fragment(), rx.box( rx.hstack( # Home breadcrumb rx.button( rx.hstack( rx.icon("home", size=14, color=COLOR_ACCENT), rx.text("Home", color=COLOR_ACCENT), spacing="1", align="center", ), on_click=BrowserState.reset_navigation, background="transparent", border="none", cursor="pointer", _hover={"background": "transparent"}, padding="0", ), rx.icon("chevron-right", size=14, color=COLOR_MUTED), # Current section breadcrumb rx.cond( BrowserState.selected_table == "Custom Tables", rx.button( rx.hstack( rx.icon("database", size=14, color=COLOR_TEXT), rx.text("Custom Tables", color=COLOR_TEXT), spacing="1", align="center", ), on_click=BrowserState.select_table_group("Custom Tables"), background="transparent", border="none", cursor="pointer", _hover={"background": "transparent"}, padding="0", ), rx.cond( BrowserState.selected_table == "Master/Detail", rx.button( rx.hstack( rx.icon("git-branch", size=14, color=COLOR_TEXT), rx.text("Master/Detail", color=COLOR_TEXT), spacing="1", align="center", ), on_click=BrowserState.select_master_detail, background="transparent", border="none", cursor="pointer", _hover={"background": "transparent"}, padding="0", ), rx.cond( BrowserState.selected_table == "Service Tables", rx.button( rx.hstack( rx.icon("server", size=14, color=COLOR_TEXT), rx.text("Service Tables", color=COLOR_TEXT), spacing="1", align="center", ), on_click=BrowserState.select_table_group("Service Tables"), background="transparent", border="none", cursor="pointer", _hover={"background": "transparent"}, padding="0", ), rx.cond( BrowserState.selected_table == "Visuals", rx.button( rx.hstack( rx.icon("bar-chart-3", size=14, color=COLOR_TEXT), rx.text("Charts", color=COLOR_TEXT), spacing="1", align="center", ), on_click=BrowserState.select_visuals, background="transparent", border="none", cursor="pointer", _hover={"background": "transparent"}, padding="0", ), rx.cond( BrowserState.selected_table == "Authentication", rx.hstack( rx.button( rx.hstack( rx.icon("shield", size=14, color=COLOR_TEXT), rx.text("Authentication", color=COLOR_TEXT), spacing="1", align="center", ), on_click=BrowserState.select_authentication, background="transparent", border="none", cursor="pointer", _hover={"background": "transparent"}, padding="0", ), rx.cond( BrowserState.auth_selected_table != "", rx.hstack( rx.icon("chevron-right", size=14, color=COLOR_MUTED), rx.button( rx.hstack( rx.icon("file-text", size=14, color=COLOR_TEXT), rx.text(BrowserState.auth_selected_table, color=COLOR_TEXT), spacing="1", align="center", ), background="transparent", border="none", cursor="pointer", _hover={"background": "transparent"}, padding="0", ), spacing="2", align="center", ), rx.fragment(), ), spacing="2", align="center", ), rx.cond( # For individual tables BrowserState.tables.contains(BrowserState.selected_table), rx.hstack( rx.cond( BrowserState.custom_tables.contains(BrowserState.selected_table), rx.hstack( rx.button( rx.hstack( rx.icon("database", size=14, color=COLOR_TEXT), rx.text("Custom Tables", color=COLOR_TEXT), spacing="1", align="center", ), on_click=BrowserState.select_table_group("Custom Tables"), background="transparent", border="none", cursor="pointer", _hover={"background": "transparent"}, padding="0", ), rx.icon("chevron-right", size=14, color=COLOR_MUTED), ), rx.cond( (BrowserState.selected_table == "application_groups") | (BrowserState.selected_table == "applications"), rx.cond( BrowserState.applications_breadcrumb_source == "sidebar_applications", rx.hstack( rx.button( rx.hstack( rx.icon("layers", size=14, color=COLOR_TEXT), rx.text("Applications", color=COLOR_TEXT), spacing="1", align="center", ), on_click=BrowserState.select_applications, background="transparent", border="none", cursor="pointer", _hover={"background": "transparent"}, padding="0", ), rx.icon("chevron-right", size=14, color=COLOR_MUTED), ), rx.cond( BrowserState.applications_breadcrumb_source == "apps_management", rx.hstack( rx.button( rx.hstack( rx.icon( "app-window", size=14, color=COLOR_TEXT, ), rx.text( "Apps management", color=COLOR_TEXT, ), spacing="1", align="center", ), on_click=BrowserState.focus_apps_management_nav, background="transparent", border="none", cursor="pointer", _hover={"background": "transparent"}, padding="0", ), rx.icon("chevron-right", size=14, color=COLOR_MUTED), ), rx.hstack( rx.button( rx.hstack( rx.icon( "server", size=14, color=COLOR_TEXT, ), rx.text( "Service Tables", color=COLOR_TEXT, ), spacing="1", align="center", ), on_click=BrowserState.select_table_group( "Service Tables" ), background="transparent", border="none", cursor="pointer", _hover={"background": "transparent"}, padding="0", ), rx.icon("chevron-right", size=14, color=COLOR_MUTED), ), ), ), rx.cond( BrowserState.service_tables.contains( BrowserState.selected_table ), rx.hstack( rx.button( rx.hstack( rx.icon("server", size=14, color=COLOR_TEXT), rx.text("Service Tables", color=COLOR_TEXT), spacing="1", align="center", ), on_click=BrowserState.select_table_group("Service Tables"), background="transparent", border="none", cursor="pointer", _hover={"background": "transparent"}, padding="0", ), rx.icon("chevron-right", size=14, color=COLOR_MUTED), ), rx.fragment(), ), ), ), rx.button( rx.hstack( rx.icon("table", size=14, color=COLOR_TEXT), rx.text( rx.cond( BrowserState.selected_table == "applications", "applications", rx.cond( BrowserState.selected_table == "application_groups", rx.cond( BrowserState.applications_breadcrumb_source == "sidebar_applications", BrowserState.applications_dashboard_title, "dashboards", ), BrowserState.selected_table, ), ), color=COLOR_TEXT, ), spacing="1", align="center", ), background="transparent", border="none", cursor="pointer", _hover={"background": "transparent"}, padding="0", ), ), rx.button( rx.hstack( rx.icon("table", size=14, color=COLOR_TEXT), rx.text( rx.cond( BrowserState.selected_table == "applications", "applications", rx.cond( BrowserState.selected_table == "application_groups", rx.cond( BrowserState.applications_breadcrumb_source == "sidebar_applications", BrowserState.applications_dashboard_title, "dashboards", ), BrowserState.selected_table, ), ), color=COLOR_TEXT, ), spacing="1", align="center", ), background="transparent", border="none", cursor="pointer", _hover={"background": "transparent"}, padding="0", ), ), ), ), ), ), ), spacing="2", align="center", padding="0.5rem 0", ), border_bottom=f"1px solid {COLOR_BORDER}", margin_bottom="1rem", ), ) def _generate_breadcrumbs_fixed() -> rx.Component: """Breadcrumbs mirror the navigation tree (Platform + dynamic trail).""" return rx.cond( BrowserState.is_authenticated, rx.cond( BrowserState.selected_table == "", rx.fragment(), rx.box( rx.hstack( rx.button( rx.hstack( rx.icon("globe", size=14, color=COLOR_ACCENT), rx.text(ui_txt("nav.platform"), color=COLOR_ACCENT), spacing="1", align="center", ), background="transparent", border="none", cursor="pointer", _hover={"background": "transparent"}, padding="0", ), rx.foreach( BrowserState.nav_breadcrumb_items, lambda item: rx.hstack( rx.icon("chevron-right", size=14, color=COLOR_MUTED), rx.cond( item.is_current, rx.hstack( rx.text( item.label, color=COLOR_TEXT, font_weight="500", ), spacing="1", align="center", ), rx.button( rx.text(item.label, color=COLOR_TEXT), on_click=BrowserState.breadcrumb_go(item.idx), background="transparent", border="none", cursor="pointer", _hover={"background": "transparent"}, padding="0", ), ), spacing="2", align="center", ), ), spacing="2", align="center", padding="0.5rem 0", ), border_bottom=f"1px solid {COLOR_BORDER}", margin_bottom="1rem", ), ), rx.box( rx.hstack( rx.button( rx.hstack( rx.icon("globe", size=14, color=COLOR_ACCENT), rx.text(ui_txt("nav.platform"), color=COLOR_ACCENT), spacing="1", align="center", ), background="transparent", border="none", cursor="default", _hover={"background": "transparent"}, padding="0", ), spacing="2", align="center", padding="0.5rem 0", ), border_bottom=f"1px solid {COLOR_BORDER}", margin_bottom="1rem", ), ) def _main_layout( left_content: rx.Component, right_content: rx.Component, header_actions: rx.Component, heading_text: rx.Component | str | None = None, ) -> rx.Component: if heading_text is None: heading_text = ui_txt("layout.sphere_platform") return rx.box( rx.vstack( rx.hstack( rx.image( src="/logo.jpg", height="2.5rem", width="2.5rem", border_radius="8px", object_fit="cover", ), rx.heading( heading_text, size="7", color=COLOR_TEXT, letter_spacing="0.02em", ), rx.spacer(), header_actions, width="100%", align="center", spacing="3", ), _generate_breadcrumbs_fixed(), rx.hstack( rx.cond( BrowserState.is_authenticated, rx.box( rx.text( ui_txt("nav.navigation"), font_weight="bold", margin_bottom="0.75rem", color=COLOR_TEXT, ), rx.box( left_content, width="100%", height="100%", border=f"1px solid {COLOR_BORDER}", border_radius="12px", background=COLOR_CARD_ALT, padding="0.6rem", overflow_y="auto", ), width="25%", height="100%", background=COLOR_CARD, border=f"1px solid {COLOR_BORDER}", border_radius="14px", box_shadow="0 8px 30px rgba(0, 0, 0, 0.2)", padding="1rem", ), rx.fragment(), ), rx.box( rx.cond( BrowserState.is_authenticated, rx.fragment( rx.cond( BrowserState.selected_table != "", rx.cond( (BrowserState.selected_table == "Master/Detail") & BrowserState.master_detail_opened_from_application_tile & (BrowserState.master_detail_run_app_name != ""), rx.vstack( rx.text( ui_txt("nav.master_detail"), font_weight="bold", margin_bottom="0.2rem", color=COLOR_TEXT, ), rx.text( BrowserState.master_detail_run_app_name, font_size="0.95rem", color=COLOR_MUTED, margin_bottom="0.75rem", ), spacing="0", align="start", width="100%", ), rx.cond( (BrowserState.selected_table == "Reports") & BrowserState.reports_opened_from_application_tile & (BrowserState.reports_run_app_name != ""), rx.vstack( rx.text( ui_txt("nav.reports"), font_weight="bold", margin_bottom="0.2rem", color=COLOR_TEXT, ), rx.text( BrowserState.reports_run_app_name, font_size="0.95rem", color=COLOR_MUTED, margin_bottom="0.75rem", ), spacing="0", align="start", width="100%", ), rx.cond( (BrowserState.selected_table == "Visuals") & ~BrowserState.visuals_show_create_app & (BrowserState.visuals_run_app_name != ""), rx.vstack( rx.text( ui_txt("nav.charts"), font_weight="bold", margin_bottom="0.2rem", color=COLOR_TEXT, ), rx.text( BrowserState.visuals_run_app_name, font_size="0.95rem", color=COLOR_MUTED, margin_bottom="0.75rem", ), spacing="0", align="start", width="100%", ), rx.cond( BrowserState.custom_tables.contains( BrowserState.selected_table ), rx.vstack( rx.text( ui_txt("nav.custom_table"), font_weight="bold", margin_bottom="0.2rem", color=COLOR_TEXT, ), rx.text( BrowserState.selected_table, font_size="0.95rem", color=COLOR_MUTED, margin_bottom="0.75rem", ), spacing="0", align="start", width="100%", ), rx.text( rx.cond( BrowserState.selected_table == "application_groups", ui_txt("nav.dashboards"), rx.cond( BrowserState.selected_table == "Visuals", ui_txt("nav.charts"), rx.cond( BrowserState.selected_table == "Master/Detail", ui_txt( "nav.master_detail" ), rx.cond( BrowserState.selected_table == "Reports", ui_txt( "nav.reports" ), rx.cond( BrowserState.selected_table == "Custom Tables", ui_txt( "nav.custom_tables" ), rx.cond( BrowserState.selected_table == "Authentication", ui_txt( "nav.authentication" ), rx.cond( BrowserState.selected_table == "Service Tables", ui_txt( "nav.service_tables" ), BrowserState.selected_table, ), ), ), ), ), ), ), font_weight="bold", margin_bottom="0.75rem", color=COLOR_TEXT, ), ), ), ), ), rx.text( ui_txt("nav.view"), font_weight="bold", margin_bottom="0.75rem", color=COLOR_TEXT, ), ), right_content, ), rx.center( rx.text( ui_txt("layout.please_login"), color=COLOR_MUTED, font_size="1.2rem", ), height="100%", width="100%", ), ), width=rx.cond(BrowserState.is_authenticated, "75%", "100%"), height="100%", background=COLOR_CARD, border=f"1px solid {COLOR_BORDER}", border_radius="14px", box_shadow="0 8px 30px rgba(0, 0, 0, 0.2)", padding="1rem", ), width="100%", spacing="6", align="start", height="100%", ), width="100%", spacing="4", align="start", height="100%", ), rx.text( ui_txt("layout.version"), font_size="0.6em", color=COLOR_MUTED, position="fixed", bottom="10px", right="10px", z_index="9999", ), rx.cond( BrowserState.is_login_form_open, rx.box( rx.box( rx.vstack( rx.heading(ui_txt("layout.sphere_platform"), size="6", color=COLOR_TEXT), rx.input( placeholder=rx.cond( BrowserState.ui_locale == "ru", UI_STR_RU["auth.email_ph"], UI_STR_EN["auth.email_ph"], ), value=BrowserState.login_email, on_change=BrowserState.set_login_email, **INPUT_STYLE, width="100%", ), rx.input( placeholder=rx.cond( BrowserState.ui_locale == "ru", UI_STR_RU["auth.password_ph"], UI_STR_EN["auth.password_ph"], ), type="password", value=BrowserState.login_password, on_change=BrowserState.set_login_password, **INPUT_STYLE, width="100%", ), rx.cond( BrowserState.login_error != "", rx.text( BrowserState.login_error, color=COLOR_ERROR, font_size="0.82rem", ), rx.fragment(), ), rx.hstack( rx.button( ui_txt("auth.cancel"), on_click=BrowserState.close_login_form, background="transparent", border=f"1px solid {COLOR_BORDER}", color=COLOR_TEXT, border_radius="8px", _hover={ "background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT, }, ), rx.button( ui_txt("auth.login_submit"), on_click=BrowserState.submit_mock_login, background=COLOR_ACCENT, color="white", border="none", border_radius="8px", _hover={"opacity": 0.9}, ), width="100%", justify="end", spacing="2", ), width="100%", spacing="3", align="start", ), width="24rem", background=COLOR_CARD, border=f"1px solid {COLOR_BORDER}", border_radius="12px", padding="1rem", box_shadow="0 12px 30px rgba(0, 0, 0, 0.45)", ), position="fixed", inset="0", background="rgba(11, 16, 32, 0.55)", display="flex", align_items="center", justify_content="center", z_index="999", ), rx.fragment(), ), rx.cond( BrowserState.schema_help_open, rx.box( rx.box( rx.vstack( rx.hstack( rx.heading(ui_txt("schema.title"), size="5", color=COLOR_TEXT), rx.spacer(), rx.button( ui_txt("schema.close"), on_click=BrowserState.close_schema_help, background="transparent", border=f"1px solid {COLOR_BORDER}", color=COLOR_TEXT, border_radius="8px", size="1", _hover={ "background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT, }, ), width="100%", align="center", ), rx.cond( BrowserState.schema_help_loading, rx.text(ui_txt("schema.loading"), color=COLOR_ACCENT), rx.fragment(), ), rx.cond( BrowserState.schema_help_error != "", rx.text( BrowserState.schema_help_error, color=COLOR_ERROR, font_size="0.85rem", ), rx.fragment(), ), rx.cond( BrowserState.schema_help_loading, rx.fragment(), rx.box( rx.text( BrowserState.schema_help_text, color=COLOR_TEXT, white_space="pre-wrap", font_family="monospace", font_size="0.82rem", ), width="100%", max_height="65vh", overflow_y="auto", background=COLOR_CARD_ALT, border=f"1px solid {COLOR_BORDER}", border_radius="10px", padding="0.7rem", ), ), width="100%", spacing="3", align="start", ), width="min(70rem, 92vw)", max_height="80vh", background=COLOR_CARD, border=f"1px solid {COLOR_BORDER}", border_radius="12px", padding="1rem", box_shadow="0 12px 30px rgba(0, 0, 0, 0.45)", ), position="fixed", inset="0", background="rgba(11, 16, 32, 0.55)", display="flex", align_items="center", justify_content="center", z_index="998", ), rx.fragment(), ), rx.cond( BrowserState.dashboard_pick_app_open, rx.box( rx.box( rx.vstack( rx.hstack( rx.heading( ui_txt("dash.pick_title"), size="5", color=COLOR_TEXT, ), rx.spacer(), rx.button( ui_txt("schema.close"), on_click=BrowserState.close_dashboard_pick_app, background="transparent", border=f"1px solid {COLOR_BORDER}", color=COLOR_TEXT, border_radius="8px", size="1", _hover={ "background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT, }, ), width="100%", align="center", ), rx.cond( BrowserState.dashboard_target_group_id != "", rx.text( ui_txt("dash.pick_hint_group"), color=COLOR_MUTED, font_size="0.82rem", ), rx.text( ui_txt("dash.pick_hint_ungroup"), color=COLOR_MUTED, font_size="0.82rem", ), ), rx.cond( BrowserState.dashboard_pick_app_error != "", rx.text( BrowserState.dashboard_pick_app_error, color=COLOR_ERROR, font_size="0.85rem", ), rx.fragment(), ), rx.cond( BrowserState.dashboard_pickable_apps.length() == 0, rx.text( ui_txt("dash.no_apps_pick"), color=COLOR_MUTED, font_size="0.88rem", ), rx.box( rx.vstack( rx.foreach( BrowserState.dashboard_pickable_apps, _dashboard_pick_app_row, ), spacing="2", width="100%", align="start", ), width="100%", max_height="min(50vh, 22rem)", overflow_y="auto", padding_right="0.25rem", ), ), rx.hstack( rx.spacer(), rx.button( ui_txt("auth.cancel"), on_click=BrowserState.close_dashboard_pick_app, background="transparent", border=f"1px solid {COLOR_BORDER}", color=COLOR_TEXT, border_radius="8px", _hover={ "background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT, }, ), spacing="2", width="100%", align="center", ), width="100%", spacing="3", align="start", ), width="min(36rem, 92vw)", background=COLOR_CARD, border=f"1px solid {COLOR_BORDER}", border_radius="12px", padding="1rem", box_shadow="0 12px 30px rgba(0, 0, 0, 0.45)", ), position="fixed", inset="0", background="rgba(11, 16, 32, 0.55)", display="flex", align_items="center", justify_content="center", z_index="997", ), rx.fragment(), ), rx.cond( BrowserState.dashboard_add_group_open, rx.box( rx.box( rx.vstack( rx.hstack( rx.heading( ui_txt("dash.new_group_title"), size="5", color=COLOR_TEXT, ), rx.spacer(), rx.button( ui_txt("schema.close"), on_click=BrowserState.close_dashboard_add_group, background="transparent", border=f"1px solid {COLOR_BORDER}", color=COLOR_TEXT, border_radius="8px", size="1", _hover={ "background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT, }, ), width="100%", align="center", ), rx.text( ui_txt("dash.new_group_hint"), color=COLOR_MUTED, font_size="0.82rem", ), rx.input( placeholder=rx.cond( BrowserState.ui_locale == "ru", UI_STR_RU["ph.group_name"], UI_STR_EN["ph.group_name"], ), value=BrowserState.new_application_group_name, on_change=BrowserState.set_new_application_group_name, width="100%", color_scheme="gray", **INPUT_STYLE, ), rx.cond( BrowserState.dashboard_add_group_error != "", rx.text( BrowserState.dashboard_add_group_error, color=COLOR_ERROR, font_size="0.85rem", ), rx.fragment(), ), rx.hstack( rx.spacer(), rx.button( ui_txt("auth.cancel"), on_click=BrowserState.close_dashboard_add_group, background="transparent", border=f"1px solid {COLOR_BORDER}", color=COLOR_TEXT, border_radius="8px", _hover={ "background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT, }, ), rx.button( ui_txt("dash.create_group"), on_click=BrowserState.create_application_group, background=COLOR_ACCENT, color=COLOR_TEXT, border=f"1px solid {COLOR_ACCENT}", border_radius="8px", _hover={ "background": "#2563eb", "border_color": "#2563eb", }, ), spacing="2", width="100%", align="center", ), width="100%", spacing="3", align="start", ), width="min(28rem, 92vw)", background=COLOR_CARD, border=f"1px solid {COLOR_BORDER}", border_radius="12px", padding="1rem", box_shadow="0 12px 30px rgba(0, 0, 0, 0.45)", ), position="fixed", inset="0", background="rgba(11, 16, 32, 0.55)", display="flex", align_items="center", justify_content="center", z_index="996", ), rx.fragment(), ), position="relative", padding_top="1.5rem", padding_bottom="1.5rem", padding_left="10%", padding_right="10%", min_height="100vh", background="radial-gradient(circle at top, #111a33 0%, #0b1020 65%)", ) def _main_placeholder_left() -> rx.Component: return rx.vstack( rx.text("Tree", color=COLOR_TEXT, font_weight="bold", margin_bottom="0.5rem"), rx.text( "Select an item from the tree. Open Admin (⚙) to manage data.", color=COLOR_MUTED, font_size="0.9rem", ), width="100%", spacing="2", align="start", ) def _main_placeholder_right() -> rx.Component: return rx.vstack( rx.text( "Details", color=COLOR_TEXT, font_weight="bold", margin_bottom="0.5rem", ), rx.text( "Select an item in the left panel to see details here.", color=COLOR_MUTED, font_size="0.9rem", ), width="100%", align="start", spacing="2", ) def _main_header_actions() -> rx.Component: return rx.hstack( rx.link( rx.button( rx.text("⚙", font_size="1.25rem", color=COLOR_TEXT), background="transparent", border=f"1px solid {COLOR_BORDER}", padding="0.35rem", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), href="/admin", ), rx.button( rx.text("⟳", font_size="1.25rem", color=COLOR_TEXT), on_click=AppState.refresh_data, background="transparent", border=f"1px solid {COLOR_BORDER}", padding="0.35rem", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), spacing="2", align="center", ) def _admin_header_actions() -> rx.Component: return rx.hstack( rx.link( rx.button( rx.text("←", font_size="1.25rem", color=COLOR_TEXT), background="transparent", border=f"1px solid {COLOR_BORDER}", padding="0.35rem", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), href="/", ), rx.button( rx.text("⟳", font_size="1.25rem", color=COLOR_TEXT), on_click=AdminState.refresh_data, background="transparent", border=f"1px solid {COLOR_BORDER}", padding="0.35rem", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), spacing="2", align="center", ) TABLE_DISCOVERY_CANDIDATES = [ "class", "class_field", "object", "object_history", "emp", "dept", "salgrade", "bonus", "application_groups", "applications", "service_groups", "services", "user_groups", "user_group_members", "users", ] SERVICE_TABLES = { "user_groups", "user_group_members", "users", "application_groups", "applications", "service_groups", "services", "permissions", "class", "class_field", "object", "object_history", } # Synthetic row_ids entry while adding a row; survives re-sort of the service grid. SERVICE_GRID_DRAFT_ROW_ID = "__sphere_new_row__" CUSTOM_FIELD_TYPES = [ "text", "integer", "numeric", "boolean", "date", "timestamp", "uuid", "jsonb", ] APPLICATION_TYPE_OPTIONS = ["url", "custom table", "master/detail", "chart", "sql"] # Administration → Apps management → applications grid column order (visible columns only). APPLICATIONS_ADMIN_GRID_COLUMN_ORDER = ( "name", "type", "dashboard_name", "description", "content", ) def _table_discovery_candidates() -> list[str]: extra_raw = os.getenv("SUPABASE_TABLE_CANDIDATES", "") extras = [item.strip() for item in extra_raw.split(",") if item.strip()] merged = [*TABLE_DISCOVERY_CANDIDATES, *extras] seen: set[str] = set() ordered_unique: list[str] = [] for name in merged: if name not in seen: seen.add(name) ordered_unique.append(name) return ordered_unique class BrowserState(rx.State): tables: list[str] = [] service_tables: list[str] = [] custom_tables: list[str] = [] custom_tables_grid_rows: list[list[str]] = [] custom_tables_sort_column_idx: int = -1 custom_tables_sort_desc: bool = False selected_table: str = "" columns: list[str] = [] rows: list[list[str]] = [] row_ids: list[str] = [] service_tables_expanded: bool = False custom_tables_expanded: bool = False administration_expanded: bool = False apps_constructor_expanded: bool = False applications_management_expanded: bool = False is_loading_tables: bool = False is_loading_rows: bool = False load_error: str = "" custom_table_target: str = "" custom_tables_selected_row_idx: int = -1 custom_tables_grid_selection: dict[str, Any] = {} # True when the current custom table view was opened from Applications dashboard tile. custom_table_opened_from_application_tile: bool = False is_create_table_mode: bool = False is_create_app_mode: bool = False new_app_name: str = "" is_create_master_detail_app_mode: bool = False new_master_detail_app_name: str = "" is_edit_table_mode: bool = False new_table_name: str = "" new_table_fields: list[list[str]] = [["field_1", "text"]] edit_table_name: str = "" edit_table_fields: list[list[str]] = [] edit_type_options: list[str] = CUSTOM_FIELD_TYPES[:] edit_original_names: list[str] = [] edit_original_types: dict[str, str] = {} is_adding_row: bool = False editing_row_idx: int = -1 editing_original_row: list[str] = [] applications_inline_type_row_idx: int = -1 applications_inline_type_col_idx: int = -1 applications_inline_type_open: bool = False applications_inline_type_value: str = "" applications_inline_dashboard_row_idx: int = -1 applications_inline_dashboard_col_idx: int = -1 applications_inline_dashboard_open: bool = False applications_inline_dashboard_value: str = "" applications_edit_target_row_idx: int = -1 applications_edit_target_col: str = "" applications_type_select_open: bool = False applications_group_select_open: bool = False service_grid_selected_row_idx: int = -1 service_grid_selection: dict[str, Any] = {} service_grid_column_widths: dict[str, int] = {} service_grid_columns: list[dict[str, Any]] = [] service_sort_column_idx: int = -1 service_sort_desc: bool = False visuals_selected_table: str = "" visuals_label_options: list[str] = [] visuals_data_options: list[str] = [] visuals_label_column: str = "" visuals_data_columns: list[str] = [] visuals_data_series: list[dict[str, str]] = [] visuals_chart_types: list[str] = ["histogram", "simple graph"] visuals_chart_type: str = "simple graph" visuals_chart_data: list[dict[str, Any]] = [] visuals_has_generated: bool = False visuals_notice: str = "" is_create_visuals_app_mode: bool = False new_visuals_app_name: str = "" # False when Charts is opened from an application tile (Applications → group → chart). visuals_show_create_app: bool = True # Name of the Applications tile when opening Charts from dashboard (no fallback label). visuals_run_app_name: str = "" master_detail_opened_from_application_tile: bool = False # Name of the Applications tile when running master/detail from dashboard (no fallback label). master_detail_run_app_name: str = "" reports_opened_from_application_tile: bool = False # Name of the Applications tile when running Reports from dashboard (no fallback label). reports_run_app_name: str = "" application_tile_constructor_label: str = "" applications_group_tiles: list[ApplicationGroupTiles] = [] applications_ungrouped_tiles: list[ApplicationTile] = [] dashboard_pick_app_open: bool = False dashboard_target_group_id: str = "" dashboard_pickable_apps: list[ApplicationTile] = [] dashboard_all_apps: list[ApplicationTile] = [] dashboard_pick_app_error: str = "" dashboard_add_group_open: bool = False new_application_group_name: str = "" dashboard_add_group_error: str = "" applications_nav_expanded: bool = False applications_nav_group_id: str = "" applications_nav_groups: list[ApplicationNavGroup] = [] applications_dashboard_visible_groups: list[ApplicationGroupTiles] = [] applications_dashboard_title: str = UI_STR_RU["nav.applications"] # Breadcrumb context for application_groups / applications (not Service Tables vs tree vs admin). applications_breadcrumb_source: str = "service_tables" nav_breadcrumb_items: list[NavBreadcrumbItem] = [] nav_breadcrumb_route: list[str] = [] # Custom table opened from app tile (full dashboard uses select_table but crumbs stay under Applications). custom_table_apps_breadcrumb_active: bool = False pending_custom_table_apps_breadcrumb: bool = False # Skip sync_nav_breadcrumb once when opening Charts/M-D/Reports from an app tile (Reflex on_click handlers must not take extra bool args). _skip_sync_after_constructor_nav: bool = False # Full dashboards (admin / Applications root): edit tiles & groups. Hidden when one group opened from tree. dashboard_show_app_management: bool = True applications_dashboard_expand_all: bool = False dashboard_expanded_apps: list[DashboardExpandedApp] = [] # Master/Detail master_detail_master_table: str = "" master_detail_detail_table: str = "" master_detail_master_key: str = "" master_detail_detail_key: str = "" master_detail_master_key_options: list[str] = [] master_detail_detail_key_options: list[str] = [] master_detail_master_columns: list[str] = [] master_detail_master_grid_columns: list[dict[str, Any]] = [] master_detail_master_rows: list[list[str]] = [] master_detail_master_key_values: list[str] = [] master_detail_master_sort_column_idx: int = -1 master_detail_master_sort_desc: bool = False master_detail_detail_columns: list[str] = [] master_detail_detail_grid_columns: list[dict[str, Any]] = [] master_detail_detail_rows: list[list[str]] = [] master_detail_detail_row_ids: list[str] = [] master_detail_detail_sort_column_idx: int = -1 master_detail_detail_sort_desc: bool = False master_detail_selected_master_idx: int = -1 master_detail_master_grid_selection: dict[str, Any] = {} master_detail_detail_selected_row_idx: int = -1 master_detail_detail_grid_selection: dict[str, Any] = {} master_detail_loading: bool = False master_detail_error: str = "" # False when Master/Detail opened from an application tile (hide builder UI). master_detail_show_configuration_ui: bool = True master_detail_detail_adding_row: bool = False master_detail_detail_editing_idx: int = -1 master_detail_detail_edit_original_row: list[str] = [] master_detail_detail_key_col_idx: int = -1 master_detail_detail_id_col_idx: int = -1 # Authentication auth_tables: list[str] = [] auth_selected_table: str = "users" auth_columns: list[str] = [] auth_grid_columns: list[dict[str, Any]] = [] auth_rows: list[list[str]] = [] auth_row_ids: list[str] = [] auth_sort_column_idx: int = -1 auth_sort_desc: bool = False auth_selected_row_idx: int = -1 auth_grid_selection: dict[str, Any] = {} auth_is_loading: bool = False auth_error: str = "" auth_is_adding_row: bool = False auth_editing_row_idx: int = -1 auth_editing_original_row: list[str] = [] auth_available_user_ids: list[str] = [] auth_available_user_labels: list[str] = [] auth_available_group_ids: list[str] = [] auth_available_group_labels: list[str] = [] application_group_ids: list[str] = [] application_group_labels: list[str] = [] auth_user_group_member_ids: list[str] = [] auth_user_group_members_display: list[str] = [] # Reports reports_sql: str = "select * from public.users limit 100" reports_result_columns: list[str] = [] reports_result_grid_columns: list[dict[str, Any]] = [] reports_result_rows: list[list[str]] = [] reports_grid_selection: dict[str, Any] = {} reports_error: str = "" reports_is_loading: bool = False reports_result_only_mode: bool = False reports_view_title: str = UI_STR_RU["nav.reports"] schema_help_open: bool = False schema_help_loading: bool = False schema_help_error: str = "" schema_help_text: str = "" is_create_reports_app_mode: bool = False new_reports_app_name: str = "" is_authenticated: bool = False is_login_form_open: bool = False login_email: str = "" login_password: str = "" login_error: str = "" # UI language: "ru" (default) or "en". Drives localized labels across the shell. ui_locale: str = "ru" def set_login_email(self, value: str): self.login_email = value self.login_error = "" def set_login_password(self, value: str): self.login_password = value self.login_error = "" def open_login_form(self): self.is_login_form_open = True self.login_error = "" def close_login_form(self): self.is_login_form_open = False self.login_error = "" def select_locale_ru(self): self.ui_locale = "ru" self._sync_applications_dashboard_visible_groups() self.sync_nav_breadcrumb() def select_locale_en(self): self.ui_locale = "en" self._sync_applications_dashboard_visible_groups() self.sync_nav_breadcrumb() def _msg(self, key: str, **kwargs: Any) -> str: return ui_str(self.ui_locale, key, **kwargs) def toggle_login_state(self): if self.is_authenticated: self.is_authenticated = False self.is_login_form_open = False self.login_email = "" self.login_password = "" self.login_error = "" return self.open_login_form() def submit_mock_login(self): if ( self.login_email.strip() == MOCK_LOGIN_EMAIL and self.login_password == MOCK_LOGIN_PASSWORD ): self.is_authenticated = True self.is_login_form_open = False self.login_error = "" self.login_email = "" self.login_password = "" self.select_applications() return self.login_error = self._msg("errors.login_invalid") @staticmethod def _auto_managed_columns() -> set[str]: return {"id", "created_at", "updated_at", "changed_at"} @staticmethod def _is_numeric_type(column_type: str) -> bool: normalized = (column_type or "").strip().lower() return normalized in { "smallint", "integer", "bigint", "decimal", "numeric", "real", "double precision", "float", "float4", "float8", } @staticmethod def _sortable_cell_value(raw: str): value = (raw or "").strip() if value == "": return (2, "") try: return (0, float(value)) except ValueError: return (1, value.casefold()) @staticmethod def _extract_header_column_index(position: Any) -> int | None: return _extract_column_index_from_header_position(position) def _is_visible_column(self, table_name: str, column_name: str) -> bool: # Ignore invalid/empty column names to avoid blank grid columns. if not (column_name or "").strip(): return False # Hide created_at from the grid for all tables. if column_name == "created_at": return False # Hide technical linkage columns in dashboards editor. if table_name == "application_groups" and column_name in {"id", "parent_id"}: return False # Hide id in applications editing grid. if table_name == "applications" and column_name == "id": return False # Hide foreign key column in applications grid (dashboard_name is shown instead). if table_name == "applications" and column_name == "application_group_id": return False # Keep id hidden in custom tables. if table_name in self.custom_tables and column_name == "id": return False return True def _service_table_row_actions_disabled(self) -> bool: return self.selected_table in self.service_tables and self.selected_table not in { "applications", "application_groups", } def _service_grid_column_default_width(self, column_name: str) -> int: key = f"{self.selected_table}:{column_name}" return int(self.service_grid_column_widths.get(key, 190)) @staticmethod def _is_applications_virtual_column(column_name: str) -> bool: return column_name == "dashboard_name" def _apply_applications_admin_column_order(self): """Reorder ``columns`` / ``rows`` for the applications admin grid.""" if self.selected_table != "applications" or not self.columns: return present = list(self.columns) col_index = {name: i for i, name in enumerate(present)} new_order = [c for c in APPLICATIONS_ADMIN_GRID_COLUMN_ORDER if c in col_index] for c in present: if c not in new_order: new_order.append(c) if new_order == present: return self.columns = new_order self.rows = [ [ row[col_index[c]] if col_index[c] < len(row) else "" for c in new_order ] for row in self.rows ] def _service_grid_draft_row_idx(self) -> int: if not self.is_adding_row: return -1 try: return self.row_ids.index(SERVICE_GRID_DRAFT_ROW_ID) except ValueError: return -1 def _fetch_table_rows_for_grid(self, client: Any, table_name: str) -> list[dict]: """Fetch up to 500 rows; prefer newest-first when metadata allows.""" column_meta = self._read_table_columns(table_name) names = {c["name"]: (c.get("type") or "text").lower() for c in column_meta} q = client.table(table_name).select("*").limit(500) if "created_at" in names: q = q.order("created_at", desc=True) elif "updated_at" in names: q = q.order("updated_at", desc=True) elif "id" in names: dtype = names.get("id", "text") if any(x in dtype for x in ("int", "serial")): q = q.order("id", desc=True) try: return q.execute().data or [] except Exception: return client.table(table_name).select("*").limit(500).execute().data or [] def _promote_service_grid_row_to_top_by_id(self, row_id: str) -> None: """Move the row with this DB id to the top (after reload).""" rid = (row_id or "").strip() if not rid or rid == SERVICE_GRID_DRAFT_ROW_ID: return try: idx = self.row_ids.index(rid) except ValueError: return if idx <= 0: return row = self.rows.pop(idx) self.row_ids.pop(idx) self.rows.insert(0, row) self.row_ids.insert(0, rid) self._rebuild_service_grid_columns() self.service_sort_column_idx = -1 self.service_sort_desc = False self.service_grid_selected_row_idx = 0 if self.service_grid_columns: self.service_grid_selection = _build_row_grid_selection( row_idx=0, col_idx=0, total_rows=len(self.rows), total_cols=len(self.service_grid_columns), ) else: self.service_grid_selection = {} def _rebuild_service_grid_columns(self): editable_table = not self._service_table_row_actions_disabled() auto_columns = self._auto_managed_columns() max_cols = min(len(row) for row in self.rows) if self.rows else len(self.columns) def is_column_editable(column_name: str) -> bool: if not editable_table or column_name in auto_columns: return False # For applications table use dedicated selectors instead of free text. if self.selected_table == "applications" and column_name in { "type", "application_group_id", }: return False if ( self.selected_table == "applications" and self._is_applications_virtual_column(column_name) ): return False return True self.service_grid_columns = [ { "title": column_name, "id": column_name, "type": "str", "width": self._service_grid_column_default_width(column_name), "editable": is_column_editable(column_name), } for column_name in self.columns if (column_name or "").strip() ][:max_cols] cols_len = len(self.service_grid_columns) self.rows = [ ([str(cell) for cell in row[:cols_len]] + [""] * max(0, cols_len - len(row))) for row in self.rows ] def _sort_pairs_by_column( self, pairs: list[tuple[str, list[str]]], column_idx: int, prev_column_idx: int, prev_desc: bool, ) -> tuple[list[tuple[str, list[str]]], bool]: desc = prev_column_idx == column_idx and not prev_desc sorted_pairs = pairs[:] sorted_pairs.sort( key=lambda pair: self._sortable_cell_value(pair[1][column_idx]) if column_idx < len(pair[1]) else (2, ""), reverse=desc, ) return sorted_pairs, desc def _sorted_pairs_first_column_desc( self, pairs: list[tuple[Any, list[str]]] ) -> list[tuple[Any, list[str]]]: if not pairs: return pairs col_idx = 0 out = pairs[:] out.sort( key=lambda pair: self._sortable_cell_value(pair[1][col_idx]) if col_idx < len(pair[1]) else (2, ""), reverse=True, ) return out def _apply_default_service_grid_sort(self) -> None: if not self.rows or not self.service_grid_columns: self.service_sort_column_idx = -1 self.service_sort_desc = False return if len(self.row_ids) == len(self.rows) and self.row_ids: pairs = self._sorted_pairs_first_column_desc(list(zip(self.row_ids, self.rows))) self.row_ids = [p[0] for p in pairs] self.rows = [p[1] for p in pairs] else: pairs = self._sorted_pairs_first_column_desc( [(i, r[:]) for i, r in enumerate(self.rows)] ) self.rows = [p[1] for p in pairs] self.service_sort_column_idx = 0 self.service_sort_desc = True def _apply_default_custom_tables_list_sort(self) -> None: if not self.custom_tables_grid_rows: self.custom_tables_sort_column_idx = -1 self.custom_tables_sort_desc = False return pairs = [ (row[0] if row else "", row[:]) for row in self.custom_tables_grid_rows ] pairs = self._sorted_pairs_first_column_desc(pairs) self.custom_tables_grid_rows = [p[1] for p in pairs] self.custom_tables = [p[0] for p in pairs] self.custom_tables_sort_column_idx = 0 self.custom_tables_sort_desc = True def _apply_default_auth_grid_sort(self) -> None: if not self.auth_rows or not self.auth_grid_columns: self.auth_sort_column_idx = -1 self.auth_sort_desc = False return pairs = self._sorted_pairs_first_column_desc( list(zip(self.auth_row_ids, self.auth_rows)) ) self.auth_row_ids = [p[0] for p in pairs] self.auth_rows = [p[1] for p in pairs] self.auth_sort_column_idx = 0 self.auth_sort_desc = True def _apply_default_reports_result_sort(self) -> None: if not self.reports_result_rows or not self.reports_result_grid_columns: return pairs = self._sorted_pairs_first_column_desc( [(i, r[:]) for i, r in enumerate(self.reports_result_rows)] ) self.reports_result_rows = [p[1] for p in pairs] def handle_custom_tables_grid_header_clicked(self, position: tuple[int, int]): column_idx = self._extract_header_column_index(position) if column_idx is None or column_idx < 0: return if not self.custom_tables_grid_rows: return selected_name = "" if 0 <= self.custom_tables_selected_row_idx < len(self.custom_tables): selected_name = self.custom_tables[self.custom_tables_selected_row_idx] pairs = [(row[0] if row else "", row[:]) for row in self.custom_tables_grid_rows] sorted_pairs, desc = self._sort_pairs_by_column( pairs, column_idx, self.custom_tables_sort_column_idx, self.custom_tables_sort_desc, ) self.custom_tables_grid_rows = [pair[1] for pair in sorted_pairs] self.custom_tables = [pair[0] for pair in sorted_pairs] self.custom_tables_sort_column_idx = column_idx self.custom_tables_sort_desc = desc if selected_name and selected_name in self.custom_tables: self.custom_tables_selected_row_idx = self.custom_tables.index(selected_name) self.custom_table_target = selected_name else: self.custom_tables_selected_row_idx = -1 self.custom_tables_grid_selection = {} def handle_service_grid_header_clicked(self, position: tuple[int, int]): column_idx = self._extract_header_column_index(position) if column_idx is None or column_idx < 0 or column_idx >= len(self.columns): return selected_row_id = "" if 0 <= self.service_grid_selected_row_idx < len(self.row_ids): selected_row_id = self.row_ids[self.service_grid_selected_row_idx] pairs = list(zip(self.row_ids, self.rows)) sorted_pairs, desc = self._sort_pairs_by_column( pairs, column_idx, self.service_sort_column_idx, self.service_sort_desc, ) self.row_ids = [pair[0] for pair in sorted_pairs] self.rows = [pair[1] for pair in sorted_pairs] self.service_sort_column_idx = column_idx self.service_sort_desc = desc self._close_inline_type_editor() self._close_inline_dashboard_editor() if selected_row_id and selected_row_id in self.row_ids: self.service_grid_selected_row_idx = self.row_ids.index(selected_row_id) else: self.service_grid_selected_row_idx = -1 self.service_grid_selection = {} def handle_master_detail_master_grid_header_clicked(self, position: tuple[int, int]): column_idx = self._extract_header_column_index(position) if ( column_idx is None or column_idx < 0 or column_idx >= len(self.master_detail_master_grid_columns) ): return selected_key = "" if 0 <= self.master_detail_selected_master_idx < len(self.master_detail_master_key_values): selected_key = self.master_detail_master_key_values[self.master_detail_selected_master_idx] pairs = list(zip(self.master_detail_master_key_values, self.master_detail_master_rows)) sorted_pairs, desc = self._sort_pairs_by_column( pairs, column_idx, self.master_detail_master_sort_column_idx, self.master_detail_master_sort_desc, ) self.master_detail_master_key_values = [pair[0] for pair in sorted_pairs] self.master_detail_master_rows = [pair[1] for pair in sorted_pairs] self.master_detail_master_sort_column_idx = column_idx self.master_detail_master_sort_desc = desc if selected_key and selected_key in self.master_detail_master_key_values: self.select_master_detail_row(self.master_detail_master_key_values.index(selected_key)) else: self.master_detail_selected_master_idx = -1 self.master_detail_master_grid_selection = {} self.master_detail_detail_rows = [] self.master_detail_detail_selected_row_idx = -1 self.master_detail_detail_grid_selection = {} def handle_master_detail_detail_grid_header_clicked(self, position: tuple[int, int]): column_idx = self._extract_header_column_index(position) if ( column_idx is None or column_idx < 0 or column_idx >= len(self.master_detail_detail_grid_columns) ): return selected_row_id = "" if 0 <= self.master_detail_detail_selected_row_idx < len(self.master_detail_detail_row_ids): selected_row_id = self.master_detail_detail_row_ids[ self.master_detail_detail_selected_row_idx ] pairs = list(zip(self.master_detail_detail_row_ids, self.master_detail_detail_rows)) sorted_pairs, desc = self._sort_pairs_by_column( pairs, column_idx, self.master_detail_detail_sort_column_idx, self.master_detail_detail_sort_desc, ) self.master_detail_detail_row_ids = [pair[0] for pair in sorted_pairs] self.master_detail_detail_rows = [pair[1] for pair in sorted_pairs] self.master_detail_detail_sort_column_idx = column_idx self.master_detail_detail_sort_desc = desc if selected_row_id and selected_row_id in self.master_detail_detail_row_ids: self.master_detail_detail_selected_row_idx = self.master_detail_detail_row_ids.index( selected_row_id ) else: self.master_detail_detail_selected_row_idx = -1 self.master_detail_detail_grid_selection = {} def handle_auth_grid_header_clicked(self, position: tuple[int, int]): column_idx = self._extract_header_column_index(position) if column_idx is None or column_idx < 0 or column_idx >= len(self.auth_grid_columns): return selected_row_id = "" if 0 <= self.auth_selected_row_idx < len(self.auth_row_ids): selected_row_id = self.auth_row_ids[self.auth_selected_row_idx] pairs = list(zip(self.auth_row_ids, self.auth_rows)) sorted_pairs, desc = self._sort_pairs_by_column( pairs, column_idx, self.auth_sort_column_idx, self.auth_sort_desc, ) self.auth_row_ids = [pair[0] for pair in sorted_pairs] self.auth_rows = [pair[1] for pair in sorted_pairs] self.auth_sort_column_idx = column_idx self.auth_sort_desc = desc if selected_row_id and selected_row_id in self.auth_row_ids: self.auth_selected_row_idx = self.auth_row_ids.index(selected_row_id) else: self.auth_selected_row_idx = -1 self.auth_grid_selection = {} def _sync_master_detail_master_grid_columns(self): max_cols = ( min(len(row) for row in self.master_detail_master_rows) if self.master_detail_master_rows else len(self.master_detail_master_columns) ) self.master_detail_master_grid_columns = [ {"title": col, "id": col, "type": "str", "editable": False} for col in self.master_detail_master_columns if (col or "").strip() ][:max_cols] cols_len = len(self.master_detail_master_grid_columns) self.master_detail_master_rows = [ ([str(cell) for cell in row[:cols_len]] + [""] * max(0, cols_len - len(row))) for row in self.master_detail_master_rows ] def _sync_master_detail_detail_grid_columns(self): max_cols = ( min(len(row) for row in self.master_detail_detail_rows) if self.master_detail_detail_rows else len(self.master_detail_detail_columns) ) self.master_detail_detail_grid_columns = [ {"title": col, "id": col, "type": "str", "editable": True} for col in self.master_detail_detail_columns if (col or "").strip() ][:max_cols] cols_len = len(self.master_detail_detail_grid_columns) self.master_detail_detail_rows = [ ([str(cell) for cell in row[:cols_len]] + [""] * max(0, cols_len - len(row))) for row in self.master_detail_detail_rows ] def _sync_auth_grid_columns(self): max_cols = min(len(row) for row in self.auth_rows) if self.auth_rows else len(self.auth_columns) self.auth_grid_columns = [ {"title": col, "id": col, "type": "str", "editable": True} for col in self.auth_columns if (col or "").strip() ][:max_cols] cols_len = len(self.auth_grid_columns) self.auth_rows = [ ([str(cell) for cell in row[:cols_len]] + [""] * max(0, cols_len - len(row))) for row in self.auth_rows ] def _sync_reports_grid_columns(self): max_cols = ( min(len(row) for row in self.reports_result_rows) if self.reports_result_rows else len(self.reports_result_columns) ) self.reports_result_grid_columns = [ {"title": col, "id": col, "type": "str", "editable": False} for col in self.reports_result_columns if (col or "").strip() ][:max_cols] cols_len = len(self.reports_result_grid_columns) self.reports_result_rows = [ ([str(cell) for cell in row[:cols_len]] + [""] * max(0, cols_len - len(row))) for row in self.reports_result_rows ] def select_service_grid_cell(self, position: tuple[int, int]): if not isinstance(position, (list, tuple)) or len(position) < 2: return try: col_idx = int(position[0]) row_idx = int(position[1]) except (TypeError, ValueError): return if row_idx < 0 or row_idx >= len(self.rows): self.service_grid_selected_row_idx = -1 self.service_grid_selection = {} return self.service_grid_selected_row_idx = row_idx self.service_grid_selection = _build_row_grid_selection( row_idx=row_idx, col_idx=col_idx, total_rows=len(self.rows), total_cols=len(self.service_grid_columns), ) if ( self.applications_inline_type_row_idx >= 0 and ( row_idx != self.applications_inline_type_row_idx or col_idx != self.applications_inline_type_col_idx ) ): self._close_inline_type_editor() if ( self.applications_inline_dashboard_row_idx >= 0 and ( row_idx != self.applications_inline_dashboard_row_idx or col_idx != self.applications_inline_dashboard_col_idx ) ): self._close_inline_dashboard_editor() def _clear_applications_edit_target(self): self.applications_edit_target_row_idx = -1 self.applications_edit_target_col = "" self.applications_type_select_open = False self.applications_group_select_open = False def _close_inline_type_editor(self): self.applications_inline_type_row_idx = -1 self.applications_inline_type_col_idx = -1 self.applications_inline_type_open = False self.applications_inline_type_value = "" def _close_inline_dashboard_editor(self): self.applications_inline_dashboard_row_idx = -1 self.applications_inline_dashboard_col_idx = -1 self.applications_inline_dashboard_open = False self.applications_inline_dashboard_value = "" def set_applications_inline_type_open(self, is_open: bool): self.applications_inline_type_open = bool(is_open) if not self.applications_inline_type_open: self._close_inline_type_editor() def set_applications_inline_dashboard_open(self, is_open: bool): self.applications_inline_dashboard_open = bool(is_open) if not self.applications_inline_dashboard_open: self._close_inline_dashboard_editor() @rx.var def applications_inline_type_top(self) -> str: if self.applications_inline_type_row_idx < 0: return "0px" top = DATA_EDITOR_HEADER_HEIGHT + (self.applications_inline_type_row_idx * DATA_EDITOR_ROW_HEIGHT) return f"{top}px" @rx.var def applications_inline_type_left(self) -> str: if self.applications_inline_type_col_idx < 0: return "0px" left = 0 for idx in range(self.applications_inline_type_col_idx): if idx < len(self.service_grid_columns): try: left += int(self.service_grid_columns[idx].get("width", 190)) except Exception: left += 190 return f"{left}px" @rx.var def applications_inline_type_width(self) -> str: idx = self.applications_inline_type_col_idx if idx < 0 or idx >= len(self.service_grid_columns): return "190px" try: return f"{int(self.service_grid_columns[idx].get('width', 190))}px" except Exception: return "190px" @rx.var def applications_inline_dashboard_top(self) -> str: if self.applications_inline_dashboard_row_idx < 0: return "0px" top = DATA_EDITOR_HEADER_HEIGHT + (self.applications_inline_dashboard_row_idx * DATA_EDITOR_ROW_HEIGHT) return f"{top}px" @rx.var def applications_inline_dashboard_left(self) -> str: if self.applications_inline_dashboard_col_idx < 0: return "0px" left = 0 for idx in range(self.applications_inline_dashboard_col_idx): if idx < len(self.service_grid_columns): try: left += int(self.service_grid_columns[idx].get("width", 190)) except Exception: left += 190 return f"{left}px" def _next_value_from_options( self, current_value: str, options: list[str] ) -> str: if not options: return current_value try: current_idx = options.index(current_value) except ValueError: return options[0] return options[(current_idx + 1) % len(options)] def _persist_applications_single_cell( self, row_idx: int, column_name: str, display_value: str ) -> bool: if self.selected_table != "applications": return False if row_idx < 0 or row_idx >= len(self.rows): return False try: column_types = { col["name"]: col["type"] for col in self._read_table_columns(self.selected_table) } db_value = display_value if column_name == "application_group_id": db_value = self._application_group_label_to_id(display_value) elif column_name == "type": normalized = (display_value or "").strip().lower() if normalized not in APPLICATION_TYPE_OPTIONS: self.load_error = self._msg( "errors.application_type_invalid", options=", ".join(APPLICATION_TYPE_OPTIONS), ) return False db_value = normalized payload = { column_name: self._coerce_row_value( db_value, column_types.get(column_name, "text") ) } client = self._create_client() query = client.table(self.selected_table).update(payload) row_id = self.row_ids[row_idx].strip() if row_idx < len(self.row_ids) else "" if row_id: query = query.eq("id", row_id) else: self.load_error = self._msg("errors.cannot_identify_row_update") return False query.execute() self.load_error = "" return True except Exception as exc: self.load_error = self._msg("errors.edit_row_failed", detail=str(exc)) return False def handle_service_grid_cell_activated(self, position: tuple[int, int]): self.select_service_grid_cell(position) if not isinstance(position, (list, tuple)) or len(position) < 2: self._clear_applications_edit_target() self._close_inline_type_editor() self._close_inline_dashboard_editor() return if self.selected_table != "applications": self._clear_applications_edit_target() self._close_inline_type_editor() self._close_inline_dashboard_editor() return try: col_idx = int(position[0]) row_idx = int(position[1]) except (TypeError, ValueError): self._clear_applications_edit_target() self._close_inline_type_editor() self._close_inline_dashboard_editor() return if row_idx < 0 or row_idx >= len(self.rows): self._clear_applications_edit_target() self._close_inline_type_editor() self._close_inline_dashboard_editor() return if col_idx < 0 or col_idx >= len(self.columns): self._clear_applications_edit_target() self._close_inline_type_editor() self._close_inline_dashboard_editor() return column_name = self.columns[col_idx] if column_name not in {"type", "dashboard_name"}: self._clear_applications_edit_target() self._close_inline_type_editor() self._close_inline_dashboard_editor() return if column_name == "type": self._close_inline_dashboard_editor() current_value = self.rows[row_idx][col_idx] if col_idx < len(self.rows[row_idx]) else "" normalized_value = (current_value or "").strip().lower() if normalized_value not in APPLICATION_TYPE_OPTIONS: normalized_value = APPLICATION_TYPE_OPTIONS[0] self.applications_inline_type_row_idx = row_idx self.applications_inline_type_col_idx = col_idx self.applications_inline_type_value = normalized_value self.applications_inline_type_open = True return self._close_inline_type_editor() current_value = self.rows[row_idx][col_idx] if col_idx < len(self.rows[row_idx]) else "" normalized_value = (current_value or "").strip() if normalized_value not in self.application_group_labels and self.application_group_labels: normalized_value = self.application_group_labels[0] self.applications_inline_dashboard_row_idx = row_idx self.applications_inline_dashboard_col_idx = col_idx self.applications_inline_dashboard_value = normalized_value self.applications_inline_dashboard_open = True def set_inline_applications_type_value(self, value: str): normalized = (value or "").strip().lower() if normalized not in APPLICATION_TYPE_OPTIONS: return row_idx = self.applications_inline_type_row_idx col_idx = self.applications_inline_type_col_idx if ( row_idx < 0 or row_idx >= len(self.rows) or col_idx < 0 or col_idx >= len(self.columns) or col_idx >= len(self.rows[row_idx]) or self.columns[col_idx] != "type" ): self._close_inline_type_editor() return old_value = self.rows[row_idx][col_idx] updated_rows = [row[:] for row in self.rows] updated_rows[row_idx][col_idx] = normalized self.rows = updated_rows if not self._persist_applications_single_cell(row_idx, "type", normalized): rollback_rows = [row[:] for row in self.rows] rollback_rows[row_idx][col_idx] = old_value self.rows = rollback_rows return self._close_inline_type_editor() def set_inline_applications_dashboard_value(self, value: str): label = (value or "").strip() if label not in self.application_group_labels: return row_idx = self.applications_inline_dashboard_row_idx col_idx = self.applications_inline_dashboard_col_idx if ( row_idx < 0 or row_idx >= len(self.rows) or col_idx < 0 or col_idx >= len(self.columns) or col_idx >= len(self.rows[row_idx]) or self.columns[col_idx] != "dashboard_name" ): self._close_inline_dashboard_editor() return old_dashboard = self.rows[row_idx][col_idx] updated_rows = [row[:] for row in self.rows] updated_rows[row_idx][col_idx] = label self.rows = updated_rows if not self._persist_applications_single_cell(row_idx, "application_group_id", label): rollback_rows = [row[:] for row in self.rows] rollback_rows[row_idx][col_idx] = old_dashboard self.rows = rollback_rows return self._close_inline_dashboard_editor() def handle_service_grid_selection_change(self, selection: Any): position = _extract_cell_from_grid_selection(selection) if position is None: self.service_grid_selected_row_idx = -1 self.service_grid_selection = {} return self.select_service_grid_cell(position) def _extract_data_editor_value(self, cell: Any) -> str: if isinstance(cell, dict): if "data" in cell: return "" if cell["data"] is None else str(cell["data"]) if "displayData" in cell: return "" if cell["displayData"] is None else str(cell["displayData"]) if cell is None: return "" return str(cell) def handle_service_grid_cell_edited(self, position: tuple[int, int], cell: Any): if not isinstance(position, (list, tuple)) or len(position) < 2: return try: col_idx = int(position[0]) row_idx = int(position[1]) except (TypeError, ValueError): return if col_idx < 0 or col_idx >= len(self.columns): return if row_idx < 0 or row_idx >= len(self.rows): return self.service_grid_selected_row_idx = row_idx self.service_grid_selection = _build_row_grid_selection( row_idx=row_idx, col_idx=col_idx, total_rows=len(self.rows), total_cols=len(self.service_grid_columns), ) value = self._extract_data_editor_value(cell) if self.is_adding_row: draft_idx = self._service_grid_draft_row_idx() if draft_idx >= 0 and row_idx == draft_idx: self.set_new_row_value(col_idx, value) return if self._service_table_row_actions_disabled(): return if self.editing_row_idx != row_idx: self.start_edit_row(row_idx) if self.editing_row_idx == row_idx: self.set_edit_row_value(row_idx, col_idx, value) def handle_service_grid_column_resize(self, column: Any, width: int): if not self.selected_table: return try: width_value = int(width) except (TypeError, ValueError): return if width_value <= 0: return column_name = "" if isinstance(column, dict): raw_name = column.get("id") or column.get("title") if raw_name is not None: column_name = str(raw_name) if not column_name: return key = f"{self.selected_table}:{column_name}" updated_widths = dict(self.service_grid_column_widths) updated_widths[key] = width_value self.service_grid_column_widths = updated_widths self._rebuild_service_grid_columns() def start_edit_selected_service_row(self): if self.service_grid_selected_row_idx < 0: self.load_error = self._msg("errors.select_row_first") return self.start_edit_row(self.service_grid_selected_row_idx) def save_selected_service_row_edit(self): if self.editing_row_idx < 0: return self.save_edit_row(self.editing_row_idx) def cancel_selected_service_row_edit(self): if self.editing_row_idx < 0: return self.cancel_edit_row(self.editing_row_idx) def delete_selected_service_row(self): if self.service_grid_selected_row_idx < 0: self.load_error = self._msg("errors.select_row_first") return self.delete_row(self.service_grid_selected_row_idx) def select_master_detail_row_from_grid(self, position: tuple[int, int]): if not isinstance(position, (list, tuple)) or len(position) < 2: return try: col_idx = int(position[0]) row_idx = int(position[1]) except (TypeError, ValueError): return self.master_detail_master_grid_selection = _build_row_grid_selection( row_idx=row_idx, col_idx=col_idx, total_rows=len(self.master_detail_master_rows), total_cols=len(self.master_detail_master_grid_columns), ) self.select_master_detail_row(row_idx) def handle_master_detail_master_grid_selection_change(self, selection: Any): position = _extract_cell_from_grid_selection(selection) if position is None: self.master_detail_selected_master_idx = -1 self.master_detail_master_grid_selection = {} return self.select_master_detail_row_from_grid(position) def select_master_detail_detail_row_from_grid(self, position: tuple[int, int]): if not isinstance(position, (list, tuple)) or len(position) < 2: return try: col_idx = int(position[0]) row_idx = int(position[1]) except (TypeError, ValueError): return if 0 <= row_idx < len(self.master_detail_detail_rows): self.master_detail_detail_selected_row_idx = row_idx self.master_detail_detail_grid_selection = _build_row_grid_selection( row_idx=row_idx, col_idx=col_idx, total_rows=len(self.master_detail_detail_rows), total_cols=len(self.master_detail_detail_grid_columns), ) else: self.master_detail_detail_selected_row_idx = -1 self.master_detail_detail_grid_selection = {} def handle_master_detail_detail_grid_selection_change(self, selection: Any): position = _extract_cell_from_grid_selection(selection) if position is None: self.master_detail_detail_selected_row_idx = -1 self.master_detail_detail_grid_selection = {} return self.select_master_detail_detail_row_from_grid(position) def handle_master_detail_detail_cell_edited(self, position: tuple[int, int], cell: Any): if not isinstance(position, (list, tuple)) or len(position) < 2: return try: col_idx = int(position[0]) row_idx = int(position[1]) except (TypeError, ValueError): return value = self._extract_data_editor_value(cell) self.master_detail_detail_selected_row_idx = row_idx self.master_detail_detail_grid_selection = _build_row_grid_selection( row_idx=row_idx, col_idx=col_idx, total_rows=len(self.master_detail_detail_rows), total_cols=len(self.master_detail_detail_grid_columns), ) if self.master_detail_detail_adding_row and row_idx == 0: self.set_master_detail_detail_new_value(col_idx, value) return if self.master_detail_detail_editing_idx != row_idx: self.start_edit_master_detail_detail_row(row_idx) if self.master_detail_detail_editing_idx == row_idx: self.set_master_detail_detail_edit_value(row_idx, col_idx, value) if not self.master_detail_detail_adding_row: self.save_edit_master_detail_detail_row(row_idx) def start_edit_selected_master_detail_detail_row(self): if self.master_detail_detail_selected_row_idx < 0: return self.start_edit_master_detail_detail_row(self.master_detail_detail_selected_row_idx) def save_selected_master_detail_detail_row(self): if self.master_detail_detail_adding_row and self.master_detail_detail_selected_row_idx == 0: self.save_master_detail_detail_new_row() return if self.master_detail_detail_editing_idx >= 0: self.save_edit_master_detail_detail_row(self.master_detail_detail_editing_idx) def cancel_selected_master_detail_detail_row(self): if self.master_detail_detail_adding_row and self.master_detail_detail_selected_row_idx == 0: self.cancel_master_detail_detail_add() return if self.master_detail_detail_editing_idx >= 0: self.cancel_edit_master_detail_detail_row(self.master_detail_detail_editing_idx) def delete_selected_master_detail_detail_row(self): if self.master_detail_detail_selected_row_idx < 0: return self.delete_master_detail_detail_row(self.master_detail_detail_selected_row_idx) def select_auth_row_from_grid(self, position: tuple[int, int]): if not isinstance(position, (list, tuple)) or len(position) < 2: return try: col_idx = int(position[0]) row_idx = int(position[1]) except (TypeError, ValueError): return if 0 <= row_idx < len(self.auth_rows): self.auth_selected_row_idx = row_idx self.auth_grid_selection = _build_row_grid_selection( row_idx=row_idx, col_idx=col_idx, total_rows=len(self.auth_rows), total_cols=len(self.auth_grid_columns), ) else: self.auth_selected_row_idx = -1 self.auth_grid_selection = {} def handle_auth_grid_selection_change(self, selection: Any): position = _extract_cell_from_grid_selection(selection) if position is None: self.auth_selected_row_idx = -1 self.auth_grid_selection = {} return self.select_auth_row_from_grid(position) def handle_auth_cell_edited(self, position: tuple[int, int], cell: Any): if not isinstance(position, (list, tuple)) or len(position) < 2: return try: col_idx = int(position[0]) row_idx = int(position[1]) except (TypeError, ValueError): return value = self._extract_data_editor_value(cell) self.auth_selected_row_idx = row_idx self.auth_grid_selection = _build_row_grid_selection( row_idx=row_idx, col_idx=col_idx, total_rows=len(self.auth_rows), total_cols=len(self.auth_grid_columns), ) if self.auth_is_adding_row and row_idx == 0: self.set_auth_new_row_value(col_idx, value) return if self.auth_editing_row_idx != row_idx: self.start_edit_auth_row(row_idx) if self.auth_editing_row_idx == row_idx: self.set_auth_edit_row_value(row_idx, col_idx, value) def start_edit_selected_auth_row(self): if self.auth_selected_row_idx < 0: return self.start_edit_auth_row(self.auth_selected_row_idx) def save_selected_auth_row(self): if self.auth_is_adding_row and self.auth_selected_row_idx == 0: self.save_auth_new_row() return if self.auth_editing_row_idx >= 0: self.save_edit_auth_row(self.auth_editing_row_idx) def cancel_selected_auth_row(self): if self.auth_is_adding_row and self.auth_selected_row_idx == 0: self.cancel_add_auth_row() return if self.auth_editing_row_idx >= 0: self.cancel_edit_auth_row(self.auth_editing_row_idx) def delete_selected_auth_row(self): if self.auth_selected_row_idx < 0: return self.delete_auth_row(self.auth_selected_row_idx) def set_new_table_name(self, value: str): self.new_table_name = value def start_create_app_mode(self): if self.selected_table not in self.custom_tables: self.load_error = self._msg("errors.create_app_custom_only") return if self.custom_table_opened_from_application_tile: self.load_error = self._msg("errors.create_app_from_custom_nav") return self.is_create_app_mode = True self.new_app_name = "" self.load_error = "" def cancel_create_app_mode(self): self.is_create_app_mode = False self.new_app_name = "" def set_new_app_name(self, value: str): self.new_app_name = value self.load_error = "" def _insert_application_record( self, app_name: str, content: str, app_type: str, application_group_id: str | None = None, ): client = self._create_client() columns_meta = self._read_table_columns("applications") column_names = { str(col.get("name", "")).strip() for col in columns_meta if isinstance(col, dict) and col.get("name") } group_id = (application_group_id or "").strip() or None payload: dict[str, Any] = { "name": app_name, "application_group_id": group_id, } if "content" in column_names: payload["content"] = content elif "description" in column_names: payload["description"] = content if "type" in column_names: payload["type"] = app_type if "code" in column_names: slug = app_name.strip().lower().replace(" ", "_").replace("-", "_") payload["code"] = f"{slug}_{str(uuid.uuid4())[:8]}" client.table("applications").insert(payload).execute() def create_app_from_selected_table(self): source_table = self.selected_table.strip() app_name = self.new_app_name.strip() if self.custom_table_opened_from_application_tile: self.load_error = self._msg("errors.create_app_from_custom_nav") return if source_table not in self.custom_tables: self.load_error = self._msg("errors.select_custom_for_app") return if not app_name: self.load_error = self._msg("errors.app_name_required") return try: self._insert_application_record( app_name=app_name, content=source_table, app_type="custom table", ) self.is_create_app_mode = False self.new_app_name = "" self.load_error = "" except Exception as exc: self.load_error = self._msg("errors.create_app_failed", detail=str(exc)) def start_create_master_detail_app_mode(self): self.is_create_master_detail_app_mode = True self.new_master_detail_app_name = "" self.master_detail_error = "" def cancel_create_master_detail_app_mode(self): self.is_create_master_detail_app_mode = False self.new_master_detail_app_name = "" def set_new_master_detail_app_name(self, value: str): self.new_master_detail_app_name = value self.master_detail_error = "" def create_app_from_master_detail(self): app_name = self.new_master_detail_app_name.strip() if not app_name: self.master_detail_error = self._msg("errors.master_detail_name_required") return if ( not self.master_detail_master_table or not self.master_detail_detail_table or not self.master_detail_master_key or not self.master_detail_detail_key ): self.master_detail_error = self._msg("errors.master_detail_select_keys") return content = ( f'Master table: "{self.master_detail_master_table}", ' f'Detail table: "{self.master_detail_detail_table}", ' f'Master key: "{self.master_detail_master_key}", ' f'Detail key: "{self.master_detail_detail_key}"' ) try: self._insert_application_record( app_name=app_name, content=content, app_type="master/detail", ) self.is_create_master_detail_app_mode = False self.new_master_detail_app_name = "" self.master_detail_error = "" except Exception as exc: self.master_detail_error = self._msg( "errors.create_app_failed", detail=str(exc) ) def start_create_visuals_app_mode(self): if not self.visuals_show_create_app: return self.is_create_visuals_app_mode = True self.new_visuals_app_name = "" self.visuals_notice = "" def cancel_create_visuals_app_mode(self): self.is_create_visuals_app_mode = False self.new_visuals_app_name = "" def set_new_visuals_app_name(self, value: str): self.new_visuals_app_name = value self.visuals_notice = "" def create_app_from_visuals(self): if not self.visuals_show_create_app: self.visuals_notice = self._msg("errors.visuals_constructor_only") return app_name = self.new_visuals_app_name.strip() if not app_name: self.visuals_notice = self._msg("errors.app_name_required") return if ( not self.visuals_selected_table or not self.visuals_label_column or len(self.visuals_data_columns) == 0 or not self.visuals_chart_type ): self.visuals_notice = self._msg("errors.visuals_select_all") return data_value = ", ".join( [item for item in self.visuals_data_columns if item.strip()] ) content = ( f'Table: "{self.visuals_selected_table}", ' f'Labels: "{self.visuals_label_column}", ' f'Data: "{data_value}", ' f'Display type: "{self.visuals_chart_type}"' ) try: self._insert_application_record( app_name=app_name, content=content, app_type="chart", ) self.is_create_visuals_app_mode = False self.new_visuals_app_name = "" self.visuals_notice = self._msg("errors.app_created") except Exception as exc: self.visuals_notice = self._msg( "errors.create_app_failed", detail=str(exc) ) def toggle_service_tables(self): self.service_tables_expanded = not self.service_tables_expanded def toggle_custom_tables(self): self.custom_tables_expanded = not self.custom_tables_expanded def toggle_administration(self): self.administration_expanded = not self.administration_expanded def toggle_apps_constructor(self): self.apps_constructor_expanded = not self.apps_constructor_expanded def toggle_applications_management(self): self.applications_management_expanded = ( not self.applications_management_expanded ) def focus_apps_management_nav(self): """Expand Administration → Apps management (breadcrumb middle segment).""" self.administration_expanded = True self.applications_management_expanded = True def select_table_from_apps_management(self, table_name: str): """Open dashboards or applications grid from Administration → Apps management.""" return self._select_table_impl(table_name, "apps_management") def _label_for_app_group(self, group_id: str) -> str: gid = (group_id or "").strip() if not gid: return self._msg("nav.group_fallback") for entry in self.applications_nav_groups: if (entry.id or "").strip() == gid: return (entry.name or "").strip() or gid return gid[:10] + "…" if len(gid) > 10 else gid def _apply_nav_breadcrumb(self, route: list[str], labels: list[str]): n = min(len(route), len(labels)) route = route[:n] labels = labels[:n] self.nav_breadcrumb_route = list(route) self.nav_breadcrumb_items = [ NavBreadcrumbItem(label=lab, idx=i, is_current=(i == n - 1)) for i, lab in enumerate(labels) ] def sync_nav_breadcrumb(self): st = (self.selected_table or "").strip() m = self._msg if not st: self.nav_breadcrumb_items = [] self.nav_breadcrumb_route = [] return if st == "application_groups": if self.applications_breadcrumb_source == "apps_management": self._apply_nav_breadcrumb( ["administration", "apps_management", "table:application_groups"], [ m("crumb.administration"), m("crumb.apps_management"), m("nav.dashboards"), ], ) return if self.applications_breadcrumb_source == "service_tables": self._apply_nav_breadcrumb( ["administration", "service_tables_group", "table:application_groups"], [ m("crumb.administration"), m("crumb.service_tables"), m("nav.dashboards"), ], ) return route_a: list[str] = ["applications"] labels_a: list[str] = [m("nav.applications")] gid = (self.applications_nav_group_id or "").strip() if gid: route_a.append(f"app_group:{gid}") labels_a.append( self.applications_dashboard_title or self._label_for_app_group(gid) ) self._apply_nav_breadcrumb(route_a, labels_a) return if st == "applications": if self.applications_breadcrumb_source == "apps_management": self._apply_nav_breadcrumb( ["administration", "apps_management", "table:applications"], [ m("crumb.administration"), m("crumb.apps_management"), m("nav.table_applications"), ], ) return self._apply_nav_breadcrumb( ["administration", "service_tables_group", "table:applications"], [ m("crumb.administration"), m("crumb.service_tables"), m("nav.table_applications"), ], ) return if st == "Custom Tables": self._apply_nav_breadcrumb( ["administration", "apps_constructor", "custom_tables"], [ m("crumb.administration"), m("crumb.apps_constructor"), m("nav.custom_tables"), ], ) return if st == "Service Tables": self._apply_nav_breadcrumb( ["administration", "service_tables_group"], [m("crumb.administration"), m("crumb.service_tables")], ) return if self.custom_tables.count(st) > 0: if ( self.custom_table_opened_from_application_tile or self.custom_table_apps_breadcrumb_active ): gid = (self.applications_nav_group_id or "").strip() if gid: self._apply_nav_breadcrumb( ["applications", f"app_group:{gid}", f"table:{st}"], [ m("nav.applications"), self.applications_dashboard_title or self._label_for_app_group(gid), st, ], ) else: self._apply_nav_breadcrumb( ["applications", f"table:{st}"], [m("nav.applications"), st], ) return self._apply_nav_breadcrumb( [ "administration", "apps_constructor", "custom_tables", f"table:{st}", ], [ m("crumb.administration"), m("crumb.apps_constructor"), m("nav.custom_tables"), st, ], ) return if st == "Visuals": if not self.visuals_show_create_app: self._apply_apps_tile_constructor_breadcrumb("visuals") return self._apply_nav_breadcrumb( ["administration", "apps_constructor", "visuals"], [ m("crumb.administration"), m("crumb.apps_constructor"), m("nav.charts"), ], ) return if st == "Master/Detail": if self.master_detail_opened_from_application_tile: self._apply_apps_tile_constructor_breadcrumb("master_detail") return self._apply_nav_breadcrumb( ["administration", "apps_constructor", "master_detail"], [ m("crumb.administration"), m("crumb.apps_constructor"), m("nav.master_detail"), ], ) return if st == "Reports": if self.reports_opened_from_application_tile: self._apply_apps_tile_constructor_breadcrumb("reports") return self._apply_nav_breadcrumb( ["administration", "apps_constructor", "reports"], [ m("crumb.administration"), m("crumb.apps_constructor"), m("nav.reports"), ], ) return if st == "Authentication": at = (self.auth_selected_table or "").strip() if at: self._apply_nav_breadcrumb( ["authentication_root", f"auth_table:{at}"], [m("nav.authentication"), at], ) else: self._apply_nav_breadcrumb( ["authentication_root"], [m("nav.authentication")], ) return if self.service_tables.count(st) > 0: self._apply_nav_breadcrumb( ["administration", "service_tables_group", f"table:{st}"], [m("crumb.administration"), m("crumb.service_tables"), st], ) return self._apply_nav_breadcrumb([f"table:{st}"], [st]) def _apply_apps_tile_constructor_breadcrumb(self, kind: str): defaults = { "visuals": self._msg("nav.charts"), "master_detail": self._msg("nav.master_detail"), "reports": self._msg("nav.reports"), } label = (self.application_tile_constructor_label or "").strip() if not label: label = defaults.get(kind, kind) gid = (self.applications_nav_group_id or "").strip() gtitle = ( (self.applications_dashboard_title or "").strip() or (self._label_for_app_group(gid) if gid else "") ) m = self._msg if gid: self._apply_nav_breadcrumb( ["applications", f"app_group:{gid}", f"constructor_view:{kind}"], [m("nav.applications"), gtitle, label], ) else: self._apply_nav_breadcrumb( ["applications", f"constructor_view:{kind}"], [m("nav.applications"), label], ) def breadcrumb_go(self, idx: int): if idx < 0: return route = self.nav_breadcrumb_route if idx >= len(route): return prefix = route[: idx + 1] head = prefix[0] if head.startswith("table:") and len(prefix) == 1: return self.select_table(head.split(":", 1)[1]) if head == "applications": if len(prefix) == 1: return self.select_applications() seg1 = prefix[1] if seg1.startswith("constructor_view:"): kind = seg1.split(":", 1)[1] self.select_applications() if kind == "visuals": return self.select_visuals() if kind == "master_detail": return self.select_master_detail() if kind == "reports": return self.select_reports() return if seg1.startswith("app_group:"): gid = seg1.split(":", 1)[1] if len(prefix) == 2: return self.select_applications_group(gid) if len(prefix) >= 3 and prefix[2].startswith("table:"): tname = prefix[2].split(":", 1)[1] self.select_applications_group(gid) return self.select_table_from_application_tile(tname) if len(prefix) >= 3 and prefix[2].startswith("constructor_view:"): return self.select_applications_group(gid) return if seg1.startswith("table:"): tname = seg1.split(":", 1)[1] self.select_applications() return self.select_table_from_application_tile(tname) return if head == "administration": if len(prefix) >= 2 and prefix[1] == "apps_management": if len(prefix) >= 3 and prefix[2].startswith("table:"): tname = prefix[2].split(":", 1)[1] return self.select_table_from_apps_management(tname) return self.focus_apps_management_nav() if len(prefix) >= 2 and prefix[1] == "service_tables_group": if len(prefix) >= 3 and prefix[2].startswith("table:"): tname = prefix[2].split(":", 1)[1] return self.select_table(tname) return self.select_table_group("Service Tables") if len(prefix) == 2 and prefix[1] == "apps_constructor": return self.select_table_group("Custom Tables") if len(prefix) >= 3: third = prefix[2] if third == "custom_tables": if len(prefix) >= 4 and prefix[3].startswith("table:"): tname = prefix[3].split(":", 1)[1] return self.select_table(tname) return self.select_table_group("Custom Tables") if third == "visuals": return self.select_visuals() if third == "master_detail": return self.select_master_detail() if third == "reports": return self.select_reports() if third.startswith("table:"): return self.select_table(third.split(":", 1)[1]) return self.select_table_group("Custom Tables") if head == "authentication_root": if len(prefix) >= 2 and prefix[1].startswith("auth_table:"): at = prefix[1].split(":", 1)[1] self.select_authentication() return self.set_auth_table(at) return self.select_authentication() def _sync_application_tile_sidebar_context(self, tile_group_id: str): self.applications_breadcrumb_source = "sidebar_applications" gid = (tile_group_id or "").strip() if gid: self.applications_nav_group_id = gid else: self.applications_nav_group_id = "" self._sync_applications_dashboard_visible_groups() def _prepare_application_tile_custom_table_nav(self, tile_group_id: str): self.pending_custom_table_apps_breadcrumb = True self._sync_application_tile_sidebar_context(tile_group_id) def noop(self): return def _recompute_dashboard_pickable_apps(self): target = (self.dashboard_target_group_id or "").strip() pickable: list[ApplicationTile] = [] for t in self.dashboard_all_apps: if not (t.id or "").strip(): continue gid = (t.group_id or "").strip() if target: if gid != target: pickable.append(t) else: if gid: pickable.append(t) self.dashboard_pickable_apps = pickable def open_dashboard_pick_app(self, group_id: str = ""): self.dashboard_pick_app_open = True self.dashboard_target_group_id = (group_id or "").strip() self.dashboard_pick_app_error = "" self._recompute_dashboard_pickable_apps() def close_dashboard_pick_app(self): self.dashboard_pick_app_open = False self.dashboard_target_group_id = "" self.dashboard_pickable_apps = [] self.dashboard_pick_app_error = "" def assign_application_to_dashboard_group(self, application_id: str): app_id = (application_id or "").strip() if not app_id: self.dashboard_pick_app_error = self._msg("errors.invalid_application") return target = (self.dashboard_target_group_id or "").strip() try: client = self._create_client() payload: dict[str, Any] = {"application_group_id": target or None} client.table("applications").update(payload).eq("id", app_id).execute() self.close_dashboard_pick_app() self._load_applications_tiles() return self.init_dashboard_drag() except Exception as exc: self.dashboard_pick_app_error = self._msg( "errors.update_failed", detail=str(exc) ) def remove_application_from_dashboard_group(self, application_id: str): """Clear application_group_id (app moves to Ungrouped).""" app_id = (application_id or "").strip() if not app_id: return try: client = self._create_client() client.table("applications").update({"application_group_id": None}).eq( "id", app_id ).execute() self._load_applications_tiles() return self.init_dashboard_drag() except Exception as exc: self.load_error = self._msg("errors.remove_group_failed", detail=str(exc)) def delete_application_group_dashboard(self, group_id: str): """Remove group row; applications in this group become ungrouped.""" gid = (group_id or "").strip() if not gid: return try: client = self._create_client() client.table("applications").update({"application_group_id": None}).eq( "application_group_id", gid ).execute() client.table("application_groups").delete().eq("id", gid).execute() self._load_application_group_options(client) self.load_error = "" self._load_applications_tiles() return self.init_dashboard_drag() except Exception as exc: message = str(exc) if "23503" in message: self.load_error = self._msg("errors.delete_group_fk") else: self.load_error = self._msg( "errors.delete_group_failed", detail=str(exc) ) def open_dashboard_add_group(self): self.close_dashboard_pick_app() self.dashboard_add_group_open = True self.new_application_group_name = "" self.dashboard_add_group_error = "" def close_dashboard_add_group(self): self.dashboard_add_group_open = False self.new_application_group_name = "" self.dashboard_add_group_error = "" def set_new_application_group_name(self, value: str): self.new_application_group_name = value self.dashboard_add_group_error = "" def create_application_group(self): name = self.new_application_group_name.strip() if not name: self.dashboard_add_group_error = self._msg("errors.enter_group_name") return try: client = self._create_client() columns_meta = self._read_table_columns("application_groups") column_names = { str(col.get("name", "")).strip() for col in columns_meta if isinstance(col, dict) and col.get("name") } payload: dict[str, Any] = {"name": name} if "parent_id" in column_names: payload["parent_id"] = None if "sort_order" in column_names: payload["sort_order"] = "" if "code" in column_names: slug = re.sub(r"[^a-z0-9_]+", "_", name.strip().lower()).strip("_") payload["code"] = f"{slug}_{str(uuid.uuid4())[:8]}" if slug else str( uuid.uuid4() )[:12] client.table("application_groups").insert(payload).execute() self._load_application_group_options(client) self.close_dashboard_add_group() self._load_applications_tiles() return self.init_dashboard_drag() except Exception as exc: self.dashboard_add_group_error = self._msg( "errors.create_group_failed", detail=str(exc) ) def init_dashboard_drag(self): return rx.call_script( """ (() => { const bindGrid = (grid) => { if (!grid || grid.dataset.dashboardDndBound === "1") return; grid.dataset.dashboardDndBound = "1"; let draggedItem = null; grid.addEventListener("dragstart", (e) => { e.stopPropagation(); const item = e.target.closest(".dashboard-grid-item"); if (!item || item.classList.contains("dashboard-grid-item--add")) return; draggedItem = item; item.classList.add("is-dragging"); if (e.dataTransfer) { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", item.getAttribute("data-item-id") || ""); } }); grid.addEventListener("dragend", () => { if (draggedItem) draggedItem.classList.remove("is-dragging"); draggedItem = null; }); grid.addEventListener("dragover", (e) => { if (!draggedItem) return; e.preventDefault(); const target = e.target.closest(".dashboard-grid-item:not(.dashboard-grid-item--add)"); if (!target || target === draggedItem || !grid.contains(target)) return; const targetPos = target.getBoundingClientRect(); const next = (e.clientY - targetPos.top) / (targetPos.bottom - targetPos.top) > 0.5 || (e.clientX - targetPos.left) / (targetPos.right - targetPos.left) > 0.5; grid.insertBefore(draggedItem, next ? target.nextSibling : target); }); grid.addEventListener("drop", (e) => { if (!draggedItem) return; e.preventDefault(); }); }; const bindGroupStack = (root) => { const stack = root.querySelector(".dashboard-groups-stack"); if (!stack || stack.dataset.dashboardGroupDragBound === "1") return; stack.dataset.dashboardGroupDragBound = "1"; let draggedGroup = null; let groupDragActive = false; let saveTimer = null; const flushOrder = () => { const panels = stack.querySelectorAll( ".dashboard-group-drag-root[data-dashboard-group-id]" ); const ordered_ids = Array.from(panels) .map((p) => p.getAttribute("data-dashboard-group-id")) .filter(Boolean); if (ordered_ids.length === 0) return; fetch("/api/sphere/application-groups-order", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ordered_ids }), }) .then((r) => { if (r.ok) { const b = document.getElementById("sphere-refresh-dashboard-tiles"); if (b) b.click(); } }) .catch(() => {}); }; const scheduleSave = () => { clearTimeout(saveTimer); saveTimer = setTimeout(flushOrder, 450); }; stack.addEventListener( "dragstart", (e) => { if (!e.target.closest(".dashboard-group-drag-handle")) return; const item = e.target.closest(".dashboard-group-drag-root"); if (!item || !stack.contains(item)) return; groupDragActive = true; draggedGroup = item; item.classList.add("is-dragging-group"); if (e.dataTransfer) { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData( "text/plain", item.getAttribute("data-dashboard-group-id") || "" ); } }, true ); stack.addEventListener( "dragend", () => { if (draggedGroup) draggedGroup.classList.remove("is-dragging-group"); draggedGroup = null; if (groupDragActive) { groupDragActive = false; scheduleSave(); } }, true ); stack.addEventListener("dragover", (e) => { if (!draggedGroup) return; e.preventDefault(); const target = e.target.closest(".dashboard-group-drag-root"); if (!target || target === draggedGroup || !stack.contains(target)) return; const targetPos = target.getBoundingClientRect(); const next = (e.clientY - targetPos.top) / (targetPos.bottom - targetPos.top) > 0.5; stack.insertBefore(draggedGroup, next ? target.nextSibling : target); }); stack.addEventListener("drop", (e) => e.preventDefault()); }; const schedule = () => { const wave = ((window.__sphereDashboardDndWave = (window.__sphereDashboardDndWave || 0) + 1)); let attempts = 0; const maxAttempts = 40; const tick = () => { if (wave !== window.__sphereDashboardDndWave) return; const root = document.querySelector('[data-dashboard-grid-root="1"]'); if (root) { root.querySelectorAll(".dashboard-app-grid").forEach(bindGrid); bindGroupStack(root); } attempts += 1; if (attempts < maxAttempts) { setTimeout(tick, 60); } }; requestAnimationFrame(() => requestAnimationFrame(tick)); }; schedule(); return true; })() """ ) def refresh_application_groups_dashboard(self): if self.selected_table == "application_groups": self._load_applications_tiles() return self.init_dashboard_drag() def _sync_applications_dashboard_visible_groups(self): """Filter dashboard panels by applications_nav_group_id (empty = all groups).""" gid = (self.applications_nav_group_id or "").strip() if gid: filtered = [g for g in self.applications_group_tiles if g.id == gid] self.applications_dashboard_visible_groups = filtered if filtered: self.applications_dashboard_title = filtered[0].name else: self.applications_dashboard_title = self._msg("nav.group_fallback") else: self.applications_dashboard_visible_groups = list(self.applications_group_tiles) self.applications_dashboard_title = self._msg("nav.applications") self.dashboard_show_app_management = not bool(gid) def _applications_tree_expand_all_allowed(self) -> bool: return bool((self.applications_nav_group_id or "").strip()) and ( not self.dashboard_show_app_management ) def set_applications_dashboard_expand_all(self, value: bool): if value and not self._applications_tree_expand_all_allowed(): return self.applications_dashboard_expand_all = bool(value) if self.applications_dashboard_expand_all: self._refresh_dashboard_expanded_apps() else: self.dashboard_expanded_apps = [] return self.init_dashboard_drag() def _maybe_refresh_dashboard_expanded_apps(self): single_group_view = ( self.selected_table == "application_groups" and not self.dashboard_show_app_management ) if self.applications_dashboard_expand_all and single_group_view: self._refresh_dashboard_expanded_apps() else: self.dashboard_expanded_apps = [] @staticmethod def _editor_columns_from_names( names: list[str], *, cell_editable: bool = True ) -> list[dict[str, Any]]: out: list[dict[str, Any]] = [] for n in names: n = (n or "").strip() if not n: continue col: dict[str, Any] = { "title": n, "id": n, "type": "str", "width": min(260, max(90, len(n) * 10 + 24)), } if not cell_editable: col["editable"] = False out.append(col) return out def _expanded_fetch_table_grid( self, client: Client, table_name: str, limit: int = 250 ) -> tuple[list[dict[str, Any]], list[list[str]], str]: try: data = ( client.table(table_name).select("*").limit(limit).execute().data or [] ) except Exception as exc: return [], [], str(exc) if not data: cmeta = self._read_table_columns(table_name) names = [str(c.get("name", "")).strip() for c in cmeta if c.get("name")] return self._editor_columns_from_names(names, cell_editable=False), [], "" row_keys = {key for row in data for key in row.keys() if isinstance(row, dict)} ordered = sorted(row_keys) cols_visible = [ c for c in ordered if self._is_visible_column(table_name, c) ] or list(ordered) cols = self._editor_columns_from_names(cols_visible, cell_editable=False) rows = [ [str(row.get(c, "")) for c in cols_visible] for row in data if isinstance(row, dict) ] return cols, rows, "" def _expanded_fetch_custom_table_editable( self, client: Client, table_name: str, limit: int = 250 ) -> tuple[list[dict[str, Any]], list[list[str]], list[str], str]: """Columns with editable flags and row ids for Expand-all custom tables.""" auto = self._auto_managed_columns() try: data = ( client.table(table_name).select("*").limit(limit).execute().data or [] ) except Exception as exc: return [], [], [], str(exc) if not data: cmeta = self._read_table_columns(table_name) names = [ str(c.get("name", "")).strip() for c in cmeta if c.get("name") ] cols: list[dict[str, Any]] = [] for n in names: if not n: continue cols.append( { "title": n, "id": n, "type": "str", "width": min(260, max(90, len(n) * 10 + 24)), "editable": n not in auto, } ) return cols, [], [], "" row_keys = {key for row in data for key in row.keys() if isinstance(row, dict)} ordered = sorted(row_keys) cols_visible = [ c for c in ordered if self._is_visible_column(table_name, c) ] or list(ordered) cols = [] for n in cols_visible: cols.append( { "title": n, "id": n, "type": "str", "width": min(260, max(90, len(n) * 10 + 24)), "editable": n not in auto, } ) rows = [ [str(row.get(c, "")) for c in cols_visible] for row in data if isinstance(row, dict) ] row_ids = [ str(row.get("id", "") or "") for row in data if isinstance(row, dict) ] return cols, rows, row_ids, "" def _expanded_chart_points( self, client: Client, table_name: str, label_col: str, data_cols: list[str], ) -> list[dict[str, Any]]: try: data = ( client.table(table_name).select("*").limit(2000).execute().data or [] ) except Exception: return [] points: list[dict[str, Any]] = [] for row in data: if not isinstance(row, dict): continue label = str(row.get(label_col, "")).strip() or "(empty)" row_hint = "\n".join( f"{k}: {row.get(k)}" for k in sorted(row.keys()) ) point: dict[str, Any] = {"label": label, "hint": row_hint} has_numeric = False for column in data_cols: numeric = self._to_float(row.get(column)) if numeric is None: continue has_numeric = True point[column] = numeric if has_numeric: points.append(point) points.sort(key=lambda item: self._label_sort_key(item.get("label", ""))) return points def _md_expand_detail_bundle( self, client: Client, detail_table: str, detail_key: str, key_value: str, ) -> tuple[list[dict[str, Any]], list[list[str]], list[str], list[str]]: """Detail grid columns (with editable flags), rows, row ids, column names.""" auto = self._auto_managed_columns() try: data_d = ( client.table(detail_table) .select("*") .eq(detail_key, key_value) .limit(200) .execute() .data or [] ) except Exception: data_d = [] if not data_d: dc_meta = self._read_table_columns(detail_table) dnames = [ str(c.get("name", "")).strip() for c in dc_meta if c.get("name") ] dc: list[dict[str, Any]] = [] for name in dnames: if not name: continue dc.append( { "title": name, "id": name, "type": "str", "width": min(260, max(90, len(name) * 10 + 24)), "editable": name not in auto and name != detail_key, } ) return dc, [], [], dnames dkeys = sorted( {k for row in data_d for k in row.keys() if isinstance(row, dict)} ) dvis = [ c for c in dkeys if self._is_visible_column(detail_table, c) ] or list(dkeys) dc = [] for name in dvis: dc.append( { "title": name, "id": name, "type": "str", "width": min(260, max(90, len(name) * 10 + 24)), "editable": name not in auto and name != detail_key, } ) drows = [ [str(row.get(c, "")) for c in dvis] for row in data_d if isinstance(row, dict) ] row_ids = [ str(row.get("id", "") or "") for row in data_d if isinstance(row, dict) ] return dc, drows, row_ids, dvis def _load_md_expand_session( self, client: Client, *, master_table: str, detail_table: str, master_key: str, detail_key: str, ) -> DashboardMdExpandSession: data_m = ( client.table(master_table).select("*").limit(120).execute().data or [] ) if not data_m: mc_meta = self._read_table_columns(master_table) mnames = [ str(c.get("name", "")).strip() for c in mc_meta if c.get("name") ] mc = self._editor_columns_from_names(mnames, cell_editable=False) dc, drows, d_ids, dcols = self._md_expand_detail_bundle( client, detail_table, detail_key, "" ) return DashboardMdExpandSession( master_table=master_table, detail_table=detail_table, master_key=master_key, detail_key=detail_key, master_grid_columns=mc, master_rows=[], master_key_values=[], selected_master_idx=-1, master_grid_selection={}, detail_grid_columns=dc, detail_rows=drows, detail_columns=dcols, detail_row_ids=d_ids, detail_selected_row_idx=-1, detail_grid_selection={}, detail_adding_row=False, detail_editing_idx=-1, detail_edit_original_row=[], caption="Detail: no master rows", ) mkeys = sorted( {k for row in data_m for k in row.keys() if isinstance(row, dict)} ) mvis = [ c for c in mkeys if self._is_visible_column(master_table, c) ] or list(mkeys) mc = self._editor_columns_from_names(mvis, cell_editable=False) mrows = [ [str(row.get(c, "")) for c in mvis] for row in data_m if isinstance(row, dict) ] key_values = [ str(row.get(master_key, "") or "") for row in data_m if isinstance(row, dict) ] first_key = key_values[0] if key_values else "" dc, drows, d_ids, dcols = self._md_expand_detail_bundle( client, detail_table, detail_key, first_key ) cap = ( f"Detail for master ({master_key}={first_key!r})" if key_values else "Detail: no master rows" ) sel = 0 if key_values else -1 mcols = len(mc) if mc else 1 mrows_c = len(mrows) msel = ( _build_row_grid_selection(0, 0, mrows_c, mcols) if sel >= 0 else {} ) return DashboardMdExpandSession( master_table=master_table, detail_table=detail_table, master_key=master_key, detail_key=detail_key, master_grid_columns=mc, master_rows=mrows, master_key_values=key_values, selected_master_idx=sel, master_grid_selection=msel, detail_grid_columns=dc, detail_rows=drows, detail_columns=dcols, detail_row_ids=d_ids, detail_selected_row_idx=-1, detail_grid_selection={}, detail_adding_row=False, detail_editing_idx=-1, detail_edit_original_row=[], caption=cap, ) def expanded_md_select_master_row(self, tile_id: str, row_idx: Any): """Expand-all: user clicked a master row — reload detail for that key.""" tid = (tile_id or "").strip() try: ridx = int(row_idx) except (TypeError, ValueError): return for i, app in enumerate(self.dashboard_expanded_apps): if (app.tile_id or "").strip() == tid and app.kind == "md": self.expand_md_slot_select_master_row(i, ridx) return def _expand_md_set_session(self, slot: int, sess: DashboardMdExpandSession): if slot < 0 or slot >= len(self.dashboard_expanded_apps): return app = self.dashboard_expanded_apps[slot] new_app = app.copy(update={"md_session": sess}) new_list = list(self.dashboard_expanded_apps) new_list[slot] = new_app self.dashboard_expanded_apps = new_list def expand_md_slot_select_master_row(self, slot: int, row_idx: Any): try: ridx = int(row_idx) except (TypeError, ValueError): return if slot < 0 or slot >= len(self.dashboard_expanded_apps): return app = self.dashboard_expanded_apps[slot] if app.kind != "md" or app.md_session is None: return sess = app.md_session if ridx < 0 or ridx >= len(sess.master_key_values): return try: client = self._create_client() except Exception: return key_val = sess.master_key_values[ridx] dc, drows, d_ids, dcols = self._md_expand_detail_bundle( client, sess.detail_table, sess.detail_key, key_val ) cap = f"Detail for master ({sess.master_key}={key_val!r})" mcols = len(sess.master_grid_columns) if sess.master_grid_columns else 1 msel = _build_row_grid_selection(ridx, 0, len(sess.master_rows), mcols) new_sess = sess.copy( update={ "selected_master_idx": ridx, "master_grid_selection": msel, "detail_grid_columns": dc, "detail_rows": drows, "detail_columns": dcols, "detail_row_ids": d_ids, "detail_selected_row_idx": -1, "detail_grid_selection": {}, "detail_adding_row": False, "detail_editing_idx": -1, "detail_edit_original_row": [], "caption": cap, } ) self._expand_md_set_session(slot, new_sess) def expand_md_slot_master_cell(self, slot: int, position: Any): if not isinstance(position, (list, tuple)) or len(position) < 2: return try: ridx = int(position[1]) except (TypeError, ValueError): return self.expand_md_slot_select_master_row(slot, ridx) def expand_md_slot_master_selection_change(self, slot: int, selection: Any): position = _extract_cell_from_grid_selection(selection) if position is None: return self.expand_md_slot_master_cell(slot, position) def expand_md_slot_detail_select_from_grid(self, slot: int, position: Any): if not isinstance(position, (list, tuple)) or len(position) < 2: return try: col_idx = int(position[0]) row_idx = int(position[1]) except (TypeError, ValueError): return if slot < 0 or slot >= len(self.dashboard_expanded_apps): return app = self.dashboard_expanded_apps[slot] if app.kind != "md" or app.md_session is None: return sess = app.md_session if row_idx < 0 or row_idx >= len(sess.detail_rows): new_sess = sess.copy( update={"detail_selected_row_idx": -1, "detail_grid_selection": {}} ) self._expand_md_set_session(slot, new_sess) return dsel = _build_row_grid_selection( row_idx, col_idx, len(sess.detail_rows), len(sess.detail_grid_columns), ) new_sess = sess.copy( update={ "detail_selected_row_idx": row_idx, "detail_grid_selection": dsel, } ) self._expand_md_set_session(slot, new_sess) def expand_md_slot_detail_selection_change(self, slot: int, selection: Any): position = _extract_cell_from_grid_selection(selection) if position is None: if slot < 0 or slot >= len(self.dashboard_expanded_apps): return app = self.dashboard_expanded_apps[slot] if app.kind != "md" or app.md_session is None: return sess = app.md_session self._expand_md_set_session( slot, sess.copy( update={ "detail_selected_row_idx": -1, "detail_grid_selection": {}, } ), ) return self.expand_md_slot_detail_select_from_grid(slot, position) def expand_md_slot_detail_cell_edited(self, slot: int, position: Any, cell: Any): if slot < 0 or slot >= len(self.dashboard_expanded_apps): return app = self.dashboard_expanded_apps[slot] if app.kind != "md" or app.md_session is None: return sess = app.md_session if not isinstance(position, (list, tuple)) or len(position) < 2: return try: col_idx = int(position[0]) row_idx = int(position[1]) except (TypeError, ValueError): return value = self._extract_data_editor_value(cell) dsel = _build_row_grid_selection( row_idx, col_idx, len(sess.detail_rows), len(sess.detail_grid_columns), ) if sess.detail_adding_row and row_idx == 0: if col_idx < 0 or col_idx >= len(sess.detail_columns): return cn = sess.detail_columns[col_idx] if cn in self._auto_managed_columns() or cn == sess.detail_key: return new_rows = [r[:] for r in sess.detail_rows] if new_rows and col_idx < len(new_rows[0]): new_rows[0][col_idx] = value self._expand_md_set_session( slot, sess.copy( update={ "detail_rows": new_rows, "detail_grid_selection": dsel, "detail_selected_row_idx": row_idx, } ), ) return if sess.detail_editing_idx != row_idx: orig = ( sess.detail_rows[row_idx][:] if row_idx < len(sess.detail_rows) else [] ) self._expand_md_set_session( slot, sess.copy( update={ "detail_editing_idx": row_idx, "detail_edit_original_row": orig, "detail_selected_row_idx": row_idx, "detail_grid_selection": dsel, } ), ) sess = self.dashboard_expanded_apps[slot].md_session if sess is None: return if sess.detail_editing_idx == row_idx: if col_idx < 0 or col_idx >= len(sess.detail_columns): return cn = sess.detail_columns[col_idx] if cn in self._auto_managed_columns() or cn == sess.detail_key: return new_rows = [r[:] for r in sess.detail_rows] if row_idx < len(new_rows) and col_idx < len(new_rows[row_idx]): new_rows[row_idx][col_idx] = value self._expand_md_set_session( slot, sess.copy( update={ "detail_rows": new_rows, "detail_grid_selection": dsel, "detail_selected_row_idx": row_idx, } ), ) def expand_md_slot_add_detail(self, slot: int): if slot < 0 or slot >= len(self.dashboard_expanded_apps): return app = self.dashboard_expanded_apps[slot] if app.kind != "md" or app.md_session is None: return sess = app.md_session if sess.selected_master_idx < 0 or not sess.detail_columns: return if sess.detail_adding_row or sess.detail_editing_idx >= 0: return key_val = sess.master_key_values[sess.selected_master_idx] new_row: list[str] = [] for col in sess.detail_columns: if col == sess.detail_key: new_row.append(key_val) else: new_row.append("") new_rows = [new_row, *sess.detail_rows] new_ids = ["", *sess.detail_row_ids] dsel = _build_row_grid_selection( 0, 0, len(new_rows), len(sess.detail_grid_columns), ) self._expand_md_set_session( slot, sess.copy( update={ "detail_rows": new_rows, "detail_row_ids": new_ids, "detail_adding_row": True, "detail_selected_row_idx": 0, "detail_grid_selection": dsel, "detail_editing_idx": -1, "detail_edit_original_row": [], } ), ) def expand_md_slot_save_detail(self, slot: int): if slot < 0 or slot >= len(self.dashboard_expanded_apps): return app = self.dashboard_expanded_apps[slot] if app.kind != "md" or app.md_session is None: return sess = app.md_session if sess.detail_adding_row and sess.detail_selected_row_idx == 0: self._expand_md_save_new_row(slot) return row_idx = sess.detail_editing_idx if row_idx < 0: return if row_idx >= len(sess.detail_rows): return try: row = sess.detail_rows[row_idx] original_row = sess.detail_edit_original_row[:] if not original_row: original_row = row[:] column_types = { col["name"]: col["type"] for col in self._read_table_columns(sess.detail_table) } auto_managed = self._auto_managed_columns() payload: dict[str, Any] = {} for col_idx, column_name in enumerate(sess.detail_columns): if column_name in auto_managed or column_name == sess.detail_key: continue new_value = row[col_idx] if col_idx < len(row) else "" old_value = ( original_row[col_idx] if col_idx < len(original_row) else "" ) if new_value == old_value: continue payload[column_name] = self._coerce_row_value( new_value, column_types.get(column_name, "text") ) if not payload: self._expand_md_set_session( slot, sess.copy( update={ "detail_editing_idx": -1, "detail_edit_original_row": [], } ), ) return client = self._create_client() query = client.table(sess.detail_table).update(payload) row_id = ( sess.detail_row_ids[row_idx].strip() if row_idx < len(sess.detail_row_ids) else "" ) if row_id: query = query.eq("id", row_id) else: filters_applied = 0 for col_idx, column_name in enumerate(sess.detail_columns): original_value = ( original_row[col_idx] if col_idx < len(original_row) else "" ) parsed = self._coerce_row_value( original_value, column_types.get(column_name, "text") ) if parsed is None: continue query = query.eq(column_name, parsed) filters_applied += 1 if filters_applied == 0: self.load_error = self._msg("errors.cannot_identify_detail_update") return query.execute() self.expand_md_slot_select_master_row(slot, sess.selected_master_idx) self.load_error = "" except Exception as exc: self.load_error = self._msg("errors.edit_detail_failed", detail=str(exc)) def _expand_md_save_new_row(self, slot: int): app = self.dashboard_expanded_apps[slot] sess = app.md_session if sess is None or not sess.detail_adding_row or not sess.detail_rows: return if sess.selected_master_idx < 0: return key_value = sess.master_key_values[sess.selected_master_idx] try: column_types = { col["name"]: col["type"] for col in self._read_table_columns(sess.detail_table) } auto_managed = self._auto_managed_columns() key_type = column_types.get(sess.detail_key, "text") coerced_key = self._coerce_row_value(key_value, key_type) payload: dict[str, Any] = { sess.detail_key: coerced_key if coerced_key is not None else key_value } for col_idx, column_name in enumerate(sess.detail_columns): if column_name in auto_managed or column_name == sess.detail_key: continue value = ( sess.detail_rows[0][col_idx] if col_idx < len(sess.detail_rows[0]) else "" ) parsed = self._coerce_row_value( value, column_types.get(column_name, "text") ) if parsed is not None: payload[column_name] = parsed client = self._create_client() client.table(sess.detail_table).insert(payload).execute() self.expand_md_slot_select_master_row(slot, sess.selected_master_idx) self.load_error = "" except Exception as exc: self.load_error = self._msg("errors.add_detail_failed", detail=str(exc)) def expand_md_slot_cancel_detail(self, slot: int): if slot < 0 or slot >= len(self.dashboard_expanded_apps): return app = self.dashboard_expanded_apps[slot] if app.kind != "md" or app.md_session is None: return sess = app.md_session if sess.detail_adding_row: new_rows = sess.detail_rows[1:] new_ids = sess.detail_row_ids[1:] self._expand_md_set_session( slot, sess.copy( update={ "detail_rows": new_rows, "detail_row_ids": new_ids, "detail_adding_row": False, "detail_selected_row_idx": -1, "detail_grid_selection": {}, } ), ) return row_idx = sess.detail_editing_idx if row_idx < 0: return if row_idx >= len(sess.detail_rows): self._expand_md_set_session( slot, sess.copy( update={ "detail_editing_idx": -1, "detail_edit_original_row": [], } ), ) return new_rows = [r[:] for r in sess.detail_rows] new_rows[row_idx] = sess.detail_edit_original_row[:] self._expand_md_set_session( slot, sess.copy( update={ "detail_rows": new_rows, "detail_editing_idx": -1, "detail_edit_original_row": [], } ), ) def expand_md_slot_delete_detail(self, slot: int): if slot < 0 or slot >= len(self.dashboard_expanded_apps): return app = self.dashboard_expanded_apps[slot] if app.kind != "md" or app.md_session is None: return sess = app.md_session row_idx = sess.detail_selected_row_idx if row_idx < 0: return if sess.detail_adding_row and row_idx == 0: self.expand_md_slot_cancel_detail(slot) return try: client = self._create_client() row_id = ( sess.detail_row_ids[row_idx].strip() if row_idx < len(sess.detail_row_ids) else "" ) if row_id: client.table(sess.detail_table).delete().eq("id", row_id).execute() else: row = sess.detail_rows[row_idx] column_types = { col["name"]: col["type"] for col in self._read_table_columns(sess.detail_table) } query = client.table(sess.detail_table).delete() for col_idx, column_name in enumerate(sess.detail_columns): value = row[col_idx] if col_idx < len(row) else "" parsed = self._coerce_row_value( value, column_types.get(column_name, "text") ) if parsed is not None: query = query.eq(column_name, parsed) query.execute() self.expand_md_slot_select_master_row(slot, sess.selected_master_idx) self.load_error = "" except Exception as exc: self.load_error = self._msg("errors.delete_detail_failed", detail=str(exc)) def expand_md_slot_start_edit_selected(self, slot: int): if slot < 0 or slot >= len(self.dashboard_expanded_apps): return app = self.dashboard_expanded_apps[slot] if app.kind != "md" or app.md_session is None: return sess = app.md_session ridx = sess.detail_selected_row_idx if ridx < 0: return if sess.detail_adding_row: return self._expand_md_set_session( slot, sess.copy( update={ "detail_editing_idx": ridx, "detail_edit_original_row": sess.detail_rows[ridx][:], } ), ) def expand_slot_grid_cell_edited(self, slot: int, position: Any, cell: Any): if not isinstance(position, (list, tuple)) or len(position) < 2: return try: col_idx = int(position[0]) row_idx = int(position[1]) except (TypeError, ValueError): return if slot < 0 or slot >= len(self.dashboard_expanded_apps): return app = self.dashboard_expanded_apps[slot] if app.kind != "grid" or not app.grid_editable or not app.grid_table_name: return if row_idx < 0 or row_idx >= len(app.grid_rows): return if col_idx < 0 or col_idx >= len(app.grid_columns): return col_meta = app.grid_columns[col_idx] if not col_meta.get("editable", False): return col_name = str(col_meta.get("id", "")) if not col_name: return value = self._extract_data_editor_value(cell) row_id = ( app.grid_row_ids[row_idx] if row_idx < len(app.grid_row_ids) else "" ).strip() if not row_id: self.load_error = self._msg("errors.cannot_update_without_id") return try: client = self._create_client() ct = self._read_table_columns(app.grid_table_name) col_types = {c["name"]: c["type"] for c in ct if c.get("name")} parsed = self._coerce_row_value(value, col_types.get(col_name, "text")) payload: dict[str, Any] = {col_name: parsed if parsed is not None else value} client.table(app.grid_table_name).update(payload).eq("id", row_id).execute() self.load_error = "" except Exception as exc: self.load_error = self._msg("errors.update_failed", detail=str(exc)) return new_rows = [r[:] for r in app.grid_rows] new_rows[row_idx][col_idx] = value new_app = app.copy(update={"grid_rows": new_rows}) new_list = list(self.dashboard_expanded_apps) new_list[slot] = new_app self.dashboard_expanded_apps = new_list def _build_expanded_app_view( self, client: Client, tile: ApplicationTile ) -> DashboardExpandedApp: tid = (tile.id or "").strip() or str(uuid.uuid4())[:10] title = (tile.name or "").strip() or self._msg("errors.application_fallback") type_label = (tile.app_type or "").strip() or "app" norm = self._normalize_application_type(tile.app_type) content = (tile.content or "").strip() def err_app(msg: str) -> DashboardExpandedApp: return DashboardExpandedApp( tile_id=tid, title=title, type_label=type_label, kind="error", message=msg, ) try: if norm == "url": if not content: return err_app(self._msg("errors.empty_url")) target_url = content if not ( target_url.startswith("http://") or target_url.startswith("https://") ): target_url = f"https://{target_url}" return DashboardExpandedApp( tile_id=tid, title=title, type_label=type_label, kind="iframe", iframe_src=target_url, ) if norm == "sql": if not content: return err_app(self._msg("errors.empty_sql")) verr = self._validate_select_sql(content) if verr: return err_app(verr) try: cols_raw, rows_raw = self._execute_reports_select_sql(content) except Exception as exc: return err_app(self._msg("errors.reports_exec_failed", detail=str(exc))) cols = self._editor_columns_from_names(cols_raw, cell_editable=False) return DashboardExpandedApp( tile_id=tid, title=title, type_label=type_label, kind="grid", grid_columns=cols, grid_rows=rows_raw, grid_editable=False, grid_table_name="", grid_row_ids=[], ) if norm == "chart": pattern = ( r'Table:\s*"(?P<table>.*?)",\s*' r'Labels:\s*"(?P<labels>.*?)",\s*' r'Data:\s*"(?P<data>.*?)",\s*' r'Display type:\s*"(?P<display_type>.*?)"\s*$' ) match = re.search(pattern, content, flags=re.IGNORECASE | re.DOTALL) if not match: return err_app(self._msg("errors.parse_chart_app")) table_value = (match.group("table") or "").strip() labels_value = (match.group("labels") or "").strip() data_value = (match.group("data") or "").strip() display_type_value = (match.group("display_type") or "").strip().lower() if not table_value or not labels_value or not data_value: return err_app(self._msg("errors.chart_incomplete")) requested_data = [ item.strip() for item in data_value.split(",") if item.strip() ] if not requested_data: return err_app(self._msg("errors.chart_no_columns")) chart_type = ( "histogram" if "histogram" in display_type_value else "simple graph" ) points = self._expanded_chart_points( client, table_value, labels_value, requested_data ) if not points: return err_app(self._msg("errors.chart_no_numeric")) chart_series = [ { "key": col, "color": VISUAL_SERIES_COLORS[ idx % len(VISUAL_SERIES_COLORS) ], } for idx, col in enumerate(requested_data) ] return DashboardExpandedApp( tile_id=tid, title=title, type_label=type_label, kind="chart", chart_src="", chart_points=points, chart_series=chart_series, chart_axis_label=labels_value, chart_is_histogram=chart_type == "histogram", ) if norm == "master/detail": pattern = ( r'Master table:\s*"(?P<master_table>.*?)",\s*' r'Detail table:\s*"(?P<detail_table>.*?)",\s*' r'Master key:\s*"(?P<master_key>.*?)",\s*' r'Detail key:\s*"(?P<detail_key>.*?)"\s*$' ) match = re.search(pattern, content, flags=re.IGNORECASE | re.DOTALL) if not match: return err_app(self._msg("errors.parse_md")) master_table = (match.group("master_table") or "").strip() detail_table = (match.group("detail_table") or "").strip() master_key = (match.group("master_key") or "").strip() detail_key = (match.group("detail_key") or "").strip() if not all((master_table, detail_table, master_key, detail_key)): return err_app(self._msg("errors.md_incomplete")) sess = self._load_md_expand_session( client, master_table=master_table, detail_table=detail_table, master_key=master_key, detail_key=detail_key, ) return DashboardExpandedApp( tile_id=tid, title=title, type_label=type_label, kind="md", md_session=sess, ) if norm == "custom table": if not content: return err_app(self._msg("errors.empty_table_name")) if content not in self.custom_tables: return err_app(self._msg("errors.table_not_available", name=content)) cols, rows, row_ids, emsg = self._expanded_fetch_custom_table_editable( client, content ) if emsg: return err_app(self._msg("errors.supabase_load_failed", detail=emsg)) return DashboardExpandedApp( tile_id=tid, title=title, type_label=type_label, kind="grid", grid_columns=cols, grid_rows=rows, grid_editable=True, grid_table_name=content, grid_row_ids=row_ids, ) if norm == "default": if content in self.custom_tables: cols, rows, row_ids, emsg = ( self._expanded_fetch_custom_table_editable(client, content) ) if emsg: return err_app(self._msg("errors.supabase_load_failed", detail=emsg)) return DashboardExpandedApp( tile_id=tid, title=title, type_label=type_label, kind="grid", grid_columns=cols, grid_rows=rows, grid_editable=True, grid_table_name=content, grid_row_ids=row_ids, ) return DashboardExpandedApp( tile_id=tid, title=title, type_label=type_label, kind="text", message=content or self._msg("errors.empty_content"), ) return err_app( self._msg("errors.unsupported_app_type", type_label=type_label) ) except Exception as exc: return err_app(self._msg("errors.supabase_load_failed", detail=str(exc))) def _refresh_dashboard_expanded_apps(self): self.dashboard_expanded_apps = [] if not self.applications_dashboard_expand_all: return if not self._applications_tree_expand_all_allowed(): return tiles: list[ApplicationTile] = [] for grp in self.applications_dashboard_visible_groups: tiles.extend(grp.tiles) if not tiles: return try: client = self._create_client() except Exception as exc: self.dashboard_expanded_apps = [ DashboardExpandedApp( tile_id="!", title=self._msg("errors.run_all_title"), type_label=self._msg("errors.error_type"), kind="error", message=str(exc), ) ] return self.dashboard_expanded_apps = [ self._build_expanded_app_view(client, t) for t in tiles ] def toggle_applications_navigation(self): """Same UX as Administration: expand/collapse; on expand show all groups dashboard.""" expanding = not self.applications_nav_expanded self.applications_nav_expanded = expanding if expanding: self.applications_nav_group_id = "" return self.select_applications() return def select_applications_group(self, group_id: str): """Left nav: show tiles for one application_group.""" gid = (group_id or "").strip() if not gid: return self.select_applications() self.is_create_table_mode = False self.is_create_app_mode = False self.new_app_name = "" self.is_create_master_detail_app_mode = False self.new_master_detail_app_name = "" self.is_create_visuals_app_mode = False self.new_visuals_app_name = "" self.is_edit_table_mode = False self.is_adding_row = False self.editing_row_idx = -1 self.editing_original_row = [] self.load_error = "" self.visuals_notice = "" self.close_dashboard_pick_app() self.applications_breadcrumb_source = "sidebar_applications" self.applications_nav_group_id = gid self.applications_nav_expanded = True self.selected_table = "application_groups" self.columns = [] self.rows = [] self.row_ids = [] self.service_grid_columns = [] self._load_applications_tiles() self.sync_nav_breadcrumb() return self.init_dashboard_drag() def select_applications(self): self.is_create_table_mode = False self.is_create_app_mode = False self.new_app_name = "" self.is_create_master_detail_app_mode = False self.new_master_detail_app_name = "" self.is_create_visuals_app_mode = False self.new_visuals_app_name = "" self.is_edit_table_mode = False self.is_adding_row = False self.editing_row_idx = -1 self.editing_original_row = [] self.load_error = "" self.visuals_notice = "" self.close_dashboard_pick_app() self.applications_breadcrumb_source = "sidebar_applications" self.applications_nav_expanded = True self.applications_nav_group_id = "" # Root Applications: open dashboards, then focus first group (same as tree pick). self.selected_table = "application_groups" self.columns = [] self.rows = [] self.row_ids = [] self.service_grid_columns = [] self._load_applications_tiles() if self.applications_nav_groups: first_id = (self.applications_nav_groups[0].id or "").strip() if first_id: self.applications_nav_group_id = first_id self._sync_applications_dashboard_visible_groups() self.sync_nav_breadcrumb() return self.init_dashboard_drag() def _open_custom_table_from_dashboard_tile(self, table_name: str): """Custom table via tile: keep admin actions on full dashboards (Apps management), hide them when the dashboards are scoped to one group (Applications tree).""" if self.dashboard_show_app_management: self.select_table(table_name) else: self.select_table_from_application_tile(table_name) def open_application_tile( self, content: str, app_type: str, app_name: str = "", tile_group_id: str = "", ): normalized_type = self._normalize_application_type(app_type) table_name = (content or "").strip() if normalized_type == "url": if not table_name: self.load_error = self._msg("errors.selected_empty_url") return target_url = table_name if not ( target_url.startswith("http://") or target_url.startswith("https://") ): target_url = f"https://{target_url}" self.load_error = "" return rx.redirect(target_url, is_external=True) if normalized_type == "master/detail": pattern = ( r'Master table:\s*"(?P<master_table>.*?)",\s*' r'Detail table:\s*"(?P<detail_table>.*?)",\s*' r'Master key:\s*"(?P<master_key>.*?)",\s*' r'Detail key:\s*"(?P<detail_key>.*?)"\s*$' ) match = re.search(pattern, table_name, flags=re.IGNORECASE | re.DOTALL) if not match: self.load_error = self._msg("errors.parse_md") return master_table = (match.group("master_table") or "").strip() detail_table = (match.group("detail_table") or "").strip() master_key = (match.group("master_key") or "").strip() detail_key = (match.group("detail_key") or "").strip() self._sync_application_tile_sidebar_context(tile_group_id) self._skip_sync_after_constructor_nav = True self.select_master_detail() self.set_master_detail_master_table(master_table) self.set_master_detail_detail_table(detail_table) self.set_master_detail_master_key(master_key) self.set_master_detail_detail_key(detail_key) self.master_detail_error = "" self.master_detail_show_configuration_ui = False self.master_detail_opened_from_application_tile = True self.master_detail_run_app_name = (app_name or "").strip() self.application_tile_constructor_label = ( self.master_detail_run_app_name or self._msg("nav.master_detail") ) self.sync_nav_breadcrumb() return if normalized_type == "chart": pattern = ( r'Table:\s*"(?P<table>.*?)",\s*' r'Labels:\s*"(?P<labels>.*?)",\s*' r'Data:\s*"(?P<data>.*?)",\s*' r'Display type:\s*"(?P<display_type>.*?)"\s*$' ) match = re.search(pattern, table_name, flags=re.IGNORECASE | re.DOTALL) if not match: self.load_error = self._msg("errors.parse_chart") return table_value = (match.group("table") or "").strip() labels_value = (match.group("labels") or "").strip() data_value = (match.group("data") or "").strip() display_type_value = (match.group("display_type") or "").strip() self._sync_application_tile_sidebar_context(tile_group_id) self._skip_sync_after_constructor_nav = True self.select_visuals() self.visuals_show_create_app = False self.visuals_run_app_name = (app_name or "").strip() self.application_tile_constructor_label = ( self.visuals_run_app_name or self._msg("nav.charts") ) self.set_visuals_table(table_value) if self.visuals_label_options.count(labels_value) > 0: self.visuals_label_column = labels_value requested_data = [ item.strip() for item in data_value.split(",") if item.strip() ] selected_data = [ item for item in requested_data if self.visuals_data_options.count(item) > 0 ] if selected_data: self.visuals_data_columns = selected_data self._refresh_visuals_data_series() if self.visuals_chart_types.count(display_type_value) > 0: self.visuals_chart_type = display_type_value self.generate_visual() self.visuals_notice = "" self.sync_nav_breadcrumb() return if normalized_type == "sql": if not table_name: self.load_error = self._msg("errors.selected_empty_sql") return self._sync_application_tile_sidebar_context(tile_group_id) self._skip_sync_after_constructor_nav = True self.select_reports() self.reports_result_only_mode = True self.reports_run_app_name = (app_name or "").strip() self.reports_view_title = self._msg("nav.reports") self.reports_opened_from_application_tile = True self.application_tile_constructor_label = ( self.reports_run_app_name or self._msg("nav.reports") ) self.set_reports_sql(table_name) self.execute_reports_sql() self.sync_nav_breadcrumb() return if normalized_type == "default": if table_name and table_name in self.custom_tables: self.load_error = "" self._prepare_application_tile_custom_table_nav(tile_group_id) self._open_custom_table_from_dashboard_tile(table_name) return if normalized_type != "custom table": return if not table_name: self.load_error = self._msg("errors.selected_empty_content") return if table_name not in self.custom_tables: self.load_error = self._msg("errors.custom_table_not_found", name=table_name) return self._prepare_application_tile_custom_table_nav(tile_group_id) self._open_custom_table_from_dashboard_tile(table_name) def open_application_tile_by_id(self, tile_id: str): """Expand-all: open the same full editor as clicking the dashboard tile.""" tid = (tile_id or "").strip() if not tid: return for t in self.dashboard_all_apps: if (t.id or "").strip() == tid: self.load_error = "" return self.open_application_tile( t.content, t.app_type, t.name, t.group_id ) for grp in self.applications_dashboard_visible_groups: for t in grp.tiles: if (t.id or "").strip() == tid: self.load_error = "" return self.open_application_tile( t.content, t.app_type, t.name, t.group_id ) self.load_error = self._msg("errors.find_app_editor") def open_selected_application_from_admin_grid(self): """Administration → Applications: edit selected row (same editor as a dashboard tile).""" if self.selected_table != "applications": self.load_error = self._msg("errors.open_applications_first") return row_idx = self.service_grid_selected_row_idx if row_idx < 0 or row_idx >= len(self.rows): self.load_error = self._msg("errors.select_app_edit") return try: ci = {name: idx for idx, name in enumerate(self.columns)} type_i = ci["type"] except KeyError: self.load_error = self._msg("errors.missing_type_column") return if "content" in ci: content_i = ci["content"] elif "description" in ci: content_i = ci["description"] else: self.load_error = self._msg("errors.missing_content_column") return name_i = ci.get("name", -1) row = self.rows[row_idx] if content_i >= len(row) or type_i >= len(row): self.load_error = self._msg("errors.invalid_row_data") return content = row[content_i] app_type = row[type_i] app_name = row[name_i] if 0 <= name_i < len(row) else "" self.load_error = "" return self.open_application_tile(content, app_type, app_name) def select_table_group(self, group_name: str): tables = ( self.custom_tables if group_name == "Custom Tables" else self.service_tables ) self.is_create_table_mode = False self.is_create_app_mode = False self.new_app_name = "" self.is_create_master_detail_app_mode = False self.new_master_detail_app_name = "" self.is_create_visuals_app_mode = False self.new_visuals_app_name = "" self.is_edit_table_mode = False self.is_adding_row = False self.editing_row_idx = -1 self.editing_original_row = [] self.visuals_notice = "" self.selected_table = group_name self.columns = ["table_name"] self.rows = [ ["dashboards" if table_name == "application_groups" else table_name] for table_name in tables ] self.row_ids = [] self.service_grid_selected_row_idx = -1 self.service_grid_selection = {} self.custom_tables_grid_selection = {} self._rebuild_service_grid_columns() self._apply_default_service_grid_sort() if group_name == "Custom Tables": self.administration_expanded = True self.apps_constructor_expanded = True self.custom_table_target = self.custom_tables[0] if self.custom_tables else "" self.sync_nav_breadcrumb() def select_visuals(self): self.is_create_table_mode = False self.is_create_app_mode = False self.new_app_name = "" self.is_create_master_detail_app_mode = False self.new_master_detail_app_name = "" self.is_create_visuals_app_mode = False self.new_visuals_app_name = "" self.is_edit_table_mode = False self.is_adding_row = False self.editing_row_idx = -1 self.editing_original_row = [] self.load_error = "" self.visuals_notice = "" self.application_tile_constructor_label = "" self.master_detail_opened_from_application_tile = False self.master_detail_run_app_name = "" self.reports_opened_from_application_tile = False self.reports_run_app_name = "" self.visuals_run_app_name = "" self.visuals_show_create_app = True self.administration_expanded = True self.apps_constructor_expanded = True self.selected_table = "Visuals" self.columns = [] self.rows = [] self.row_ids = [] if ( self.visuals_selected_table and self.custom_tables.count(self.visuals_selected_table) > 0 ): self.set_visuals_table(self.visuals_selected_table) if self._skip_sync_after_constructor_nav: self._skip_sync_after_constructor_nav = False else: self.sync_nav_breadcrumb() def select_master_detail(self): self.is_create_table_mode = False self.is_create_app_mode = False self.new_app_name = "" self.is_create_master_detail_app_mode = False self.new_master_detail_app_name = "" self.is_create_visuals_app_mode = False self.new_visuals_app_name = "" self.is_edit_table_mode = False self.is_adding_row = False self.editing_row_idx = -1 self.editing_original_row = [] self.load_error = "" self.master_detail_opened_from_application_tile = False self.master_detail_run_app_name = "" self.application_tile_constructor_label = "" self.reports_opened_from_application_tile = False self.reports_run_app_name = "" self.visuals_run_app_name = "" self.visuals_show_create_app = True self.administration_expanded = True self.apps_constructor_expanded = True self.master_detail_show_configuration_ui = True self.selected_table = "Master/Detail" self.columns = [] self.rows = [] self.row_ids = [] if self._skip_sync_after_constructor_nav: self._skip_sync_after_constructor_nav = False else: self.sync_nav_breadcrumb() def select_authentication(self): self.is_create_table_mode = False self.is_create_app_mode = False self.new_app_name = "" self.is_create_master_detail_app_mode = False self.new_master_detail_app_name = "" self.is_create_visuals_app_mode = False self.new_visuals_app_name = "" self.is_edit_table_mode = False self.is_adding_row = False self.editing_row_idx = -1 self.editing_original_row = [] self.load_error = "" self.selected_table = "Authentication" self.columns = [] self.rows = [] self.row_ids = [] self.auth_error = "" self.auth_tables = self._available_auth_tables() if self.auth_tables: if self.auth_selected_table not in self.auth_tables: self.auth_selected_table = self.auth_tables[0] self.load_auth_table_data() else: self.auth_columns = [] self._sync_auth_grid_columns() self.auth_rows = [] self.auth_row_ids = [] self.auth_error = self._msg("errors.auth_tables_unavailable") self.sync_nav_breadcrumb() def set_reports_sql(self, value: str): self.reports_sql = value self.reports_error = "" def start_create_reports_app_mode(self): self.is_create_reports_app_mode = True self.new_reports_app_name = "" self.reports_error = "" def cancel_create_reports_app_mode(self): self.is_create_reports_app_mode = False self.new_reports_app_name = "" def set_new_reports_app_name(self, value: str): self.new_reports_app_name = value self.reports_error = "" def create_app_from_reports(self): app_name = self.new_reports_app_name.strip() sql = self.reports_sql.strip() if not app_name: self.reports_error = self._msg("errors.app_name_required") return if not sql: self.reports_error = self._msg("reports.sql_required") return validation_error = self._validate_select_sql(sql) if validation_error: self.reports_error = validation_error return try: self._insert_application_record( app_name=app_name, content=sql, app_type="sql", ) self.is_create_reports_app_mode = False self.new_reports_app_name = "" self.reports_error = "" except Exception as exc: self.reports_error = self._msg( "errors.create_app_failed", detail=str(exc) ) @staticmethod def _relation_candidates_from_column(column_name: str) -> list[str]: base = column_name[:-3] candidates = [base, f"{base}s"] manual = { "user_group": "user_groups", "application_group": "application_groups", "service_group": "service_groups", } if base in manual: candidates.append(manual[base]) seen: set[str] = set() unique: list[str] = [] for item in candidates: if item and item not in seen: seen.add(item) unique.append(item) return unique def _build_schema_help_text(self) -> str: client = self._create_client() if self.tables: table_names = sorted(self.tables) else: table_names = sorted(self._fetch_public_tables(client)) if not table_names: return self._msg("schema.no_tables") table_columns: dict[str, list[dict[str, str]]] = {} for table_name in table_names: table_columns[table_name] = self._read_table_columns(table_name) relations: list[str] = [] for table_name in table_names: columns_meta = table_columns.get(table_name, []) for column in columns_meta: column_name = str(column.get("name", "")).strip() if not column_name.endswith("_id"): continue for candidate in self._relation_candidates_from_column(column_name): if candidate in table_columns and candidate != table_name: relations.append(f"{table_name}.{column_name} -> {candidate}.id") break m = self._msg lines: list[str] = [] lines.append(m("schema.header")) lines.append("") lines.append(m("schema.tables_fields")) for table_name in table_names: lines.append(f"- {table_name}") columns_meta = table_columns.get(table_name, []) if not columns_meta: lines.append(m("schema.no_fields")) continue for column in columns_meta: column_name = str(column.get("name", "")).strip() or m("schema.unnamed") column_type = str(column.get("type", "text")).strip() or "text" lines.append(f" - {column_name}: {column_type}") lines.append("") lines.append(m("schema.relations")) if relations: for relation in sorted(set(relations)): lines.append(f"- {relation}") else: lines.append(m("schema.no_relations")) return "\n".join(lines) def open_schema_help(self): self.schema_help_open = True self.schema_help_loading = True self.schema_help_error = "" self.schema_help_text = "" try: self.schema_help_text = self._build_schema_help_text() except Exception as exc: self.schema_help_error = self._msg("errors.schema_help_failed", detail=str(exc)) self.schema_help_text = "" finally: self.schema_help_loading = False def close_schema_help(self): self.schema_help_open = False def select_reports(self): self.is_create_table_mode = False self.is_create_app_mode = False self.new_app_name = "" self.is_create_master_detail_app_mode = False self.new_master_detail_app_name = "" self.is_create_visuals_app_mode = False self.new_visuals_app_name = "" self.is_edit_table_mode = False self.is_adding_row = False self.editing_row_idx = -1 self.editing_original_row = [] self.load_error = "" self.reports_opened_from_application_tile = False self.reports_run_app_name = "" self.application_tile_constructor_label = "" self.master_detail_opened_from_application_tile = False self.master_detail_run_app_name = "" self.visuals_run_app_name = "" self.visuals_show_create_app = True self.administration_expanded = True self.apps_constructor_expanded = True self.selected_table = "Reports" self.columns = [] self.rows = [] self.row_ids = [] self.reports_error = "" self.reports_result_only_mode = False self.reports_view_title = self._msg("nav.reports") self.is_create_reports_app_mode = False self.new_reports_app_name = "" self.reports_grid_selection = {} if self._skip_sync_after_constructor_nav: self._skip_sync_after_constructor_nav = False else: self.sync_nav_breadcrumb() @staticmethod def _strip_sql_comments(sql: str) -> str: no_block = re.sub(r"/\*.*?\*/", " ", sql, flags=re.DOTALL) return re.sub(r"--.*?$", " ", no_block, flags=re.MULTILINE) def _validate_select_sql(self, sql: str) -> str: normalized = BrowserState._strip_sql_comments(sql).strip() if not normalized: return self._msg("sql.empty") query = normalized[:-1].strip() if normalized.endswith(";") else normalized if ";" in query: return self._msg("sql.multi_statement") if not re.match(r"(?is)^select\b", query): return self._msg("sql.only_select") blocked = re.search( r"(?is)\b(update|delete|alter|insert|drop|truncate|create|grant|revoke|merge|call|execute)\b", query, ) if blocked: return self._msg("sql.forbidden_kw", keyword=blocked.group(1).upper()) return "" @staticmethod def _sql_value_to_text(value: Any) -> str: if value is None: return "" if isinstance(value, (dict, list)): return json.dumps(value) return str(value) @staticmethod def _extract_rows_from_sql_payload(payload: Any) -> list[dict[str, Any]]: queue: list[Any] = [payload] while queue: item = queue.pop(0) if isinstance(item, list): if not item: return [] if all(isinstance(entry, dict) for entry in item): return [dict(entry) for entry in item] return [{"value": entry} for entry in item] if isinstance(item, dict): for key in ("rows", "data", "result", "results"): if key in item: queue.append(item.get(key)) if item and all(not isinstance(v, (dict, list)) for v in item.values()): return [dict(item)] return [] @staticmethod def _rows_to_reports_grid(rows: list[dict[str, Any]]) -> tuple[list[str], list[list[str]]]: if not rows: return [], [] columns: list[str] = [] seen: set[str] = set() for row in rows: for key in row.keys(): key_str = str(key) if key_str not in seen: seen.add(key_str) columns.append(key_str) table_rows = [ [BrowserState._sql_value_to_text(row.get(col, "")) for col in columns] for row in rows ] return columns, table_rows def reset_navigation(self): self.selected_table = "" self.columns = [] self.rows = [] self.row_ids = [] self.nav_breadcrumb_items = [] self.nav_breadcrumb_route = [] def _available_auth_tables(self) -> list[str]: preferred = ["users", "user_groups", "permissions"] available = [name for name in preferred if self.tables.count(name) > 0] return available @staticmethod def _is_auth_auto_column(column_name: str) -> bool: return column_name in {"created_at", "updated_at", "changed_at"} @staticmethod def _is_auth_virtual_column(column_name: str) -> bool: return column_name in {"user_group_name"} @staticmethod def _current_user_name() -> str: return ( os.getenv("SPHERE_CURRENT_USER", "").strip() or os.getenv("USERNAME", "").strip() or os.getenv("USER", "").strip() or "current_user" ) def _default_permissions_user_group_id(self) -> str: try: client = self._create_client() rows = ( client.table("user_groups").select("id").limit(1).execute().data or [] ) if rows and isinstance(rows[0], dict) and rows[0].get("id"): return str(rows[0]["id"]) except Exception: pass # Fallback to generated UUID text when no group exists yet. return str(uuid.uuid4()) def _resolve_created_by_value(self, column_types: dict[str, str]) -> Any: column_type = str(column_types.get("created_by", "text") or "text").lower() user_name = self._current_user_name() if "uuid" not in column_type: return user_name # If created_by is uuid, try to map username -> users.id try: client = self._create_client() rows = ( client.table("users") .select("id, username") .eq("username", user_name) .limit(1) .execute() .data or [] ) if rows and rows[0].get("id"): return str(rows[0]["id"]) except Exception: pass return None def set_auth_table(self, table_name: str): if not table_name: return available = self._available_auth_tables() if available and table_name not in available: return self.auth_selected_table = table_name self.auth_is_adding_row = False self.auth_editing_row_idx = -1 self.auth_editing_original_row = [] self.auth_user_group_member_ids = [] self.auth_user_group_members_display = [] self.auth_error = "" self.load_auth_table_data() self.sync_nav_breadcrumb() def _load_auth_available_users(self, client: Client): self.auth_available_user_ids = [] self.auth_available_user_labels = [] try: data = ( client.table("users") .select("id, username, full_name") .limit(500) .execute() .data or [] ) for row in data: uid = str(row.get("id", "")) if not uid: continue raw = (str(row.get("username", "") or row.get("full_name", "") or uid)).strip() label = raw if raw else uid self.auth_available_user_ids.append(uid) self.auth_available_user_labels.append(label) except Exception: pass def _load_auth_available_groups(self, client: Client): self.auth_available_group_ids = [] self.auth_available_group_labels = [] try: rows = ( client.table("user_groups") .select("id, name, code") .limit(500) .execute() .data or [] ) for row in rows: group_id = str(row.get("id", "")) if not group_id: continue name = str(row.get("name", "")).strip() code = str(row.get("code", "")).strip() label = name if name else (code if code else group_id) self.auth_available_group_ids.append(group_id) self.auth_available_group_labels.append(label) except Exception: pass def _auth_group_label_to_id(self, value: str) -> str: if not value: return "" if value in self.auth_available_group_ids: return value try: idx = self.auth_available_group_labels.index(value) except ValueError: return value if idx < 0 or idx >= len(self.auth_available_group_ids): return value return self.auth_available_group_ids[idx] def _auth_group_id_to_label(self, group_id: str) -> str: if not group_id: return "" try: idx = self.auth_available_group_ids.index(group_id) except ValueError: return group_id if idx < 0 or idx >= len(self.auth_available_group_labels): return group_id return self.auth_available_group_labels[idx] def _load_application_group_options(self, client: Client): self.application_group_ids = [] self.application_group_labels = [] try: rows_raw = ( client.table("application_groups") .select("id, name, sort_order") .limit(500) .execute() .data or [] ) rows = _sorted_application_group_rows(rows_raw) for row in rows: if not isinstance(row, dict): continue group_id = str(row.get("id", "")).strip() if not group_id: continue name = str(row.get("name", "")).strip() label = name if name else group_id self.application_group_ids.append(group_id) self.application_group_labels.append(label) except Exception: self.application_group_ids = [] self.application_group_labels = [] def _application_group_label_to_id(self, value: str) -> str: if not value: return "" if value in self.application_group_ids: return value try: idx = self.application_group_labels.index(value) except ValueError: return value if idx < 0 or idx >= len(self.application_group_ids): return value return self.application_group_ids[idx] def _application_group_id_to_label(self, group_id: str) -> str: if not group_id: return "" try: idx = self.application_group_ids.index(group_id) except ValueError: return group_id if idx < 0 or idx >= len(self.application_group_labels): return group_id return self.application_group_labels[idx] @staticmethod def _normalize_application_type(app_type: str) -> str: normalized = (app_type or "").strip().lower() if normalized in {"", "default"}: return "default" if normalized in APPLICATION_TYPE_OPTIONS: return normalized # Backward compatibility for old records with SQL-like labels. if normalized.startswith("sql"): return "sql" return normalized @staticmethod def _app_type_color(app_type: str) -> str: normalized = BrowserState._normalize_application_type(app_type) palette = { "custom table": "#60a5fa", "service": "#34d399", "system": "#a78bfa", "integration": "#f59e0b", "chart": "#f97316", "sql": "#22c55e", "default": "#9ca3af", } return palette.get(normalized, "#22d3ee") def _load_applications_tiles(self): self.applications_group_tiles = [] self.applications_ungrouped_tiles = [] self.is_loading_rows = True self.load_error = "" try: client = self._create_client() groups_raw = ( client.table("application_groups") .select("id, name, parent_id, sort_order") .limit(1000) .execute() .data or [] ) groups = _sorted_application_group_rows(groups_raw) applications = ( client.table("applications") .select("*") .order("name") .limit(1000) .execute() .data or [] ) group_meta: dict[str, str] = {} ordered_group_ids: list[str] = [] for group in groups: if not isinstance(group, dict): continue group_id = str(group.get("id", "")).strip() if not group_id: continue group_name = str(group.get("name", "")).strip() or f"group:{group_id[:8]}" group_meta[group_id] = group_name ordered_group_ids.append(group_id) grouped_tiles: dict[str, list[ApplicationTile]] = { group_id: [] for group_id in ordered_group_ids } ungrouped: list[ApplicationTile] = [] all_flat: list[ApplicationTile] = [] for app in applications: if not isinstance(app, dict): continue raw_id = str(app.get("id", "")).strip() app_name = str(app.get("name", "")).strip() or "Unnamed application" app_type = self._normalize_application_type(str(app.get("type", ""))) app_type_label = "SQL" if app_type == "sql" else app_type content = str(app.get("content", "")).strip() description = str(app.get("description", "")).strip() group_id = str(app.get("application_group_id", "")).strip() if group_id and group_id in group_meta: g_label = group_meta[group_id] elif group_id: g_label = f"group:{group_id[:8]}" else: g_label = "Ungrouped" tile = ApplicationTile( id=raw_id, name=app_name, app_type=app_type_label, content=content, description=description, color=self._app_type_color(app_type), group_id=group_id, group_name=g_label, ) all_flat.append(tile) if group_id and group_id in grouped_tiles: grouped_tiles[group_id].append(tile) else: ungrouped.append(tile) for group_id, tiles in grouped_tiles.items(): grouped_tiles[group_id] = sorted( tiles, key=lambda tile: ( 0 if tile.description.strip() else 1, tile.name.lower(), ), ) ungrouped = sorted( ungrouped, key=lambda tile: ( 0 if tile.description.strip() else 1, tile.name.lower(), ), ) self.applications_group_tiles = [ ApplicationGroupTiles( id=group_id, name=group_meta[group_id], tiles=grouped_tiles.get(group_id, []), ) for group_id in ordered_group_ids ] self.applications_ungrouped_tiles = ungrouped self.dashboard_all_apps = all_flat self.applications_nav_groups = [ ApplicationNavGroup(id=gid, name=group_meta[gid]) for gid in ordered_group_ids ] self._sync_applications_dashboard_visible_groups() self._maybe_refresh_dashboard_expanded_apps() except Exception as exc: self.load_error = self._msg( "errors.applications_load_failed", detail=str(exc) ) self.applications_group_tiles = [] self.applications_ungrouped_tiles = [] self.dashboard_all_apps = [] self.applications_nav_groups = [] self.applications_dashboard_visible_groups = [] self.applications_dashboard_title = self._msg("nav.applications") self.dashboard_show_app_management = not bool( (self.applications_nav_group_id or "").strip() ) self.dashboard_expanded_apps = [] finally: self.is_loading_rows = False def load_auth_table_data(self): if not self.auth_selected_table: self.auth_columns = [] self._sync_auth_grid_columns() self.auth_rows = [] self.auth_row_ids = [] return self.auth_is_loading = True self.auth_error = "" self.auth_is_adding_row = False self.auth_editing_row_idx = -1 self.auth_editing_original_row = [] self.auth_selected_row_idx = -1 self.auth_grid_selection = {} self.auth_sort_column_idx = -1 self.auth_sort_desc = False try: client = self._create_client() data = ( client.table(self.auth_selected_table) .select("*") .limit(500) .execute() .data or [] ) if not data: columns_meta = self._read_table_columns(self.auth_selected_table) self.auth_columns = [ str(col.get("name", "")).strip() for col in columns_meta if col.get("name") and not self._is_auth_auto_column(str(col.get("name", "")).strip()) ] if self.auth_selected_table == "permissions": self._load_auth_available_groups(client) if self.auth_columns.count("user_group_name") == 0: self.auth_columns = [*self.auth_columns, "user_group_name"] self._sync_auth_grid_columns() self.auth_rows = [] self.auth_row_ids = [] if self.auth_selected_table == "user_groups": self._load_auth_available_users(client) if self.auth_selected_table == "permissions": self._load_auth_available_groups(client) return row_keys = {key for row in data for key in row.keys()} ordered_columns = sorted(row_keys) self.auth_columns = [ col for col in ordered_columns if not self._is_auth_auto_column(col) ] if self.auth_selected_table == "permissions": self._load_auth_available_groups(client) if self.auth_columns.count("user_group_name") == 0: self.auth_columns = [*self.auth_columns, "user_group_name"] self._sync_auth_grid_columns() self.auth_rows = [ [str(row.get(column, "")) for column in self.auth_columns] for row in data ] if self.auth_selected_table == "permissions": try: group_id_col_idx = self.auth_columns.index("user_group_id") group_name_col_idx = self.auth_columns.index("user_group_name") updated_rows: list[list[str]] = [] for row in self.auth_rows: next_row = row[:] group_id = ( next_row[group_id_col_idx] if group_id_col_idx < len(next_row) else "" ) while len(next_row) <= group_name_col_idx: next_row.append("") next_row[group_name_col_idx] = self._auth_group_id_to_label(group_id) updated_rows.append(next_row) self.auth_rows = updated_rows except ValueError: pass self.auth_row_ids = [str(row.get("id", "")) for row in data] self._apply_default_auth_grid_sort() if self.auth_selected_table == "user_groups": self._load_auth_available_users(client) except Exception as exc: self.auth_columns = [] self._sync_auth_grid_columns() self.auth_rows = [] self.auth_row_ids = [] self.auth_error = self._msg("errors.auth_load_failed", detail=str(exc)) finally: self.auth_is_loading = False def add_auth_row(self): if not self.auth_selected_table or self.auth_is_adding_row: return if self.auth_editing_row_idx >= 0: return if not self.auth_columns: self.load_auth_table_data() if not self.auth_columns: self.auth_error = self._msg("errors.auth_no_columns") return new_row = ["" for _ in self.auth_columns] if self.auth_selected_table in {"users", "user_groups"}: generated_id = str(uuid.uuid4()) current_user = self._current_user_name() for idx, col in enumerate(self.auth_columns): if col == "id": new_row[idx] = generated_id elif col == "created_by": new_row[idx] = current_user if self.auth_selected_table == "permissions": default_group_id = self._default_permissions_user_group_id() current_user = self._current_user_name() for idx, col in enumerate(self.auth_columns): if col == "user_group_id": new_row[idx] = self._auth_group_id_to_label(default_group_id) elif col == "user_group_name": new_row[idx] = self._auth_group_id_to_label(default_group_id) elif col == "table_name": new_row[idx] = ( self.custom_tables[0] if len(self.custom_tables) > 0 else "" ) elif col == "allowed": new_row[idx] = "true" elif col == "created_by": new_row[idx] = current_user elif col == "id": new_row[idx] = "" self.auth_rows = [new_row, *self.auth_rows] self.auth_row_ids = ["", *self.auth_row_ids] self.auth_is_adding_row = True self.auth_selected_row_idx = 0 self.auth_grid_selection = _build_row_grid_selection( row_idx=0, col_idx=0, total_rows=len(self.auth_rows), total_cols=len(self.auth_grid_columns), ) self.auth_error = "" if self.auth_selected_table == "user_groups": self.auth_user_group_member_ids = [] self.auth_user_group_members_display = [] def set_auth_new_row_value(self, column_idx: int, value: str): if not self.auth_is_adding_row or not self.auth_rows: return if column_idx < 0 or column_idx >= len(self.auth_columns): return column_name = self.auth_columns[column_idx] if self._is_auth_auto_column(column_name): return if self._is_auth_virtual_column(column_name): return if ( self.auth_selected_table == "permissions" and column_name in {"id", "allowed", "created_by"} ): return if ( self.auth_selected_table in {"users", "user_groups"} and column_name in {"id", "created_by"} ): return updated = [row[:] for row in self.auth_rows] if column_idx >= len(updated[0]): return updated[0][column_idx] = value if ( self.auth_selected_table == "permissions" and column_name == "user_group_id" and self.auth_columns.count("user_group_name") > 0 ): try: name_idx = self.auth_columns.index("user_group_name") if name_idx < len(updated[0]): updated[0][name_idx] = value except ValueError: pass self.auth_rows = updated def start_edit_auth_row(self, row_idx: int): if self.auth_is_adding_row: return if row_idx < 0 or row_idx >= len(self.auth_rows): return self.auth_editing_row_idx = row_idx self.auth_editing_original_row = self.auth_rows[row_idx][:] self.auth_error = "" if self.auth_selected_table == "permissions": try: group_col_idx = self.auth_columns.index("user_group_id") if group_col_idx >= 0 and group_col_idx < len(self.auth_rows[row_idx]): updated = [r[:] for r in self.auth_rows] group_id = updated[row_idx][group_col_idx] updated[row_idx][group_col_idx] = self._auth_group_id_to_label(group_id) self.auth_rows = updated except ValueError: pass if self.auth_selected_table == "user_groups" and row_idx < len(self.auth_row_ids): group_id = self.auth_row_ids[row_idx].strip() if group_id: try: client = self._create_client() members = ( client.table("user_group_members") .select("user_id") .eq("user_group_id", group_id) .execute() .data or [] ) self.auth_user_group_member_ids = [ str(m.get("user_id", "")) for m in members if m.get("user_id") ] label_by_id = dict( zip(self.auth_available_user_ids, self.auth_available_user_labels) ) self.auth_user_group_members_display = [ label_by_id.get(uid, uid) for uid in self.auth_user_group_member_ids ] except Exception: self.auth_user_group_member_ids = [] self.auth_user_group_members_display = [] else: self.auth_user_group_member_ids = [] self.auth_user_group_members_display = [] else: self.auth_user_group_member_ids = [] self.auth_user_group_members_display = [] def set_auth_edit_row_value(self, row_idx: int, column_idx: int, value: str): if row_idx != self.auth_editing_row_idx: return if column_idx < 0 or column_idx >= len(self.auth_columns): return column_name = self.auth_columns[column_idx] if self._is_auth_auto_column(column_name) or column_name == "id": return updated = [row[:] for row in self.auth_rows] if row_idx >= len(updated) or column_idx >= len(updated[row_idx]): return updated[row_idx][column_idx] = value if ( self.auth_selected_table == "permissions" and column_name == "user_group_id" and self.auth_columns.count("user_group_name") > 0 ): try: name_idx = self.auth_columns.index("user_group_name") if name_idx < len(updated[row_idx]): updated[row_idx][name_idx] = value except ValueError: pass self.auth_rows = updated def cancel_edit_auth_row(self, row_idx: int): if row_idx != self.auth_editing_row_idx: return if row_idx < 0 or row_idx >= len(self.auth_rows): self.auth_editing_row_idx = -1 self.auth_editing_original_row = [] self.auth_user_group_member_ids = [] self.auth_user_group_members_display = [] return updated = [row[:] for row in self.auth_rows] updated[row_idx] = self.auth_editing_original_row[:] self.auth_rows = updated self.auth_editing_row_idx = -1 self.auth_editing_original_row = [] self.auth_user_group_member_ids = [] self.auth_user_group_members_display = [] def save_auth_new_row(self): if not self.auth_is_adding_row or not self.auth_rows: return if not self.auth_selected_table: return try: column_types = { col["name"]: col["type"] for col in self._read_table_columns(self.auth_selected_table) } payload: dict[str, Any] = {} for col_idx, column_name in enumerate(self.auth_columns): if self._is_auth_auto_column(column_name): continue if self._is_auth_virtual_column(column_name): continue if ( self.auth_selected_table == "permissions" and column_name in {"id", "created_by"} ): continue value = self.auth_rows[0][col_idx] if col_idx < len(self.auth_rows[0]) else "" if ( self.auth_selected_table == "permissions" and column_name == "user_group_id" ): value = self._auth_group_label_to_id(value) parsed = self._coerce_row_value(value, column_types.get(column_name, "text")) if parsed is None: continue payload[column_name] = parsed if self.auth_selected_table in {"users", "user_groups"}: if "id" not in payload: payload["id"] = str(uuid.uuid4()) if "created_by" not in payload and "created_by" in column_types: created_by = self._resolve_created_by_value(column_types) if created_by is not None: payload["created_by"] = created_by if self.auth_selected_table == "permissions": payload.pop("id", None) if "user_group_id" not in payload: payload["user_group_id"] = self._default_permissions_user_group_id() if "table_name" not in payload: payload["table_name"] = ( self.custom_tables[0] if len(self.custom_tables) > 0 else "" ) if "allowed" not in payload: payload["allowed"] = True if "created_by" not in payload: created_by = self._resolve_created_by_value(column_types) if created_by is not None: payload["created_by"] = created_by client = self._create_client() result = client.table(self.auth_selected_table).insert(payload).execute() self.auth_is_adding_row = False if self.auth_selected_table == "user_groups" and self.auth_user_group_member_ids: inserted = result.data or [] new_id = str(inserted[0]["id"]) if inserted and inserted[0].get("id") else "" if new_id: for user_id in self.auth_user_group_member_ids: if not user_id: continue try: client.table("user_group_members").insert( {"user_group_id": new_id, "user_id": user_id} ).execute() except Exception: pass self.auth_user_group_member_ids = [] self.auth_user_group_members_display = [] self.load_auth_table_data() self.auth_error = "" except Exception as exc: self.auth_error = self._msg("errors.auth_add_failed", detail=str(exc)) def save_edit_auth_row(self, row_idx: int): if row_idx != self.auth_editing_row_idx: return if row_idx < 0 or row_idx >= len(self.auth_rows): return if not self.auth_selected_table: return try: row = self.auth_rows[row_idx] original_row = self.auth_editing_original_row[:] if not original_row: original_row = row[:] column_types = { col["name"]: col["type"] for col in self._read_table_columns(self.auth_selected_table) } payload: dict[str, Any] = {} for col_idx, column_name in enumerate(self.auth_columns): if self._is_auth_auto_column(column_name) or column_name == "id": continue if self._is_auth_virtual_column(column_name): continue new_value = row[col_idx] if col_idx < len(row) else "" old_value = original_row[col_idx] if col_idx < len(original_row) else "" if new_value == old_value: continue if ( self.auth_selected_table == "permissions" and column_name == "user_group_id" ): new_value = self._auth_group_label_to_id(new_value) payload[column_name] = self._coerce_row_value( new_value, column_types.get(column_name, "text") ) if not payload: self.auth_editing_row_idx = -1 self.auth_editing_original_row = [] self.auth_error = "" return client = self._create_client() query = client.table(self.auth_selected_table).update(payload) row_id = self.auth_row_ids[row_idx].strip() if row_idx < len(self.auth_row_ids) else "" if row_id: query = query.eq("id", row_id) else: filters_applied = 0 for col_idx, column_name in enumerate(self.auth_columns): original_value = ( original_row[col_idx] if col_idx < len(original_row) else "" ) parsed = self._coerce_row_value( original_value, column_types.get(column_name, "text") ) if parsed is None: continue query = query.eq(column_name, parsed) filters_applied += 1 if filters_applied == 0: self.auth_error = self._msg("errors.auth_identify_update") return query.execute() if self.auth_selected_table == "user_groups": group_id = ( self.auth_row_ids[row_idx].strip() if row_idx < len(self.auth_row_ids) else "" ) if group_id: try: client.table("user_group_members").delete().eq( "user_group_id", group_id ).execute() for user_id in self.auth_user_group_member_ids: if not user_id: continue try: client.table("user_group_members").insert( {"user_group_id": group_id, "user_id": user_id} ).execute() except Exception: pass except Exception: pass self.auth_user_group_member_ids = [] self.auth_user_group_members_display = [] self.auth_editing_row_idx = -1 self.auth_editing_original_row = [] self.load_auth_table_data() self.auth_error = "" except Exception as exc: self.auth_error = self._msg("errors.auth_edit_failed", detail=str(exc)) def add_auth_group_member_by_label(self, label: str): if not label or not self.auth_available_user_ids: return try: idx = self.auth_available_user_labels.index(label) except ValueError: return if idx >= len(self.auth_available_user_ids): return user_id = self.auth_available_user_ids[idx] if user_id in self.auth_user_group_member_ids: return self.auth_user_group_member_ids = [ *self.auth_user_group_member_ids, user_id, ] self.auth_user_group_members_display = [ *self.auth_user_group_members_display, label, ] def remove_auth_group_member_at_index(self, idx: int): if idx < 0 or idx >= len(self.auth_user_group_member_ids): return self.auth_user_group_member_ids = [ x for i, x in enumerate(self.auth_user_group_member_ids) if i != idx ] self.auth_user_group_members_display = [ x for i, x in enumerate(self.auth_user_group_members_display) if i != idx ] def cancel_add_auth_row(self): if not self.auth_is_adding_row: return self.auth_rows = self.auth_rows[1:] self.auth_row_ids = self.auth_row_ids[1:] self.auth_is_adding_row = False self.auth_user_group_member_ids = [] self.auth_user_group_members_display = [] self.auth_error = "" def delete_auth_row(self, row_idx: int): if row_idx < 0 or row_idx >= len(self.auth_rows): return if not self.auth_selected_table: return if self.auth_is_adding_row and row_idx == 0: self.cancel_add_auth_row() return try: client = self._create_client() query = client.table(self.auth_selected_table).delete() row_id = self.auth_row_ids[row_idx].strip() if row_idx < len(self.auth_row_ids) else "" if row_id: query = query.eq("id", row_id) else: column_types = { col["name"]: col["type"] for col in self._read_table_columns(self.auth_selected_table) } row = self.auth_rows[row_idx] filters_applied = 0 for col_idx, column_name in enumerate(self.auth_columns): value = row[col_idx] if col_idx < len(row) else "" parsed = self._coerce_row_value( value, column_types.get(column_name, "text") ) if parsed is None: continue query = query.eq(column_name, parsed) filters_applied += 1 if filters_applied == 0: self.auth_error = self._msg("errors.auth_identify_delete") return query.execute() self.load_auth_table_data() self.auth_error = "" except Exception as exc: self.auth_error = self._msg("errors.auth_delete_failed", detail=str(exc)) def export_auth_to_excel(self): if not self.auth_selected_table or not self.auth_columns or not self.auth_rows: self.auth_error = self._msg("errors.no_data_export") return if pd is None: self.auth_error = self._msg("errors.no_excel") return try: # Prepare data for export export_data = [] for row in self.auth_rows: row_dict = {} for i, col in enumerate(self.auth_columns): if i < len(row): row_dict[col] = row[i] else: row_dict[col] = "" export_data.append(row_dict) # Create DataFrame and Excel file df = pd.DataFrame(export_data) output = io.BytesIO() with pd.ExcelWriter(output, engine='openpyxl') as writer: df.to_excel(writer, sheet_name=self.auth_selected_table, index=False) # Return file for download output.seek(0) return rx.download( data=output.getvalue(), filename=f"{self.auth_selected_table}.xlsx", mime_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) except Exception as exc: self.auth_error = self._msg("errors.export_failed", detail=str(exc)) def export_master_detail_to_excel(self): if not self.master_detail_detail_columns or not self.master_detail_detail_rows: self.master_detail_error = self._msg("errors.md_no_export") return if pd is None: self.master_detail_error = self._msg("errors.no_excel") return try: # Prepare data for export export_data = [] for row in self.master_detail_detail_rows: row_dict = {} for i, col in enumerate(self.master_detail_detail_columns): if i < len(row): row_dict[col] = row[i] else: row_dict[col] = "" export_data.append(row_dict) # Create DataFrame and Excel file df = pd.DataFrame(export_data) output = io.BytesIO() with pd.ExcelWriter(output, engine='openpyxl') as writer: df.to_excel(writer, sheet_name=f"{self.master_detail_detail_table}_detail", index=False) # Return file for download output.seek(0) return rx.download( data=output.getvalue(), filename=f"{self.master_detail_detail_table}_detail.xlsx", mime_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) except Exception as exc: self.master_detail_error = self._msg( "errors.export_failed", detail=str(exc) ) def set_master_detail_master_table(self, table_name: str): self.master_detail_master_table = table_name or "" self.master_detail_master_key = "" self.master_detail_detail_table = "" self.master_detail_detail_key = "" self.master_detail_master_key_options = [] self.master_detail_detail_key_options = [] self.master_detail_master_columns = [] self._sync_master_detail_master_grid_columns() self.master_detail_master_rows = [] self.master_detail_master_key_values = [] self.master_detail_master_sort_column_idx = -1 self.master_detail_master_sort_desc = False self.master_detail_detail_columns = [] self._sync_master_detail_detail_grid_columns() self.master_detail_detail_rows = [] self.master_detail_detail_selected_row_idx = -1 self.master_detail_detail_sort_column_idx = -1 self.master_detail_detail_sort_desc = False self.master_detail_selected_master_idx = -1 self.master_detail_master_grid_selection = {} self.master_detail_detail_grid_selection = {} self.master_detail_error = "" if not table_name: return try: columns_meta = self._read_table_columns(table_name) self.master_detail_master_key_options = [ str(c.get("name", "")).strip() for c in columns_meta if c.get("name") ] self._maybe_autoload_master_detail() except Exception as exc: self.master_detail_error = self._msg( "errors.md_read_master", detail=str(exc) ) def set_master_detail_detail_table(self, table_name: str): self.master_detail_detail_table = table_name or "" self.master_detail_detail_key = "" self.master_detail_detail_key_options = [] self.master_detail_detail_columns = [] self._sync_master_detail_detail_grid_columns() self.master_detail_detail_rows = [] self.master_detail_detail_selected_row_idx = -1 self.master_detail_detail_sort_column_idx = -1 self.master_detail_detail_sort_desc = False self.master_detail_selected_master_idx = -1 self.master_detail_master_grid_selection = {} self.master_detail_detail_grid_selection = {} self.master_detail_error = "" if not table_name: return try: columns_meta = self._read_table_columns(table_name) self.master_detail_detail_key_options = [ str(c.get("name", "")).strip() for c in columns_meta if c.get("name") ] self._maybe_autoload_master_detail() except Exception as exc: self.master_detail_error = self._msg( "errors.md_read_detail", detail=str(exc) ) def set_master_detail_master_key(self, key: str): self.master_detail_master_key = key or "" self.master_detail_master_rows = [] self.master_detail_master_key_values = [] self.master_detail_master_sort_column_idx = -1 self.master_detail_master_sort_desc = False self.master_detail_detail_rows = [] self.master_detail_detail_selected_row_idx = -1 self.master_detail_detail_sort_column_idx = -1 self.master_detail_detail_sort_desc = False self.master_detail_selected_master_idx = -1 self.master_detail_master_grid_selection = {} self.master_detail_detail_grid_selection = {} self._maybe_autoload_master_detail() def set_master_detail_detail_key(self, key: str): self.master_detail_detail_key = key or "" self.master_detail_detail_rows = [] self.master_detail_detail_selected_row_idx = -1 self.master_detail_detail_sort_column_idx = -1 self.master_detail_detail_sort_desc = False self.master_detail_selected_master_idx = -1 self.master_detail_master_grid_selection = {} self.master_detail_detail_grid_selection = {} self._maybe_autoload_master_detail() def _can_autoload_master_detail(self) -> bool: return ( self.master_detail_master_table != "" and self.master_detail_detail_table != "" and self.master_detail_master_key != "" and self.master_detail_detail_key != "" ) def _maybe_autoload_master_detail(self): if not self._can_autoload_master_detail(): return self.load_master_detail_master_rows() def load_master_detail_master_rows(self): if not self.master_detail_master_table or not self.master_detail_master_key: self.master_detail_error = self._msg("errors.md_select_master_key") return self.master_detail_loading = True self.master_detail_error = "" self.master_detail_master_rows = [] self.master_detail_master_key_values = [] self.master_detail_master_sort_column_idx = -1 self.master_detail_master_sort_desc = False self.master_detail_detail_rows = [] self.master_detail_detail_sort_column_idx = -1 self.master_detail_detail_sort_desc = False self.master_detail_selected_master_idx = -1 self.master_detail_master_grid_selection = {} self.master_detail_detail_grid_selection = {} try: client = self._create_client() data = ( client.table(self.master_detail_master_table) .select("*") .limit(500) .execute() .data or [] ) if not data: columns_meta = self._read_table_columns(self.master_detail_master_table) self.master_detail_master_columns = [ str(c.get("name", "")).strip() for c in columns_meta if c.get("name") ] self._sync_master_detail_master_grid_columns() self.master_detail_loading = False return row_keys = {key for row in data for key in row.keys()} ordered = sorted(row_keys) self.master_detail_master_columns = [ c for c in ordered if self._is_visible_column(self.master_detail_master_table, c) ] if not self.master_detail_master_columns: self.master_detail_master_columns = ordered self._sync_master_detail_master_grid_columns() self.master_detail_master_rows = [ [str(row.get(c, "")) for c in self.master_detail_master_columns] for row in data ] self.master_detail_master_key_values = [ str(data[i].get(self.master_detail_master_key, "")) for i in range(len(data)) ] mpairs = self._sorted_pairs_first_column_desc( list( zip( self.master_detail_master_key_values, self.master_detail_master_rows, ) ) ) self.master_detail_master_key_values = [p[0] for p in mpairs] self.master_detail_master_rows = [p[1] for p in mpairs] self.master_detail_master_sort_column_idx = 0 self.master_detail_master_sort_desc = True if self.master_detail_master_key_values: self.select_master_detail_row(0) else: self.master_detail_selected_master_idx = -1 self.master_detail_master_grid_selection = {} self.master_detail_detail_rows = [] self.master_detail_detail_selected_row_idx = -1 self.master_detail_detail_grid_selection = {} except Exception as exc: self.master_detail_error = self._msg( "errors.md_load_master", detail=str(exc) ) finally: self.master_detail_loading = False def select_master_detail_row(self, row_idx: int): if row_idx < 0 or row_idx >= len(self.master_detail_master_key_values): self.master_detail_selected_master_idx = -1 self.master_detail_master_grid_selection = {} self.master_detail_detail_rows = [] self.master_detail_detail_selected_row_idx = -1 self.master_detail_detail_grid_selection = {} return self.master_detail_selected_master_idx = row_idx self.master_detail_master_grid_selection = _build_row_grid_selection( row_idx=row_idx, col_idx=0, total_rows=len(self.master_detail_master_rows), total_cols=len(self.master_detail_master_grid_columns), ) if not self.master_detail_detail_table or not self.master_detail_detail_key: self.master_detail_detail_rows = [] self.master_detail_detail_selected_row_idx = -1 self.master_detail_detail_grid_selection = {} return key_value = self.master_detail_master_key_values[row_idx] self.master_detail_loading = True self.master_detail_error = "" try: client = self._create_client() data = ( client.table(self.master_detail_detail_table) .select("*") .eq(self.master_detail_detail_key, key_value) .limit(500) .execute() .data or [] ) if not data: columns_meta = self._read_table_columns(self.master_detail_detail_table) self.master_detail_detail_columns = [ str(c.get("name", "")).strip() for c in columns_meta if c.get("name") ] self._sync_master_detail_detail_grid_columns() self.master_detail_detail_key_col_idx = ( self.master_detail_detail_columns.index(self.master_detail_detail_key) if self.master_detail_detail_key in self.master_detail_detail_columns else -1 ) self.master_detail_detail_id_col_idx = ( self.master_detail_detail_columns.index("id") if "id" in self.master_detail_detail_columns else -1 ) self.master_detail_detail_rows = [] self.master_detail_detail_row_ids = [] self.master_detail_detail_selected_row_idx = -1 self.master_detail_detail_grid_selection = {} self.master_detail_loading = False return row_keys = {key for row in data for key in row.keys()} ordered = sorted(row_keys) self.master_detail_detail_columns = [ c for c in ordered if self._is_visible_column(self.master_detail_detail_table, c) ] if not self.master_detail_detail_columns: self.master_detail_detail_columns = ordered self._sync_master_detail_detail_grid_columns() self.master_detail_detail_key_col_idx = ( self.master_detail_detail_columns.index(self.master_detail_detail_key) if self.master_detail_detail_key in self.master_detail_detail_columns else -1 ) self.master_detail_detail_id_col_idx = ( self.master_detail_detail_columns.index("id") if "id" in self.master_detail_detail_columns else -1 ) detail_pairs = [ ( str(row.get("id", "")), [str(row.get(c, "")) for c in self.master_detail_detail_columns], ) for row in data ] dpairs = self._sorted_pairs_first_column_desc(detail_pairs) self.master_detail_detail_row_ids = [p[0] for p in dpairs] self.master_detail_detail_rows = [p[1] for p in dpairs] self.master_detail_detail_sort_column_idx = 0 self.master_detail_detail_sort_desc = True self.master_detail_detail_selected_row_idx = -1 self.master_detail_detail_grid_selection = {} self.master_detail_detail_adding_row = False self.master_detail_detail_editing_idx = -1 self.master_detail_detail_edit_original_row = [] except Exception as exc: self.master_detail_error = self._msg( "errors.md_load_detail", detail=str(exc) ) finally: self.master_detail_loading = False def add_master_detail_detail_row(self): if self.master_detail_selected_master_idx < 0 or not self.master_detail_detail_columns: return if self.master_detail_detail_adding_row or self.master_detail_detail_editing_idx >= 0: return key_value = self.master_detail_master_key_values[self.master_detail_selected_master_idx] new_row: list[str] = [] for col in self.master_detail_detail_columns: if col == self.master_detail_detail_key: new_row.append(key_value) else: new_row.append("") self.master_detail_detail_rows = [new_row, *self.master_detail_detail_rows] self.master_detail_detail_row_ids = ["", *self.master_detail_detail_row_ids] self.master_detail_detail_adding_row = True self.master_detail_detail_selected_row_idx = 0 self.master_detail_detail_grid_selection = _build_row_grid_selection( row_idx=0, col_idx=0, total_rows=len(self.master_detail_detail_rows), total_cols=len(self.master_detail_detail_grid_columns), ) self.master_detail_error = "" def set_master_detail_detail_new_value(self, column_idx: int, value: str): if not self.master_detail_detail_adding_row or not self.master_detail_detail_rows: return if column_idx < 0 or column_idx >= len(self.master_detail_detail_columns): return if self.master_detail_detail_columns[column_idx] in self._auto_managed_columns(): return if self.master_detail_detail_columns[column_idx] == self.master_detail_detail_key: return updated = [row[:] for row in self.master_detail_detail_rows] if not updated or column_idx >= len(updated[0]): return updated[0][column_idx] = value self.master_detail_detail_rows = updated def save_master_detail_detail_new_row(self): if not self.master_detail_detail_adding_row or not self.master_detail_detail_rows: return if self.master_detail_selected_master_idx < 0: return key_value = self.master_detail_master_key_values[self.master_detail_selected_master_idx] try: column_types = { col["name"]: col["type"] for col in self._read_table_columns(self.master_detail_detail_table) } auto_managed = self._auto_managed_columns() key_type = column_types.get(self.master_detail_detail_key, "text") coerced_key = self._coerce_row_value(key_value, key_type) payload: dict[str, Any] = { self.master_detail_detail_key: coerced_key if coerced_key is not None else key_value } for col_idx, column_name in enumerate(self.master_detail_detail_columns): if column_name in auto_managed or column_name == self.master_detail_detail_key: continue value = self.master_detail_detail_rows[0][col_idx] if col_idx < len(self.master_detail_detail_rows[0]) else "" parsed = self._coerce_row_value(value, column_types.get(column_name, "text")) if parsed is not None: payload[column_name] = parsed client = self._create_client() client.table(self.master_detail_detail_table).insert(payload).execute() self.master_detail_detail_adding_row = False self.select_master_detail_row(self.master_detail_selected_master_idx) self.master_detail_error = "" except Exception as exc: self.master_detail_error = self._msg( "errors.md_add_detail", detail=str(exc) ) def cancel_master_detail_detail_add(self): if not self.master_detail_detail_adding_row: return self.master_detail_detail_rows = self.master_detail_detail_rows[1:] self.master_detail_detail_row_ids = self.master_detail_detail_row_ids[1:] self.master_detail_detail_adding_row = False self.master_detail_error = "" def start_edit_master_detail_detail_row(self, row_idx: int): if self.master_detail_detail_adding_row: return if row_idx < 0 or row_idx >= len(self.master_detail_detail_rows): return self.master_detail_detail_editing_idx = row_idx self.master_detail_detail_edit_original_row = self.master_detail_detail_rows[row_idx][:] self.master_detail_error = "" def set_master_detail_detail_edit_value(self, row_idx: int, column_idx: int, value: str): if row_idx != self.master_detail_detail_editing_idx: return if column_idx < 0 or column_idx >= len(self.master_detail_detail_columns): return if self.master_detail_detail_columns[column_idx] in self._auto_managed_columns(): return if self.master_detail_detail_columns[column_idx] == self.master_detail_detail_key: return updated = [row[:] for row in self.master_detail_detail_rows] if row_idx >= len(updated) or column_idx >= len(updated[row_idx]): return updated[row_idx][column_idx] = value self.master_detail_detail_rows = updated def cancel_edit_master_detail_detail_row(self, row_idx: int): if row_idx != self.master_detail_detail_editing_idx: return if row_idx < 0 or row_idx >= len(self.master_detail_detail_rows): self.master_detail_detail_editing_idx = -1 self.master_detail_detail_edit_original_row = [] return updated = [row[:] for row in self.master_detail_detail_rows] updated[row_idx] = self.master_detail_detail_edit_original_row[:] self.master_detail_detail_rows = updated self.master_detail_detail_editing_idx = -1 self.master_detail_detail_edit_original_row = [] def save_edit_master_detail_detail_row(self, row_idx: int): if row_idx != self.master_detail_detail_editing_idx: return if row_idx < 0 or row_idx >= len(self.master_detail_detail_rows): return try: row = self.master_detail_detail_rows[row_idx] original_row = self.master_detail_detail_edit_original_row[:] column_types = { col["name"]: col["type"] for col in self._read_table_columns(self.master_detail_detail_table) } auto_managed = self._auto_managed_columns() payload: dict[str, Any] = {} for col_idx, column_name in enumerate(self.master_detail_detail_columns): if column_name in auto_managed or column_name == self.master_detail_detail_key: continue new_value = row[col_idx] if col_idx < len(row) else "" old_value = original_row[col_idx] if col_idx < len(original_row) else "" if new_value == old_value: continue payload[column_name] = self._coerce_row_value( new_value, column_types.get(column_name, "text") ) if not payload: self.master_detail_detail_editing_idx = -1 self.master_detail_detail_edit_original_row = [] self.master_detail_error = "" return client = self._create_client() query = client.table(self.master_detail_detail_table).update(payload) row_id = self.master_detail_detail_row_ids[row_idx].strip() if row_idx < len(self.master_detail_detail_row_ids) else "" if row_id: query = query.eq("id", row_id) else: # Fallback: identify row by original column values (e.g. table without "id" or id hidden) filters_applied = 0 for col_idx, column_name in enumerate(self.master_detail_detail_columns): original_value = ( original_row[col_idx] if col_idx < len(original_row) else "" ) parsed = self._coerce_row_value( original_value, column_types.get(column_name, "text") ) if parsed is None: continue query = query.eq(column_name, parsed) filters_applied += 1 if filters_applied == 0: self.master_detail_error = self._msg("errors.md_identify_update") return query.execute() self.master_detail_detail_editing_idx = -1 self.master_detail_detail_edit_original_row = [] self.select_master_detail_row(self.master_detail_selected_master_idx) self.master_detail_error = "" except Exception as exc: self.master_detail_error = self._msg( "errors.md_edit_detail", detail=str(exc) ) def delete_master_detail_detail_row(self, row_idx: int): if row_idx < 0 or row_idx >= len(self.master_detail_detail_rows): return if self.master_detail_detail_adding_row and row_idx == 0: self.master_detail_detail_rows = self.master_detail_detail_rows[1:] self.master_detail_detail_row_ids = self.master_detail_detail_row_ids[1:] self.master_detail_detail_adding_row = False self.master_detail_error = "" return try: client = self._create_client() row_id = self.master_detail_detail_row_ids[row_idx].strip() if row_idx < len(self.master_detail_detail_row_ids) else "" if row_id: client.table(self.master_detail_detail_table).delete().eq("id", row_id).execute() else: row = self.master_detail_detail_rows[row_idx] column_types = { col["name"]: col["type"] for col in self._read_table_columns(self.master_detail_detail_table) } query = client.table(self.master_detail_detail_table).delete() for col_idx, column_name in enumerate(self.master_detail_detail_columns): value = row[col_idx] if col_idx < len(row) else "" parsed = self._coerce_row_value(value, column_types.get(column_name, "text")) if parsed is not None: query = query.eq(column_name, parsed) query.execute() self.select_master_detail_row(self.master_detail_selected_master_idx) self.master_detail_error = "" except Exception as exc: self.master_detail_error = self._msg( "errors.md_delete_detail", detail=str(exc) ) def set_visuals_table(self, table_name: str): if table_name and self.custom_tables.count(table_name) == 0: table_name = "" self.visuals_selected_table = table_name self.visuals_label_column = "" self.visuals_data_columns = [] self.visuals_data_series = [] self.visuals_chart_type = "simple graph" self.visuals_chart_data = [] self.visuals_has_generated = False self.visuals_label_options = [] self.visuals_data_options = [] self.visuals_notice = "" if not table_name: return columns_meta = self._read_table_columns(table_name) labels: list[str] = [] data: list[str] = [] for col in columns_meta: col_name = str(col.get("name", "")).strip() col_type = str(col.get("type", "")).strip() if not col_name: continue if self._is_numeric_type(col_type): data.append(col_name) else: labels.append(col_name) if not labels and columns_meta: labels = [str(col.get("name", "")).strip() for col in columns_meta if col.get("name")] self.visuals_label_options = labels self.visuals_data_options = data if labels: self.visuals_label_column = labels[0] if data: self.visuals_data_columns = [data[0]] self._refresh_visuals_data_series() self.generate_visual() def set_visuals_label_column(self, value: str): self.visuals_label_column = value self.generate_visual() def toggle_visuals_data_column(self, value: str): current = [item for item in self.visuals_data_columns if item] if value in current: self.visuals_data_columns = [item for item in current if item != value] else: current.append(value) self.visuals_data_columns = current self._refresh_visuals_data_series() self.generate_visual() def _refresh_visuals_data_series(self): self.visuals_data_series = [ { "key": column, "color": VISUAL_SERIES_COLORS[idx % len(VISUAL_SERIES_COLORS)], } for idx, column in enumerate(self.visuals_data_columns) ] def set_visuals_chart_type(self, value: str): self.visuals_chart_type = value self.generate_visual() @staticmethod def _to_float(value: Any) -> float | None: if value is None: return None if isinstance(value, (int, float)): return float(value) raw = str(value).strip() if raw == "": return None try: return float(raw) except Exception: return None @staticmethod def _label_sort_key(value: Any) -> tuple[int, Any]: raw = str(value or "").strip() try: return (0, float(raw)) except Exception: return (1, raw.lower()) def _build_histogram_data( self, rows: list[tuple[float, str]], bins_count: int = 10 ) -> list[dict[str, Any]]: if not rows: return [] values = [item[0] for item in rows] low = min(values) high = max(values) if low == high: all_hints = "\n\n".join([item[1] for item in rows]) return [{"label": f"{low:g}", "value": len(values), "hint": all_hints}] bins_count = max(1, bins_count) step = (high - low) / bins_count counts = [0 for _ in range(bins_count)] bin_hints: list[list[str]] = [[] for _ in range(bins_count)] for v, hint in rows: idx = int((v - low) / step) if idx >= bins_count: idx = bins_count - 1 counts[idx] += 1 bin_hints[idx].append(hint) histogram: list[dict[str, Any]] = [] for idx, count in enumerate(counts): start = low + idx * step end = start + step histogram.append( { "label": f"{start:.2f}-{end:.2f}", "value": count, "hint": "\n\n".join(bin_hints[idx]), } ) return histogram def generate_visual(self): if ( not self.visuals_selected_table or not self.visuals_label_column or len(self.visuals_data_columns) == 0 ): self.visuals_notice = self._msg("visuals.select_first") self.visuals_chart_data = [] self.visuals_has_generated = False return self.visuals_notice = "" self.visuals_chart_data = [] self.visuals_has_generated = False try: client = self._create_client() data = ( client.table(self.visuals_selected_table).select("*").limit(2000).execute().data or [] ) points: list[dict[str, Any]] = [] for row in data: if not isinstance(row, dict): continue label = str(row.get(self.visuals_label_column, "")).strip() or "(empty)" row_hint_parts: list[str] = [] for key in sorted(row.keys()): row_hint_parts.append(f"{key}: {row.get(key)}") row_hint = "\n".join(row_hint_parts) point: dict[str, Any] = {"label": label, "hint": row_hint} has_numeric = False for column in self.visuals_data_columns: numeric = self._to_float(row.get(column)) if numeric is None: continue has_numeric = True point[column] = numeric if has_numeric: points.append(point) points.sort(key=lambda item: self._label_sort_key(item.get("label", ""))) self.visuals_chart_data = points self.visuals_has_generated = True if not self.visuals_chart_data: self.visuals_notice = self._msg("visuals.no_numeric") except Exception as exc: self.visuals_notice = self._msg( "visuals.generate_failed", detail=str(exc) ) self.visuals_chart_data = [] self.visuals_has_generated = False def export_visuals_to_excel(self): if not self.visuals_chart_data: self.visuals_notice = self._msg("visuals.no_chart_export") return if pd is None: self.visuals_notice = self._msg("errors.no_excel") return try: from openpyxl import Workbook from openpyxl.drawing.image import Image from openpyxl.styles import Alignment from openpyxl.utils import get_column_letter # Create DataFrame from chart data df = pd.DataFrame(self.visuals_chart_data) # Create workbook and add data sheet wb = Workbook() ws_data = wb.active ws_data.title = "Chart_Data" # Write DataFrame to Excel for r, (idx, row) in enumerate(df.iterrows(), 1): for c, (col_name, value) in enumerate(row.items(), 1): if r == 1: # Header row ws_data.cell(row=r, column=c, value=col_name) else: ws_data.cell(row=r, column=c, value=value) # Auto-adjust column widths for col_num, column in enumerate(df.columns, 1): column_letter = get_column_letter(col_num) max_length = max( df[column].astype(str).map(len).max(), len(str(column)) ) + 2 ws_data.column_dimensions[column_letter].width = min(max_length, 50) # Generate chart image using matplotlib if plt is not None: try: # Create figure fig, ax = plt.subplots(figsize=(10, 6), dpi=100) fig.patch.set_facecolor('#121a2e') # Match app background ax.set_facecolor('#0f172a') # Match card background # Prepare data for plotting labels = [row.get('label', '') for row in self.visuals_chart_data] data_columns = [col for col in df.columns if col not in ['label', 'hint']] if data_columns and labels: x_positions = range(len(labels)) if self.visuals_chart_type == "histogram": # For histogram/bar chart for i, col in enumerate(data_columns): values = [row.get(col, 0) for row in self.visuals_chart_data] ax.bar([x + i*0.8/len(data_columns) for x in x_positions], values, width=0.8/len(data_columns), label=col, alpha=0.7) else: # For line chart for i, col in enumerate(data_columns): values = [row.get(col, 0) for row in self.visuals_chart_data] ax.plot(x_positions, values, marker='o', label=col, linewidth=2) # Customize appearance ax.set_xlabel(self.visuals_label_column or 'Labels', color='#9ca3af', fontsize=10) ax.set_ylabel('Values', color='#9ca3af', fontsize=10) ax.set_title(f'{self.visuals_selected_table} Chart', color='#e5e7eb', fontsize=12, pad=20) ax.set_xticks(x_positions) ax.set_xticklabels(labels, rotation=45, ha='right', color='#9ca3af', fontsize=8) ax.tick_params(colors='#9ca3af') ax.grid(True, alpha=0.3, color='#2a3655') ax.legend(loc='upper right', facecolor='#0f172a', edgecolor='#2a3655') # Set spines color for spine in ax.spines.values(): spine.set_edgecolor('#2a3655') # Save chart as PNG chart_buffer = io.BytesIO() plt.savefig(chart_buffer, format='png', bbox_inches='tight', facecolor='#121a2e', edgecolor='none') plt.close(fig) # Add chart sheet and insert image ws_chart = wb.create_sheet("Chart") img = Image(chart_buffer) img.width = 800 img.height = 500 ws_chart.add_image(img, 'A1') # Add title ws_chart.cell(row=1, column=1, value=f"{self.visuals_selected_table} Chart") ws_chart.cell(row=1, column=1).alignment = Alignment(horizontal='center') ws_chart.merge_cells('A1:J1') except Exception as chart_exc: # If chart generation fails, continue without image print(f"Chart generation failed: {chart_exc}") pass # Save workbook to buffer output = io.BytesIO() wb.save(output) output.seek(0) return rx.download( data=output.getvalue(), filename=f"{self.visuals_selected_table}_chart.xlsx", mime_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) except Exception as exc: self.visuals_notice = self._msg( "errors.export_failed", detail=str(exc) ) def _load_users_via_auth_admin(self) -> list[dict[str, Any]]: supabase_url = os.getenv("SUPABASE_URL", "").rstrip("/") service_key = os.getenv("SUPABASE_SERVICE_ROLE_KEY", "").strip() if not supabase_url or not service_key: return [] req = url_request.Request( f"{supabase_url}/auth/v1/admin/users?per_page=500", headers={ "apikey": service_key, "Authorization": f"Bearer {service_key}", }, ) try: with url_request.urlopen(req, timeout=20) as resp: payload = json.loads(resp.read().decode("utf-8")) users = payload.get("users", []) normalized: list[dict[str, Any]] = [] for item in users: if not isinstance(item, dict): continue normalized.append( { "email": item.get("email"), "id": item.get("id"), "phone": item.get("phone"), "last_sign_in_at": item.get("last_sign_in_at"), } ) return normalized except url_error.HTTPError as exc: # Most often 401 when service role key doesn't match current project. if exc.code in (401, 403): return [] raise def select_users(self): self.is_create_table_mode = False self.is_edit_table_mode = False self.is_adding_row = False self.editing_row_idx = -1 self.editing_original_row = [] self.is_loading_rows = True self.load_error = "" try: users = self._load_users_via_auth_admin() if not users: client = self._create_client() env_candidate = os.getenv("SUPABASE_USERS_TABLE", "").strip() candidates = [name for name in [env_candidate, "users", "profiles"] if name] seen: set[str] = set() unique_candidates: list[str] = [] for name in candidates: if name not in seen: seen.add(name) unique_candidates.append(name) for table_name in unique_candidates: try: data = ( client.table(table_name).select("*").limit(500).execute().data or [] ) users = data break except Exception: continue self.selected_table = "Users" if not users: self.columns = [] self.rows = [] self.row_ids = [] self.load_error = self._msg("errors.users_unavailable") return ordered_columns = ["email", "id", "phone", "last_sign_in_at"] self.columns = [col for col in ordered_columns if any(col in u for u in users)] self.rows = [ [str(user.get(column, "")) for column in self.columns] for user in users ] self.row_ids = [str(user.get("id", "")) for user in users] except Exception as exc: self.selected_table = "Users" self.columns = [] self.rows = [] self.row_ids = [] self.load_error = self._msg("errors.users_load_failed", detail=str(exc)) finally: self.is_loading_rows = False def _create_client(self) -> Client: url = os.getenv("SUPABASE_URL", "http://127.0.0.1:8000") key = os.getenv("SUPABASE_ANON_KEY") if not key: raise ValueError("Set SUPABASE_ANON_KEY in environment.") return create_client(url, key) def _execute_sql(self, sql: str): client = self._create_client() rpc_candidates = [ ("exec_sql", {"sql": sql}), ("exec_sql", {"query": sql}), ("run_sql", {"sql": sql}), ("run_sql", {"query": sql}), ("execute_sql", {"sql": sql}), ("execute_sql", {"query": sql}), ("execute_sql", {"sql_query": sql}), ] missing_rpc_markers = ( "could not find the function", "function public.exec_sql", "function public.run_sql", "function public.execute_sql", "does not exist in the schema cache", ) last_error: Exception | None = None for rpc_name, params in rpc_candidates: try: client.rpc(rpc_name, params).execute() return except Exception as exc: last_error = exc message = str(exc).lower() # Continue only when RPC itself is missing. # If RPC exists but SQL failed (permissions/DDL/etc), surface real error. if any(marker in message for marker in missing_rpc_markers): continue raise access_token = os.getenv("SUPABASE_ACCESS_TOKEN") project_ref = os.getenv("SUPABASE_PROJECT_REF") or self._derive_project_ref() if access_token and project_ref: self._execute_sql_via_management_api(sql, project_ref, access_token) return db_url = os.getenv("SUPABASE_DB_URL") or os.getenv("DATABASE_URL") if db_url: if psycopg is not None: with psycopg.connect(db_url, autocommit=True) as conn: with conn.cursor() as cur: cur.execute(sql) return if psycopg2 is not None: conn = psycopg2.connect(db_url) try: conn.autocommit = True with conn.cursor() as cur: cur.execute(sql) return finally: conn.close() raise RuntimeError( "No SQL RPC function found and no API/DB fallback available. " "Provide SUPABASE_ACCESS_TOKEN (+ SUPABASE_PROJECT_REF if needed) " "for Management API, or provide SUPABASE_DB_URL/DATABASE_URL, " "or create RPC function exec_sql/run_sql/execute_sql." ) from last_error def _execute_sql_query_via_management_api( self, sql: str, project_ref: str, access_token: str ) -> Any: endpoint = f"https://api.supabase.com/v1/projects/{project_ref}/database/query" payload_variants = [ {"query": sql}, {"sql": sql}, ] last_error: Exception | None = None for payload in payload_variants: req = url_request.Request( endpoint, data=json.dumps(payload).encode("utf-8"), headers={ "Authorization": f"Bearer {access_token}", "Content-Type": "application/json", }, method="POST", ) try: with url_request.urlopen(req, timeout=20) as resp: body = resp.read().decode("utf-8", errors="ignore") if not (200 <= resp.status < 300): last_error = RuntimeError(f"Management API status: {resp.status}") continue if not body.strip(): return [] try: return json.loads(body) except Exception: return [] except url_error.HTTPError as exc: details = exc.read().decode("utf-8", errors="ignore") last_error = RuntimeError( f"Management API HTTP {exc.code}: {details[:300]}" ) except Exception as exc: last_error = exc if last_error: raise last_error return [] def _execute_reports_select_sql(self, sql: str) -> tuple[list[str], list[list[str]]]: access_token = os.getenv("SUPABASE_ACCESS_TOKEN") project_ref = os.getenv("SUPABASE_PROJECT_REF") or self._derive_project_ref() if access_token and project_ref: payload = self._execute_sql_query_via_management_api(sql, project_ref, access_token) extracted = self._extract_rows_from_sql_payload(payload) if extracted: return self._rows_to_reports_grid(extracted) # Prefer direct PostgREST access for simple SELECT queries to avoid # DDL-only RPC functions that may exist in the project. direct = self._execute_simple_select_via_client(sql) if direct is not None: return direct joined = self._execute_join_select_via_client(sql) if joined is not None: return joined db_url = os.getenv("SUPABASE_DB_URL") or os.getenv("DATABASE_URL") if db_url and psycopg is not None: with psycopg.connect(db_url, autocommit=True) as conn: with conn.cursor() as cur: cur.execute(sql) description = cur.description or [] columns = [str(item[0]) for item in description] data_rows = cur.fetchall() if description else [] rows = [ [self._sql_value_to_text(value) for value in row] for row in data_rows ] return columns, rows if db_url and psycopg2 is not None: conn = psycopg2.connect(db_url) try: conn.autocommit = True with conn.cursor() as cur: cur.execute(sql) description = cur.description or [] columns = [str(item[0]) for item in description] data_rows = cur.fetchall() if description else [] rows = [ [self._sql_value_to_text(value) for value in row] for row in data_rows ] return columns, rows finally: conn.close() raise RuntimeError( "No SELECT execution path available. " "Provide SUPABASE_ACCESS_TOKEN (+ SUPABASE_PROJECT_REF if needed), " "or SUPABASE_DB_URL/DATABASE_URL." ) @staticmethod def _parse_table_alias(raw: str) -> tuple[str, str] | None: part = (raw or "").strip() match = re.match( r"(?is)^(?P<table>[a-zA-Z_][a-zA-Z0-9_\.]*)\s+(?:as\s+)?(?P<alias>[a-zA-Z_][a-zA-Z0-9_]*)$", part, ) if match: table = str(match.group("table") or "").strip() alias = str(match.group("alias") or "").strip() if table and alias: return table.split(".")[-1], alias simple = re.match(r"(?is)^(?P<table>[a-zA-Z_][a-zA-Z0-9_\.]*)$", part) if simple: table = str(simple.group("table") or "").strip() short = table.split(".")[-1] if short: return short, short return None @staticmethod def _parse_join_condition(raw: str) -> tuple[str, str, str, str] | None: condition = (raw or "").strip() match = re.match( r"(?is)^(?P<a_alias>[a-zA-Z_][a-zA-Z0-9_]*)\.(?P<a_col>[a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(?P<b_alias>[a-zA-Z_][a-zA-Z0-9_]*)\.(?P<b_col>[a-zA-Z_][a-zA-Z0-9_]*)$", condition, ) if not match: return None return ( str(match.group("a_alias")), str(match.group("a_col")), str(match.group("b_alias")), str(match.group("b_col")), ) @staticmethod def _parse_where_clause(raw: str) -> tuple[str, str, str] | None: expr = (raw or "").strip() match = re.match( r"(?is)^(?P<left>[a-zA-Z_][a-zA-Z0-9_\.]*)\s*(?P<op>=|!=|<>|>=|<=|>|<|like|ilike)\s*(?P<right>.+?)$", expr, ) if not match: return None return ( str(match.group("left")).strip(), str(match.group("op")).strip().lower(), str(match.group("right")).strip(), ) @staticmethod def _split_where_conditions(where_part: str) -> list[str]: return [item.strip() for item in re.split(r"(?is)\s+and\s+", where_part) if item.strip()] @staticmethod def _like_pattern_matches(value: str, pattern: str, case_sensitive: bool) -> bool: regex = "^" + re.escape(pattern).replace(r"\%", ".*").replace(r"\_", ".") + "$" flags = 0 if case_sensitive else re.IGNORECASE return re.match(regex, value, flags=flags) is not None @staticmethod def _decode_sql_literal(raw: str) -> Any: text = (raw or "").strip() if re.match(r"^'.*'$", text, flags=re.DOTALL): return text[1:-1].replace("''", "'") lowered = text.lower() if lowered == "null": return None if lowered == "true": return True if lowered == "false": return False if re.match(r"^-?\d+$", text): try: return int(text) except Exception: return text if re.match(r"^-?\d+\.\d+$", text): try: return float(text) except Exception: return text return text @staticmethod def _is_column_reference(raw: str) -> bool: return re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*\.[a-zA-Z_][a-zA-Z0-9_]*$", raw.strip()) is not None def _resolve_where_operand( self, partial: dict[str, dict[str, Any]], token: str ) -> tuple[bool, Any]: raw = (token or "").strip() if self._is_column_reference(raw): alias, column = raw.split(".", 1) if alias not in partial: return False, None return True, partial.get(alias, {}).get(column) if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", raw): matches: list[Any] = [] for alias_row in partial.values(): if isinstance(alias_row, dict) and raw in alias_row: matches.append(alias_row.get(raw)) if len(matches) == 1: return True, matches[0] if len(matches) > 1: return False, None return True, self._decode_sql_literal(raw) def _where_matches( self, partial: dict[str, dict[str, Any]], where_clauses: list[tuple[str, str, str]] ) -> bool: for left_token, op, right_token in where_clauses: ok_left, left_value = self._resolve_where_operand(partial, left_token) ok_right, right_value = self._resolve_where_operand(partial, right_token) if not ok_left or not ok_right: return False if op in {"=", "=="}: if left_value != right_value: return False elif op in {"!=", "<>"}: if left_value == right_value: return False elif op in {">", "<", ">=", "<="}: try: left_num = float(left_value) right_num = float(right_value) except Exception: left_num = None right_num = None if left_num is not None and right_num is not None: if op == ">" and not (left_num > right_num): return False if op == "<" and not (left_num < right_num): return False if op == ">=" and not (left_num >= right_num): return False if op == "<=" and not (left_num <= right_num): return False else: left_text = "" if left_value is None else str(left_value) right_text = "" if right_value is None else str(right_value) if op == ">" and not (left_text > right_text): return False if op == "<" and not (left_text < right_text): return False if op == ">=" and not (left_text >= right_text): return False if op == "<=" and not (left_text <= right_text): return False elif op == "like": left_text = "" if left_value is None else str(left_value) right_text = "" if right_value is None else str(right_value) if not self._like_pattern_matches(left_text, right_text, case_sensitive=True): return False elif op == "ilike": left_text = "" if left_value is None else str(left_value) right_text = "" if right_value is None else str(right_value) if not self._like_pattern_matches(left_text, right_text, case_sensitive=False): return False else: return False return True def _execute_join_select_via_client( self, sql: str ) -> tuple[list[str], list[list[str]]] | None: normalized = self._strip_sql_comments(sql).strip() if not normalized: return None query = normalized[:-1].strip() if normalized.endswith(";") else normalized limit_value = 100 limit_match = re.search(r"(?is)\s+limit\s+(?P<limit>\d+)\s*$", query) if limit_match: limit_value = max(1, int(limit_match.group("limit"))) query = query[: limit_match.start()].strip() from_match = re.match( r"(?is)^select\s+(?P<select>.+?)\s+from\s+(?P<rest>.+)$", query ) if not from_match: return None select_part = str(from_match.group("select") or "").strip() if not select_part: return None select_items = [item.strip() for item in select_part.split(",") if item.strip()] if not select_items: return None from_part_raw = str(from_match.group("rest") or "").strip() where_part = "" where_match = re.search(r"(?is)\s+where\s+(?P<where>.+)$", from_part_raw) if where_match: where_part = str(where_match.group("where") or "").strip() from_part_raw = from_part_raw[: where_match.start()].strip() where_clauses: list[tuple[str, str, str]] = [] if where_part: for clause in self._split_where_conditions(where_part): parsed = self._parse_where_clause(clause) if parsed is None: return None where_clauses.append(parsed) parts = re.split(r"(?is)\s+(left\s+join|join)\s+", from_part_raw) if len(parts) < 3: return None base = self._parse_table_alias(parts[0]) if base is None: return None base_table, base_alias = base join_specs: list[dict[str, str]] = [] alias_to_table: dict[str, str] = {base_alias: base_table} for idx in range(1, len(parts), 2): join_kind = str(parts[idx] or "").strip().lower() part = str(parts[idx + 1] or "").strip() if idx + 1 < len(parts) else "" join_match = re.match(r"(?is)^(?P<table_part>.+?)\s+on\s+(?P<cond>.+)$", part) if not join_match: return None parsed_table = self._parse_table_alias(str(join_match.group("table_part") or "")) parsed_cond = self._parse_join_condition(str(join_match.group("cond") or "")) if parsed_table is None or parsed_cond is None: return None join_table, join_alias = parsed_table if join_alias in alias_to_table: return None alias_to_table[join_alias] = join_table a_alias, a_col, b_alias, b_col = parsed_cond join_specs.append( { "join_alias": join_alias, "join_kind": "left" if "left" in join_kind else "inner", "a_alias": a_alias, "a_col": a_col, "b_alias": b_alias, "b_col": b_col, } ) client = self._create_client() per_table_limit = max(limit_value * 5, 500) table_rows: dict[str, list[dict[str, Any]]] = {} for alias, table_name in alias_to_table.items(): data = client.table(table_name).select("*").limit(per_table_limit).execute().data or [] table_rows[alias] = [row for row in data if isinstance(row, dict)] result_rows: list[dict[str, dict[str, Any]]] = [{base_alias: row} for row in table_rows[base_alias]] for spec in join_specs: join_alias = spec["join_alias"] is_left_join = spec.get("join_kind", "inner") == "left" a_alias = spec["a_alias"] a_col = spec["a_col"] b_alias = spec["b_alias"] b_col = spec["b_col"] joined_batch: list[dict[str, dict[str, Any]]] = [] for partial in result_rows: left_alias = "" left_col = "" right_col = "" if a_alias == join_alias and b_alias in partial: left_alias = b_alias left_col = b_col right_col = a_col elif b_alias == join_alias and a_alias in partial: left_alias = a_alias left_col = a_col right_col = b_col else: continue left_row = partial.get(left_alias, {}) left_value = left_row.get(left_col) matched = False for right_row in table_rows.get(join_alias, []): if left_value == right_row.get(right_col): merged = dict(partial) merged[join_alias] = right_row joined_batch.append(merged) matched = True if is_left_join and not matched: merged = dict(partial) merged[join_alias] = {} joined_batch.append(merged) result_rows = joined_batch if not result_rows: break if where_clauses: result_rows = [ partial for partial in result_rows if self._where_matches(partial, where_clauses) ] if not result_rows: return [], [] alias_order = [base_alias, *[item["join_alias"] for item in join_specs]] columns: list[str] = [] seen_columns: set[str] = set() for alias in alias_order: for row in table_rows.get(alias, []): for key in row.keys(): col = f"{alias}.{key}" if col not in seen_columns: seen_columns.add(col) columns.append(col) if columns: break selected_columns: list[str] = [] seen_selected: set[str] = set() for item in select_items: if item == "*": for col in columns: if col not in seen_selected: seen_selected.add(col) selected_columns.append(col) continue if item.endswith(".*"): alias = item[:-2].strip() if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", alias): return None for col in columns: if col.startswith(f"{alias}.") and col not in seen_selected: seen_selected.add(col) selected_columns.append(col) continue if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*\.[a-zA-Z_][a-zA-Z0-9_]*$", item): if item in columns and item not in seen_selected: seen_selected.add(item) selected_columns.append(item) continue if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", item): matched = [col for col in columns if col.endswith(f".{item}")] if len(matched) == 1 and matched[0] not in seen_selected: seen_selected.add(matched[0]) selected_columns.append(matched[0]) elif len(matched) > 1: return None continue return None if not selected_columns: selected_columns = columns flat_rows: list[list[str]] = [] for partial in result_rows[:limit_value]: cells: list[str] = [] for column in selected_columns: alias, key = column.split(".", 1) value = partial.get(alias, {}).get(key) cells.append(self._sql_value_to_text(value)) flat_rows.append(cells) return selected_columns, flat_rows def _execute_simple_select_via_client( self, sql: str ) -> tuple[list[str], list[list[str]]] | None: normalized = self._strip_sql_comments(sql).strip() if not normalized: return None query = normalized[:-1].strip() if normalized.endswith(";") else normalized match = re.match( r"(?is)^select\s+(?P<select>.*?)\s+from\s+(?P<table>[a-zA-Z_][a-zA-Z0-9_\.]*)\s*(?:limit\s+(?P<limit>\d+))?\s*$", query, ) if not match: return None select_part = (match.group("select") or "").strip() table_name_raw = (match.group("table") or "").strip() if not select_part or not table_name_raw: return None table_name = table_name_raw.split(".")[-1] if not table_name: return None if select_part == "*": selected_columns: list[str] = [] select_clause = "*" else: selected_columns = [ part.strip() for part in select_part.split(",") if part.strip() ] if not selected_columns: return None if not all(re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", col) for col in selected_columns): return None select_clause = ",".join(selected_columns) query_builder = self._create_client().table(table_name).select(select_clause) limit_raw = (match.group("limit") or "").strip() if limit_raw: query_builder = query_builder.limit(int(limit_raw)) data = query_builder.execute().data or [] if not isinstance(data, list): data = [] dict_rows: list[dict[str, Any]] = [ row for row in data if isinstance(row, dict) ] if dict_rows: return self._rows_to_reports_grid(dict_rows) if selected_columns: return selected_columns, [] return [], [] def execute_reports_sql(self): self.reports_error = "" validation_error = self._validate_select_sql(self.reports_sql) if validation_error: self.reports_error = validation_error self.reports_result_columns = [] self.reports_result_rows = [] self.reports_result_grid_columns = [] self.reports_grid_selection = {} return self.reports_is_loading = True try: columns, rows = self._execute_reports_select_sql(self.reports_sql.strip()) self.reports_result_columns = columns self.reports_result_rows = rows self.reports_grid_selection = {} self._sync_reports_grid_columns() self._apply_default_reports_result_sort() except Exception as exc: self.reports_result_columns = [] self.reports_result_rows = [] self.reports_result_grid_columns = [] self.reports_grid_selection = {} self.reports_error = self._msg( "reports.exec_failed", detail=str(exc) ) finally: self.reports_is_loading = False def _derive_project_ref(self) -> str: supabase_url = os.getenv("SUPABASE_URL", "") if not supabase_url: return "" parsed = url_parse.urlparse(supabase_url) host = parsed.netloc or "" if host.endswith(".supabase.co"): return host.split(".")[0] return "" def _execute_sql_via_management_api( self, sql: str, project_ref: str, access_token: str ): endpoint = f"https://api.supabase.com/v1/projects/{project_ref}/database/query" payload_variants = [ {"query": sql}, {"sql": sql}, ] last_error: Exception | None = None for payload in payload_variants: req = url_request.Request( endpoint, data=json.dumps(payload).encode("utf-8"), headers={ "Authorization": f"Bearer {access_token}", "Content-Type": "application/json", }, method="POST", ) try: with url_request.urlopen(req, timeout=20) as resp: if 200 <= resp.status < 300: return last_error = RuntimeError(f"Management API status: {resp.status}") except url_error.HTTPError as exc: details = exc.read().decode("utf-8", errors="ignore") last_error = RuntimeError( f"Management API HTTP {exc.code}: {details[:300]}" ) except Exception as exc: last_error = exc if last_error: raise last_error def _safe_identifier(self, raw_name: str) -> str: normalized = re.sub(r"[^a-zA-Z0-9_]+", "_", raw_name.strip().lower()) normalized = re.sub(r"_+", "_", normalized).strip("_") if not normalized: raise ValueError("Name must contain letters or numbers.") if normalized[0].isdigit(): normalized = f"t_{normalized}" return normalized def _fetch_public_tables(self, client: Client) -> list[str]: supabase_url = os.getenv("SUPABASE_URL", "").rstrip("/") anon_key = os.getenv("SUPABASE_ANON_KEY", "") if supabase_url and anon_key: try: req = url_request.Request( f"{supabase_url}/rest/v1/", headers={ "apikey": anon_key, "Authorization": f"Bearer {anon_key}", "Accept": "application/openapi+json", }, ) with url_request.urlopen(req, timeout=20) as resp: payload = json.loads(resp.read().decode("utf-8")) paths = payload.get("paths", {}) discovered = sorted( { path.strip("/").split("/")[0] for path in paths.keys() if path.startswith("/") } ) tables = [ name for name in discovered if name and name not in {"rpc", "openapi.json"} ] if tables: return tables except Exception: pass rpc_names = ["list_public_tables", "get_public_tables"] for rpc_name in rpc_names: try: result = client.rpc(rpc_name, {}).execute() raw = result.data or [] names: list[str] = [] for item in raw: if isinstance(item, str): names.append(item) elif isinstance(item, dict): name = item.get("table_name") or item.get("name") if name: names.append(str(name)) if names: seen: set[str] = set() deduped: list[str] = [] for name in names: if name not in seen: seen.add(name) deduped.append(name) return deduped except Exception: continue existing: list[str] = [] for table_name in _table_discovery_candidates(): try: client.table(table_name).select("*").limit(1).execute() existing.append(table_name) except Exception: continue return existing def load_tables(self): self.is_loading_tables = True self.load_error = "" self.visuals_notice = "" self.custom_tables_selected_row_idx = -1 self.custom_tables_grid_selection = {} self.custom_tables_sort_column_idx = -1 self.custom_tables_sort_desc = False try: client = self._create_client() existing = self._fetch_public_tables(client) self.tables = existing self.service_tables = [name for name in existing if name in SERVICE_TABLES] self.custom_tables = [name for name in existing if name not in SERVICE_TABLES] self.custom_tables_grid_rows = [[name] for name in self.custom_tables] self._apply_default_custom_tables_list_sort() self.custom_table_target = ( self.custom_tables[0] if self.custom_tables else "" ) if existing: preferred = ( self.custom_tables[0] if self.custom_tables else self.service_tables[0] ) return self.select_table(preferred) else: self.selected_table = "" self.columns = [] self.rows = [] self.row_ids = [] self.service_tables = [] self.custom_tables = [] self.custom_tables_grid_rows = [] self.custom_table_target = "" except Exception as exc: self.load_error = self._msg( "errors.supabase_load_failed", detail=str(exc) ) finally: self.is_loading_tables = False def select_table(self, table_name: str): """Open a table from sidebar navigation (resets application-tile context).""" return self._select_table_impl(table_name, "sidebar") def select_table_reload(self, table_name: str): """Reload grid after mutations; preserves custom_table_opened_from_application_tile.""" return self._select_table_impl(table_name, "reload") def select_table_from_application_tile(self, table_name: str): """Open a custom table from Applications dashboard tile.""" return self._select_table_impl(table_name, "application_tile") def _select_table_impl(self, table_name: str, entry_source: str): """entry_source: sidebar, apps_management, application_tile, or reload (keep flag).""" self.is_create_table_mode = False self.is_create_app_mode = False self.new_app_name = "" self.is_create_master_detail_app_mode = False self.new_master_detail_app_name = "" self.is_create_visuals_app_mode = False self.new_visuals_app_name = "" self.is_edit_table_mode = False self.is_adding_row = False self.editing_row_idx = -1 self.editing_original_row = [] self._clear_applications_edit_target() self._close_inline_type_editor() self._close_inline_dashboard_editor() self.service_grid_selected_row_idx = -1 self.service_grid_selection = {} self.service_sort_column_idx = -1 self.service_sort_desc = False self.visuals_notice = "" self.custom_tables_selected_row_idx = -1 self.custom_tables_grid_selection = {} if entry_source == "reload": pass elif entry_source == "application_tile": self.custom_table_opened_from_application_tile = table_name in self.custom_tables self.custom_table_apps_breadcrumb_active = ( self.custom_table_opened_from_application_tile ) elif entry_source == "apps_management": self.custom_table_opened_from_application_tile = False self.custom_table_apps_breadcrumb_active = False self.applications_breadcrumb_source = "apps_management" else: self.custom_table_opened_from_application_tile = False if self.pending_custom_table_apps_breadcrumb: self.custom_table_apps_breadcrumb_active = True self.pending_custom_table_apps_breadcrumb = False else: self.custom_table_apps_breadcrumb_active = False if table_name in ("application_groups", "applications"): self.applications_breadcrumb_source = "service_tables" self.selected_table = table_name if table_name in self.custom_tables: self.administration_expanded = True self.apps_constructor_expanded = True self.is_loading_rows = True self.load_error = "" try: if table_name == "application_groups": # Dashboards view renders tiles from applications data, # so always refresh tiles when this node is selected. self.applications_nav_group_id = "" self.applications_nav_expanded = True self.applications_dashboard_expand_all = False self.dashboard_expanded_apps = [] self.columns = [] self.rows = [] self.row_ids = [] self.service_grid_columns = [] self._load_applications_tiles() # Run drag-and-drop init on the client after the DOM exists (avoid on_mount hook order issues). return self.init_dashboard_drag() client = self._create_client() if table_name == "applications": self._load_application_group_options(client) else: self.application_group_ids = [] self.application_group_labels = [] data = self._fetch_table_rows_for_grid(client, table_name) if not data: columns = self._read_table_columns(table_name) self.columns = [ col["name"] for col in columns if self._is_visible_column(table_name, col["name"]) ] self.rows = [] self.row_ids = [] if table_name == "applications": if "dashboard_name" not in self.columns: self.columns.append("dashboard_name") self._apply_applications_admin_column_order() self._rebuild_service_grid_columns() self._apply_default_service_grid_sort() return row_keys = {key for row in data for key in row.keys()} ordered_columns = sorted(row_keys) visible_columns = [ column for column in ordered_columns if self._is_visible_column(table_name, column) ] self.columns = ordered_columns self.rows = [ [str(row.get(column, "")) for column in ordered_columns] for row in data ] self.row_ids = [str(row.get("id", "")) for row in data] self.columns = visible_columns self.rows = [ [str(row.get(column, "")) for column in visible_columns] for row in data ] if table_name == "applications": self.columns = [*self.columns, "dashboard_name"] self.rows = [ [ *self.rows[row_idx], self._application_group_id_to_label( str(data[row_idx].get("application_group_id", "")) ), ] for row_idx in range(len(self.rows)) ] self._apply_applications_admin_column_order() self._rebuild_service_grid_columns() self._apply_default_service_grid_sort() except Exception as exc: self.load_error = self._msg("errors.table_load_failed", detail=str(exc)) self.columns = [] self.rows = [] self.row_ids = [] self.service_grid_columns = [] finally: self.is_loading_rows = False self.sync_nav_breadcrumb() def add_row(self): if self.selected_table not in self.tables: return if self.selected_table == "applications": return if self.is_adding_row: return if self.editing_row_idx >= 0: return if not self.columns: columns = self._read_table_columns(self.selected_table) self.columns = [ col["name"] for col in columns if self._is_visible_column(self.selected_table, col["name"]) ] if not self.columns: self.load_error = self._msg("errors.no_columns_table") return self.rows = [["" for _ in self.columns], *self.rows] self.row_ids = [SERVICE_GRID_DRAFT_ROW_ID, *self.row_ids] self.is_adding_row = True self.load_error = "" def start_edit_row(self, row_idx: int): if self.selected_table not in self.tables: return if self.selected_table in self.service_tables and self.selected_table not in { "applications", "application_groups", }: self.load_error = self._msg("errors.edit_disabled_service") return if self.is_adding_row: return if row_idx < 0 or row_idx >= len(self.rows): return self.editing_row_idx = row_idx self.editing_original_row = self.rows[row_idx][:] self.load_error = "" if self.selected_table == "applications": try: group_col_idx = self.columns.index("application_group_id") if group_col_idx >= 0 and group_col_idx < len(self.rows[row_idx]): updated = [r[:] for r in self.rows] group_id = updated[row_idx][group_col_idx] group_label = self._application_group_id_to_label(group_id) updated[row_idx][group_col_idx] = group_label try: dashboard_col_idx = self.columns.index("dashboard_name") if dashboard_col_idx < len(updated[row_idx]): updated[row_idx][dashboard_col_idx] = group_label except ValueError: pass self.rows = updated except ValueError: pass def _set_applications_edit_field(self, column_name: str, value: str): if self.selected_table != "applications": return row_idx = ( self.applications_edit_target_row_idx if self.applications_edit_target_row_idx >= 0 else self.editing_row_idx ) if row_idx < 0 or row_idx >= len(self.rows): return if self.editing_row_idx != row_idx: self.start_edit_row(row_idx) try: column_idx = self.columns.index(column_name) except ValueError: return self.set_edit_row_value(row_idx, column_idx, value) if column_name == "application_group_id": try: dashboard_col_idx = self.columns.index("dashboard_name") except ValueError: return group_label = self._application_group_id_to_label( self._application_group_label_to_id(value) ) updated_rows = [row[:] for row in self.rows] if row_idx < len(updated_rows) and dashboard_col_idx < len(updated_rows[row_idx]): updated_rows[row_idx][dashboard_col_idx] = group_label self.rows = updated_rows def set_edit_applications_type(self, value: str): normalized = (value or "").strip().lower() if normalized not in APPLICATION_TYPE_OPTIONS: return self._set_applications_edit_field("type", normalized) self._clear_applications_edit_target() def set_edit_applications_group_name(self, group_name: str): self._set_applications_edit_field("application_group_id", group_name) self._clear_applications_edit_target() def set_edit_row_value(self, row_idx: int, column_idx: int, value: str): if row_idx != self.editing_row_idx: return if column_idx < 0 or column_idx >= len(self.columns): return if self.columns[column_idx] in self._auto_managed_columns(): return if ( self.selected_table == "applications" and self._is_applications_virtual_column(self.columns[column_idx]) ): return updated_rows = [row[:] for row in self.rows] if row_idx >= len(updated_rows) or column_idx >= len(updated_rows[row_idx]): return updated_rows[row_idx][column_idx] = value self.rows = updated_rows def cancel_edit_row(self, row_idx: int): if row_idx != self.editing_row_idx: return if row_idx < 0 or row_idx >= len(self.rows): self.editing_row_idx = -1 self.editing_original_row = [] self._clear_applications_edit_target() return updated_rows = [row[:] for row in self.rows] updated_rows[row_idx] = self.editing_original_row[:] self.rows = updated_rows self.editing_row_idx = -1 self.editing_original_row = [] self._clear_applications_edit_target() def save_edit_row(self, row_idx: int): if row_idx != self.editing_row_idx: return if self.selected_table not in self.tables: return if self.selected_table in self.service_tables and self.selected_table not in { "applications", "application_groups", }: self.load_error = self._msg("errors.edit_disabled_service") return if row_idx < 0 or row_idx >= len(self.rows): return try: row = self.rows[row_idx] original_row = self.editing_original_row[:] if not original_row: original_row = row[:] column_types = { col["name"]: col["type"] for col in self._read_table_columns(self.selected_table) } auto_managed = self._auto_managed_columns() payload: dict[str, Any] = {} for col_idx, column_name in enumerate(self.columns): if column_name in auto_managed: continue if ( self.selected_table == "applications" and self._is_applications_virtual_column(column_name) ): continue new_value = row[col_idx] if col_idx < len(row) else "" old_value = original_row[col_idx] if col_idx < len(original_row) else "" if ( self.selected_table == "applications" and column_name == "application_group_id" ): new_value = self._application_group_label_to_id(new_value) old_value = self._application_group_label_to_id(old_value) if ( self.selected_table == "applications" and column_name == "type" ): normalized = (new_value or "").strip().lower() if normalized not in APPLICATION_TYPE_OPTIONS: self.load_error = self._msg( "errors.application_type_invalid", options=", ".join(APPLICATION_TYPE_OPTIONS), ) return new_value = normalized old_value = (old_value or "").strip().lower() if new_value == old_value: continue payload[column_name] = self._coerce_row_value( new_value, column_types.get(column_name, "text") ) if not payload: self.editing_row_idx = -1 self.editing_original_row = [] self._clear_applications_edit_target() self.load_error = "" return client = self._create_client() query = client.table(self.selected_table).update(payload) row_id = self.row_ids[row_idx].strip() if row_idx < len(self.row_ids) else "" if row_id: query = query.eq("id", row_id) else: filters_applied = 0 for col_idx, column_name in enumerate(self.columns): if ( self.selected_table == "applications" and self._is_applications_virtual_column(column_name) ): continue original_value = ( original_row[col_idx] if col_idx < len(original_row) else "" ) parsed = self._coerce_row_value( original_value, column_types.get(column_name, "text") ) if parsed is None: continue query = query.eq(column_name, parsed) filters_applied += 1 if filters_applied == 0: self.load_error = self._msg("errors.cannot_identify_update_empty") return query.execute() table_name = self.selected_table self.editing_row_idx = -1 self.editing_original_row = [] self._clear_applications_edit_target() self.select_table_reload(table_name) self.load_error = "" except Exception as exc: self.load_error = self._msg("errors.edit_row_failed", detail=str(exc)) def set_new_row_value(self, column_idx: int, value: str): if not self.is_adding_row or not self.rows: return draft_idx = self._service_grid_draft_row_idx() if draft_idx < 0: return if column_idx < 0 or column_idx >= len(self.columns): return if self.columns[column_idx] in self._auto_managed_columns(): return if ( self.selected_table == "applications" and self._is_applications_virtual_column(self.columns[column_idx]) ): return updated = [row[:] for row in self.rows] if not updated[draft_idx] or column_idx >= len(updated[draft_idx]): return updated[draft_idx][column_idx] = value self.rows = updated def _coerce_row_value(self, value: str, column_type: str) -> Any: raw = value.strip() if raw == "" or raw.lower() in ("none", "null"): return None normalized_type = (column_type or "text").lower() if normalized_type in {"integer", "bigint", "smallint"}: return int(raw) if normalized_type in {"numeric", "decimal", "real", "double precision"}: return float(raw) if normalized_type in {"boolean", "bool"}: lowered = raw.lower() if lowered in {"true", "t", "1", "yes", "y"}: return True if lowered in {"false", "f", "0", "no", "n"}: return False raise ValueError(f"Invalid boolean value: {value}") if "json" in normalized_type: return json.loads(raw) return value def save_new_row(self): if not self.is_adding_row or not self.rows: return if self.selected_table not in self.tables: return try: column_types = { col["name"]: col["type"] for col in self._read_table_columns(self.selected_table) } auto_managed = self._auto_managed_columns() payload: dict[str, Any] = {} draft_idx = self._service_grid_draft_row_idx() if draft_idx < 0: return draft_row = self.rows[draft_idx] for col_idx, column_name in enumerate(self.columns): if column_name in auto_managed: continue if ( self.selected_table == "applications" and self._is_applications_virtual_column(column_name) ): continue value = draft_row[col_idx] if col_idx < len(draft_row) else "" if ( self.selected_table == "applications" and column_name == "application_group_id" ): value = self._application_group_label_to_id(value) if ( self.selected_table == "applications" and column_name == "type" ): normalized = (value or "").strip().lower() if normalized not in APPLICATION_TYPE_OPTIONS: self.load_error = self._msg( "errors.application_type_invalid", options=", ".join(APPLICATION_TYPE_OPTIONS), ) return value = normalized parsed = self._coerce_row_value(value, column_types.get(column_name, "text")) if parsed is None: continue payload[column_name] = parsed client = self._create_client() ins_res = client.table(self.selected_table).insert(payload).execute() new_id = "" ins_rows = ins_res.data or [] if ins_rows and isinstance(ins_rows[0], dict): new_id = str(ins_rows[0].get("id", "") or "") table_name = self.selected_table self.is_adding_row = False self.select_table_reload(table_name) if new_id: self._promote_service_grid_row_to_top_by_id(new_id) self.load_error = "" except Exception as exc: self.load_error = self._msg("errors.add_row_failed", detail=str(exc)) def delete_row(self, row_idx: int): if self.selected_table not in self.tables: return if self.selected_table in self.service_tables and self.selected_table not in { "applications", "application_groups", }: self.load_error = self._msg("errors.delete_disabled_service") return if row_idx < 0 or row_idx >= len(self.rows): return # If user deletes unsaved draft row, just cancel add mode. if self.is_adding_row: d = self._service_grid_draft_row_idx() if d >= 0 and row_idx == d: self.rows = self.rows[:d] + self.rows[d + 1 :] self.row_ids = self.row_ids[:d] + self.row_ids[d + 1 :] self.is_adding_row = False self.load_error = "" return try: client = self._create_client() row = self.rows[row_idx] query = client.table(self.selected_table).delete() # Prefer point delete by hidden/internal id when available. row_id = self.row_ids[row_idx].strip() if row_idx < len(self.row_ids) else "" if row_id: query = query.eq("id", row_id) elif "id" in self.columns: id_idx = self.columns.index("id") row_id = row[id_idx].strip() if id_idx < len(row) else "" if not row_id: self.load_error = self._msg("errors.empty_row_id") return query = query.eq("id", row_id) else: # Fallback for tables without id: filter by row values. column_types = { col["name"]: col["type"] for col in self._read_table_columns(self.selected_table) } filters_applied = 0 for col_idx, column_name in enumerate(self.columns): value = row[col_idx] if col_idx < len(row) else "" parsed = self._coerce_row_value( value, column_types.get(column_name, "text") ) if parsed is None: continue query = query.eq(column_name, parsed) filters_applied += 1 if filters_applied == 0: self.load_error = self._msg("errors.cannot_delete_row_identify") return if self.selected_table == "application_groups" and row_id: # Break FK link first so group can be removed safely. client.table("applications").update( {"application_group_id": None} ).eq("application_group_id", row_id).execute() query.execute() table_name = self.selected_table self.select_table_reload(table_name) self.load_error = "" except Exception as exc: message = str(exc) if self.selected_table == "application_groups" and "23503" in message: self.load_error = self._msg("errors.delete_group_apps_ref") elif self.selected_table == "applications": self.load_error = self._msg( "errors.delete_application_failed", detail=str(exc) ) else: self.load_error = self._msg("errors.delete_row_failed", detail=str(exc)) def export_to_excel(self): if not self.selected_table or not self.columns or not self.rows: self.load_error = self._msg("errors.no_data_export") return if pd is None: self.load_error = self._msg("errors.no_excel") return try: # Prepare data for export export_data = [] for row in self.rows: row_dict = {} for i, col in enumerate(self.columns): if i < len(row): row_dict[col] = row[i] else: row_dict[col] = "" export_data.append(row_dict) # Create DataFrame and Excel file df = pd.DataFrame(export_data) output = io.BytesIO() with pd.ExcelWriter(output, engine='openpyxl') as writer: df.to_excel(writer, sheet_name=self.selected_table, index=False) # Return file for download output.seek(0) return rx.download( data=output.getvalue(), filename=f"{self.selected_table}.xlsx", mime_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) except Exception as exc: self.load_error = self._msg("errors.export_failed", detail=str(exc)) def select_custom_table_target(self, table_name: str): self.custom_table_target = table_name def start_create_table_mode(self): self.is_create_table_mode = True self.is_edit_table_mode = False self.is_adding_row = False self.new_table_name = "" self.new_table_fields = [["field_1", "text"]] self.load_error = "" def select_custom_table_grid_cell(self, position: tuple[int, int]): if not isinstance(position, (list, tuple)) or len(position) < 2: return try: col_idx = int(position[0]) row_idx = int(position[1]) except (TypeError, ValueError): return if 0 <= row_idx < len(self.custom_tables): self.custom_tables_selected_row_idx = row_idx self.custom_table_target = self.custom_tables[row_idx] self.custom_tables_grid_selection = _build_row_grid_selection( row_idx=row_idx, col_idx=col_idx, total_rows=len(self.custom_tables_grid_rows), total_cols=1, ) else: self.custom_tables_selected_row_idx = -1 self.custom_tables_grid_selection = {} def handle_custom_tables_grid_selection_change(self, selection: Any): position = _extract_cell_from_grid_selection(selection) if position is None: self.custom_tables_selected_row_idx = -1 self.custom_tables_grid_selection = {} return self.select_custom_table_grid_cell(position) def edit_selected_custom_table(self): if self.custom_tables_selected_row_idx < 0: self.load_error = self._msg("errors.select_custom_first") return if self.custom_tables_selected_row_idx >= len(self.custom_tables): return self.start_edit_table_mode(self.custom_tables[self.custom_tables_selected_row_idx]) def delete_selected_custom_table(self): if self.custom_tables_selected_row_idx < 0: self.load_error = self._msg("errors.select_custom_first") return if self.custom_tables_selected_row_idx >= len(self.custom_tables): return self.select_custom_table_target(self.custom_tables[self.custom_tables_selected_row_idx]) self.delete_custom_table() def cancel_create_table_mode(self): self.is_create_table_mode = False def _read_table_columns(self, table_name: str) -> list[dict[str, str]]: client = self._create_client() # Read columns only from the selected table via strict RPC. try: result = client.rpc("list_table_columns", {"p_table_name": table_name}).execute() rows = result.data or [] parsed: list[dict[str, str]] = [] for row in rows: if not isinstance(row, dict): continue name = str(row.get("column_name") or "") dtype = str(row.get("data_type") or "text").lower() if name: parsed.append({"name": name, "type": dtype}) if parsed: return parsed except Exception: pass # Fallback for environments without RPC metadata function. try: rows = client.table(table_name).select("*").limit(1).execute().data or [] if not rows: return [] first_row = rows[0] if isinstance(rows[0], dict) else {} return [{"name": str(col), "type": "text"} for col in first_row.keys()] except Exception: return [] def start_edit_table_mode(self, table_name: str): self.is_create_table_mode = False self.is_edit_table_mode = True self.is_adding_row = False self.selected_table = "Custom Tables" self.edit_table_name = table_name self.custom_table_target = table_name self.load_error = "" fields = self._read_table_columns(table_name) if not fields: # Fallback: use currently loaded columns as best-effort with text type. if self.selected_table == table_name and self.columns: fields = [{"name": col, "type": "text"} for col in self.columns] else: fields = [] protected = {"id", "created_at"} editable_fields = [f for f in fields if f["name"] not in protected] if not editable_fields: editable_fields = [{"name": "field_1", "type": "text"}] self.edit_original_names = [ self._safe_identifier(field["name"]) for field in editable_fields ] self.edit_original_types = { self._safe_identifier(field["name"]): ( field["type"].strip().lower() if field["type"] else "text" ) for field in editable_fields } type_options = set(CUSTOM_FIELD_TYPES) for field in editable_fields: if field["type"]: type_options.add(field["type"]) self.edit_type_options = sorted(type_options) self.edit_table_fields = [ [field["name"], field["type"] if field["type"] else "text", field["name"]] for field in editable_fields ] def cancel_edit_table_mode(self): self.is_edit_table_mode = False self.edit_table_name = "" self.edit_table_fields = [] self.edit_original_names = [] self.edit_original_types = {} def add_edit_field(self): next_idx = len(self.edit_table_fields) + 1 self.edit_table_fields = [ *self.edit_table_fields, [f"field_{next_idx}", "text", ""], ] def remove_edit_field(self, field_idx: int): if field_idx < 0 or field_idx >= len(self.edit_table_fields): return self.edit_table_fields = [ field for idx, field in enumerate(self.edit_table_fields) if idx != field_idx ] def set_edit_field_name(self, field_idx: int, value: str): if field_idx < 0 or field_idx >= len(self.edit_table_fields): return updated = [field[:] for field in self.edit_table_fields] updated[field_idx][0] = value self.edit_table_fields = updated def set_edit_field_type(self, field_idx: int, value: str): if field_idx < 0 or field_idx >= len(self.edit_table_fields): return updated = [field[:] for field in self.edit_table_fields] updated[field_idx][1] = value self.edit_table_fields = updated def _quote_ident(self, name: str) -> str: return '"' + name.replace('"', '""') + '"' def save_edit_table(self): if not self.edit_table_name: self.load_error = self._msg("errors.no_table_edit") return try: table_name = self._safe_identifier(self.edit_table_name) desired: list[tuple[str, str, str]] = [] seen: set[str] = set() for raw_name, raw_type, original_name in self.edit_table_fields: safe_name = self._safe_identifier(raw_name) if safe_name in seen: continue seen.add(safe_name) safe_type = raw_type.strip().lower() if safe_type not in self.edit_type_options: self.edit_type_options = sorted( set([*self.edit_type_options, safe_type]) ) desired.append((safe_name, safe_type or "text", original_name.strip())) if not desired: raise ValueError(self._msg("errors.edit_table_min_field")) original_by_name = dict(self.edit_original_types) desired_names = {name for name, _, _ in desired} original_names = set(self.edit_original_names) statements: list[str] = [] q_table = self._quote_ident(table_name) for new_name, new_type, original_name in desired: if original_name: safe_original = self._safe_identifier(original_name) if safe_original != new_name: statements.append( f"alter table public.{q_table} rename column " f"{self._quote_ident(safe_original)} to {self._quote_ident(new_name)};" ) old_type = original_by_name.get(safe_original, "") if old_type and old_type != new_type: statements.append( f"alter table public.{q_table} alter column {self._quote_ident(new_name)} " f"type {new_type} using {self._quote_ident(new_name)}::{new_type};" ) else: statements.append( f"alter table public.{q_table} add column if not exists " f"{self._quote_ident(new_name)} {new_type};" ) # Columns deleted from editor are dropped. renamed_from = { self._safe_identifier(orig) for _, _, orig in desired if orig.strip() and self._safe_identifier(orig) not in desired_names } to_drop = (original_names - desired_names) - renamed_from for col_name in sorted(to_drop): statements.append( f"alter table public.{q_table} drop column if exists {self._quote_ident(col_name)} cascade;" ) for stmt in statements: self._execute_sql(stmt) self.is_edit_table_mode = False self.load_tables() self.select_table_reload(table_name) self.load_error = "" except Exception as exc: self.load_error = self._msg("errors.edit_table_failed", detail=str(exc)) def add_table_field(self): next_idx = len(self.new_table_fields) + 1 self.new_table_fields = [*self.new_table_fields, [f"field_{next_idx}", "text"]] def remove_table_field(self, field_idx: int): if field_idx < 0 or field_idx >= len(self.new_table_fields): return self.new_table_fields = [ field for idx, field in enumerate(self.new_table_fields) if idx != field_idx ] if not self.new_table_fields: self.new_table_fields = [["field_1", "text"]] def set_table_field_name(self, field_idx: int, value: str): if field_idx < 0 or field_idx >= len(self.new_table_fields): return updated = [field[:] for field in self.new_table_fields] updated[field_idx][0] = value self.new_table_fields = updated def set_table_field_type(self, field_idx: int, value: str): if field_idx < 0 or field_idx >= len(self.new_table_fields): return updated = [field[:] for field in self.new_table_fields] updated[field_idx][1] = value self.new_table_fields = updated def create_custom_table(self): try: table_name = self._safe_identifier(self.new_table_name) field_defs: list[str] = [] seen: set[str] = set() for raw_name, raw_type in self.new_table_fields: safe_name = self._safe_identifier(raw_name) if safe_name in seen: continue seen.add(safe_name) safe_type = raw_type.strip().lower() if safe_type not in CUSTOM_FIELD_TYPES: raise ValueError( self._msg("errors.unsupported_field_type", type_label=raw_type) ) field_defs.append(f'"{safe_name}" {safe_type}') if not field_defs: raise ValueError(self._msg("errors.create_table_min_field")) ddl = ( f'create table if not exists public."{table_name}" (' "id uuid primary key default gen_random_uuid(), " f"{', '.join(field_defs)}, " "created_at timestamptz default now()" ");" ) self._execute_sql(ddl) self.is_create_table_mode = False self.load_tables() # Fallback: table discovery endpoints can lag after DDL; # keep UI in sync immediately after successful creation. if table_name not in self.tables: self.tables = [*self.tables, table_name] if table_name not in SERVICE_TABLES and table_name not in self.custom_tables: self.custom_tables = [*self.custom_tables, table_name] self.custom_tables_grid_rows = [[name] for name in self.custom_tables] self.select_table_group("Custom Tables") self.custom_table_target = table_name self.load_error = "" except Exception as exc: self.load_error = self._msg("errors.create_table", detail=str(exc)) def delete_custom_table(self): if self.custom_table_opened_from_application_tile: self.load_error = self._msg("errors.delete_table_nav") return if not self.custom_table_target: self.load_error = self._msg("errors.select_table_delete") return if self.custom_table_target in SERVICE_TABLES: self.load_error = self._msg("errors.service_delete_blocked") return try: safe_name = self._safe_identifier(self.custom_table_target) self._execute_sql(f'drop table if exists public."{safe_name}" cascade;') self.load_tables() self.select_table_group("Custom Tables") self.load_error = "" except Exception as exc: self.load_error = self._msg("errors.delete_table_failed", detail=str(exc)) def _navigation_panel() -> rx.Component: return rx.vstack( rx.cond( BrowserState.is_loading_tables, rx.text(ui_txt("layout.loading_tables"), color=COLOR_ACCENT), rx.fragment(), ), rx.cond( BrowserState.load_error != "", rx.text(BrowserState.load_error, color=COLOR_ERROR, font_size="0.85rem"), rx.fragment(), ), rx.vstack( rx.button( rx.hstack( rx.icon("layers", size=16, color=COLOR_TEXT), rx.text(ui_txt("nav.applications"), color=COLOR_TEXT, font_weight="bold"), rx.cond( BrowserState.applications_nav_expanded, rx.text("▾", color=COLOR_ACCENT), rx.text("▸", color=COLOR_ACCENT), ), spacing="2", align="center", width="100%", ), on_click=BrowserState.toggle_applications_navigation, width="100%", justify_content="start", background="#17223d", border=rx.cond( BrowserState.applications_nav_expanded | (BrowserState.selected_table == "application_groups"), "2px solid #4f72a9", "1px solid #324b74", ), border_radius="8px", _hover={"background": "#1f2e50", "border_color": "#4f72a9"}, ), rx.cond( BrowserState.applications_nav_expanded, rx.vstack( rx.foreach( BrowserState.applications_nav_groups, lambda g: rx.button( rx.text( g.name, color=COLOR_TEXT, white_space="nowrap", ), on_click=BrowserState.select_applications_group(g.id), width="100%", justify_content="start", padding_left="1.2rem", background=rx.cond( BrowserState.applications_nav_group_id == g.id, COLOR_CARD_ALT, "transparent", ), border=rx.cond( BrowserState.applications_nav_group_id == g.id, "2px solid #4f72a9", f"1px solid {COLOR_BORDER}", ), border_radius="8px", _hover={ "background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT, }, ), ), width="100%", spacing="1", align="start", padding_left="0.6rem", ), rx.fragment(), ), rx.button( rx.hstack( rx.icon("settings", size=16, color=COLOR_TEXT), rx.text(ui_txt("nav.administration"), color=COLOR_TEXT, font_weight="bold"), rx.cond( BrowserState.administration_expanded, rx.text("▾", color=COLOR_ACCENT), rx.text("▸", color=COLOR_ACCENT), ), spacing="2", align="center", width="100%", ), on_click=BrowserState.toggle_administration, width="100%", justify_content="start", background="#17223d", border=rx.cond( BrowserState.administration_expanded | (BrowserState.selected_table == "Custom Tables") | (BrowserState.selected_table == "Master/Detail") | (BrowserState.selected_table == "Visuals") | (BrowserState.selected_table == "Reports") | (BrowserState.selected_table == "Service Tables") | BrowserState.custom_tables.contains(BrowserState.selected_table) | BrowserState.service_tables.contains(BrowserState.selected_table), "2px solid #4f72a9", "1px solid #324b74", ), border_radius="8px", _hover={"background": "#1f2e50", "border_color": "#4f72a9"}, margin_top="0.4rem", ), rx.cond( BrowserState.administration_expanded, rx.vstack( rx.button( rx.hstack( rx.icon("blocks", size=16, color=COLOR_TEXT), rx.text( ui_txt("nav.apps_constructor"), color=COLOR_TEXT, font_weight="bold", ), rx.cond( BrowserState.apps_constructor_expanded, rx.text("▾", color=COLOR_ACCENT), rx.text("▸", color=COLOR_ACCENT), ), spacing="2", align="center", width="100%", ), on_click=BrowserState.toggle_apps_constructor, width="100%", justify_content="start", background="#17223d", border=rx.cond( BrowserState.apps_constructor_expanded | (BrowserState.selected_table == "Visuals") | (BrowserState.selected_table == "Reports") | (BrowserState.selected_table == "Custom Tables") | (BrowserState.selected_table == "Master/Detail") | BrowserState.custom_tables.contains( BrowserState.selected_table ), "2px solid #4f72a9", "1px solid #324b74", ), border_radius="8px", _hover={"background": "#1f2e50", "border_color": "#4f72a9"}, ), rx.cond( BrowserState.apps_constructor_expanded, rx.vstack( rx.button( rx.hstack( rx.icon("bar-chart-3", size=16, color=COLOR_TEXT), rx.text( ui_txt("nav.charts"), color=COLOR_TEXT, font_weight="bold", ), spacing="2", align="center", width="100%", ), on_click=BrowserState.select_visuals, width="100%", justify_content="start", background=rx.cond( BrowserState.selected_table == "Visuals", COLOR_CARD_ALT, "#17223d", ), border=rx.cond( BrowserState.selected_table == "Visuals", "2px solid #4f72a9", "1px solid #324b74", ), border_radius="8px", _hover={ "background": "#1f2e50", "border_color": "#4f72a9", }, ), rx.button( rx.hstack( rx.icon("file-text", size=16, color=COLOR_TEXT), rx.text( ui_txt("nav.reports"), color=COLOR_TEXT, font_weight="bold", ), spacing="2", align="center", width="100%", ), on_click=BrowserState.select_reports, width="100%", justify_content="start", background=rx.cond( BrowserState.selected_table == "Reports", COLOR_CARD_ALT, "#17223d", ), border=rx.cond( BrowserState.selected_table == "Reports", "2px solid #4f72a9", "1px solid #324b74", ), border_radius="8px", _hover={ "background": "#1f2e50", "border_color": "#4f72a9", }, ), rx.button( rx.hstack( rx.icon("database", size=16, color=COLOR_TEXT), rx.text( ui_txt("nav.custom_tables"), color=COLOR_TEXT, font_weight="bold", ), rx.cond( BrowserState.custom_tables_expanded, rx.text("▾", color=COLOR_ACCENT), rx.text("▸", color=COLOR_ACCENT), ), spacing="2", align="center", width="100%", ), on_click=BrowserState.toggle_custom_tables, width="100%", justify_content="start", border=rx.cond( BrowserState.custom_tables_expanded | (BrowserState.selected_table == "Custom Tables") | BrowserState.custom_tables.contains( BrowserState.selected_table ), "2px solid #4f72a9", "1px solid #324b74", ), background="#17223d", border_radius="8px", _hover={ "background": "#1f2e50", "border_color": "#4f72a9", }, ), rx.cond( BrowserState.custom_tables_expanded, rx.foreach( BrowserState.custom_tables, lambda table_name: rx.button( rx.text( rx.cond( table_name == "application_groups", ui_txt("nav.dashboards"), table_name, ), color=COLOR_TEXT, white_space="nowrap", ), on_click=BrowserState.select_table(table_name), width="100%", justify_content="start", padding_left="1.2rem", background=rx.cond( BrowserState.selected_table == table_name, COLOR_CARD_ALT, "transparent", ), border=rx.cond( BrowserState.selected_table == table_name, "2px solid #4f72a9", f"1px solid {COLOR_BORDER}", ), border_radius="8px", _hover={ "background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT, }, ), ), rx.fragment(), ), rx.button( rx.hstack( rx.icon("git-branch", size=16, color=COLOR_TEXT), rx.text( ui_txt("nav.master_detail"), color=COLOR_TEXT, font_weight="bold", ), spacing="2", align="center", width="100%", ), on_click=BrowserState.select_master_detail, width="100%", justify_content="start", background=rx.cond( BrowserState.selected_table == "Master/Detail", COLOR_CARD_ALT, "#17223d", ), border=rx.cond( BrowserState.selected_table == "Master/Detail", "2px solid #4f72a9", "1px solid #324b74", ), border_radius="8px", _hover={ "background": "#1f2e50", "border_color": "#4f72a9", }, ), width="100%", spacing="1", align="start", padding_left="0.6rem", ), rx.fragment(), ), rx.button( rx.hstack( rx.icon("app-window", size=16, color=COLOR_TEXT), rx.text( ui_txt("nav.apps_management"), color=COLOR_TEXT, font_weight="bold", ), spacing="2", align="center", width="100%", ), on_click=BrowserState.toggle_applications_management, width="100%", justify_content="start", background=rx.cond( BrowserState.applications_management_expanded | (BrowserState.selected_table == "applications") | (BrowserState.selected_table == "application_groups"), COLOR_CARD_ALT, "#17223d", ), border=rx.cond( BrowserState.applications_management_expanded | (BrowserState.selected_table == "applications") | (BrowserState.selected_table == "application_groups"), "2px solid #4f72a9", "1px solid #324b74", ), border_radius="8px", _hover={"background": "#1f2e50", "border_color": "#4f72a9"}, ), rx.cond( BrowserState.applications_management_expanded, rx.vstack( rx.button( rx.text(ui_txt("nav.dashboards"), color=COLOR_TEXT, white_space="nowrap"), on_click=BrowserState.select_table_from_apps_management( "application_groups" ), width="100%", justify_content="start", padding_left="1.2rem", background=rx.cond( BrowserState.selected_table == "application_groups", COLOR_CARD_ALT, "transparent", ), border=rx.cond( BrowserState.selected_table == "application_groups", "2px solid #4f72a9", f"1px solid {COLOR_BORDER}", ), border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), rx.button( rx.text(ui_txt("nav.table_applications"), color=COLOR_TEXT, white_space="nowrap"), on_click=BrowserState.select_table_from_apps_management("applications"), width="100%", justify_content="start", padding_left="1.2rem", background=rx.cond( BrowserState.selected_table == "applications", COLOR_CARD_ALT, "transparent", ), border=rx.cond( BrowserState.selected_table == "applications", "2px solid #4f72a9", f"1px solid {COLOR_BORDER}", ), border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), width="100%", spacing="1", align="start", padding_left="0.6rem", ), rx.fragment(), ), rx.button( rx.hstack( rx.icon("server", size=16, color=COLOR_TEXT), rx.text(ui_txt("nav.service_tables"), color=COLOR_TEXT, font_weight="bold"), rx.cond( BrowserState.service_tables_expanded, rx.text("▾", color=COLOR_ACCENT), rx.text("▸", color=COLOR_ACCENT), ), spacing="2", align="center", width="100%", ), on_click=BrowserState.toggle_service_tables, width="100%", justify_content="start", background="#17223d", border=rx.cond( BrowserState.service_tables_expanded | (BrowserState.selected_table == "Service Tables") | BrowserState.service_tables.contains( BrowserState.selected_table ), "2px solid #4f72a9", "1px solid #324b74", ), border_radius="8px", _hover={"background": "#1f2e50", "border_color": "#4f72a9"}, ), rx.cond( BrowserState.service_tables_expanded, rx.foreach( BrowserState.service_tables, lambda table_name: rx.button( rx.text(table_name, color=COLOR_TEXT, white_space="nowrap"), on_click=BrowserState.select_table(table_name), width="100%", justify_content="start", padding_left="1.2rem", background=rx.cond( BrowserState.selected_table == table_name, COLOR_CARD_ALT, "transparent", ), border=rx.cond( BrowserState.selected_table == table_name, "2px solid #4f72a9", f"1px solid {COLOR_BORDER}", ), border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), ), rx.fragment(), ), width="100%", spacing="2", align="start", padding_left="0.6rem", ), rx.fragment(), ), width="100%", spacing="2", align="start", ), width="100%", height="100%", spacing="3", align="start", ) def _dashboard_pick_app_row(tile: ApplicationTile) -> rx.Component: return rx.box( rx.hstack( rx.vstack( rx.text(tile.name, color=COLOR_TEXT, font_weight="bold", font_size="0.88rem"), rx.hstack( rx.text(ui_txt("apps.current"), color=COLOR_MUTED, font_size="0.72rem"), rx.text(tile.group_name, color=COLOR_MUTED, font_size="0.72rem"), spacing="1", align="center", ), spacing="1", align="start", ), rx.spacer(), rx.box( rx.text(tile.app_type, color="#0f172a", font_size="0.65rem", font_weight="bold"), padding="0.2rem 0.5rem", border_radius="999px", background=tile.color, ), width="100%", align="center", ), padding="0.65rem 0.75rem", border_radius="10px", border=f"1px solid {COLOR_BORDER}", background=COLOR_CARD_ALT, cursor="pointer", width="100%", _hover={"border_color": COLOR_ACCENT, "background": "#1a2743"}, on_click=BrowserState.assign_application_to_dashboard_group(tile.id), ) def _application_tile_type_icon(app_type: str, accent_color: str) -> rx.Component: return rx.match( app_type, ( "SQL", rx.icon("database", size=22, color=accent_color, class_name="dashboard-app-tile__icon"), ), ( "sql", rx.icon("database", size=22, color=accent_color, class_name="dashboard-app-tile__icon"), ), ( "custom table", rx.icon("table", size=22, color=accent_color, class_name="dashboard-app-tile__icon"), ), ( "url", rx.icon("link", size=22, color=accent_color, class_name="dashboard-app-tile__icon"), ), ( "chart", rx.icon("bar-chart-3", size=22, color=accent_color, class_name="dashboard-app-tile__icon"), ), ( "master/detail", rx.icon("layers", size=22, color=accent_color, class_name="dashboard-app-tile__icon"), ), rx.icon("layout-grid", size=22, color=accent_color, class_name="dashboard-app-tile__icon"), ) def _application_tile( tile: ApplicationTile, *, in_group: bool = False ) -> rx.Component: return rx.box( rx.vstack( rx.hstack( rx.box( _application_tile_type_icon(tile.app_type, tile.color), class_name="dashboard-app-tile__icon-wrap", ), rx.spacer(), rx.box( rx.text(tile.app_type, color="#0f172a", font_size="0.65rem", font_weight="bold"), padding="0.2rem 0.55rem", border_radius="999px", background=tile.color, class_name="dashboard-app-tile__badge", ), width="100%", align="start", ), rx.text( tile.name, color=COLOR_TEXT, font_weight="bold", font_size="0.92rem", line_height="1.25", class_name="dashboard-app-tile__title", ), rx.box( flex="1", min_height="0", width="100%", ), rx.box( rx.text( rx.cond( tile.description == "", ui_txt("tile.no_description"), tile.description, ), color=COLOR_MUTED, font_size="0.72rem", line_height="1.35", class_name="dashboard-app-tile__preview", ), width="100%", padding_right=rx.cond( in_group, rx.cond( BrowserState.dashboard_show_app_management, "2rem", "0", ), "0", ), ), spacing="3", width="100%", flex="1", min_height="0", align="start", class_name="dashboard-app-tile__inner", ), rx.cond( in_group & BrowserState.dashboard_show_app_management, rx.button( rx.icon("x", size=14, color=COLOR_ERROR), variant="ghost", size="1", flex_shrink="0", padding="0.15rem", min_width="1.75rem", height="1.75rem", draggable=False, title=ui_txt("dash.remove_from_group_tt"), position="absolute", bottom="0.5rem", right="0.5rem", z_index="2", on_click=[ BrowserState.remove_application_from_dashboard_group(tile.id), rx.stop_propagation, ], _hover={ "background": "rgba(248, 113, 113, 0.12)", }, ), rx.fragment(), ), width="100%", min_height="0", padding="0.85rem 0.9rem", position="relative", class_name="dashboard-grid-item dashboard-app-tile", custom_attrs={"data-item-id": tile.name}, style={"--tile-accent": tile.color}, draggable=True, on_click=BrowserState.open_application_tile( tile.content, tile.app_type, tile.name, tile.group_id ), ) def _add_application_tile_for_group(group: ApplicationGroupTiles) -> rx.Component: return rx.box( rx.vstack( rx.box( rx.icon("plus", size=26, color=COLOR_ACCENT), class_name="dashboard-add-tile__circle", ), rx.text(ui_txt("apps.new_app_title"), color=COLOR_TEXT, font_size="0.8rem", font_weight="bold"), rx.text(ui_txt("apps.add_to_group"), color=COLOR_MUTED, font_size="0.68rem"), spacing="2", align="center", justify="center", width="100%", height="100%", class_name="dashboard-add-tile__inner", ), width="100%", min_height="0", padding="0.75rem", class_name="dashboard-grid-item dashboard-grid-item--add", draggable=False, on_click=BrowserState.open_dashboard_pick_app(group.id), cursor="pointer", ) def _application_group_tiles(group: ApplicationGroupTiles) -> rx.Component: return rx.box( rx.vstack( rx.box( rx.hstack( rx.hstack( rx.icon("grip-vertical", size=14, color=COLOR_MUTED), spacing="1", align="center", class_name="dashboard-group-drag-handle", draggable=True, padding="0.2rem", border_radius="6px", cursor="grab", _hover={"background": "rgba(255,255,255,0.06)"}, ), rx.box( rx.icon("folder", size=16, color=COLOR_ACCENT), class_name="dashboard-group-panel__folder", ), rx.text( group.name, color=COLOR_TEXT, font_weight="bold", font_size="0.95rem", ), rx.spacer(), spacing="3", align="center", width="100%", padding_right=rx.cond( BrowserState.dashboard_show_app_management, "2.5rem", "0", ), class_name="dashboard-group-panel__head", ), rx.cond( BrowserState.dashboard_show_app_management, rx.button( rx.icon("x", size=14, color=COLOR_ERROR), variant="ghost", size="1", flex_shrink="0", padding="0.15rem", min_width="1.75rem", height="1.75rem", draggable=False, title=ui_txt("dash.delete_group_tt"), position="absolute", top="0", right="0", z_index="3", on_click=[ BrowserState.delete_application_group_dashboard(group.id), rx.stop_propagation, ], _hover={ "background": "rgba(248, 113, 113, 0.12)", }, ), rx.fragment(), ), width="100%", position="relative", ), rx.grid( rx.foreach( group.tiles, lambda t: _application_tile(t, in_group=True), ), rx.cond( BrowserState.dashboard_show_app_management, _add_application_tile_for_group(group), rx.fragment(), ), columns="repeat(auto-fill, minmax(180px, 1fr))", spacing="2", width="100%", class_name="dashboard-app-grid", ), spacing="3", align="start", width="100%", ), width="100%", class_name="dashboard-group-drag-root dashboard-group-panel", custom_attrs={ "data-dashboard-group-id": group.id, }, ) def _expanded_readonly_table(columns, rows) -> rx.Component: """Read-only grid without ``rx.data_editor``: DataEditor's getData hook breaks inside ``rx.foreach`` (ReferenceError: app_rx_state_ is not defined). """ head_cell = { "border": f"1px solid {COLOR_BORDER}", "padding": "0.35rem 0.55rem", "fontSize": "0.8rem", "background": "#1a2743", "fontWeight": "600", "color": "#cbd5e1", "verticalAlign": "top", "textAlign": "left", } body_cell = { "border": f"1px solid {COLOR_BORDER}", "padding": "0.35rem 0.55rem", "fontSize": "0.8rem", "color": "#e5e7eb", "verticalAlign": "top", "textAlign": "left", } return rx.box( rx.el.table( rx.el.thead( rx.el.tr( rx.foreach( columns, lambda c: rx.el.th(c["title"], style=head_cell), ), ), ), rx.el.tbody( rx.foreach( rows, lambda row: rx.el.tr( rx.foreach( row, lambda cell: rx.el.td(cell, style=body_cell), ), ), ), ), style={ "borderCollapse": "collapse", "width": "100%", "background": "#0f172a", }, ), width="100%", max_height="28rem", overflow_x="auto", overflow_y="auto", border=f"1px solid {COLOR_BORDER}", border_radius="8px", ) def _expand_data_editor_height(row_count_var) -> rx.Component: return rx.cond( row_count_var > 0, rx.cond( row_count_var > 28, 28 * DATA_EDITOR_ROW_HEIGHT + DATA_EDITOR_HEADER_HEIGHT + DATA_EDITOR_SCROLLBAR_GUTTER, row_count_var * DATA_EDITOR_ROW_HEIGHT + DATA_EDITOR_HEADER_HEIGHT + DATA_EDITOR_SCROLLBAR_GUTTER, ), DATA_EDITOR_HEADER_HEIGHT + DATA_EDITOR_SCROLLBAR_GUTTER, ) def _expanded_app_recharts(app) -> rx.Component: """Recharts block for Run all (tooltips/legend like Application Visuals).""" axis_label = { "value": app.chart_axis_label, "position": "bottom", "offset": 14, "fill": COLOR_MUTED, } tooltip_content = { "background": COLOR_CARD_ALT, "borderColor": "#243452", "borderRadius": "8px", "color": COLOR_TEXT, "whiteSpace": "pre-line", "maxWidth": "28rem", } tooltip_label = { "color": COLOR_TEXT, "whiteSpace": "pre-line", "fontSize": "0.78rem", "lineHeight": 1.35, } legend_wrap = { "color": COLOR_TEXT, "display": "flex", "justifyContent": "center", "alignItems": "center", "textAlign": "center", "paddingLeft": "0.75rem", } grid_shared = rx.recharts.cartesian_grid( stroke_dasharray="3 3", stroke="#2a3655" ) x_labels = rx.recharts.x_axis( data_key="label", x_axis_id="labels", label=axis_label, tick={"fill": COLOR_MUTED, "fontSize": 12}, ) x_hint = rx.recharts.x_axis( data_key="hint", x_axis_id="hint", hide=True, ) y_ax = rx.recharts.y_axis() legend = rx.recharts.legend( layout="vertical", align="right", vertical_align="middle", width=116, icon_size=10, wrapper_style=legend_wrap, ) tooltip = rx.recharts.graphing_tooltip( content_style=tooltip_content, label_style=tooltip_label, item_style={"color": "#93c5fd"}, ) bar_chart = rx.recharts.bar_chart( grid_shared, x_labels, x_hint, y_ax, legend, tooltip, rx.foreach( app.chart_series, lambda s: rx.recharts.bar( data_key=s["key"], name=s["key"], x_axis_id="hint", fill=s["color"], ), ), data=app.chart_points, width="100%", height=340, margin={"right": 8, "left": 8, "top": 8, "bottom": 25}, ) line_chart = rx.recharts.line_chart( grid_shared, x_labels, x_hint, y_ax, legend, tooltip, rx.foreach( app.chart_series, lambda s: rx.recharts.line( data_key=s["key"], name=s["key"], x_axis_id="hint", type_="monotone", stroke=s["color"], dot=False, ), ), data=app.chart_points, width="100%", height=340, margin={"right": 8, "left": 8, "top": 8, "bottom": 25}, ) return rx.box( rx.cond(app.chart_is_histogram, bar_chart, line_chart), width="100%", border=f"1px solid {COLOR_BORDER}", border_radius="10px", padding="0.75rem", background=COLOR_CARD_ALT, ) def _dashboard_expanded_app_slot(slot_idx: int) -> rx.Component: """One Expand-all block; ``slot_idx`` is a compile-time constant (stable DataEditor state paths).""" app = BrowserState.dashboard_expanded_apps[slot_idx] return rx.box( rx.vstack( rx.hstack( rx.text( app.title, color=COLOR_TEXT, font_weight="bold", font_size="1rem", ), rx.box( rx.text( app.type_label, color="#0f172a", font_size="0.65rem", font_weight="bold", ), padding="0.2rem 0.55rem", border_radius="999px", background=COLOR_ACCENT, ), width="100%", align="center", spacing="3", ), rx.match( app.kind, ( "grid", rx.vstack( rx.cond( app.grid_editable, rx.box( rx.data_editor( columns=app.grid_columns, data=app.grid_rows, theme=SERVICE_DATA_EDITOR_THEME, row_markers="none", scale_to_rem=True, smooth_scroll_x=True, smooth_scroll_y=True, row_height=DATA_EDITOR_ROW_HEIGHT, header_height=DATA_EDITOR_HEADER_HEIGHT, min_column_width=90, max_column_width=720, width="100%", height=_expand_data_editor_height( app.grid_rows.length() ), on_cell_edited=BrowserState.expand_slot_grid_cell_edited( slot_idx ), class_name="service-data-editor", ), width="100%", overflow="hidden", background=COLOR_CARD_ALT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", padding="0", ), rx.cond( app.grid_columns.length() > 0, _expanded_readonly_table( app.grid_columns, app.grid_rows ), rx.text( ui_txt("expand.no_columns"), color=COLOR_MUTED ), ), ), spacing="2", align="start", width="100%", ), ), ( "iframe", rx.el.iframe( src=app.iframe_src, width="100%", height="480", style={ "border": f"1px solid {COLOR_BORDER}", "borderRadius": "8px", "background": "#0f172a", }, loading="lazy", referrer_policy="no-referrer-when-downgrade", ), ), ( "chart", rx.cond( app.chart_points.length() > 0, _expanded_app_recharts(app), rx.cond( app.chart_src != "", rx.image( src=app.chart_src, width="100%", max_height="32rem", object_fit="contain", alt=app.title, ), rx.text( ui_txt("expand.chart_unavailable"), color=COLOR_MUTED ), ), ), ), ( "md", rx.cond( app.md_session, rx.vstack( rx.text( ui_txt("expand.md_pick_master"), color=COLOR_MUTED, font_size="0.85rem", ), rx.box( rx.data_editor( columns=app.md_session.master_grid_columns, data=app.md_session.master_rows, theme=SERVICE_DATA_EDITOR_THEME, row_markers="none", scale_to_rem=True, smooth_scroll_x=True, smooth_scroll_y=True, row_height=DATA_EDITOR_ROW_HEIGHT, header_height=DATA_EDITOR_HEADER_HEIGHT, min_column_width=90, max_column_width=720, width="100%", height=_expand_data_editor_height( app.md_session.master_rows.length() ), on_cell_activated=BrowserState.expand_md_slot_master_cell( slot_idx ), on_cell_clicked=BrowserState.expand_md_slot_master_cell( slot_idx ), on_grid_selection_change=BrowserState.expand_md_slot_master_selection_change( slot_idx ), grid_selection=app.md_session.master_grid_selection, class_name="service-data-editor", ), width="100%", overflow="hidden", background=COLOR_CARD_ALT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", padding="0", ), rx.text( app.md_session.caption, color=COLOR_MUTED, font_size="0.8rem", ), rx.text( ui_txt("expand.detail"), color=COLOR_TEXT, font_weight="bold", ), rx.hstack( rx.button( ui_txt("expand.add"), on_click=BrowserState.expand_md_slot_add_detail( slot_idx ), background="#253a63", color=COLOR_TEXT, border="1px solid #3a537f", border_radius="8px", size="1", _hover={ "background": "#314a76", "border_color": "#476596", }, ), rx.button( ui_txt("expand.save_row"), on_click=BrowserState.expand_md_slot_save_detail( slot_idx ), background=COLOR_ACCENT, color=COLOR_TEXT, border=f"1px solid {COLOR_ACCENT}", border_radius="8px", size="1", ), rx.button( ui_txt("auth.cancel"), on_click=BrowserState.expand_md_slot_cancel_detail( slot_idx ), background="transparent", color=COLOR_MUTED, border=f"1px solid {COLOR_BORDER}", border_radius="8px", size="1", ), rx.button( ui_txt("expand.edit_selected"), on_click=BrowserState.expand_md_slot_start_edit_selected( slot_idx ), background="transparent", color=COLOR_TEXT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", size="1", ), rx.button( ui_txt("expand.delete_selected"), on_click=BrowserState.expand_md_slot_delete_detail( slot_idx ), background="transparent", color=COLOR_ERROR, border=f"1px solid {COLOR_ERROR}", border_radius="8px", size="1", _hover={ "background": "rgba(248, 113, 113, 0.12)", }, ), spacing="2", align="center", flex_wrap="wrap", width="100%", ), rx.box( rx.cond( app.md_session.detail_rows.length() > 0, rx.data_editor( columns=app.md_session.detail_grid_columns, data=app.md_session.detail_rows, theme=SERVICE_DATA_EDITOR_THEME, row_markers="none", scale_to_rem=True, smooth_scroll_x=True, smooth_scroll_y=True, row_height=DATA_EDITOR_ROW_HEIGHT, header_height=DATA_EDITOR_HEADER_HEIGHT, min_column_width=90, max_column_width=720, width="100%", height=_expand_data_editor_height( app.md_session.detail_rows.length() ), on_cell_activated=BrowserState.expand_md_slot_detail_select_from_grid( slot_idx ), on_cell_clicked=BrowserState.expand_md_slot_detail_select_from_grid( slot_idx ), on_grid_selection_change=BrowserState.expand_md_slot_detail_selection_change( slot_idx ), on_cell_edited=BrowserState.expand_md_slot_detail_cell_edited( slot_idx ), grid_selection=app.md_session.detail_grid_selection, class_name="service-data-editor", ), rx.text( ui_txt("expand.no_detail_master"), color=COLOR_MUTED, ), ), width="100%", overflow="hidden", background=COLOR_CARD_ALT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", padding="0.5rem", ), spacing="2", align="start", width="100%", ), rx.text( ui_txt("expand.md_unavailable"), color=COLOR_ERROR, font_size="0.85rem", ), ), ), ( "error", rx.text(app.message, color=COLOR_ERROR, font_size="0.85rem"), ), ( "text", rx.text(app.message, color=COLOR_MUTED, font_size="0.85rem"), ), rx.text( rx.cond(app.message != "", app.message, ui_txt("expand.unsupported")), color=COLOR_MUTED, font_size="0.85rem", ), ), spacing="3", align="start", width="100%", ), width="100%", padding="0.85rem", border_radius="12px", border=f"1px solid {COLOR_BORDER}", background=COLOR_CARD_ALT, margin_bottom="0.75rem", class_name="dashboard-expanded-app", ) def _view_panel() -> rx.Component: return rx.vstack( rx.cond( BrowserState.is_loading_rows, rx.text(ui_txt("layout.loading_rows"), color=COLOR_ACCENT), rx.fragment(), ), rx.cond( BrowserState.selected_table == "application_groups", rx.vstack( rx.hstack( rx.text( BrowserState.applications_dashboard_title, color=COLOR_TEXT, font_weight="bold", font_size="1.15rem", ), rx.spacer(), rx.cond( BrowserState.dashboard_show_app_management, rx.button( rx.hstack( rx.icon("folder-plus", size=16, color=COLOR_TEXT), rx.text(ui_txt("apps.new_group"), font_size="0.85rem"), spacing="2", align="center", ), on_click=BrowserState.open_dashboard_add_group, background="transparent", border=f"1px solid {COLOR_BORDER}", color=COLOR_TEXT, border_radius="8px", size="2", padding_x="0.65rem", padding_y="0.35rem", _hover={ "background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT, }, ), rx.box( rx.hstack( rx.button( rx.hstack( rx.icon( "layout-grid", size=15, color=COLOR_TEXT, ), rx.text( ui_txt("apps.tiles"), color=COLOR_TEXT, font_size="0.85rem", font_weight="500", ), spacing="2", align="center", ), on_click=BrowserState.set_applications_dashboard_expand_all( False ), size="2", flex="1", min_width="0", justify_content="center", border_radius="7px 0 0 7px", background=rx.cond( BrowserState.applications_dashboard_expand_all, "#17223d", COLOR_CARD_ALT, ), border=rx.cond( BrowserState.applications_dashboard_expand_all, f"1px solid #324b74", "2px solid #4f72a9", ), color=COLOR_TEXT, _hover={ "background": "#1f2e50", "border_color": "#4f72a9", }, ), rx.box( width="1px", align_self="stretch", min_height="30px", background="#324b74", flex_shrink="0", ), rx.button( rx.hstack( rx.icon( "layout-list", size=15, color=COLOR_TEXT, ), rx.text( ui_txt("apps.run_all"), color=COLOR_TEXT, font_size="0.85rem", font_weight="500", ), spacing="2", align="center", ), on_click=BrowserState.set_applications_dashboard_expand_all( True ), size="2", flex="1", min_width="0", justify_content="center", border_radius="0 7px 7px 0", background=rx.cond( BrowserState.applications_dashboard_expand_all, COLOR_CARD_ALT, "#17223d", ), border=rx.cond( BrowserState.applications_dashboard_expand_all, "2px solid #4f72a9", f"1px solid #324b74", ), color=COLOR_TEXT, _hover={ "background": "#1f2e50", "border_color": "#4f72a9", }, ), spacing="0", align="stretch", width="100%", ), width="100%", max_width="20rem", padding="3px", border_radius="10px", border=f"1px solid rgba(79, 114, 169, 0.45)", background="linear-gradient(165deg, rgba(24, 34, 58, 0.55) 0%, rgba(15, 23, 42, 0.85) 100%)", class_name="dashboard-expand-mode-toggle", ), ), width="100%", align="center", class_name="dashboard-page__intro", ), rx.cond( BrowserState.applications_dashboard_visible_groups.length() > 0, rx.cond( BrowserState.dashboard_show_app_management, rx.vstack( rx.foreach( BrowserState.applications_dashboard_visible_groups, _application_group_tiles, ), spacing="2", width="100%", align="start", class_name="dashboard-groups-stack", ), rx.cond( BrowserState.applications_dashboard_expand_all, rx.vstack( *[ rx.cond( BrowserState.dashboard_expanded_apps.length() > idx, _dashboard_expanded_app_slot(idx), rx.fragment(), ) for idx in range(EXPAND_ALL_MAX_SLOTS) ], spacing="0", width="100%", align="start", class_name="dashboard-expanded-apps-stack", ), rx.vstack( rx.foreach( BrowserState.applications_dashboard_visible_groups, _application_group_tiles, ), spacing="2", width="100%", align="start", class_name="dashboard-groups-stack", ), ), ), rx.fragment(), ), rx.button( "refresh-dashboard-tiles", custom_attrs={"id": "sphere-refresh-dashboard-tiles", "aria-hidden": "true"}, display="none", on_click=BrowserState.refresh_application_groups_dashboard, ), rx.cond( BrowserState.applications_dashboard_visible_groups.length() == 0, rx.text(ui_txt("apps.no_apps"), color=COLOR_MUTED), rx.fragment(), ), width="100%", spacing="3", align="start", custom_attrs={"data-dashboard-grid-root": "1"}, ), rx.fragment(), ), rx.cond( BrowserState.selected_table == "Custom Tables", rx.cond( BrowserState.is_create_table_mode, rx.vstack( rx.text(ui_txt("apps.create_custom_table"), color=COLOR_TEXT, font_weight="bold"), rx.input( placeholder=ui_txt("ph.table_name"), value=BrowserState.new_table_name, on_change=BrowserState.set_new_table_name, width="22rem", color_scheme="gray", **INPUT_STYLE, ), rx.vstack( rx.foreach( BrowserState.new_table_fields, lambda field, field_idx: rx.hstack( rx.input( placeholder=ui_txt("ph.field_name"), value=field[0], on_change=BrowserState.set_table_field_name(field_idx), width="10rem", _placeholder={"color": COLOR_MUTED}, **INPUT_STYLE, ), rx.select( CUSTOM_FIELD_TYPES, value=field[1], on_change=BrowserState.set_table_field_type(field_idx), width="10rem", class_name="field-type-select", **SELECT_STYLE, ), rx.button( ui_txt("table.remove"), on_click=BrowserState.remove_table_field(field_idx), background="transparent", color=COLOR_ERROR, border=f"1px solid {COLOR_ERROR}", border_radius="8px", size="1", ), width="100%", spacing="2", align="center", ), ), width="100%", spacing="2", align="start", ), rx.hstack( rx.button( ui_txt("table.add_field"), on_click=BrowserState.add_table_field, background="transparent", color=COLOR_TEXT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", ), rx.spacer(), rx.button( ui_txt("auth.cancel"), on_click=BrowserState.cancel_create_table_mode, background="transparent", color=COLOR_MUTED, border=f"1px solid {COLOR_BORDER}", border_radius="8px", ), rx.button( ui_txt("table.create_table"), on_click=BrowserState.create_custom_table, background=COLOR_ACCENT, color=COLOR_TEXT, border=f"1px solid {COLOR_ACCENT}", border_radius="8px", _hover={"background": "#2563eb", "border_color": "#2563eb"}, ), width="100%", align="center", ), width="100%", spacing="3", align="start", ), rx.cond( BrowserState.is_edit_table_mode, rx.vstack( rx.text( ui_txt("table.edit_title"), color=COLOR_TEXT, font_weight="bold", ), rx.text( BrowserState.edit_table_name, color=COLOR_MUTED, font_size="0.9rem", ), rx.vstack( rx.foreach( BrowserState.edit_table_fields, lambda field, field_idx: rx.hstack( rx.input( placeholder=ui_txt("ph.field_name"), value=field[0], on_change=BrowserState.set_edit_field_name( field_idx ), width="10rem", _placeholder={"color": COLOR_MUTED}, **INPUT_STYLE, ), rx.select( BrowserState.edit_type_options, value=field[1], on_change=BrowserState.set_edit_field_type( field_idx ), width="10rem", class_name="field-type-select", **SELECT_STYLE, ), rx.button( ui_txt("table.remove"), on_click=BrowserState.remove_edit_field(field_idx), background="transparent", color=COLOR_ERROR, border=f"1px solid {COLOR_ERROR}", border_radius="8px", size="1", ), width="100%", spacing="2", align="center", ), ), width="100%", spacing="2", align="start", ), rx.hstack( rx.button( ui_txt("table.add_field"), on_click=BrowserState.add_edit_field, background="transparent", color=COLOR_TEXT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", ), rx.spacer(), rx.button( ui_txt("auth.cancel"), on_click=BrowserState.cancel_edit_table_mode, background="transparent", color=COLOR_MUTED, border=f"1px solid {COLOR_BORDER}", border_radius="8px", ), rx.button( ui_txt("table.save_changes"), on_click=BrowserState.save_edit_table, background="#253a63", color=COLOR_TEXT, border="1px solid #3a537f", border_radius="8px", _hover={ "background": "#314a76", "border_color": "#476596", }, ), width="100%", align="center", ), width="100%", spacing="3", align="start", ), rx.vstack( rx.hstack( rx.button( ui_txt("reports.create"), on_click=BrowserState.start_create_table_mode, background="#253a63", color=COLOR_TEXT, border="1px solid #3a537f", border_radius="8px", _hover={ "background": "#314a76", "border_color": "#476596", }, ), rx.button( ui_txt("table.delete"), on_click=BrowserState.delete_selected_custom_table, disabled=BrowserState.custom_tables_selected_row_idx < 0, background="transparent", color=COLOR_ERROR, border=f"1px solid {COLOR_ERROR}", border_radius="8px", _hover={"background": "rgba(248, 113, 113, 0.12)"}, ), spacing="2", align="center", width="100%", ), rx.box( rx.vstack( rx.hstack( rx.text( ui_txt("table.selected"), color=COLOR_MUTED, font_size="0.85rem", ), rx.text( rx.cond( BrowserState.custom_tables_selected_row_idx >= 0, BrowserState.custom_tables[ BrowserState.custom_tables_selected_row_idx ], "-", ), color=COLOR_TEXT, font_size="0.85rem", ), spacing="2", align="center", width="100%", ), rx.data_editor( columns=[{"title": "table_name", "id": "table_name", "type": "str", "editable": False}], data=BrowserState.custom_tables_grid_rows, theme=SERVICE_DATA_EDITOR_THEME, row_markers="none", scale_to_rem=True, row_height=DATA_EDITOR_ROW_HEIGHT, header_height=DATA_EDITOR_HEADER_HEIGHT, on_cell_activated=BrowserState.select_custom_table_grid_cell, on_cell_clicked=BrowserState.select_custom_table_grid_cell, on_header_clicked=BrowserState.handle_custom_tables_grid_header_clicked, on_grid_selection_change=BrowserState.handle_custom_tables_grid_selection_change, grid_selection=BrowserState.custom_tables_grid_selection, width="100%", height=( BrowserState.custom_tables_grid_rows.length() * DATA_EDITOR_ROW_HEIGHT ) + DATA_EDITOR_HEADER_HEIGHT + DATA_EDITOR_SCROLLBAR_GUTTER, class_name="service-data-editor", ), width="100%", spacing="2", align="start", ), width="100%", min_width="0", overflow="hidden", background=COLOR_CARD_ALT, border=f"1px solid {COLOR_BORDER}", border_radius="10px", padding="0.5rem", ), width="100%", spacing="3", align="start", ), ), ), rx.fragment(), ), rx.cond( BrowserState.selected_table == "Master/Detail", rx.vstack( rx.cond( BrowserState.master_detail_show_configuration_ui, rx.vstack( rx.hstack( rx.text(ui_txt("md.master_table"), color=COLOR_MUTED, width="8rem"), rx.select( BrowserState.custom_tables, value=BrowserState.master_detail_master_table, on_change=BrowserState.set_master_detail_master_table, placeholder=ui_txt("ph.master_table"), _placeholder={"color": COLOR_MUTED}, width="18rem", class_name="field-type-select", **SELECT_STYLE, ), width="100%", align="center", ), rx.hstack( rx.text(ui_txt("md.detail_table"), color=COLOR_MUTED, width="8rem"), rx.select( BrowserState.custom_tables, value=BrowserState.master_detail_detail_table, on_change=BrowserState.set_master_detail_detail_table, placeholder=ui_txt("ph.detail_table"), _placeholder={"color": COLOR_MUTED}, width="18rem", class_name="field-type-select", **SELECT_STYLE, ), width="100%", align="center", ), rx.hstack( rx.text(ui_txt("md.master_key"), color=COLOR_MUTED, width="8rem"), rx.cond( BrowserState.master_detail_master_key_options.length() > 0, rx.select( BrowserState.master_detail_master_key_options, value=BrowserState.master_detail_master_key, on_change=BrowserState.set_master_detail_master_key, placeholder=ui_txt("ph.master_key_link"), _placeholder={"color": COLOR_MUTED}, width="18rem", class_name="field-type-select", **SELECT_STYLE, ), rx.input( value="", placeholder=ui_txt("ph.select_master_first"), _placeholder={"color": COLOR_MUTED}, width="18rem", is_disabled=True, **INPUT_STYLE, ), ), width="100%", align="center", ), rx.hstack( rx.text(ui_txt("md.detail_key"), color=COLOR_MUTED, width="8rem"), rx.cond( BrowserState.master_detail_detail_key_options.length() > 0, rx.select( BrowserState.master_detail_detail_key_options, value=BrowserState.master_detail_detail_key, on_change=BrowserState.set_master_detail_detail_key, placeholder=ui_txt("ph.detail_key_link"), _placeholder={"color": COLOR_MUTED}, width="18rem", class_name="field-type-select", **SELECT_STYLE, ), rx.input( value="", placeholder=ui_txt("ph.select_detail_first"), _placeholder={"color": COLOR_MUTED}, width="18rem", is_disabled=True, **INPUT_STYLE, ), ), width="100%", align="center", ), rx.hstack( rx.button( ui_txt("reports.create_app"), on_click=BrowserState.start_create_master_detail_app_mode, disabled=( (BrowserState.master_detail_master_table == "") | (BrowserState.master_detail_detail_table == "") | (BrowserState.master_detail_master_key == "") | (BrowserState.master_detail_detail_key == "") ), background="transparent", color=COLOR_TEXT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), rx.cond( BrowserState.master_detail_loading, rx.text(ui_txt("md.loading"), color=COLOR_ACCENT), rx.fragment(), ), rx.cond( BrowserState.master_detail_error != "", rx.text( BrowserState.master_detail_error, color=COLOR_ERROR, font_size="0.85rem", ), rx.fragment(), ), width="100%", align="center", spacing="3", ), rx.cond( BrowserState.is_create_master_detail_app_mode, rx.hstack( rx.text(ui_txt("md.app_name"), color=COLOR_MUTED, width="9rem"), rx.input( value=BrowserState.new_master_detail_app_name, on_change=BrowserState.set_new_master_detail_app_name, placeholder=ui_txt("ph.app_name"), width="18rem", _placeholder={"color": COLOR_MUTED}, **INPUT_STYLE, ), rx.button( ui_txt("reports.create"), on_click=BrowserState.create_app_from_master_detail, disabled=BrowserState.new_master_detail_app_name == "", background="#253a63", color=COLOR_TEXT, border="1px solid #3a537f", border_radius="8px", _hover={"background": "#314a76", "border_color": "#476596"}, ), rx.button( ui_txt("auth.cancel"), on_click=BrowserState.cancel_create_master_detail_app_mode, background="transparent", color=COLOR_MUTED, border=f"1px solid {COLOR_BORDER}", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), spacing="2", align="center", width="100%", ), rx.fragment(), ), spacing="3", width="100%", align="start", ), rx.vstack( rx.cond( BrowserState.master_detail_loading, rx.text(ui_txt("md.loading"), color=COLOR_ACCENT), rx.fragment(), ), rx.cond( BrowserState.master_detail_error != "", rx.text( BrowserState.master_detail_error, color=COLOR_ERROR, font_size="0.85rem", ), rx.fragment(), ), spacing="2", width="100%", align="start", ), ), rx.cond( BrowserState.master_detail_master_rows.length() > 0, rx.vstack( rx.text(ui_txt("md.select_master"), color=COLOR_MUTED, font_size="0.9rem"), rx.box( rx.data_editor( columns=BrowserState.master_detail_master_grid_columns, data=BrowserState.master_detail_master_rows, theme=SERVICE_DATA_EDITOR_THEME, row_markers="none", scale_to_rem=True, smooth_scroll_x=True, smooth_scroll_y=True, row_height=DATA_EDITOR_ROW_HEIGHT, header_height=DATA_EDITOR_HEADER_HEIGHT, on_cell_activated=BrowserState.select_master_detail_row_from_grid, on_cell_clicked=BrowserState.select_master_detail_row_from_grid, on_header_clicked=BrowserState.handle_master_detail_master_grid_header_clicked, on_grid_selection_change=BrowserState.handle_master_detail_master_grid_selection_change, grid_selection=BrowserState.master_detail_master_grid_selection, width="100%", height=( BrowserState.master_detail_master_rows.length() * DATA_EDITOR_ROW_HEIGHT ) + DATA_EDITOR_HEADER_HEIGHT + DATA_EDITOR_SCROLLBAR_GUTTER, class_name="service-data-editor", ), width="100%", overflow="hidden", background=COLOR_CARD_ALT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", padding="0", ), rx.cond( BrowserState.master_detail_selected_master_idx >= 0, rx.vstack( rx.text(ui_txt("md.detail_records"), color=COLOR_MUTED, font_size="0.9rem"), rx.hstack( rx.button( ui_txt("auth.add"), on_click=BrowserState.add_master_detail_detail_row, disabled=( BrowserState.master_detail_detail_adding_row | (BrowserState.master_detail_detail_editing_idx >= 0) ), background="#253a63", color=COLOR_TEXT, border="1px solid #3a537f", border_radius="8px", _hover={"background": "#314a76", "border_color": "#476596"}, ), rx.button( ui_txt("grid.export_excel"), on_click=BrowserState.export_master_detail_to_excel, background="transparent", color=COLOR_TEXT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), rx.cond( BrowserState.master_detail_detail_adding_row, rx.hstack( rx.button( ui_txt("grid.save"), on_click=BrowserState.save_master_detail_detail_new_row, background=COLOR_ACCENT, color=COLOR_TEXT, border=f"1px solid {COLOR_ACCENT}", border_radius="8px", size="1", ), rx.button( ui_txt("auth.cancel"), on_click=BrowserState.cancel_master_detail_detail_add, background="transparent", color=COLOR_MUTED, border=f"1px solid {COLOR_BORDER}", border_radius="8px", size="1", ), spacing="2", ), rx.fragment(), ), spacing="2", align="center", ), rx.box( rx.cond( BrowserState.master_detail_detail_rows.length() > 0, rx.vstack( rx.hstack( rx.button( ui_txt("md.delete_selected"), on_click=BrowserState.delete_selected_master_detail_detail_row, disabled=BrowserState.master_detail_detail_selected_row_idx < 0, background="transparent", color=COLOR_ERROR, border=f"1px solid {COLOR_ERROR}", border_radius="8px", size="1", _hover={"background": "rgba(248, 113, 113, 0.12)"}, ), spacing="2", align="center", width="100%", ), rx.data_editor( columns=BrowserState.master_detail_detail_grid_columns, data=BrowserState.master_detail_detail_rows, theme=SERVICE_DATA_EDITOR_THEME, row_markers="none", scale_to_rem=True, smooth_scroll_x=True, smooth_scroll_y=True, row_height=DATA_EDITOR_ROW_HEIGHT, header_height=DATA_EDITOR_HEADER_HEIGHT, on_cell_activated=BrowserState.select_master_detail_detail_row_from_grid, on_cell_clicked=BrowserState.select_master_detail_detail_row_from_grid, on_header_clicked=BrowserState.handle_master_detail_detail_grid_header_clicked, on_grid_selection_change=BrowserState.handle_master_detail_detail_grid_selection_change, on_cell_edited=BrowserState.handle_master_detail_detail_cell_edited, grid_selection=BrowserState.master_detail_detail_grid_selection, width="100%", height=( BrowserState.master_detail_detail_rows.length() * DATA_EDITOR_ROW_HEIGHT ) + DATA_EDITOR_HEADER_HEIGHT + DATA_EDITOR_SCROLLBAR_GUTTER, class_name="service-data-editor", ), width="100%", spacing="2", align="start", ), rx.text(ui_txt("md.no_detail_rows"), color=COLOR_MUTED), ), width="100%", overflow="hidden", background=COLOR_CARD_ALT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", padding="0.5rem", ), width="100%", spacing="2", align="start", ), rx.fragment(), ), width="100%", spacing="2", align="start", ), rx.fragment(), ), width="100%", spacing="3", align="start", ), rx.fragment(), ), rx.cond( BrowserState.selected_table == "Visuals", rx.vstack( rx.hstack( rx.text(ui_txt("viz.table"), color=COLOR_MUTED, width="7rem"), rx.select( BrowserState.custom_tables, value=BrowserState.visuals_selected_table, on_change=BrowserState.set_visuals_table, width="18rem", class_name="field-type-select", **SELECT_STYLE, ), width="100%", align="center", ), rx.hstack( rx.text(ui_txt("viz.labels"), color=COLOR_MUTED, width="7rem"), rx.cond( BrowserState.visuals_label_options.length() > 0, rx.select( BrowserState.visuals_label_options, value=BrowserState.visuals_label_column, on_change=BrowserState.set_visuals_label_column, width="18rem", class_name="field-type-select", **SELECT_STYLE, ), rx.input( value="", placeholder=ui_txt("ph.no_label_cols"), width="18rem", is_disabled=True, **INPUT_STYLE, ), ), width="100%", align="center", ), rx.hstack( rx.text(ui_txt("viz.data"), color=COLOR_MUTED, width="7rem"), rx.cond( BrowserState.visuals_data_options.length() > 0, rx.box( rx.flex( rx.foreach( BrowserState.visuals_data_options, lambda data_column: rx.button( data_column, on_click=BrowserState.toggle_visuals_data_column( data_column ), background=rx.cond( BrowserState.visuals_data_columns.contains( data_column ), "#314a76", COLOR_CARD_ALT, ), color=COLOR_TEXT, border=rx.cond( BrowserState.visuals_data_columns.contains( data_column ), "1px solid #60a5fa", f"1px solid {COLOR_BORDER}", ), border_radius="8px", _hover={"background": "#253a63"}, size="1", ), ), wrap="wrap", spacing="2", ), width="18rem", ), rx.input( value="", placeholder=ui_txt("ph.no_numeric_cols"), width="18rem", is_disabled=True, **INPUT_STYLE, ), ), width="100%", align="center", ), rx.hstack( rx.text(ui_txt("viz.display_type"), color=COLOR_MUTED, width="7rem"), rx.select( BrowserState.visuals_chart_types, value=BrowserState.visuals_chart_type, on_change=BrowserState.set_visuals_chart_type, width="18rem", class_name="field-type-select", **SELECT_STYLE, ), width="100%", align="center", ), rx.hstack( rx.cond( BrowserState.visuals_show_create_app, rx.button( ui_txt("reports.create_app"), on_click=BrowserState.start_create_visuals_app_mode, disabled=( (BrowserState.visuals_selected_table == "") | (BrowserState.visuals_label_column == "") | (BrowserState.visuals_data_columns.length() == 0) | (BrowserState.visuals_chart_type == "") ), background="transparent", color=COLOR_TEXT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), rx.fragment(), ), rx.button( ui_txt("grid.export_excel"), on_click=BrowserState.export_visuals_to_excel, disabled=BrowserState.visuals_chart_data.length() == 0, background="transparent", color=COLOR_TEXT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), rx.cond( BrowserState.visuals_notice != "", rx.text(BrowserState.visuals_notice, color=COLOR_MUTED), rx.fragment(), ), width="100%", align="center", spacing="3", ), rx.cond( BrowserState.visuals_show_create_app & BrowserState.is_create_visuals_app_mode, rx.hstack( rx.text(ui_txt("md.app_name"), color=COLOR_MUTED, width="7rem"), rx.input( value=BrowserState.new_visuals_app_name, on_change=BrowserState.set_new_visuals_app_name, placeholder=ui_txt("ph.app_name"), width="18rem", _placeholder={"color": COLOR_MUTED}, **INPUT_STYLE, ), rx.button( ui_txt("reports.create"), on_click=BrowserState.create_app_from_visuals, disabled=BrowserState.new_visuals_app_name == "", background="#253a63", color=COLOR_TEXT, border="1px solid #3a537f", border_radius="8px", _hover={"background": "#314a76", "border_color": "#476596"}, ), rx.button( ui_txt("auth.cancel"), on_click=BrowserState.cancel_create_visuals_app_mode, background="transparent", color=COLOR_MUTED, border=f"1px solid {COLOR_BORDER}", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), width="100%", align="center", spacing="2", ), rx.fragment(), ), rx.cond( BrowserState.visuals_has_generated, rx.box( rx.cond( BrowserState.visuals_chart_type == "histogram", rx.recharts.bar_chart( rx.recharts.cartesian_grid( stroke_dasharray="3 3", stroke="#2a3655" ), rx.recharts.x_axis( data_key="label", x_axis_id="labels", label={ "value": BrowserState.visuals_label_column, "position": "bottom", "offset": 14, "fill": COLOR_MUTED, }, tick={"fill": COLOR_MUTED, "fontSize": 12}, ), rx.recharts.x_axis( data_key="hint", x_axis_id="hint", hide=True, ), rx.recharts.y_axis(), rx.recharts.legend( layout="vertical", align="right", vertical_align="middle", width=116, icon_size=10, wrapper_style={ "color": COLOR_TEXT, "display": "flex", "justifyContent": "center", "alignItems": "center", "textAlign": "center", "paddingLeft": "0.75rem", }, ), rx.recharts.graphing_tooltip( content_style={ "background": COLOR_CARD_ALT, "borderColor": "#243452", "borderRadius": "8px", "color": COLOR_TEXT, "whiteSpace": "pre-line", "maxWidth": "28rem", }, label_style={ "color": COLOR_TEXT, "whiteSpace": "pre-line", "fontSize": "0.78rem", "lineHeight": 1.35, }, item_style={"color": "#93c5fd"}, ), rx.foreach( BrowserState.visuals_data_series, lambda series: rx.recharts.bar( data_key=series["key"], name=series["key"], x_axis_id="hint", fill=series["color"], ), ), data=BrowserState.visuals_chart_data, width="100%", height=340, margin={"right": 8, "left": 8, "top": 8, "bottom": 25}, ), rx.recharts.line_chart( rx.recharts.cartesian_grid( stroke_dasharray="3 3", stroke="#2a3655" ), rx.recharts.x_axis( data_key="label", x_axis_id="labels", label={ "value": BrowserState.visuals_label_column, "position": "bottom", "offset": 14, "fill": COLOR_MUTED, }, tick={"fill": COLOR_MUTED, "fontSize": 12}, ), rx.recharts.x_axis( data_key="hint", x_axis_id="hint", hide=True, ), rx.recharts.y_axis(), rx.recharts.legend( layout="vertical", align="right", vertical_align="middle", width=116, icon_size=10, wrapper_style={ "color": COLOR_TEXT, "display": "flex", "justifyContent": "center", "alignItems": "center", "textAlign": "center", "paddingLeft": "0.75rem", }, ), rx.recharts.graphing_tooltip( content_style={ "background": COLOR_CARD_ALT, "borderColor": "#243452", "borderRadius": "8px", "color": COLOR_TEXT, "whiteSpace": "pre-line", "maxWidth": "28rem", }, label_style={ "color": COLOR_TEXT, "whiteSpace": "pre-line", "fontSize": "0.78rem", "lineHeight": 1.35, }, item_style={"color": "#93c5fd"}, ), rx.foreach( BrowserState.visuals_data_series, lambda series: rx.recharts.line( data_key=series["key"], name=series["key"], x_axis_id="hint", type_="monotone", stroke=series["color"], dot=False, ), ), data=BrowserState.visuals_chart_data, width="100%", height=340, margin={"right": 8, "left": 8, "top": 8, "bottom": 25}, ), ), width="100%", border=f"1px solid {COLOR_BORDER}", border_radius="10px", padding="0.75rem", background=COLOR_CARD_ALT, ), rx.fragment(), ), width="100%", spacing="3", align="start", ), rx.fragment(), ), rx.cond( BrowserState.selected_table == "Authentication", rx.vstack( rx.text(ui_txt("auth.panel"), color=COLOR_TEXT, font_weight="bold"), rx.hstack( rx.button( ui_txt("auth.tab_users"), on_click=BrowserState.set_auth_table("users"), background=rx.cond( BrowserState.auth_selected_table == "users", COLOR_CARD_ALT, "transparent", ), color=COLOR_TEXT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), rx.button( ui_txt("auth.tab_user_groups"), on_click=BrowserState.set_auth_table("user_groups"), background=rx.cond( BrowserState.auth_selected_table == "user_groups", COLOR_CARD_ALT, "transparent", ), color=COLOR_TEXT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), rx.button( ui_txt("auth.tab_permissions"), on_click=BrowserState.set_auth_table("permissions"), background=rx.cond( BrowserState.auth_selected_table == "permissions", COLOR_CARD_ALT, "transparent", ), color=COLOR_TEXT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), spacing="2", align="center", width="100%", ), rx.hstack( rx.button( ui_txt("auth.add"), on_click=BrowserState.add_auth_row, disabled=BrowserState.auth_is_adding_row | (BrowserState.auth_editing_row_idx >= 0), background="#253a63", color=COLOR_TEXT, border="1px solid #3a537f", border_radius="8px", _hover={"background": "#314a76", "border_color": "#476596"}, ), rx.button( ui_txt("grid.export_excel"), on_click=BrowserState.export_auth_to_excel, background="transparent", color=COLOR_TEXT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), rx.cond( BrowserState.auth_is_adding_row, rx.hstack( rx.button( ui_txt("grid.save"), on_click=BrowserState.save_auth_new_row, background=COLOR_ACCENT, color=COLOR_TEXT, border=f"1px solid {COLOR_ACCENT}", border_radius="8px", size="1", ), rx.button( ui_txt("auth.cancel"), on_click=BrowserState.cancel_add_auth_row, background="transparent", color=COLOR_MUTED, border=f"1px solid {COLOR_BORDER}", border_radius="8px", size="1", ), spacing="2", align="center", ), rx.fragment(), ), rx.cond( BrowserState.auth_is_loading, rx.text(ui_txt("md.loading"), color=COLOR_ACCENT), rx.fragment(), ), width="100%", spacing="3", align="center", ), rx.cond( BrowserState.auth_error != "", rx.text(BrowserState.auth_error, color=COLOR_ERROR, font_size="0.85rem"), rx.fragment(), ), rx.cond( (BrowserState.auth_selected_table == "user_groups") & ( BrowserState.auth_is_adding_row | (BrowserState.auth_editing_row_idx >= 0) ), rx.vstack( rx.text(ui_txt("auth.members"), color=COLOR_MUTED, font_size="0.9rem"), rx.hstack( rx.text(ui_txt("auth.add_user"), color=COLOR_MUTED, font_size="0.85rem"), rx.cond( BrowserState.auth_available_user_labels.length() > 0, rx.select( BrowserState.auth_available_user_labels, value="", placeholder=ui_txt("ph.select_user"), on_change=BrowserState.add_auth_group_member_by_label, width="18rem", class_name="field-type-select", **SELECT_STYLE, ), rx.text(ui_txt("auth.no_public_users"), color=COLOR_MUTED, font_size="0.85rem"), ), spacing="2", align="center", ), rx.cond( BrowserState.auth_user_group_members_display.length() > 0, rx.vstack( rx.foreach( BrowserState.auth_user_group_members_display, lambda label, idx: rx.hstack( rx.text(label, color=COLOR_TEXT, font_size="0.85rem"), rx.button( ui_txt("auth.remove"), on_click=BrowserState.remove_auth_group_member_at_index(idx), size="1", background="transparent", color=COLOR_ERROR, border=f"1px solid {COLOR_ERROR}", border_radius="6px", _hover={"background": "rgba(248, 113, 113, 0.12)"}, ), spacing="2", align="center", ), ), spacing="1", align="start", ), rx.fragment(), ), width="100%", spacing="2", align="start", ), rx.fragment(), ), rx.cond( BrowserState.auth_columns.length() > 0, rx.box( rx.vstack( rx.hstack( rx.text(ui_txt("auth.selected_row"), color=COLOR_MUTED, font_size="0.85rem"), rx.text( rx.cond( BrowserState.auth_selected_row_idx >= 0, BrowserState.auth_selected_row_idx + 1, "-", ), color=COLOR_TEXT, font_size="0.85rem", ), rx.button( ui_txt("expand.edit_selected"), on_click=BrowserState.start_edit_selected_auth_row, disabled=( (BrowserState.auth_selected_row_idx < 0) | BrowserState.auth_is_adding_row ), background="transparent", color=COLOR_TEXT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", size="1", ), rx.button( ui_txt("grid.save"), on_click=BrowserState.save_selected_auth_row, background=COLOR_ACCENT, color=COLOR_TEXT, border=f"1px solid {COLOR_ACCENT}", border_radius="8px", size="1", ), rx.button( ui_txt("auth.cancel"), on_click=BrowserState.cancel_selected_auth_row, background="transparent", color=COLOR_MUTED, border=f"1px solid {COLOR_BORDER}", border_radius="8px", size="1", ), rx.button( ui_txt("expand.delete_selected"), on_click=BrowserState.delete_selected_auth_row, disabled=BrowserState.auth_selected_row_idx < 0, background="transparent", color=COLOR_ERROR, border=f"1px solid {COLOR_ERROR}", border_radius="8px", size="1", _hover={"background": "rgba(248, 113, 113, 0.12)"}, ), spacing="2", align="center", width="100%", ), rx.data_editor( columns=BrowserState.auth_grid_columns, data=BrowserState.auth_rows, theme=SERVICE_DATA_EDITOR_THEME, row_markers="none", scale_to_rem=True, row_height=DATA_EDITOR_ROW_HEIGHT, header_height=DATA_EDITOR_HEADER_HEIGHT, on_cell_activated=BrowserState.select_auth_row_from_grid, on_cell_clicked=BrowserState.select_auth_row_from_grid, on_header_clicked=BrowserState.handle_auth_grid_header_clicked, on_grid_selection_change=BrowserState.handle_auth_grid_selection_change, on_cell_edited=BrowserState.handle_auth_cell_edited, grid_selection=BrowserState.auth_grid_selection, width="100%", height=( BrowserState.auth_rows.length() * DATA_EDITOR_ROW_HEIGHT ) + DATA_EDITOR_HEADER_HEIGHT + DATA_EDITOR_SCROLLBAR_GUTTER, class_name="service-data-editor", ), width="100%", spacing="2", align="start", ), width="100%", min_width="0", overflow="hidden", background=COLOR_CARD_ALT, border=f"1px solid {COLOR_BORDER}", border_radius="10px", padding="0.5rem", ), rx.text(ui_txt("auth.no_rows"), color=COLOR_MUTED), ), width="100%", spacing="3", align="start", ), rx.fragment(), ), rx.cond( BrowserState.selected_table == "Reports", rx.vstack( rx.cond( BrowserState.reports_result_only_mode, rx.fragment(), rx.vstack( rx.text( ui_txt("reports.select_only_hint"), color=COLOR_MUTED, font_size="0.85rem", ), rx.text_area( value=BrowserState.reports_sql, on_change=BrowserState.set_reports_sql, placeholder=ui_txt("ph.reports_sql"), width="100%", min_height="10rem", max_height="20rem", resize="vertical", color=COLOR_TEXT, background=COLOR_CARD_ALT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", padding="0.6rem", ), rx.hstack( rx.button( ui_txt("reports.execute"), on_click=BrowserState.execute_reports_sql, disabled=BrowserState.reports_is_loading | (BrowserState.reports_sql == ""), background="#253a63", color=COLOR_TEXT, border="1px solid #3a537f", border_radius="8px", _hover={"background": "#314a76", "border_color": "#476596"}, ), rx.button( ui_txt("reports.create_app"), on_click=BrowserState.start_create_reports_app_mode, disabled=BrowserState.reports_sql == "", background="transparent", color=COLOR_TEXT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), rx.button( ui_txt("reports.scheme_help"), on_click=BrowserState.open_schema_help, background="transparent", color=COLOR_TEXT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), rx.cond( BrowserState.reports_is_loading, rx.text(ui_txt("reports.executing"), color=COLOR_ACCENT), rx.fragment(), ), spacing="2", align="center", width="100%", ), rx.cond( BrowserState.is_create_reports_app_mode, rx.hstack( rx.text(ui_txt("md.app_name"), color=COLOR_MUTED, width="9rem"), rx.input( value=BrowserState.new_reports_app_name, on_change=BrowserState.set_new_reports_app_name, placeholder=ui_txt("ph.app_name"), width="18rem", _placeholder={"color": COLOR_MUTED}, **INPUT_STYLE, ), rx.button( ui_txt("reports.create"), on_click=BrowserState.create_app_from_reports, disabled=BrowserState.new_reports_app_name == "", background="#253a63", color=COLOR_TEXT, border="1px solid #3a537f", border_radius="8px", _hover={"background": "#314a76", "border_color": "#476596"}, ), rx.button( ui_txt("auth.cancel"), on_click=BrowserState.cancel_create_reports_app_mode, background="transparent", color=COLOR_MUTED, border=f"1px solid {COLOR_BORDER}", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), spacing="2", align="center", width="100%", ), rx.fragment(), ), width="100%", spacing="2", align="start", ), ), rx.box( rx.vstack( rx.text(ui_txt("reports.result"), color=COLOR_TEXT, font_weight="bold"), rx.cond( BrowserState.reports_error != "", rx.text( BrowserState.reports_error, color=COLOR_ERROR, font_size="0.85rem", ), rx.cond( BrowserState.reports_result_grid_columns.length() > 0, rx.data_editor( columns=BrowserState.reports_result_grid_columns, data=BrowserState.reports_result_rows, theme=SERVICE_DATA_EDITOR_THEME, row_markers="none", scale_to_rem=True, smooth_scroll_x=True, smooth_scroll_y=True, row_height=DATA_EDITOR_ROW_HEIGHT, header_height=DATA_EDITOR_HEADER_HEIGHT, min_column_width=90, max_column_width=720, width="100%", height=rx.cond( BrowserState.reports_result_rows.length() > 0, ( BrowserState.reports_result_rows.length() * DATA_EDITOR_ROW_HEIGHT ) + DATA_EDITOR_HEADER_HEIGHT + DATA_EDITOR_SCROLLBAR_GUTTER, DATA_EDITOR_HEADER_HEIGHT + DATA_EDITOR_SCROLLBAR_GUTTER, ), class_name="service-data-editor", ), rx.text( ui_txt("reports.run_hint"), color=COLOR_MUTED, ), ), ), width="100%", spacing="2", align="start", ), width="100%", min_width="0", overflow="hidden", background=COLOR_CARD_ALT, border=f"1px solid {COLOR_BORDER}", border_radius="10px", padding="0.6rem", ), width="100%", spacing="3", align="start", ), rx.fragment(), ), rx.cond( BrowserState.tables.contains(BrowserState.selected_table) & (BrowserState.selected_table != "application_groups"), rx.hstack( rx.cond( BrowserState.selected_table != "applications", rx.button( ui_txt("grid.add_row"), on_click=BrowserState.add_row, background="#253a63", color=COLOR_TEXT, border="1px solid #3a537f", border_radius="8px", _hover={"background": "#314a76", "border_color": "#476596"}, ), rx.fragment(), ), rx.cond( BrowserState.custom_tables.contains(BrowserState.selected_table) & ~BrowserState.custom_table_opened_from_application_tile, rx.button( ui_txt("grid.create_app"), on_click=BrowserState.start_create_app_mode, background="transparent", color=COLOR_TEXT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), rx.fragment(), ), rx.cond( BrowserState.custom_tables.contains(BrowserState.selected_table) & ~BrowserState.custom_table_opened_from_application_tile, rx.button( ui_txt("grid.delete_table"), on_click=[ BrowserState.select_custom_table_target( BrowserState.selected_table ), BrowserState.delete_custom_table, ], background="transparent", color=COLOR_ERROR, border=f"1px solid {COLOR_ERROR}", border_radius="8px", _hover={"background": "rgba(248, 113, 113, 0.12)"}, ), rx.fragment(), ), rx.cond( BrowserState.selected_table == "applications", rx.button( ui_txt("grid.edit_application"), on_click=BrowserState.open_selected_application_from_admin_grid, background="transparent", color=COLOR_TEXT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", title=ui_txt("grid.edit_app_title"), _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), rx.fragment(), ), rx.button( ui_txt("grid.export_excel"), on_click=BrowserState.export_to_excel, background="transparent", color=COLOR_TEXT, border=f"1px solid {COLOR_BORDER}", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), rx.button( rx.cond( BrowserState.selected_table == "applications", ui_txt("grid.delete_application"), ui_txt("grid.delete_row"), ), on_click=BrowserState.delete_selected_service_row, disabled=( (BrowserState.service_grid_selected_row_idx < 0) | ( BrowserState.service_tables.contains(BrowserState.selected_table) & (BrowserState.selected_table != "applications") & (BrowserState.selected_table != "application_groups") ) ), background="transparent", color=COLOR_ERROR, border=f"1px solid {COLOR_ERROR}", border_radius="8px", _hover={"background": "rgba(248, 113, 113, 0.12)"}, ), rx.cond( BrowserState.is_adding_row, rx.button( ui_txt("grid.save"), on_click=BrowserState.save_new_row, background=COLOR_ACCENT, color=COLOR_TEXT, border=f"1px solid {COLOR_ACCENT}", border_radius="8px", _hover={"background": "#2563eb", "border_color": "#2563eb"}, ), rx.fragment(), ), spacing="2", align="center", width="100%", ), rx.fragment(), ), rx.cond( BrowserState.is_create_app_mode, rx.hstack( rx.text(ui_txt("md.app_name"), color=COLOR_MUTED, width="9rem"), rx.input( value=BrowserState.new_app_name, on_change=BrowserState.set_new_app_name, placeholder=ui_txt("ph.app_name"), width="18rem", _placeholder={"color": COLOR_MUTED}, **INPUT_STYLE, ), rx.button( ui_txt("reports.create"), on_click=BrowserState.create_app_from_selected_table, disabled=BrowserState.new_app_name == "", background="#253a63", color=COLOR_TEXT, border="1px solid #3a537f", border_radius="8px", _hover={"background": "#314a76", "border_color": "#476596"}, ), rx.button( ui_txt("auth.cancel"), on_click=BrowserState.cancel_create_app_mode, background="transparent", color=COLOR_MUTED, border=f"1px solid {COLOR_BORDER}", border_radius="8px", _hover={"background": COLOR_CARD_ALT, "border_color": COLOR_ACCENT}, ), spacing="2", align="center", width="100%", ), rx.fragment(), ), rx.cond( BrowserState.service_tables.contains(BrowserState.selected_table) & (BrowserState.selected_table != "application_groups"), rx.hstack( rx.cond( BrowserState.editing_row_idx >= 0, rx.hstack( rx.button( ui_txt("grid.save_edit"), on_click=BrowserState.save_selected_service_row_edit, background=COLOR_ACCENT, color=COLOR_TEXT, border=f"1px solid {COLOR_ACCENT}", border_radius="8px", size="1", ), rx.button( ui_txt("grid.cancel_edit"), on_click=BrowserState.cancel_selected_service_row_edit, background="transparent", color=COLOR_MUTED, border=f"1px solid {COLOR_BORDER}", border_radius="8px", size="1", ), spacing="1", ), rx.fragment(), ), spacing="2", align="center", width="100%", ), rx.fragment(), ), rx.cond( BrowserState.tables.contains(BrowserState.selected_table) & ~BrowserState.service_tables.contains(BrowserState.selected_table) & (BrowserState.selected_table != "application_groups"), rx.hstack( rx.cond( BrowserState.editing_row_idx >= 0, rx.hstack( rx.button( ui_txt("grid.save_edit"), on_click=BrowserState.save_selected_service_row_edit, background=COLOR_ACCENT, color=COLOR_TEXT, border=f"1px solid {COLOR_ACCENT}", border_radius="8px", size="1", ), rx.button( ui_txt("grid.cancel_edit"), on_click=BrowserState.cancel_selected_service_row_edit, background="transparent", color=COLOR_MUTED, border=f"1px solid {COLOR_BORDER}", border_radius="8px", size="1", ), spacing="1", ), rx.fragment(), ), spacing="2", align="center", width="100%", ), rx.fragment(), ), rx.cond( (BrowserState.selected_table != "Custom Tables") & (BrowserState.selected_table != "application_groups") & (BrowserState.selected_table != "Visuals") & (BrowserState.selected_table != "Master/Detail") & (BrowserState.selected_table != "Authentication"), rx.cond( BrowserState.columns.length() > 0, rx.cond( BrowserState.service_tables.contains(BrowserState.selected_table), rx.box( rx.data_editor( columns=BrowserState.service_grid_columns, data=BrowserState.rows, theme=SERVICE_DATA_EDITOR_THEME, row_markers="none", scale_to_rem=True, smooth_scroll_x=True, smooth_scroll_y=True, row_height=DATA_EDITOR_ROW_HEIGHT, header_height=DATA_EDITOR_HEADER_HEIGHT, min_column_width=90, max_column_width=720, on_cell_activated=BrowserState.handle_service_grid_cell_activated, on_cell_clicked=BrowserState.select_service_grid_cell, on_header_clicked=BrowserState.handle_service_grid_header_clicked, on_grid_selection_change=BrowserState.handle_service_grid_selection_change, on_cell_edited=BrowserState.handle_service_grid_cell_edited, on_column_resize=BrowserState.handle_service_grid_column_resize, grid_selection=BrowserState.service_grid_selection, width="100%", height=(BrowserState.rows.length() * DATA_EDITOR_ROW_HEIGHT) + DATA_EDITOR_HEADER_HEIGHT + DATA_EDITOR_SCROLLBAR_GUTTER, class_name="service-data-editor", ), rx.cond( (BrowserState.selected_table == "applications") & (BrowserState.applications_inline_type_row_idx >= 0), rx.box( rx.select( APPLICATION_TYPE_OPTIONS, value=BrowserState.applications_inline_type_value, on_change=BrowserState.set_inline_applications_type_value, open=BrowserState.applications_inline_type_open, on_open_change=BrowserState.set_applications_inline_type_open, width="fit-content", class_name="field-type-select", **SELECT_STYLE, ), position="absolute", top=BrowserState.applications_inline_type_top, left=BrowserState.applications_inline_type_left, width="fit-content", z_index="20", ), rx.fragment(), ), rx.cond( (BrowserState.selected_table == "applications") & (BrowserState.applications_inline_dashboard_row_idx >= 0), rx.box( rx.select( BrowserState.application_group_labels, value=BrowserState.applications_inline_dashboard_value, on_change=BrowserState.set_inline_applications_dashboard_value, open=BrowserState.applications_inline_dashboard_open, on_open_change=BrowserState.set_applications_inline_dashboard_open, width="fit-content", class_name="field-type-select", **SELECT_STYLE, ), position="absolute", top=BrowserState.applications_inline_dashboard_top, left=BrowserState.applications_inline_dashboard_left, width="fit-content", z_index="20", ), rx.fragment(), ), class_name="service-data-editor-host", width="100%", min_width="0", position="relative", overflow="hidden", background=COLOR_CARD_ALT, border=f"1px solid {COLOR_BORDER}", border_radius="10px", padding="0", ), rx.box( rx.data_editor( columns=BrowserState.service_grid_columns, data=BrowserState.rows, theme=SERVICE_DATA_EDITOR_THEME, row_markers="none", scale_to_rem=True, smooth_scroll_x=True, smooth_scroll_y=True, row_height=DATA_EDITOR_ROW_HEIGHT, header_height=DATA_EDITOR_HEADER_HEIGHT, min_column_width=90, max_column_width=720, on_cell_activated=BrowserState.handle_service_grid_cell_activated, on_cell_clicked=BrowserState.select_service_grid_cell, on_header_clicked=BrowserState.handle_service_grid_header_clicked, on_grid_selection_change=BrowserState.handle_service_grid_selection_change, on_cell_edited=BrowserState.handle_service_grid_cell_edited, on_column_resize=BrowserState.handle_service_grid_column_resize, grid_selection=BrowserState.service_grid_selection, width="100%", height=(BrowserState.rows.length() * DATA_EDITOR_ROW_HEIGHT) + DATA_EDITOR_HEADER_HEIGHT + DATA_EDITOR_SCROLLBAR_GUTTER, class_name="service-data-editor", ), class_name="service-data-editor-host", width="100%", min_width="0", overflow="hidden", background=COLOR_CARD_ALT, border=f"1px solid {COLOR_BORDER}", border_radius="10px", padding="0", ), ), rx.text(ui_txt("grid.no_rows_table"), color=COLOR_MUTED), ), rx.fragment(), ), width="100%", height="100%", spacing="3", align="start", ) def index() -> rx.Component: return _main_layout( left_content=_navigation_panel(), right_content=_view_panel(), header_actions=rx.hstack( rx.box( rx.hstack( rx.button( "Ru", on_click=BrowserState.select_locale_ru, flex="1", min_width="2.25rem", background=rx.cond( BrowserState.ui_locale == "ru", COLOR_ACCENT, "transparent", ), color=rx.cond( BrowserState.ui_locale == "ru", "#f1f5f9", COLOR_TEXT, ), border="none", border_radius="0", padding="0.35rem 0.6rem", font_size="0.85rem", font_weight="medium", cursor="pointer", _hover={"opacity": "0.92"}, ), rx.button( "En", on_click=BrowserState.select_locale_en, flex="1", min_width="2.25rem", background=rx.cond( BrowserState.ui_locale == "en", COLOR_ACCENT, "transparent", ), color=rx.cond( BrowserState.ui_locale == "en", "#f1f5f9", COLOR_TEXT, ), border="none", border_radius="0", padding="0.35rem 0.6rem", font_size="0.85rem", font_weight="medium", cursor="pointer", _hover={"opacity": "0.92"}, ), spacing="0", align="stretch", ), border=f"1px solid {COLOR_BORDER}", border_radius="8px", overflow="hidden", display="flex", ), rx.button( rx.text( rx.cond( BrowserState.is_authenticated, ui_txt("auth.logout"), ui_txt("auth.login"), ), font_size="0.9rem", font_weight="medium", ), on_click=BrowserState.toggle_login_state, background="transparent", border=rx.cond( BrowserState.is_authenticated, f"1px solid {COLOR_BORDER}", f"1px solid {COLOR_ERROR}", ), padding="0.35rem", border_radius="8px", _hover={ "background": COLOR_CARD_ALT, "border_color": rx.cond( BrowserState.is_authenticated, COLOR_ACCENT, "#fca5a5", ), }, ), spacing="2", align="center", ), heading_text=ui_txt("layout.sphere_platform"), ) class ApplicationGroupsOrderPayload(BaseModel): ordered_ids: list[str] = Field(default_factory=list) async def api_save_application_groups_order(request: Request): """Persist group order after DnD (Starlette route on Reflex backend).""" try: data = await request.json() body = ApplicationGroupsOrderPayload(**data) client = _sphere_supabase_client_for_api() for i, gid in enumerate(body.ordered_ids): gid = (gid or "").strip() if not gid: continue client.table("application_groups").update({"sort_order": f"{i:06d}"}).eq( "id", gid ).execute() return JSONResponse({"ok": True}) except Exception as exc: return JSONResponse({"detail": str(exc)}, status_code=500) app = rx.App(stylesheets=["/theme_overrides.css"]) app._api.add_route( "/api/sphere/application-groups-order", api_save_application_groups_order, methods=["POST"], ) app.add_page( index, on_load=BrowserState.load_tables, title="Sphere Platform", image="sphere-favicon.svg", meta=[ rx.el.link( rel="icon", href="/sphere-favicon.svg", type="image/svg+xml", ), rx.el.link( rel="shortcut icon", href="/sphere-favicon.svg", type="image/svg+xml", ), ], )