diff --git a/backend/open_webui/models/automations.py b/backend/open_webui/models/automations.py index fcb133a903..3591fc8315 100644 --- a/backend/open_webui/models/automations.py +++ b/backend/open_webui/models/automations.py @@ -4,7 +4,7 @@ from typing import Optional from uuid import uuid4 from pydantic import BaseModel, ConfigDict -from sqlalchemy import Column, Text, JSON, Boolean, BigInteger, Index, select +from sqlalchemy import Column, Text, JSON, Boolean, BigInteger, Index, select, or_, func, cast, String from sqlalchemy.orm import Session from open_webui.internal.db import Base, get_db, get_db_context @@ -108,6 +108,11 @@ class AutomationResponse(AutomationModel): next_runs: Optional[list[int]] = None +class AutomationListResponse(BaseModel): + items: list[AutomationModel] + total: int + + #################### # AutomationTable #################### @@ -159,6 +164,48 @@ class AutomationTable: ) return [AutomationModel.model_validate(r) for r in rows] + def search_automations( + self, + user_id: str, + query: Optional[str] = None, + status: Optional[str] = None, + skip: int = 0, + limit: int = 30, + db: Optional[Session] = None, + ) -> 'AutomationListResponse': + with get_db_context(db) as db: + q = db.query(Automation).filter_by(user_id=user_id) + + if query: + search = f'%{query}%' + # Search in name and prompt inside JSON data + q = q.filter( + or_( + Automation.name.ilike(search), + cast(Automation.data, String).ilike(search), + ) + ) + + if status == 'active': + q = q.filter(Automation.is_active == True) + elif status == 'paused': + q = q.filter(Automation.is_active == False) + + q = q.order_by(Automation.created_at.desc()) + + total = q.count() + + if skip: + q = q.offset(skip) + if limit: + q = q.limit(limit) + + rows = q.all() + return AutomationListResponse( + items=[AutomationModel.model_validate(r) for r in rows], + total=total, + ) + def get_all(self, db: Optional[Session] = None) -> list[AutomationModel]: with get_db_context(db) as db: rows = ( diff --git a/backend/open_webui/routers/automations.py b/backend/open_webui/routers/automations.py index 94d6178ca3..41dc761df7 100644 --- a/backend/open_webui/routers/automations.py +++ b/backend/open_webui/routers/automations.py @@ -1,6 +1,7 @@ import asyncio import logging +from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Request, status from sqlalchemy.orm import Session @@ -11,6 +12,7 @@ from open_webui.models.automations import ( AutomationModel, AutomationResponse, AutomationRunModel, + AutomationListResponse, ) from open_webui.utils.automations import ( validate_rrule, @@ -26,6 +28,8 @@ log = logging.getLogger(__name__) router = APIRouter() +PAGE_ITEM_COUNT = 30 + ############################ # Helpers @@ -71,6 +75,42 @@ async def get_automations( return [enrich_automation(automation, db, tz=user.timezone) for automation in automations] +############################ +# GetAutomationItems (paginated) +############################ + + +@router.get('/list') +async def get_automation_items( + request: Request, + query: Optional[str] = None, + status: Optional[str] = None, + page: Optional[int] = 1, + user=Depends(get_verified_user), + db: Session = Depends(get_session), +): + limit = PAGE_ITEM_COUNT + page = max(1, page) + skip = (page - 1) * limit + + result = Automations.search_automations( + user_id=user.id, + query=query, + status=status, + skip=skip, + limit=limit, + db=db, + ) + + return { + 'items': [ + enrich_automation(item, db, tz=user.timezone) + for item in result.items + ], + 'total': result.total, + } + + ############################ # CreateNewAutomation ############################ diff --git a/src/lib/apis/automations/index.ts b/src/lib/apis/automations/index.ts index 30aeb85a16..3d4c9cd4fd 100644 --- a/src/lib/apis/automations/index.ts +++ b/src/lib/apis/automations/index.ts @@ -77,6 +77,50 @@ export const getAutomations = async (token: string) => { return res; }; +export const getAutomationItems = async ( + token: string, + query: string | null, + status: string | null, + page: number +): Promise<{ items: AutomationResponse[]; total: number }> => { + let error = null; + + const searchParams = new URLSearchParams(); + if (query) { + searchParams.append('query', query); + } + if (status && status !== 'all') { + searchParams.append('status', status); + } + if (page) { + searchParams.append('page', page.toString()); + } + + const res = await fetch(`${WEBUI_API_BASE_URL}/automations/list?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const createAutomation = async (token: string, form: AutomationForm) => { let error = null; diff --git a/src/routes/(app)/automations/+page.svelte b/src/routes/(app)/automations/+page.svelte index 6d486d7487..ca972fd7a3 100644 --- a/src/routes/(app)/automations/+page.svelte +++ b/src/routes/(app)/automations/+page.svelte @@ -1,11 +1,11 @@ @@ -215,7 +234,7 @@ {/if}
{$i18n.t('Automations')}
- {automations.length} + {total ?? ''}
@@ -296,7 +315,11 @@ - {#if getFilteredAutomations(automations, query, statusFilter).length === 0} + {#if automations === null || loading} +
+ +
+ {:else if (automations ?? []).length === 0}
@@ -312,7 +335,7 @@
{:else}
- {#each getFilteredAutomations(automations, query, statusFilter) as automation (automation.id)} + {#each automations as automation (automation.id)}
@@ -369,6 +392,12 @@
{/each}
+ + {#if total > 30} +
+ +
+ {/if} {/if}