diff --git a/src/components/admin/AdminApp.py b/src/components/admin/AdminApp.py index fb307dc..46ca798 100644 --- a/src/components/admin/AdminApp.py +++ b/src/components/admin/AdminApp.py @@ -66,4 +66,10 @@ def post(session, _id: str, args: dict): def post(session, _id: str): logger.debug(f"Entering {Routes.ConfigureJiraCancel} with args {debug_session(session)}, {_id=}") instance = InstanceManager.get(session, _id) - return instance.cancel_jira_settings() \ No newline at end of file + return instance.cancel_jira_settings() + +@rt(Routes.ConfigureJiraTest) +def post(session, _id: str, args: dict): + logger.debug(f"Entering {Routes.ConfigureJiraTest} with args {debug_session(session)}, {_id=}, {args=}") + instance = InstanceManager.get(session, _id) + return instance.test_jira_settings(args) \ No newline at end of file diff --git a/src/components/admin/assets/icons.py b/src/components/admin/assets/icons.py index 6caaea7..45097b8 100644 --- a/src/components/admin/assets/icons.py +++ b/src/components/admin/assets/icons.py @@ -1,10 +1,31 @@ from fastcore.basics import NotStr -icon_jira = NotStr(""" +icon_jira = NotStr(""" -""") \ No newline at end of file +""") + + +icon_msg_info = NotStr(""" + + +""") + +icon_msg_success = NotStr(""" + + +""") + +icon_msg_warning = NotStr(""" + + +""") + +icon_msg_error = NotStr(""" + + +""") \ No newline at end of file diff --git a/src/components/admin/commands.py b/src/components/admin/commands.py index 4b6e0df..eaaf9a6 100644 --- a/src/components/admin/commands.py +++ b/src/components/admin/commands.py @@ -63,6 +63,14 @@ class AdminCommandManager(BaseCommandManager): "hx-swap": "outerHTML", "hx-vals": f'js:{{"_id": "{self._id}"}}', } + + def test_jira(self): + return { + "hx-post": f"{ROUTE_ROOT}{Routes.ConfigureJiraTest}", + "hx-target": f"#{self._owner.tabs_manager.get_id()}", + "hx-swap": "outerHTML", + "hx-vals": f'js:{{"_id": "{self._id}"}}', + } class ImportHolidaysCommandManager(BaseCommandManager): def __init__(self, owner): diff --git a/src/components/admin/components/Admin.py b/src/components/admin/components/Admin.py index c7623df..9157d4f 100644 --- a/src/components/admin/components/Admin.py +++ b/src/components/admin/components/Admin.py @@ -6,7 +6,7 @@ from components.BaseComponent import BaseComponent from components.admin.admin_db_manager import AdminDbManager from components.admin.assets.icons import icon_jira from components.admin.commands import AdminCommandManager -from components.admin.components.AdminForm import AdminFormItem, AdminFormType, AdminForm +from components.admin.components.AdminForm import AdminFormItem, AdminFormType, AdminForm, AdminButton, AdminMessageType from components.admin.components.ImportHolidays import ImportHolidays from components.admin.constants import ADMIN_INSTANCE_ID, ADMIN_AI_BUDDY_INSTANCE_ID, ADMIN_JIRA_INSTANCE_ID from components.aibuddy.assets.icons import icon_brain_ok @@ -14,6 +14,7 @@ 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 +from core.jira import Jira class Admin(BaseComponent): @@ -36,7 +37,7 @@ class Admin(BaseComponent): hooks = { "on_ok": self.commands.save_ai_buddy(), "on_cancel": self.commands.cancel_ai_buddy(), - "ok_title": "Apply" + "ok_title": "Apply", } form = InstanceManager.get(self._session, AdminForm.create_component_id(self._session, prefix=self._id), @@ -68,7 +69,8 @@ class Admin(BaseComponent): hooks = { "on_ok": self.commands.save_configure_jira(), "on_cancel": self.commands.cancel_configure_jira(), - "ok_title": "Apply" + "ok_title": "Apply", + "extra_buttons": [AdminButton("Test", self.commands.test_jira)] } form = InstanceManager.get(self._session, @@ -85,7 +87,7 @@ class Admin(BaseComponent): return self._add_tab(ADMIN_JIRA_INSTANCE_ID, "Admin - Jira Configuration", form) def update_ai_buddy_settings(self, values: dict): - values = self.manage_lists(values) + values = AdminForm.get_fields_values(values) self.db.ai_buddy.update(values, ignore_missing=True) return self.tabs_manager.render() @@ -95,15 +97,25 @@ class Admin(BaseComponent): return self.tabs_manager.render() def update_jira_settings(self, values: dict): - values = self.manage_lists(values) + values = AdminForm.get_fields_values(values) self.db.jira.update(values, ignore_missing=True) return self.tabs_manager.render() - + def cancel_jira_settings(self): tab_id = self.tabs_manager.get_tab_id(ADMIN_JIRA_INSTANCE_ID) self.tabs_manager.remove_tab(tab_id) return self.tabs_manager.render() - + + def test_jira_settings(self, values: dict): + values = AdminForm.get_fields_values(values) + jira = Jira(values["user_name"], values["api_token"]) + form = self.tabs_manager.get_tab_content_by_key(ADMIN_JIRA_INSTANCE_ID) + res = jira.test() + if res.status_code == 200: + form.set_message("Success !", AdminMessageType.SUCCESS) + else: + form.set_message(f"Error {res.status_code} - {res.text}", AdminMessageType.ERROR) + return self.tabs_manager.render() def __ft__(self): return Div( @@ -138,40 +150,3 @@ class Admin(BaseComponent): @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 diff --git a/src/components/admin/components/AdminForm.py b/src/components/admin/components/AdminForm.py index eda596f..fcf3033 100644 --- a/src/components/admin/components/AdminForm.py +++ b/src/components/admin/components/AdminForm.py @@ -1,10 +1,12 @@ from dataclasses import dataclass -from typing import Any +from typing import Any, Callable from fasthtml.components import * +from assets.icons import icon_error from components.BaseComponent import BaseComponent -from components_helpers import apply_boundaries, mk_dialog_buttons, safe_get_dialog_buttons_parameters +from components.admin.assets.icons import icon_msg_success, icon_msg_info, icon_msg_error, icon_msg_warning +from components_helpers import apply_boundaries, mk_dialog_buttons, safe_get_dialog_buttons_parameters, mk_icon from core.utils import get_unique_id @@ -18,6 +20,14 @@ class AdminFormType: TEXTAREA = "textarea" +class AdminMessageType: + NONE = "none" + SUCCESS = "success" + ERROR = "error" + INFO = "info" + WARNING = "warning" + + @dataclass class AdminFormItem: name: str @@ -27,6 +37,12 @@ class AdminFormItem: possible_values: list[str] = None +@dataclass +class AdminButton: + title: str + on_click: Callable = None + + class AdminForm(BaseComponent): def __init__(self, session, _id, owner, title: str, obj: Any, form_fields: list[AdminFormItem], hooks=None, key=None, boundaries=None): @@ -38,6 +54,21 @@ class AdminForm(BaseComponent): self.title = title self.obj = obj self.form_fields = form_fields + self.message = None + + def set_message(self, message, msg_type: AdminMessageType.NONE): + if msg_type == AdminMessageType.NONE: + self.message = message + else: + if msg_type == AdminMessageType.SUCCESS: + icon = icon_msg_success + elif msg_type == AdminMessageType.ERROR: + icon = icon_msg_error + elif msg_type == AdminMessageType.WARNING: + icon = icon_msg_warning + else: + icon = icon_msg_info + self.message = Div(icon, Span(message), role=msg_type, cls=f"alert alert-{msg_type} mr-2") def mk_input(self, item: AdminFormItem): return Input( @@ -62,7 +93,7 @@ class AdminForm(BaseComponent): 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") @@ -95,9 +126,20 @@ class AdminForm(BaseComponent): else: return self.mk_input(item) + def mk_extra_buttons(self): + extra_buttons = self._hooks.get("extra_buttons", None) + if not extra_buttons: + return None + + return Div( + *[Button(btn.title, cls="btn btn-ghost btn-sm", **btn.on_click()) for btn in extra_buttons], + cls="flex justify-end" + ) + def __ft__(self): return Form( Fieldset(Legend(self.title, cls="fieldset-legend"), + Div(self.message), *[ Div( Label(item.title, cls="label"), @@ -107,6 +149,7 @@ class AdminForm(BaseComponent): for item in self.form_fields ], + self.mk_extra_buttons(), mk_dialog_buttons(**safe_get_dialog_buttons_parameters(self._hooks)), **apply_boundaries(self._boundaries), cls="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4" @@ -119,3 +162,40 @@ class AdminForm(BaseComponent): suffix = get_unique_id() return f"{prefix}{suffix}" + + @staticmethod + def get_fields_values(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 diff --git a/src/components/admin/constants.py b/src/components/admin/constants.py index cbde2ba..7294484 100644 --- a/src/components/admin/constants.py +++ b/src/components/admin/constants.py @@ -12,3 +12,4 @@ class Routes: PasteHolidays = "/paste-holidays" ConfigureJira = "/configure-jira" ConfigureJiraCancel = "/configure-jira-cancel" + ConfigureJiraTest = "/configure-jira-test" diff --git a/src/components/workflows/WorkflowsApp.py b/src/components/workflows/WorkflowsApp.py index da45fb0..a77342f 100644 --- a/src/components/workflows/WorkflowsApp.py +++ b/src/components/workflows/WorkflowsApp.py @@ -35,7 +35,7 @@ def post(session, _id: str, name: str, tab_boundaries: str): @rt(Routes.AddComponent) -def post(session, _id: str, component_type: str, x: int, y: int): +def post(session, _id: str, component_type: str, x: float, y: float): logger.debug( f"Entering {Routes.AddComponent} with args {debug_session(session)}, {_id=}, {component_type=}, {x=}, {y=}") instance = InstanceManager.get(session, _id) diff --git a/src/core/jira.py b/src/core/jira.py index a7e1741..10923b2 100644 --- a/src/core/jira.py +++ b/src/core/jira.py @@ -39,6 +39,18 @@ class Jira: self.api_token = api_token self.auth = HTTPBasicAuth(self.user_name, self.api_token) + def test(self): + url = f"{JIRA_ROOT}/myself" + + response = requests.request( + "GET", + url, + headers=DEFAULT_HEADERS, + auth=self.auth + ) + + return response + def issue(self, issue_id: str) -> Expando: """ Retrieve an issue