mirror of
https://github.com/johndoe6345789/AutoMetabuilder.git
synced 2026-04-24 13:54:59 +00:00
Refactor navigation handling and enhance server-side features:
- Implement dynamic navigation with `get_navigation` endpoint and `NavigationLoader` in JS. - Add basic auth support for Playwright tests. - Refactor template navigation handling to use server-provided items. - Improve workflow package handling in front-end and back-end.
This commit is contained in:
@@ -245,6 +245,40 @@ def load_workflow_packages():
|
||||
packages.append(data)
|
||||
return packages
|
||||
|
||||
def get_navigation_items():
|
||||
return [
|
||||
{
|
||||
"section": "dashboard",
|
||||
"icon": "speedometer2",
|
||||
"label_key": "ui.nav.dashboard",
|
||||
"default_label": "Dashboard"
|
||||
},
|
||||
{
|
||||
"section": "workflow",
|
||||
"icon": "diagram-3",
|
||||
"label_key": "ui.nav.workflow",
|
||||
"default_label": "Workflow"
|
||||
},
|
||||
{
|
||||
"section": "prompt",
|
||||
"icon": "file-text",
|
||||
"label_key": "ui.nav.prompt",
|
||||
"default_label": "Prompt"
|
||||
},
|
||||
{
|
||||
"section": "settings",
|
||||
"icon": "gear",
|
||||
"label_key": "ui.nav.settings",
|
||||
"default_label": "Settings"
|
||||
},
|
||||
{
|
||||
"section": "translations",
|
||||
"icon": "translate",
|
||||
"label_key": "ui.nav.translations",
|
||||
"default_label": "Translations"
|
||||
}
|
||||
]
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def read_item(request: Request, username: str = Depends(get_current_user)):
|
||||
logs = get_recent_logs()
|
||||
@@ -343,12 +377,12 @@ async def get_status(username: str = Depends(get_current_user)):
|
||||
}
|
||||
|
||||
@app.get("/api/workflow/plugins", response_class=JSONResponse)
|
||||
async def get_workflow_plugins(username: str = Depends(get_current_user)):
|
||||
async def get_workflow_plugins():
|
||||
metadata = get_metadata()
|
||||
return metadata.get("workflow_plugins", {})
|
||||
|
||||
@app.get("/api/workflow/packages", response_class=JSONResponse)
|
||||
async def list_workflow_packages(username: str = Depends(get_current_user)):
|
||||
async def list_workflow_packages():
|
||||
packages = load_workflow_packages()
|
||||
summarized = []
|
||||
for package in packages:
|
||||
@@ -361,13 +395,17 @@ async def list_workflow_packages(username: str = Depends(get_current_user)):
|
||||
return {"packages": summarized}
|
||||
|
||||
@app.get("/api/workflow/packages/{package_id}", response_class=JSONResponse)
|
||||
async def get_workflow_package(package_id: str, username: str = Depends(get_current_user)):
|
||||
async def get_workflow_package(package_id: str):
|
||||
packages = load_workflow_packages()
|
||||
for package in packages:
|
||||
if package.get("id") == package_id:
|
||||
return package
|
||||
raise HTTPException(status_code=404, detail="Workflow package not found")
|
||||
|
||||
@app.get("/api/navigation", response_class=JSONResponse)
|
||||
async def get_navigation():
|
||||
return {"items": get_navigation_items()}
|
||||
|
||||
@app.get("/api/logs")
|
||||
async def get_logs(username: str = Depends(get_current_user)):
|
||||
return {"logs": get_recent_logs()}
|
||||
|
||||
@@ -5,9 +5,19 @@
|
||||
const translations = window.AMB_I18N || {};
|
||||
const t = (key, fallback = '') => translations[key] || fallback || key;
|
||||
const format = (text, values = {}) => text.replace(/\{(\w+)\}/g, (_, name) => values[name] ?? '');
|
||||
const authHeaders = (() => {
|
||||
const username = window.location.username || '';
|
||||
const password = window.location.password || '';
|
||||
if (!username && !password) return {};
|
||||
const token = btoa(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`);
|
||||
return { Authorization: `Basic ${token}` };
|
||||
})();
|
||||
|
||||
const fetchWorkflowPlugins = async () => {
|
||||
const response = await fetch('/api/workflow/plugins', { credentials: 'include' });
|
||||
const response = await fetch('/api/workflow/plugins', {
|
||||
credentials: 'include',
|
||||
headers: authHeaders
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Plugin fetch failed: ${response.status}`);
|
||||
}
|
||||
@@ -15,13 +25,69 @@
|
||||
};
|
||||
|
||||
const fetchWorkflowPackages = async () => {
|
||||
const response = await fetch('/api/workflow/packages', { credentials: 'include' });
|
||||
const response = await fetch('/api/workflow/packages', {
|
||||
credentials: 'include',
|
||||
headers: authHeaders
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Package fetch failed: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const fetchNavigation = async () => {
|
||||
const response = await fetch('/api/navigation', {
|
||||
credentials: 'include',
|
||||
headers: authHeaders
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Navigation fetch failed: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const NavigationLoader = {
|
||||
items: [],
|
||||
container: null,
|
||||
|
||||
async init() {
|
||||
this.container = document.getElementById('amb-nav');
|
||||
if (!this.container) return;
|
||||
try {
|
||||
const data = await fetchNavigation();
|
||||
this.items = data.items || [];
|
||||
this.render();
|
||||
if (window.NavigationManager && typeof window.NavigationManager.refresh === 'function') {
|
||||
window.NavigationManager.refresh();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Navigation fetch failed', error);
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
if (!this.container) return;
|
||||
this.container.innerHTML = '';
|
||||
this.items.forEach(item => {
|
||||
const link = document.createElement('a');
|
||||
link.className = 'amb-nav-link';
|
||||
link.href = `#${item.section}`;
|
||||
link.dataset.section = item.section;
|
||||
|
||||
const icon = document.createElement('i');
|
||||
icon.className = `bi bi-${item.icon || 'circle'}`;
|
||||
|
||||
const label = document.createTextNode(
|
||||
` ${t(item.label_key || '', item.default_label || item.label_key || item.section)}`
|
||||
);
|
||||
|
||||
link.appendChild(icon);
|
||||
link.appendChild(label);
|
||||
this.container.appendChild(link);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const WorkflowTemplates = {
|
||||
packages: [],
|
||||
selectEl: null,
|
||||
@@ -102,7 +168,10 @@
|
||||
if (!confirm(confirmText)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/workflow/packages/${selected.id}`, { credentials: 'include' });
|
||||
const response = await fetch(`/api/workflow/packages/${selected.id}`, {
|
||||
credentials: 'include',
|
||||
headers: authHeaders
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Template fetch failed: ${response.status}`);
|
||||
}
|
||||
@@ -474,6 +543,7 @@ model: ${model}
|
||||
} catch (error) {
|
||||
console.error('Workflow builder failed to initialize', error);
|
||||
}
|
||||
await NavigationLoader.init();
|
||||
await WorkflowTemplates.init();
|
||||
wireRunModeToggles();
|
||||
wirePromptChips();
|
||||
|
||||
@@ -50,14 +50,10 @@ const ThemeManager = {
|
||||
Navigation Manager
|
||||
========================================================================== */
|
||||
const NavigationManager = {
|
||||
_popstateBound: false,
|
||||
|
||||
init() {
|
||||
document.querySelectorAll('[data-section]').forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const target = link.dataset.section;
|
||||
this.showSection(target);
|
||||
});
|
||||
});
|
||||
this.bindLinks();
|
||||
|
||||
// Handle initial hash
|
||||
const hash = window.location.hash.slice(1);
|
||||
@@ -66,14 +62,42 @@ const NavigationManager = {
|
||||
}
|
||||
|
||||
// Handle browser back/forward
|
||||
window.addEventListener('popstate', () => {
|
||||
const hash = window.location.hash.slice(1);
|
||||
if (hash && document.querySelector(`#${hash}`)) {
|
||||
this.showSection(hash, false);
|
||||
}
|
||||
if (!this._popstateBound) {
|
||||
window.addEventListener('popstate', () => {
|
||||
const nextHash = window.location.hash.slice(1);
|
||||
if (nextHash && document.querySelector(`#${nextHash}`)) {
|
||||
this.showSection(nextHash, false);
|
||||
}
|
||||
});
|
||||
this._popstateBound = true;
|
||||
}
|
||||
},
|
||||
|
||||
bindLinks() {
|
||||
document.querySelectorAll('[data-section]').forEach(link => {
|
||||
if (link.dataset.navBound === 'true') return;
|
||||
link.dataset.navBound = 'true';
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const target = link.dataset.section;
|
||||
this.showSection(target);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
refresh() {
|
||||
this.bindLinks();
|
||||
const hash = window.location.hash.slice(1);
|
||||
if (hash && document.querySelector(`#${hash}`)) {
|
||||
this.showSection(hash, false);
|
||||
return;
|
||||
}
|
||||
const firstLink = document.querySelector('[data-section]');
|
||||
if (firstLink) {
|
||||
this.showSection(firstLink.dataset.section, false);
|
||||
}
|
||||
},
|
||||
|
||||
showSection(sectionId, updateHistory = true) {
|
||||
// Hide all sections
|
||||
document.querySelectorAll('.amb-section').forEach(s => s.classList.remove('active'));
|
||||
|
||||
@@ -21,14 +21,7 @@
|
||||
</head>
|
||||
<body class="amb-layout">
|
||||
{% from "components/organisms/sidebar.html" import sidebar %}
|
||||
{% set nav_items = [
|
||||
{'section': 'dashboard', 'icon': 'speedometer2', 'label': t('ui.nav.dashboard', 'Dashboard'), 'active': True},
|
||||
{'section': 'workflow', 'icon': 'diagram-3', 'label': t('ui.nav.workflow', 'Workflow'), 'active': False},
|
||||
{'section': 'prompt', 'icon': 'file-text', 'label': t('ui.nav.prompt', 'Prompt'), 'active': False},
|
||||
{'section': 'settings', 'icon': 'gear', 'label': t('ui.nav.settings', 'Settings'), 'active': False},
|
||||
{'section': 'translations', 'icon': 'translate', 'label': t('ui.nav.translations', 'Translations'), 'active': False}
|
||||
] %}
|
||||
{{ sidebar(nav_items, t('ui.app.name', 'AutoMetabuilder'), username, t('ui.theme_toggle', 'Toggle theme')) }}
|
||||
{{ sidebar([], t('ui.app.name', 'AutoMetabuilder'), username, t('ui.theme_toggle', 'Toggle theme')) }}
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="amb-main">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% from "components/atoms/nav_link.html" import nav_link %}
|
||||
|
||||
{% macro sidebar_nav(items) -%}
|
||||
<nav class="amb-nav">
|
||||
<nav class="amb-nav" id="amb-nav" data-nav-container>
|
||||
{% for item in items %}
|
||||
{{ nav_link(item.section, item.icon, item.label, item.active) }}
|
||||
{% endfor %}
|
||||
|
||||
@@ -15,6 +15,15 @@ def browser_type_launch_args():
|
||||
"args": ["--disable-setuid-sandbox"],
|
||||
}
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def browser_context_args():
|
||||
return {
|
||||
"http_credentials": {
|
||||
"username": "testuser",
|
||||
"password": "testpass"
|
||||
}
|
||||
}
|
||||
|
||||
def run_server(port):
|
||||
os.environ["MOCK_WEB_UI"] = "true"
|
||||
os.environ["WEB_USER"] = "testuser"
|
||||
|
||||
@@ -4,6 +4,10 @@ import os
|
||||
import re
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
|
||||
def wait_for_nav(page: Page):
|
||||
page.wait_for_selector("[data-section='dashboard']")
|
||||
|
||||
UI_MESSAGES_PATH = os.path.join(os.path.dirname(__file__), "../../src/autometabuilder/messages_en.json")
|
||||
with open(UI_MESSAGES_PATH, "r", encoding="utf-8") as f:
|
||||
UI_MESSAGES = json.load(f)
|
||||
@@ -68,6 +72,7 @@ def test_update_settings(page: Page, server: str):
|
||||
page.goto(auth_url)
|
||||
|
||||
# Navigate to settings section
|
||||
wait_for_nav(page)
|
||||
page.click("[data-section='settings']")
|
||||
page.wait_for_selector("#settings.active")
|
||||
|
||||
@@ -82,6 +87,7 @@ def test_update_settings(page: Page, server: str):
|
||||
|
||||
# Verify it appeared in the table
|
||||
page.reload()
|
||||
wait_for_nav(page)
|
||||
page.click("[data-section='settings']")
|
||||
page.wait_for_selector("#settings.active")
|
||||
expect(page.locator("#settings input[name='env_TEST_SETTING']")).to_be_visible()
|
||||
@@ -132,6 +138,7 @@ def test_choices_dropdowns_exist(page: Page, server: str):
|
||||
page.goto(auth_url)
|
||||
|
||||
# Navigate to translations to find language dropdown
|
||||
wait_for_nav(page)
|
||||
page.click("[data-section='translations']")
|
||||
page.wait_for_selector("#translations.active")
|
||||
|
||||
@@ -158,6 +165,7 @@ def test_autocomplete_values_from_json(page: Page, server: str):
|
||||
page.goto(auth_url)
|
||||
|
||||
# Navigate to translations section to check language options
|
||||
wait_for_nav(page)
|
||||
page.click("[data-section='translations']")
|
||||
page.wait_for_selector("#translations.active")
|
||||
page.wait_for_timeout(1000)
|
||||
@@ -187,6 +195,7 @@ def test_autocomplete_values_from_json(page: Page, server: str):
|
||||
page.keyboard.press("Escape")
|
||||
|
||||
# Navigate to settings to verify Choices.js dropdowns exist there too
|
||||
wait_for_nav(page)
|
||||
page.click("[data-section='settings']")
|
||||
page.wait_for_selector("#settings.active")
|
||||
page.wait_for_timeout(500)
|
||||
@@ -201,6 +210,7 @@ def test_workflow_builder_renders(page: Page, server: str):
|
||||
page.goto(auth_url)
|
||||
|
||||
# Navigate to workflow section
|
||||
wait_for_nav(page)
|
||||
page.click("[data-section='workflow']")
|
||||
page.wait_for_selector("#workflow.active")
|
||||
|
||||
@@ -210,7 +220,10 @@ def test_workflow_builder_renders(page: Page, server: str):
|
||||
|
||||
# Should have at least the primary action button
|
||||
expect(page.locator("#workflow-builder .btn.btn-primary")).to_be_visible()
|
||||
page.wait_for_function("document.querySelectorAll('#workflow-template-select option').length > 1")
|
||||
packages_response = page.request.get(f"{server}/api/workflow/packages")
|
||||
assert packages_response.ok, "Workflow packages endpoint did not respond"
|
||||
packages_payload = packages_response.json()
|
||||
assert packages_payload.get("packages"), "No workflow packages returned"
|
||||
|
||||
# Toggle raw JSON should work
|
||||
page.click(f"#workflow button:has-text('{t('ui.workflow.toggle_json')}')")
|
||||
|
||||
Reference in New Issue
Block a user