Added Application HolidayViewer
This commit is contained in:
51
src/components/admin/AdminApp.py
Normal file
51
src/components/admin/AdminApp.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from fasthtml.fastapp import fast_app
|
||||
|
||||
from components.admin.constants import Routes
|
||||
from core.instance_manager import debug_session, InstanceManager
|
||||
|
||||
admin_app, rt = fast_app()
|
||||
|
||||
logger = logging.getLogger("admin")
|
||||
|
||||
|
||||
@rt(Routes.AiBuddy)
|
||||
def get(session, _id: str, boundaries: str):
|
||||
logger.debug(f"Entering {Routes.AiBuddy} - GET with args {debug_session(session)}, {_id=}, {boundaries=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.show_ai_buddy_form(json.loads(boundaries) if boundaries else None)
|
||||
|
||||
|
||||
@rt(Routes.AiBuddy)
|
||||
def post(session, _id: str, args: dict):
|
||||
logger.debug(f"Entering {Routes.AiBuddy} - POST with args {debug_session(session)}, {_id=}, {args=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.update_ai_buddy_settings(args)
|
||||
|
||||
@rt(Routes.AiBuddyCancel)
|
||||
def post(session, _id: str):
|
||||
logger.debug(f"Entering {Routes.AiBuddyCancel} with args {debug_session(session)}, {_id=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.cancel_ai_buddy_settings()
|
||||
|
||||
|
||||
@rt(Routes.ImportHolidays)
|
||||
def get(session, _id: str, boundaries: str):
|
||||
logger.debug(f"Entering {Routes.ImportHolidays} - GET with args {debug_session(session)}, {_id=}, {boundaries=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.show_import_holidays_form(json.loads(boundaries) if boundaries else None)
|
||||
|
||||
|
||||
@rt(Routes.PasteHolidays)
|
||||
def post(session, _id: str, content: str):
|
||||
logger.debug(f"Entering {Routes.PasteHolidays} with args {debug_session(session)}, {_id=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.on_paste(content)
|
||||
|
||||
@rt(Routes.ImportHolidays)
|
||||
def post(session, _id: str):
|
||||
logger.debug(f"Entering {Routes.ImportHolidays} with args {debug_session(session)}, {_id=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.import_holidays()
|
||||
0
src/components/admin/__init__.py
Normal file
0
src/components/admin/__init__.py
Normal file
39
src/components/admin/admin_db_manager.py
Normal file
39
src/components/admin/admin_db_manager.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from ai.mcp_client import InProcessMCPClientCustomTools
|
||||
from ai.mcp_tools import MCPServerTools
|
||||
from auth.auth_manager import AuthManager
|
||||
from components.admin.constants import ADMIN_SETTINGS_ENTRY
|
||||
from config import OLLAMA_HOST
|
||||
from core.settings_management import SettingsManager, NestedSettingsManager
|
||||
from core.utils import split_host_port
|
||||
|
||||
|
||||
@dataclass
|
||||
class AiBuddySettingsEntry:
|
||||
ollama_ip: str = ""
|
||||
ollama_port: int = 11434
|
||||
ollama_model: str = "mistral"
|
||||
llm_mode: str = InProcessMCPClientCustomTools.ID
|
||||
available_tools: list = field(default_factory=MCPServerTools.list_tools)
|
||||
|
||||
def __post_init__(self):
|
||||
host, port = split_host_port(OLLAMA_HOST)
|
||||
self.ollama_ip = host
|
||||
self.ollama_port = port
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminSettings:
|
||||
ai_buddy: AiBuddySettingsEntry = field(default_factory=AiBuddySettingsEntry)
|
||||
|
||||
|
||||
class AdminDbManager:
|
||||
def __init__(self, session: dict, settings_manager: SettingsManager):
|
||||
self._session = session
|
||||
self._settings_manager = settings_manager
|
||||
self.ai_buddy = NestedSettingsManager(AuthManager.admin_session(),
|
||||
settings_manager,
|
||||
ADMIN_SETTINGS_ENTRY,
|
||||
AdminSettings,
|
||||
"ai_buddy")
|
||||
8
src/components/admin/assets/Admin.css
Normal file
8
src/components/admin/assets/Admin.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.adm-items-group {
|
||||
border: 1px solid color-mix(in oklab, var(--color-base-content) 20%, #0000);
|
||||
border-radius: .25rem;
|
||||
padding: 4px;
|
||||
background-color: var(--color-base-100);
|
||||
align-items: center;
|
||||
padding-inline: .75rem;
|
||||
}
|
||||
0
src/components/admin/assets/__init__.py
Normal file
0
src/components/admin/assets/__init__.py
Normal file
62
src/components/admin/commands.py
Normal file
62
src/components/admin/commands.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from components.BaseCommandManager import BaseCommandManager
|
||||
from components.admin.constants import ROUTE_ROOT, Routes
|
||||
|
||||
|
||||
class AdminCommandManager(BaseCommandManager):
|
||||
def __init__(self, owner):
|
||||
super().__init__(owner)
|
||||
|
||||
def show_ai_buddy(self):
|
||||
return {
|
||||
"hx-get": f"{ROUTE_ROOT}{Routes.AiBuddy}",
|
||||
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'js:{{"_id": "{self._id}", boundaries: getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}',
|
||||
}
|
||||
|
||||
def save_ai_buddy(self):
|
||||
return {
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.AiBuddy}",
|
||||
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'js:{{"_id": "{self._id}"}}',
|
||||
# The form adds the rest
|
||||
}
|
||||
|
||||
def cancel_ai_buddy(self):
|
||||
return {
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.AiBuddyCancel}",
|
||||
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'js:{{"_id": "{self._id}"}}',
|
||||
}
|
||||
|
||||
def show_import_holidays(self):
|
||||
return {
|
||||
"hx-get": f"{ROUTE_ROOT}{Routes.ImportHolidays}",
|
||||
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'js:{{"_id": "{self._id}", boundaries: getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}',
|
||||
}
|
||||
|
||||
|
||||
class ImportHolidaysCommandManager(BaseCommandManager):
|
||||
def __init__(self, owner):
|
||||
super().__init__(owner)
|
||||
|
||||
def on_paste(self):
|
||||
return {
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.PasteHolidays}",
|
||||
"hx-target": f"#{self._owner.datagrid.get_id()}",
|
||||
"hx_trigger": "keydown[ctrlKey && key=='Enter']",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'js:{{"_id": "{self._id}"}}',
|
||||
}
|
||||
|
||||
def import_holidays(self):
|
||||
return {
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.ImportHolidays}",
|
||||
"hx-target": f"#{self._owner.datagrid.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'js:{{"_id": "{self._id}"}}',
|
||||
}
|
||||
136
src/components/admin/components/Admin.py
Normal file
136
src/components/admin/components/Admin.py
Normal file
@@ -0,0 +1,136 @@
|
||||
from fasthtml.components import *
|
||||
|
||||
from ai.mcp_client import MPC_CLIENTS_IDS
|
||||
from ai.mcp_tools import MCPServerTools
|
||||
from components.BaseComponent import BaseComponent
|
||||
from components.admin.admin_db_manager import AdminDbManager
|
||||
from components.admin.commands import AdminCommandManager
|
||||
from components.admin.components.AdminForm import AdminFormItem, AdminFormType, AdminForm
|
||||
from components.admin.components.ImportHolidays import ImportHolidays
|
||||
from components.admin.constants import ADMIN_INSTANCE_ID, ADMIN_AI_BUDDY_INSTANCE_ID, ADMIN_IMPORT_HOLIDAYS_INSTANCE_ID
|
||||
from components.aibuddy.assets.icons import icon_brain_ok
|
||||
from components.hoildays.assets.icons import icon_holidays
|
||||
from components.tabs.components.MyTabs import MyTabs
|
||||
from components_helpers import mk_ellipsis, mk_icon
|
||||
from core.instance_manager import InstanceManager
|
||||
|
||||
|
||||
class Admin(BaseComponent):
|
||||
def __init__(self, session, _id: str = None, settings_manager=None, tabs_manager: MyTabs = None):
|
||||
super().__init__(session, _id)
|
||||
self.settings_manager = settings_manager
|
||||
self.tabs_manager = tabs_manager
|
||||
self.commands = AdminCommandManager(self)
|
||||
self.db = AdminDbManager(session, settings_manager)
|
||||
|
||||
def show_ai_buddy_form(self, boundaries):
|
||||
fields = [
|
||||
AdminFormItem('ollama_ip', "Ollama IP", "IP address of the Ollama server.", AdminFormType.TEXT),
|
||||
AdminFormItem("ollama_port", "Ollama Port", "Port of the Ollama server.", AdminFormType.NUMBER),
|
||||
AdminFormItem("ollama_model", "LLM Model", "Model to use for Ollama.", AdminFormType.SELECT, ["mistral"]),
|
||||
AdminFormItem("llm_mode", "LLM Mode", "Type of LLM to use (Dev)", AdminFormType.SELECT, MPC_CLIENTS_IDS),
|
||||
AdminFormItem("available_tools", "Available Tools", "Possible tools", AdminFormType.CHECKBOX,
|
||||
MCPServerTools.list_tools()),
|
||||
]
|
||||
hooks = {
|
||||
"on_ok": self.commands.save_ai_buddy(),
|
||||
"on_cancel": self.commands.cancel_ai_buddy(),
|
||||
"ok_title": "Apply"
|
||||
}
|
||||
form = InstanceManager.get(self._session,
|
||||
AdminForm.create_component_id(self._session, prefix=self._id),
|
||||
AdminForm,
|
||||
owner=self,
|
||||
title="AI Buddy Configuration Page",
|
||||
obj=self.db.ai_buddy,
|
||||
form_fields=fields,
|
||||
hooks=hooks,
|
||||
key=ADMIN_AI_BUDDY_INSTANCE_ID,
|
||||
boundaries=boundaries)
|
||||
|
||||
return self._add_tab(ADMIN_AI_BUDDY_INSTANCE_ID, "Admin - AI Buddy", form)
|
||||
|
||||
def show_import_holidays_form(self, boundaries):
|
||||
form = InstanceManager.get(self._session,
|
||||
ImportHolidays.create_component_id(self._session, prefix=self._id),
|
||||
ImportHolidays,
|
||||
settings_manager=self.settings_manager,
|
||||
boundaries=boundaries)
|
||||
|
||||
return self._add_tab(ADMIN_AI_BUDDY_INSTANCE_ID, "Admin - Import Holidays", form)
|
||||
|
||||
def update_ai_buddy_settings(self, values: dict):
|
||||
values = self.manage_lists(values)
|
||||
self.db.ai_buddy.update(values, ignore_missing=True)
|
||||
return self.tabs_manager.render()
|
||||
|
||||
def cancel_ai_buddy_settings(self):
|
||||
tab_id = self.tabs_manager.get_tab_id(ADMIN_AI_BUDDY_INSTANCE_ID)
|
||||
self.tabs_manager.remove_tab(tab_id)
|
||||
return self.tabs_manager.render()
|
||||
|
||||
def __ft__(self):
|
||||
return Div(
|
||||
Div(cls="divider"),
|
||||
Div(mk_ellipsis("Admin", cls="text-sm font-medium mb-1 mr-3")),
|
||||
#
|
||||
Div(
|
||||
mk_icon(icon_brain_ok, can_select=False),
|
||||
mk_ellipsis("AI Buddy", cls="text-sm", **self.commands.show_ai_buddy()),
|
||||
cls="flex p-0 min-h-0 truncate",
|
||||
),
|
||||
Div(
|
||||
mk_icon(icon_holidays, can_select=False),
|
||||
mk_ellipsis("holidays", cls="text-sm", **self.commands.show_import_holidays()),
|
||||
cls="flex p-0 min-h-0 truncate",
|
||||
),
|
||||
#
|
||||
# cls=""),
|
||||
# Script(f"bindAdmin('{self._id}')"),
|
||||
id=f"{self._id}"
|
||||
)
|
||||
|
||||
def _add_tab(self, tab_key, title, content):
|
||||
self.tabs_manager.add_tab(title, content, key=tab_key)
|
||||
return self.tabs_manager.render()
|
||||
|
||||
@staticmethod
|
||||
def create_component_id(session):
|
||||
return f"{ADMIN_INSTANCE_ID}{session['user_id']}"
|
||||
|
||||
@staticmethod
|
||||
def manage_lists(data_dict):
|
||||
"""
|
||||
Processes a dictionary of key-value pairs to reorganize keys based on specific
|
||||
criteria. If a key ends with its corresponding string value, the method extracts
|
||||
the prefix of the key (the portion of the key before the value) and groups the
|
||||
value under this prefix in a list. Otherwise, the original key-value pair is
|
||||
preserved in the resulting dictionary.
|
||||
|
||||
:param data_dict: Dictionary where each key is a string and its corresponding
|
||||
value can be of any type.
|
||||
:type data_dict: dict
|
||||
:return: A dictionary where the keys have been categorized into groups
|
||||
based on whether they end with the same string value, reorganized into
|
||||
lists, while preserving other key-value pairs as they are.
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
result_dict = {}
|
||||
|
||||
for key, value in data_dict.items():
|
||||
# Check if the value is a string and the key ends with the value
|
||||
if isinstance(value, str) and key.endswith(value):
|
||||
# Find the beginning part of the key (before the value)
|
||||
prefix = key.replace(value, '').rstrip('_')
|
||||
|
||||
# Add the value to the list under the prefix key
|
||||
if prefix not in result_dict:
|
||||
result_dict[prefix] = []
|
||||
|
||||
result_dict[prefix].append(value)
|
||||
|
||||
else:
|
||||
result_dict[key] = value
|
||||
|
||||
return result_dict
|
||||
121
src/components/admin/components/AdminForm.py
Normal file
121
src/components/admin/components/AdminForm.py
Normal file
@@ -0,0 +1,121 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from fasthtml.components import *
|
||||
|
||||
from components.BaseComponent import BaseComponent
|
||||
from components_helpers import set_boundaries, mk_dialog_buttons, safe_get_dialog_buttons_parameters
|
||||
from core.utils import get_unique_id
|
||||
|
||||
|
||||
class AdminFormType:
|
||||
TEXT = "text"
|
||||
NUMBER = "number"
|
||||
DATE = "date"
|
||||
SELECT = "select"
|
||||
CHECKBOX = "checkbox"
|
||||
RADIO = "radio"
|
||||
TEXTAREA = "textarea"
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminFormItem:
|
||||
name: str
|
||||
title: str
|
||||
description: str
|
||||
type: str
|
||||
possible_values: list[str] = None
|
||||
|
||||
|
||||
class AdminForm(BaseComponent):
|
||||
def __init__(self, session, _id, owner, title: str, obj: Any, form_fields: list[AdminFormItem], hooks=None, key=None,
|
||||
boundaries=None):
|
||||
super().__init__(session, _id)
|
||||
self._owner = owner
|
||||
self._hooks = hooks
|
||||
self._key = key
|
||||
self._boundaries = boundaries
|
||||
self.title = title
|
||||
self.obj = obj
|
||||
self.form_fields = form_fields
|
||||
|
||||
def mk_input(self, item: AdminFormItem):
|
||||
return Input(
|
||||
type=item.type,
|
||||
value=getattr(self.obj, item.name),
|
||||
id=item.name,
|
||||
cls="input"
|
||||
)
|
||||
|
||||
def mk_checkbox(self, item: AdminFormItem):
|
||||
if not item.possible_values:
|
||||
return Div("No options available")
|
||||
|
||||
current_values = getattr(self.obj, item.name, [])
|
||||
|
||||
checkbox_items = [Div(
|
||||
Input(
|
||||
Label(value),
|
||||
type="checkbox",
|
||||
value=value,
|
||||
id=f"{item.name}_{value}",
|
||||
cls="checkbox checkbox-xs",
|
||||
checked=value in current_values
|
||||
),
|
||||
|
||||
cls="checkbox-item") for value in item.possible_values]
|
||||
|
||||
return Div(*checkbox_items, cls="adm-items-group")
|
||||
|
||||
def mk_select(self, item: AdminFormItem):
|
||||
if not item.possible_values:
|
||||
return Div("No options available")
|
||||
|
||||
current_value = getattr(self.obj, item.name, "")
|
||||
|
||||
options = [
|
||||
Option(
|
||||
value,
|
||||
value=value,
|
||||
selected=value == current_value
|
||||
) for value in item.possible_values
|
||||
]
|
||||
|
||||
return Select(
|
||||
*options,
|
||||
id=item.name,
|
||||
cls="select select-bordered w-full"
|
||||
)
|
||||
|
||||
def mk_item(self, item: AdminFormItem):
|
||||
if item.type == AdminFormType.CHECKBOX:
|
||||
return self.mk_checkbox(item)
|
||||
elif item.type == AdminFormType.SELECT:
|
||||
return self.mk_select(item)
|
||||
else:
|
||||
return self.mk_input(item)
|
||||
|
||||
def __ft__(self):
|
||||
return Form(
|
||||
Fieldset(Legend(self.title, cls="fieldset-legend"),
|
||||
*[
|
||||
Div(
|
||||
Label(item.title, cls="label"),
|
||||
self.mk_item(item),
|
||||
P(item.description, cls="label"),
|
||||
)
|
||||
|
||||
for item in self.form_fields
|
||||
],
|
||||
mk_dialog_buttons(**safe_get_dialog_buttons_parameters(self._hooks)),
|
||||
**set_boundaries(self._boundaries),
|
||||
cls="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4"
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_component_id(session, prefix=None, suffix=None):
|
||||
if suffix is None:
|
||||
suffix = get_unique_id()
|
||||
|
||||
return f"{prefix}{suffix}"
|
||||
59
src/components/admin/components/ImportHolidays.py
Normal file
59
src/components/admin/components/ImportHolidays.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from fasthtml.components import *
|
||||
from pandas import DataFrame
|
||||
|
||||
from components.BaseComponent import BaseComponent
|
||||
from helpers.ComponentsInstancesHelper import ComponentsInstancesHelper
|
||||
from components.admin.commands import ImportHolidaysCommandManager
|
||||
from components.admin.constants import ADMIN_IMPORT_HOLIDAYS_INSTANCE_ID
|
||||
from components.datagrid_new.components.DataGrid import DataGrid
|
||||
from components.datagrid_new.settings import DataGridSettings
|
||||
from components.hoildays.helpers.nibelisparser import NibelisParser
|
||||
from components.repositories.constants import USERS_REPOSITORY_NAME, HOLIDAYS_TABLE_NAME
|
||||
from components_helpers import mk_dialog_buttons, set_boundaries
|
||||
from conftest import session
|
||||
from core.instance_manager import InstanceManager
|
||||
|
||||
|
||||
class ImportHolidays(BaseComponent):
|
||||
def __init__(self, session, _id, settings_manager, boundaries=None):
|
||||
super().__init__(session, _id)
|
||||
self._settings_manager = settings_manager
|
||||
self._boundaries = boundaries
|
||||
self.commands = ImportHolidaysCommandManager(self)
|
||||
self.datagrid = InstanceManager.get(session,
|
||||
DataGrid.create_component_id(session),
|
||||
DataGrid,
|
||||
grid_settings=DataGridSettings(views_visible=False,
|
||||
open_file_visible=False,
|
||||
open_settings_visible=False))
|
||||
|
||||
def on_paste(self, content):
|
||||
parser = NibelisParser(content)
|
||||
holidays = parser.parse()
|
||||
self.datagrid.init_from_dataframe(DataFrame(holidays))
|
||||
return self.datagrid
|
||||
|
||||
def import_holidays(self):
|
||||
repositories = ComponentsInstancesHelper.get_repositories(self._session)
|
||||
key = repositories.db.ensure_exists(USERS_REPOSITORY_NAME, HOLIDAYS_TABLE_NAME)
|
||||
datagrid = DataGrid(self._session, DataGrid.create_component_id(self._session), key, self._settings_manager)
|
||||
datagrid.init_from_dataframe(self.datagrid.get_dataframe(), save_state=True)
|
||||
return repositories.refresh()
|
||||
|
||||
def __ft__(self):
|
||||
return Div(
|
||||
Div("Import holidays...", cls="mb-2"),
|
||||
Textarea(name="content",
|
||||
cls="textarea",
|
||||
placeholder="Paste the content.\nCtl+Enter when done.",
|
||||
**self.commands.on_paste()),
|
||||
Div(self.datagrid, cls="mt-2"),
|
||||
mk_dialog_buttons(ok_title="Import", cls="mt-2", on_ok=self.commands.import_holidays()),
|
||||
id=self._id,
|
||||
cls="m-2",
|
||||
**set_boundaries(self._boundaries, other=26),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_component_id(session, prefix=None, suffix=None):
|
||||
return f"{ADMIN_IMPORT_HOLIDAYS_INSTANCE_ID}{session['user_id']}"
|
||||
0
src/components/admin/components/__init__.py
Normal file
0
src/components/admin/components/__init__.py
Normal file
11
src/components/admin/constants.py
Normal file
11
src/components/admin/constants.py
Normal file
@@ -0,0 +1,11 @@
|
||||
ADMIN_INSTANCE_ID = "__Admin__"
|
||||
ADMIN_AI_BUDDY_INSTANCE_ID = "__AdminAIBuddy__"
|
||||
ADMIN_IMPORT_HOLIDAYS_INSTANCE_ID = "__AdminImportHolidays__"
|
||||
ROUTE_ROOT = "/admin"
|
||||
ADMIN_SETTINGS_ENTRY = "Admin"
|
||||
|
||||
class Routes:
|
||||
AiBuddy = "/ai-buddy"
|
||||
AiBuddyCancel = "/ai-buddy-cancel"
|
||||
ImportHolidays = "/import-holidays"
|
||||
PasteHolidays = "/paste-holidays"
|
||||
0
src/components/admin/settings.py
Normal file
0
src/components/admin/settings.py
Normal file
Reference in New Issue
Block a user