mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(platform/library): Scheduling UX (#10246)
Complete the implementation of the Agent Run Scheduling UX in the Library. Demo: https://github.com/user-attachments/assets/701adc63-452c-4d37-aeea-51788b2774f2 ### Changes 🏗️ Frontend: - Add "Schedule" button + dialog + logic to `AgentRunDraftView` - Update corresponding logic on `AgentRunsPage` - Add schedule name field to `CronSchedulerDialog` - Amend Builder components `useAgentGraph`, `FlowEditor`, `RunnerUIWrapper` to also handle schedule name input - Split `CronScheduler` into `CronScheduler`+`CronSchedulerDialog` - Make `AgentScheduleDetailsView` more fully functional - Add schedule description to info box - Add "Delete schedule" button - Update schedule create/select/delete logic in `AgentRunsPage` - Improve schedule UX in `AgentRunsSelectorList` - Switch tabs automatically when a run or schedule is selected - Remove now-redundant schedule filters - Refactor `@/lib/monitor/cronExpressionManager` into `@/lib/cron-expression-utils` Backend + API: - Add name and credentials to graph execution schedule job params - Update schedule API - `POST /schedules` -> `POST /graphs/{graph_id}/schedules` - Add `GET /graphs/{graph_id}/schedules` - Add not found error handling to `DELETE /schedules/{schedule_id}` - Minor refactoring Backend: - Fix "`GraphModel`->`NodeModel` is not fully defined" error in scheduler - Add support for all exceptions defined in `backend.util.exceptions` to RPC logic in `backend.util.service` - Fix inconsistent log prefixing in `backend.executor.scheduler` ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - Create a simple agent with inputs and blocks that require credentials; go to this agent in the Library - Fill out the inputs and click "Schedule"; make it run every minute (for testing purposes) - [x] -> newly created schedule appears in the list - [x] -> scheduled runs are successful - Click "Delete schedule" - [x] -> schedule no longer in list - [x] -> on deleting the last schedule, view switches back to the Runs list - [x] -> no new runs occur from the deleted schedule
This commit is contained in:
committed by
GitHub
parent
c4056cbae9
commit
5421ccf86a
5
autogpt_platform/backend/backend/data/__init__.py
Normal file
5
autogpt_platform/backend/backend/data/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .graph import NodeModel
|
||||
from .integrations import Webhook # noqa: F401
|
||||
|
||||
# Resolve Webhook <- NodeModel forward reference
|
||||
NodeModel.model_rebuild()
|
||||
@@ -77,10 +77,6 @@ class WebhookWithRelations(Webhook):
|
||||
)
|
||||
|
||||
|
||||
# Fix Webhook <- NodeModel relations
|
||||
NodeModel.model_rebuild()
|
||||
|
||||
|
||||
# --------------------- CRUD functions --------------------- #
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import logging
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
||||
|
||||
from apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_EXECUTED
|
||||
@@ -14,13 +15,16 @@ from apscheduler.triggers.cron import CronTrigger
|
||||
from autogpt_libs.utils.cache import thread_cached
|
||||
from dotenv import load_dotenv
|
||||
from prisma.enums import NotificationType
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
from sqlalchemy import MetaData, create_engine
|
||||
|
||||
from backend.data.block import BlockInput
|
||||
from backend.data.execution import ExecutionStatus
|
||||
from backend.data.model import CredentialsMetaInput
|
||||
from backend.executor import utils as execution_utils
|
||||
from backend.notifications.notifications import NotificationManagerClient
|
||||
from backend.util.exceptions import NotAuthorizedError, NotFoundError
|
||||
from backend.util.logging import PrefixFilter
|
||||
from backend.util.metrics import sentry_capture_error
|
||||
from backend.util.service import (
|
||||
AppService,
|
||||
@@ -52,19 +56,19 @@ def _extract_schema_from_url(database_url) -> tuple[str, str]:
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.addFilter(PrefixFilter("[Scheduler]"))
|
||||
apscheduler_logger = logger.getChild("apscheduler")
|
||||
apscheduler_logger.addFilter(PrefixFilter("[Scheduler] [APScheduler]"))
|
||||
|
||||
config = Config()
|
||||
|
||||
|
||||
def log(msg, **kwargs):
|
||||
logger.info("[Scheduler] " + msg, **kwargs)
|
||||
|
||||
|
||||
def job_listener(event):
|
||||
"""Logs job execution outcomes for better monitoring."""
|
||||
if event.exception:
|
||||
log(f"Job {event.job_id} failed.")
|
||||
logger.error(f"Job {event.job_id} failed.")
|
||||
else:
|
||||
log(f"Job {event.job_id} completed successfully.")
|
||||
logger.info(f"Job {event.job_id} completed successfully.")
|
||||
|
||||
|
||||
@thread_cached
|
||||
@@ -84,16 +88,17 @@ def execute_graph(**kwargs):
|
||||
async def _execute_graph(**kwargs):
|
||||
args = GraphExecutionJobArgs(**kwargs)
|
||||
try:
|
||||
log(f"Executing recurring job for graph #{args.graph_id}")
|
||||
logger.info(f"Executing recurring job for graph #{args.graph_id}")
|
||||
await execution_utils.add_graph_execution(
|
||||
graph_id=args.graph_id,
|
||||
inputs=args.input_data,
|
||||
user_id=args.user_id,
|
||||
graph_id=args.graph_id,
|
||||
graph_version=args.graph_version,
|
||||
inputs=args.input_data,
|
||||
graph_credentials_inputs=args.input_credentials,
|
||||
use_db_query=False,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error executing graph {args.graph_id}: {e}")
|
||||
logger.error(f"Error executing graph {args.graph_id}: {e}")
|
||||
|
||||
|
||||
class LateExecutionException(Exception):
|
||||
@@ -137,20 +142,20 @@ def report_late_executions() -> str:
|
||||
def process_existing_batches(**kwargs):
|
||||
args = NotificationJobArgs(**kwargs)
|
||||
try:
|
||||
log(
|
||||
logger.info(
|
||||
f"Processing existing batches for notification type {args.notification_types}"
|
||||
)
|
||||
get_notification_client().process_existing_batches(args.notification_types)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error processing existing batches: {e}")
|
||||
logger.error(f"Error processing existing batches: {e}")
|
||||
|
||||
|
||||
def process_weekly_summary(**kwargs):
|
||||
try:
|
||||
log("Processing weekly summary")
|
||||
logger.info("Processing weekly summary")
|
||||
get_notification_client().queue_weekly_summary()
|
||||
except Exception as e:
|
||||
logger.exception(f"Error processing weekly summary: {e}")
|
||||
logger.error(f"Error processing weekly summary: {e}")
|
||||
|
||||
|
||||
class Jobstores(Enum):
|
||||
@@ -160,11 +165,12 @@ class Jobstores(Enum):
|
||||
|
||||
|
||||
class GraphExecutionJobArgs(BaseModel):
|
||||
graph_id: str
|
||||
input_data: BlockInput
|
||||
user_id: str
|
||||
graph_id: str
|
||||
graph_version: int
|
||||
cron: str
|
||||
input_data: BlockInput
|
||||
input_credentials: dict[str, CredentialsMetaInput] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class GraphExecutionJobInfo(GraphExecutionJobArgs):
|
||||
@@ -247,7 +253,8 @@ class Scheduler(AppService):
|
||||
),
|
||||
# These don't really need persistence
|
||||
Jobstores.WEEKLY_NOTIFICATIONS.value: MemoryJobStore(),
|
||||
}
|
||||
},
|
||||
logger=apscheduler_logger,
|
||||
)
|
||||
|
||||
if self.register_system_tasks:
|
||||
@@ -285,34 +292,40 @@ class Scheduler(AppService):
|
||||
|
||||
def cleanup(self):
|
||||
super().cleanup()
|
||||
logger.info(f"[{self.service_name}] ⏳ Shutting down scheduler...")
|
||||
logger.info("⏳ Shutting down scheduler...")
|
||||
if self.scheduler:
|
||||
self.scheduler.shutdown(wait=False)
|
||||
|
||||
@expose
|
||||
def add_graph_execution_schedule(
|
||||
self,
|
||||
user_id: str,
|
||||
graph_id: str,
|
||||
graph_version: int,
|
||||
cron: str,
|
||||
input_data: BlockInput,
|
||||
user_id: str,
|
||||
input_credentials: dict[str, CredentialsMetaInput],
|
||||
name: Optional[str] = None,
|
||||
) -> GraphExecutionJobInfo:
|
||||
job_args = GraphExecutionJobArgs(
|
||||
graph_id=graph_id,
|
||||
input_data=input_data,
|
||||
user_id=user_id,
|
||||
graph_id=graph_id,
|
||||
graph_version=graph_version,
|
||||
cron=cron,
|
||||
input_data=input_data,
|
||||
input_credentials=input_credentials,
|
||||
)
|
||||
job = self.scheduler.add_job(
|
||||
execute_graph,
|
||||
CronTrigger.from_crontab(cron),
|
||||
kwargs=job_args.model_dump(),
|
||||
replace_existing=True,
|
||||
name=name,
|
||||
trigger=CronTrigger.from_crontab(cron),
|
||||
jobstore=Jobstores.EXECUTION.value,
|
||||
replace_existing=True,
|
||||
)
|
||||
logger.info(
|
||||
f"Added job {job.id} with cron schedule '{cron}' input data: {input_data}"
|
||||
)
|
||||
log(f"Added job {job.id} with cron schedule '{cron}' input data: {input_data}")
|
||||
return GraphExecutionJobInfo.from_db(job_args, job)
|
||||
|
||||
@expose
|
||||
@@ -321,14 +334,13 @@ class Scheduler(AppService):
|
||||
) -> GraphExecutionJobInfo:
|
||||
job = self.scheduler.get_job(schedule_id, jobstore=Jobstores.EXECUTION.value)
|
||||
if not job:
|
||||
log(f"Job {schedule_id} not found.")
|
||||
raise ValueError(f"Job #{schedule_id} not found.")
|
||||
raise NotFoundError(f"Job #{schedule_id} not found.")
|
||||
|
||||
job_args = GraphExecutionJobArgs(**job.kwargs)
|
||||
if job_args.user_id != user_id:
|
||||
raise ValueError("User ID does not match the job's user ID.")
|
||||
raise NotAuthorizedError("User ID does not match the job's user ID")
|
||||
|
||||
log(f"Deleting job {schedule_id}")
|
||||
logger.info(f"Deleting job {schedule_id}")
|
||||
job.remove()
|
||||
|
||||
return GraphExecutionJobInfo.from_db(job_args, job)
|
||||
|
||||
@@ -27,6 +27,7 @@ async def test_agent_schedule(server: SpinTestServer):
|
||||
graph_version=1,
|
||||
cron="0 0 * * *",
|
||||
input_data={"input": "data"},
|
||||
input_credentials={},
|
||||
)
|
||||
assert schedule
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import stripe
|
||||
from autogpt_libs.auth.middleware import auth_middleware
|
||||
from autogpt_libs.feature_flag.client import feature_flag
|
||||
from autogpt_libs.utils.cache import thread_cached
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Request, Response
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Request, Response
|
||||
from starlette.status import HTTP_204_NO_CONTENT, HTTP_404_NOT_FOUND
|
||||
from typing_extensions import Optional, TypedDict
|
||||
|
||||
@@ -72,6 +72,7 @@ from backend.server.model import (
|
||||
UpdatePermissionsRequest,
|
||||
)
|
||||
from backend.server.utils import get_user_id
|
||||
from backend.util.exceptions import NotFoundError
|
||||
from backend.util.service import get_service_client
|
||||
from backend.util.settings import Settings
|
||||
|
||||
@@ -765,70 +766,94 @@ async def delete_graph_execution(
|
||||
|
||||
|
||||
class ScheduleCreationRequest(pydantic.BaseModel):
|
||||
graph_version: Optional[int] = None
|
||||
name: str
|
||||
cron: str
|
||||
input_data: dict[Any, Any]
|
||||
graph_id: str
|
||||
graph_version: int
|
||||
inputs: dict[str, Any]
|
||||
credentials: dict[str, CredentialsMetaInput] = pydantic.Field(default_factory=dict)
|
||||
|
||||
|
||||
@v1_router.post(
|
||||
path="/schedules",
|
||||
path="/graphs/{graph_id}/schedules",
|
||||
summary="Create execution schedule",
|
||||
tags=["schedules"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
async def create_schedule(
|
||||
async def create_graph_execution_schedule(
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
schedule: ScheduleCreationRequest,
|
||||
graph_id: str = Path(..., description="ID of the graph to schedule"),
|
||||
schedule_params: ScheduleCreationRequest = Body(),
|
||||
) -> scheduler.GraphExecutionJobInfo:
|
||||
graph = await graph_db.get_graph(
|
||||
schedule.graph_id, schedule.graph_version, user_id=user_id
|
||||
graph_id=graph_id,
|
||||
version=schedule_params.graph_version,
|
||||
user_id=user_id,
|
||||
)
|
||||
if not graph:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Graph #{schedule.graph_id} v.{schedule.graph_version} not found.",
|
||||
detail=f"Graph #{graph_id} v{schedule_params.graph_version} not found.",
|
||||
)
|
||||
|
||||
return await execution_scheduler_client().add_execution_schedule(
|
||||
graph_id=schedule.graph_id,
|
||||
graph_version=graph.version,
|
||||
cron=schedule.cron,
|
||||
input_data=schedule.input_data,
|
||||
user_id=user_id,
|
||||
graph_id=graph_id,
|
||||
graph_version=graph.version,
|
||||
name=schedule_params.name,
|
||||
cron=schedule_params.cron,
|
||||
input_data=schedule_params.inputs,
|
||||
input_credentials=schedule_params.credentials,
|
||||
)
|
||||
|
||||
|
||||
@v1_router.get(
|
||||
path="/graphs/{graph_id}/schedules",
|
||||
summary="List execution schedules for a graph",
|
||||
tags=["schedules"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
async def list_graph_execution_schedules(
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
graph_id: str = Path(),
|
||||
) -> list[scheduler.GraphExecutionJobInfo]:
|
||||
return await execution_scheduler_client().get_execution_schedules(
|
||||
user_id=user_id,
|
||||
graph_id=graph_id,
|
||||
)
|
||||
|
||||
|
||||
@v1_router.get(
|
||||
path="/schedules",
|
||||
summary="List execution schedules for a user",
|
||||
tags=["schedules"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
async def list_all_graphs_execution_schedules(
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
) -> list[scheduler.GraphExecutionJobInfo]:
|
||||
return await execution_scheduler_client().get_execution_schedules(user_id=user_id)
|
||||
|
||||
|
||||
@v1_router.delete(
|
||||
path="/schedules/{schedule_id}",
|
||||
summary="Delete execution schedule",
|
||||
tags=["schedules"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
async def delete_schedule(
|
||||
schedule_id: str,
|
||||
async def delete_graph_execution_schedule(
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
) -> dict[Any, Any]:
|
||||
await execution_scheduler_client().delete_schedule(schedule_id, user_id=user_id)
|
||||
schedule_id: str = Path(..., description="ID of the schedule to delete"),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
await execution_scheduler_client().delete_schedule(schedule_id, user_id=user_id)
|
||||
except NotFoundError:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_404_NOT_FOUND,
|
||||
detail=f"Schedule #{schedule_id} not found",
|
||||
)
|
||||
return {"id": schedule_id}
|
||||
|
||||
|
||||
@v1_router.get(
|
||||
path="/schedules",
|
||||
summary="List execution schedules",
|
||||
tags=["schedules"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
async def get_execution_schedules(
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
graph_id: str | None = None,
|
||||
) -> list[scheduler.GraphExecutionJobInfo]:
|
||||
return await execution_scheduler_client().get_execution_schedules(
|
||||
user_id=user_id,
|
||||
graph_id=graph_id,
|
||||
)
|
||||
|
||||
|
||||
########################################################
|
||||
##################### API KEY ##############################
|
||||
########################################################
|
||||
|
||||
@@ -10,6 +10,10 @@ class NeedConfirmation(Exception):
|
||||
"""The user must explicitly confirm that they want to proceed"""
|
||||
|
||||
|
||||
class NotAuthorizedError(ValueError):
|
||||
"""The user is not authorized to perform the requested operation"""
|
||||
|
||||
|
||||
class InsufficientBalanceError(ValueError):
|
||||
user_id: str
|
||||
message: str
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from logging import Logger
|
||||
import logging
|
||||
|
||||
from backend.util.settings import AppEnvironment, BehaveAs, Settings
|
||||
|
||||
@@ -6,8 +6,6 @@ settings = Settings()
|
||||
|
||||
|
||||
def configure_logging():
|
||||
import logging
|
||||
|
||||
import autogpt_libs.logging.config
|
||||
|
||||
if (
|
||||
@@ -25,7 +23,7 @@ def configure_logging():
|
||||
class TruncatedLogger:
|
||||
def __init__(
|
||||
self,
|
||||
logger: Logger,
|
||||
logger: logging.Logger,
|
||||
prefix: str = "",
|
||||
metadata: dict | None = None,
|
||||
max_length: int = 1000,
|
||||
@@ -65,3 +63,13 @@ class TruncatedLogger:
|
||||
if len(text) > self.max_length:
|
||||
text = text[: self.max_length] + "..."
|
||||
return text
|
||||
|
||||
|
||||
class PrefixFilter(logging.Filter):
|
||||
def __init__(self, prefix: str):
|
||||
super().__init__()
|
||||
self.prefix = prefix
|
||||
|
||||
def filter(self, record):
|
||||
record.msg = f"{self.prefix} {record.msg}"
|
||||
return True
|
||||
|
||||
@@ -31,7 +31,7 @@ from tenacity import (
|
||||
wait_exponential_jitter,
|
||||
)
|
||||
|
||||
from backend.util.exceptions import InsufficientBalanceError
|
||||
import backend.util.exceptions as exceptions
|
||||
from backend.util.json import to_dict
|
||||
from backend.util.metrics import sentry_init
|
||||
from backend.util.process import AppProcess, get_service_name
|
||||
@@ -106,7 +106,13 @@ EXCEPTION_MAPPING = {
|
||||
ValueError,
|
||||
TimeoutError,
|
||||
ConnectionError,
|
||||
InsufficientBalanceError,
|
||||
*[
|
||||
ErrorType
|
||||
for _, ErrorType in inspect.getmembers(exceptions)
|
||||
if inspect.isclass(ErrorType)
|
||||
and issubclass(ErrorType, Exception)
|
||||
and ErrorType.__module__ == exceptions.__name__
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -3191,6 +3191,48 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/library/agents/by-graph/{graph_id}": {
|
||||
"get": {
|
||||
"tags": ["v2", "library", "private"],
|
||||
"summary": "Get Library Agent By Graph Id",
|
||||
"operationId": "getV2GetLibraryAgentByGraphId",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "graph_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "type": "string", "title": "Graph Id" }
|
||||
},
|
||||
{
|
||||
"name": "version",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [{ "type": "integer" }, { "type": "null" }],
|
||||
"title": "Version"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/LibraryAgent" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/library/agents/marketplace/{store_listing_version_id}": {
|
||||
"get": {
|
||||
"tags": ["v2", "library", "private", "store, library"],
|
||||
@@ -5012,6 +5054,7 @@
|
||||
"AGENT_INPUT",
|
||||
"CONGRATS",
|
||||
"GET_RESULTS",
|
||||
"RUN_AGENTS",
|
||||
"MARKETPLACE_VISIT",
|
||||
"MARKETPLACE_ADD_AGENT",
|
||||
"MARKETPLACE_RUN_AGENT",
|
||||
|
||||
@@ -64,9 +64,10 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
const [selectedRun, setSelectedRun] = useState<
|
||||
GraphExecution | GraphExecutionMeta | null
|
||||
>(null);
|
||||
const [selectedSchedule, setSelectedSchedule] = useState<Schedule | null>(
|
||||
null,
|
||||
);
|
||||
const selectedSchedule =
|
||||
selectedView.type == "schedule"
|
||||
? schedules.find((s) => s.id == selectedView.id)
|
||||
: null;
|
||||
const [isFirstLoad, setIsFirstLoad] = useState<boolean>(true);
|
||||
const [agentDeleteDialogOpen, setAgentDeleteDialogOpen] =
|
||||
useState<boolean>(false);
|
||||
@@ -100,9 +101,8 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
selectView({ type: "preset", id });
|
||||
}, []);
|
||||
|
||||
const selectSchedule = useCallback((schedule: Schedule) => {
|
||||
selectView({ type: "schedule", id: schedule.id });
|
||||
setSelectedSchedule(schedule);
|
||||
const selectSchedule = useCallback((id: ScheduleID) => {
|
||||
selectView({ type: "schedule", id });
|
||||
}, []);
|
||||
|
||||
const graphVersions = useRef<Record<number, Graph>>({});
|
||||
@@ -315,11 +315,8 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
const fetchSchedules = useCallback(async () => {
|
||||
if (!agent) return;
|
||||
|
||||
// TODO: filter in backend - https://github.com/Significant-Gravitas/AutoGPT/issues/9183
|
||||
setSchedules(
|
||||
(await api.listSchedules()).filter((s) => s.graph_id == agent.graph_id),
|
||||
);
|
||||
}, [api, agent]);
|
||||
setSchedules(await api.listGraphExecutionSchedules(agent.graph_id));
|
||||
}, [api, agent?.graph_id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSchedules();
|
||||
@@ -358,8 +355,28 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
|
||||
const deleteSchedule = useCallback(
|
||||
async (scheduleID: ScheduleID) => {
|
||||
const removedSchedule = await api.deleteSchedule(scheduleID);
|
||||
setSchedules(schedules.filter((s) => s.id !== removedSchedule.id));
|
||||
const removedSchedule =
|
||||
await api.deleteGraphExecutionSchedule(scheduleID);
|
||||
|
||||
setSchedules((schedules) => {
|
||||
const newSchedules = schedules.filter(
|
||||
(s) => s.id !== removedSchedule.id,
|
||||
);
|
||||
if (
|
||||
selectedView.type == "schedule" &&
|
||||
selectedView.id == removedSchedule.id
|
||||
) {
|
||||
if (newSchedules.length > 0) {
|
||||
// Select next schedule if available
|
||||
selectSchedule(newSchedules[0].id);
|
||||
} else {
|
||||
// Reset to draft view if current schedule was deleted
|
||||
openRunDraftView();
|
||||
}
|
||||
}
|
||||
return newSchedules;
|
||||
});
|
||||
openRunDraftView();
|
||||
},
|
||||
[schedules, api],
|
||||
);
|
||||
@@ -417,6 +434,14 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
[agent, downloadGraph],
|
||||
);
|
||||
|
||||
const onCreateSchedule = useCallback(
|
||||
(schedule: Schedule) => {
|
||||
setSchedules((prev) => [...prev, schedule]);
|
||||
selectSchedule(schedule.id);
|
||||
},
|
||||
[selectView],
|
||||
);
|
||||
|
||||
const onCreatePreset = useCallback(
|
||||
(preset: LibraryAgentPreset) => {
|
||||
setAgentPresets((prev) => [...prev, preset]);
|
||||
@@ -454,9 +479,9 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
onSelectPreset={selectPreset}
|
||||
onSelectSchedule={selectSchedule}
|
||||
onSelectDraftNewRun={openRunDraftView}
|
||||
onDeleteRun={setConfirmingDeleteAgentRun}
|
||||
onDeletePreset={setConfirmingDeleteAgentPreset}
|
||||
onDeleteSchedule={deleteSchedule}
|
||||
doDeleteRun={setConfirmingDeleteAgentRun}
|
||||
doDeletePreset={setConfirmingDeleteAgentPreset}
|
||||
doDeleteSchedule={deleteSchedule}
|
||||
/>
|
||||
|
||||
<div className="flex-1">
|
||||
@@ -486,6 +511,7 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
<AgentRunDraftView
|
||||
agent={agent}
|
||||
onRun={selectRun}
|
||||
onCreateSchedule={onCreateSchedule}
|
||||
onCreatePreset={onCreatePreset}
|
||||
agentActions={agentActions}
|
||||
/>
|
||||
@@ -497,6 +523,7 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
agentPresets.find((preset) => preset.id == selectedView.id)!
|
||||
}
|
||||
onRun={selectRun}
|
||||
onCreateSchedule={onCreateSchedule}
|
||||
onUpdatePreset={onUpdatePreset}
|
||||
doDeletePreset={setConfirmingDeleteAgentPreset}
|
||||
agentActions={agentActions}
|
||||
@@ -506,8 +533,10 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
<AgentScheduleDetailsView
|
||||
graph={graph}
|
||||
schedule={selectedSchedule}
|
||||
onForcedRun={selectRun}
|
||||
// agent={agent}
|
||||
agentActions={agentActions}
|
||||
onForcedRun={selectRun}
|
||||
doDeleteSchedule={deleteSchedule}
|
||||
/>
|
||||
)
|
||||
) : null) || <LoadingBox className="h-[70vh]" />}
|
||||
|
||||
@@ -32,12 +32,13 @@ const Monitor = () => {
|
||||
const api = useBackendAPI();
|
||||
|
||||
const fetchSchedules = useCallback(async () => {
|
||||
setSchedules(await api.listSchedules());
|
||||
setSchedules(await api.listAllGraphsExecutionSchedules());
|
||||
}, [api]);
|
||||
|
||||
const removeSchedule = useCallback(
|
||||
async (scheduleId: ScheduleID) => {
|
||||
const removedSchedule = await api.deleteSchedule(scheduleId);
|
||||
const removedSchedule =
|
||||
await api.deleteGraphExecutionSchedule(scheduleId);
|
||||
setSchedules(schedules.filter((s) => s.id !== removedSchedule.id));
|
||||
},
|
||||
[schedules, api],
|
||||
|
||||
@@ -52,7 +52,7 @@ import PrimaryActionBar from "@/components/PrimaryActionButton";
|
||||
import OttoChatWidget from "@/components/OttoChatWidget";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { useCopyPaste } from "../hooks/useCopyPaste";
|
||||
import { CronScheduler } from "./cronScheduler";
|
||||
import { CronSchedulerDialog } from "@/components/cron-scheduler-dialog";
|
||||
|
||||
// This is for the history, this is the minimum distance a block must move before it is logged
|
||||
// It helps to prevent spamming the history with small movements especially when pressing on a input in a block
|
||||
@@ -639,8 +639,11 @@ const FlowEditor: React.FC<{
|
||||
|
||||
// This function is called after cron expression is created
|
||||
// So you can collect inputs for scheduling
|
||||
const afterCronCreation = (cronExpression: string) => {
|
||||
runnerUIRef.current?.collectInputsForScheduling(cronExpression);
|
||||
const afterCronCreation = (cronExpression: string, scheduleName: string) => {
|
||||
runnerUIRef.current?.collectInputsForScheduling(
|
||||
cronExpression,
|
||||
scheduleName,
|
||||
);
|
||||
};
|
||||
|
||||
// This function Opens up form for creating cron expression
|
||||
@@ -728,10 +731,11 @@ const FlowEditor: React.FC<{
|
||||
requestStopRun={requestStopRun}
|
||||
runAgentTooltip={!isRunning ? "Run Agent" : "Stop Agent"}
|
||||
/>
|
||||
<CronScheduler
|
||||
afterCronCreation={afterCronCreation}
|
||||
<CronSchedulerDialog
|
||||
open={openCron}
|
||||
setOpen={setOpenCron}
|
||||
afterCronCreation={afterCronCreation}
|
||||
defaultScheduleName={agentName}
|
||||
/>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
|
||||
@@ -36,14 +36,21 @@ interface RunnerUIWrapperProps {
|
||||
isRunning: boolean;
|
||||
isScheduling: boolean;
|
||||
requestSaveAndRun: () => void;
|
||||
scheduleRunner: (cronExpression: string, input: InputItem[]) => Promise<void>;
|
||||
scheduleRunner: (
|
||||
cronExpression: string,
|
||||
input: InputItem[],
|
||||
scheduleName: string,
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface RunnerUIWrapperRef {
|
||||
openRunnerInput: () => void;
|
||||
openRunnerOutput: () => void;
|
||||
runOrOpenInput: () => void;
|
||||
collectInputsForScheduling: (cronExpression: string) => void;
|
||||
collectInputsForScheduling: (
|
||||
cronExpression: string,
|
||||
scheduleName: string,
|
||||
) => void;
|
||||
}
|
||||
|
||||
const RunnerUIWrapper = forwardRef<RunnerUIWrapperRef, RunnerUIWrapperProps>(
|
||||
@@ -63,6 +70,7 @@ const RunnerUIWrapper = forwardRef<RunnerUIWrapperRef, RunnerUIWrapperProps>(
|
||||
const [isRunnerOutputOpen, setIsRunnerOutputOpen] = useState(false);
|
||||
const [scheduledInput, setScheduledInput] = useState(false);
|
||||
const [cronExpression, setCronExpression] = useState("");
|
||||
const [scheduleName, setScheduleName] = useState("");
|
||||
|
||||
const getBlockInputsAndOutputs = useCallback((): {
|
||||
inputs: InputItem[];
|
||||
@@ -149,15 +157,19 @@ const RunnerUIWrapper = forwardRef<RunnerUIWrapperRef, RunnerUIWrapperProps>(
|
||||
}
|
||||
};
|
||||
|
||||
const collectInputsForScheduling = (cron_exp: string) => {
|
||||
const collectInputsForScheduling = (
|
||||
cronExpression: string,
|
||||
scheduleName: string,
|
||||
) => {
|
||||
const { inputs } = getBlockInputsAndOutputs();
|
||||
setCronExpression(cron_exp);
|
||||
setCronExpression(cronExpression);
|
||||
setScheduleName(scheduleName);
|
||||
|
||||
if (inputs.length > 0) {
|
||||
setScheduledInput(true);
|
||||
setIsRunnerInputOpen(true);
|
||||
} else {
|
||||
scheduleRunner(cron_exp, []);
|
||||
scheduleRunner(cronExpression, [], scheduleName);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -186,6 +198,7 @@ const RunnerUIWrapper = forwardRef<RunnerUIWrapperRef, RunnerUIWrapperProps>(
|
||||
await scheduleRunner(
|
||||
cronExpression,
|
||||
getBlockInputsAndOutputs().inputs,
|
||||
scheduleName,
|
||||
);
|
||||
setIsScheduling(false);
|
||||
setIsRunnerInputOpen(false);
|
||||
|
||||
@@ -9,17 +9,19 @@ import {
|
||||
LibraryAgentPreset,
|
||||
LibraryAgentPresetID,
|
||||
LibraryAgentPresetUpdatable,
|
||||
Schedule,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
|
||||
import type { ButtonAction } from "@/components/agptui/types";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { IconCross, IconPlay, IconSave } from "@/components/ui/icons";
|
||||
import { CalendarClockIcon, Trash2Icon } from "lucide-react";
|
||||
import { CronSchedulerDialog } from "@/components/cron-scheduler-dialog";
|
||||
import { CredentialsInput } from "@/components/integrations/credentials-input";
|
||||
import { TypeBasedInput } from "@/components/type-based-input";
|
||||
import { useToastOnFail } from "@/components/ui/use-toast";
|
||||
import ActionButtonGroup from "@/components/agptui/action-button-group";
|
||||
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
|
||||
import { Trash2Icon } from "lucide-react";
|
||||
import SchemaTooltip from "@/components/SchemaTooltip";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { isEmpty } from "lodash";
|
||||
@@ -32,11 +34,13 @@ export default function AgentRunDraftView({
|
||||
onCreatePreset,
|
||||
onUpdatePreset,
|
||||
doDeletePreset,
|
||||
onCreateSchedule,
|
||||
agentActions,
|
||||
}: {
|
||||
agent: LibraryAgent;
|
||||
agentActions: ButtonAction[];
|
||||
onRun: (runID: GraphExecutionID) => void;
|
||||
onCreateSchedule: (schedule: Schedule) => void;
|
||||
} & (
|
||||
| {
|
||||
onCreatePreset: (preset: LibraryAgentPreset) => void;
|
||||
@@ -66,6 +70,7 @@ export default function AgentRunDraftView({
|
||||
>(new Set());
|
||||
const { state: onboardingState, completeStep: completeOnboardingStep } =
|
||||
useOnboarding();
|
||||
const [cronScheduleDialogOpen, setCronScheduleDialogOpen] = useState(false);
|
||||
|
||||
// Update values if agentPreset parameter is changed
|
||||
useEffect(() => {
|
||||
@@ -314,6 +319,43 @@ export default function AgentRunDraftView({
|
||||
completeOnboardingStep,
|
||||
]);
|
||||
|
||||
const openScheduleDialog = useCallback(() => {
|
||||
// Scheduling is not supported for webhook-triggered agents
|
||||
if (agent.has_external_trigger) return;
|
||||
|
||||
if (!allRequiredInputsAreSet || !allCredentialsAreSet) {
|
||||
notifyMissingInputs(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setCronScheduleDialogOpen(true);
|
||||
}, [
|
||||
agent,
|
||||
allRequiredInputsAreSet,
|
||||
allCredentialsAreSet,
|
||||
notifyMissingInputs,
|
||||
]);
|
||||
|
||||
const doSetupSchedule = useCallback(
|
||||
(cronExpression: string, scheduleName: string) => {
|
||||
// Scheduling is not supported for webhook-triggered agents
|
||||
if (agent.has_external_trigger) return;
|
||||
|
||||
api
|
||||
.createGraphExecutionSchedule({
|
||||
graph_id: agent.graph_id,
|
||||
graph_version: agent.graph_version,
|
||||
name: scheduleName || agent.name,
|
||||
cron: cronExpression,
|
||||
inputs: inputValues,
|
||||
credentials: inputCredentials,
|
||||
})
|
||||
.then((schedule) => onCreateSchedule(schedule))
|
||||
.catch(toastOnFail("set up agent run schedule"));
|
||||
},
|
||||
[api, agent, inputValues, inputCredentials, onCreateSchedule, toastOnFail],
|
||||
);
|
||||
|
||||
const runActions: ButtonAction[] = useMemo(
|
||||
() => [
|
||||
// "Regular" agent: [run] + [save as preset] buttons
|
||||
@@ -328,6 +370,14 @@ export default function AgentRunDraftView({
|
||||
variant: "accent",
|
||||
callback: doRun,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<>
|
||||
<CalendarClockIcon className="mr-2 size-4" /> Schedule
|
||||
</>
|
||||
),
|
||||
callback: openScheduleDialog,
|
||||
},
|
||||
// {
|
||||
// label: (
|
||||
// <>
|
||||
@@ -418,12 +468,12 @@ export default function AgentRunDraftView({
|
||||
[
|
||||
agent.has_external_trigger,
|
||||
agentPreset,
|
||||
api,
|
||||
doRun,
|
||||
doSetupTrigger,
|
||||
doCreatePreset,
|
||||
doUpdatePreset,
|
||||
doDeletePreset,
|
||||
openScheduleDialog,
|
||||
changedPresetAttributes,
|
||||
presetName,
|
||||
allRequiredInputsAreSet,
|
||||
@@ -545,6 +595,12 @@ export default function AgentRunDraftView({
|
||||
title={`${agent.has_external_trigger ? "Trigger" : agentPreset ? "Preset" : "Run"} actions`}
|
||||
actions={runActions}
|
||||
/>
|
||||
<CronSchedulerDialog
|
||||
open={cronScheduleDialogOpen}
|
||||
setOpen={setCronScheduleDialogOpen}
|
||||
afterCronCreation={doSetupSchedule}
|
||||
defaultScheduleName={agent.name}
|
||||
/>
|
||||
|
||||
<ActionButtonGroup title="Agent actions" actions={agentActions} />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -30,11 +30,11 @@ interface AgentRunsSelectorListProps {
|
||||
allowDraftNewRun?: boolean;
|
||||
onSelectRun: (id: GraphExecutionID) => void;
|
||||
onSelectPreset: (preset: LibraryAgentPresetID) => void;
|
||||
onSelectSchedule: (schedule: Schedule) => void;
|
||||
onSelectSchedule: (id: ScheduleID) => void;
|
||||
onSelectDraftNewRun: () => void;
|
||||
onDeleteRun: (id: GraphExecutionMeta) => void;
|
||||
onDeletePreset: (id: LibraryAgentPresetID) => void;
|
||||
onDeleteSchedule: (id: ScheduleID) => void;
|
||||
doDeleteRun: (id: GraphExecutionMeta) => void;
|
||||
doDeletePreset: (id: LibraryAgentPresetID) => void;
|
||||
doDeleteSchedule: (id: ScheduleID) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -49,15 +49,23 @@ export default function AgentRunsSelectorList({
|
||||
onSelectPreset,
|
||||
onSelectSchedule,
|
||||
onSelectDraftNewRun,
|
||||
onDeleteRun,
|
||||
onDeletePreset,
|
||||
onDeleteSchedule,
|
||||
doDeleteRun,
|
||||
doDeletePreset,
|
||||
doDeleteSchedule,
|
||||
className,
|
||||
}: AgentRunsSelectorListProps): React.ReactElement {
|
||||
const [activeListTab, setActiveListTab] = useState<"runs" | "scheduled">(
|
||||
"runs",
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedView.type === "schedule") {
|
||||
setActiveListTab("scheduled");
|
||||
} else {
|
||||
setActiveListTab("runs");
|
||||
}
|
||||
}, [selectedView]);
|
||||
|
||||
const listItemClasses = "h-28 w-72 lg:h-32 xl:w-80";
|
||||
|
||||
return (
|
||||
@@ -94,9 +102,7 @@ export default function AgentRunsSelectorList({
|
||||
onClick={() => setActiveListTab("scheduled")}
|
||||
>
|
||||
<span>Scheduled</span>
|
||||
<span className="text-neutral-600">
|
||||
{schedules.filter((s) => s.graph_id === agent.graph_id).length}
|
||||
</span>
|
||||
<span className="text-neutral-600">{schedules.length}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -136,7 +142,7 @@ export default function AgentRunsSelectorList({
|
||||
// timestamp={preset.last_run_time} // TODO: implement this
|
||||
selected={selectedView.id === preset.id}
|
||||
onClick={() => onSelectPreset(preset.id)}
|
||||
onDelete={() => onDeletePreset(preset.id)}
|
||||
onDelete={() => doDeletePreset(preset.id)}
|
||||
/>
|
||||
))}
|
||||
{agentPresets.length > 0 && <Separator className="my-1" />}
|
||||
@@ -158,26 +164,24 @@ export default function AgentRunsSelectorList({
|
||||
timestamp={run.started_at}
|
||||
selected={selectedView.id === run.id}
|
||||
onClick={() => onSelectRun(run.id)}
|
||||
onDelete={() => onDeleteRun(run)}
|
||||
onDelete={() => doDeleteRun(run)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
schedules
|
||||
.filter((schedule) => schedule.graph_id === agent.graph_id)
|
||||
.map((schedule) => (
|
||||
<AgentRunSummaryCard
|
||||
className={listItemClasses}
|
||||
key={schedule.id}
|
||||
type="schedule"
|
||||
status="scheduled" // TODO: implement active/inactive status for schedules
|
||||
title={schedule.name}
|
||||
timestamp={schedule.next_run_time}
|
||||
selected={selectedView.id === schedule.id}
|
||||
onClick={() => onSelectSchedule(schedule)}
|
||||
onDelete={() => onDeleteSchedule(schedule.id)}
|
||||
/>
|
||||
))
|
||||
schedules.map((schedule) => (
|
||||
<AgentRunSummaryCard
|
||||
className={listItemClasses}
|
||||
key={schedule.id}
|
||||
type="schedule"
|
||||
status="scheduled" // TODO: implement active/inactive status for schedules
|
||||
title={schedule.name}
|
||||
timestamp={schedule.next_run_time}
|
||||
selected={selectedView.id === schedule.id}
|
||||
onClick={() => onSelectSchedule(schedule.id)}
|
||||
onDelete={() => doDeleteSchedule(schedule.id)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
@@ -5,27 +5,33 @@ import {
|
||||
GraphExecutionID,
|
||||
GraphMeta,
|
||||
Schedule,
|
||||
ScheduleID,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
|
||||
import type { ButtonAction } from "@/components/agptui/types";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { humanizeCronExpression } from "@/lib/cron-expression-utils";
|
||||
import { AgentRunStatus } from "@/components/agents/agent-run-status-chip";
|
||||
import { useToastOnFail } from "@/components/ui/use-toast";
|
||||
import ActionButtonGroup from "@/components/agptui/action-button-group";
|
||||
import { IconCross } from "@/components/ui/icons";
|
||||
import { PlayIcon } from "lucide-react";
|
||||
import LoadingBox from "@/components/ui/loading";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
export default function AgentScheduleDetailsView({
|
||||
graph,
|
||||
schedule,
|
||||
onForcedRun,
|
||||
agentActions,
|
||||
onForcedRun,
|
||||
doDeleteSchedule,
|
||||
}: {
|
||||
graph: GraphMeta;
|
||||
schedule: Schedule;
|
||||
onForcedRun: (runID: GraphExecutionID) => void;
|
||||
agentActions: ButtonAction[];
|
||||
onForcedRun: (runID: GraphExecutionID) => void;
|
||||
doDeleteSchedule: (scheduleID: ScheduleID) => void;
|
||||
}): React.ReactNode {
|
||||
const api = useBackendAPI();
|
||||
|
||||
@@ -42,7 +48,11 @@ export default function AgentScheduleDetailsView({
|
||||
selectedRunStatus.slice(1),
|
||||
},
|
||||
{
|
||||
label: "Scheduled for",
|
||||
label: "Schedule",
|
||||
value: humanizeCronExpression(schedule.cron),
|
||||
},
|
||||
{
|
||||
label: "Next run",
|
||||
value: schedule.next_run_time.toLocaleString(),
|
||||
},
|
||||
];
|
||||
@@ -70,14 +80,39 @@ export default function AgentScheduleDetailsView({
|
||||
const runNow = useCallback(
|
||||
() =>
|
||||
api
|
||||
.executeGraph(graph.id, graph.version, schedule.input_data)
|
||||
.executeGraph(
|
||||
graph.id,
|
||||
graph.version,
|
||||
schedule.input_data,
|
||||
schedule.input_credentials,
|
||||
)
|
||||
.then((run) => onForcedRun(run.graph_exec_id))
|
||||
.catch(toastOnFail("execute agent")),
|
||||
[api, graph, schedule, onForcedRun, toastOnFail],
|
||||
);
|
||||
|
||||
const runActions: ButtonAction[] = useMemo(
|
||||
() => [{ label: "Run now", callback: () => runNow() }],
|
||||
() => [
|
||||
{
|
||||
label: (
|
||||
<>
|
||||
<PlayIcon className="mr-2 size-4" />
|
||||
Run now
|
||||
</>
|
||||
),
|
||||
callback: runNow,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<>
|
||||
<IconCross className="mr-2 size-4 px-0.5" />
|
||||
Delete schedule
|
||||
</>
|
||||
),
|
||||
callback: () => doDeleteSchedule(schedule.id),
|
||||
variant: "destructive",
|
||||
},
|
||||
],
|
||||
[runNow],
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { CronScheduler } from "@/components/cron-scheduler";
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||
|
||||
type CronSchedulerDialogProps = {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
afterCronCreation: (cronExpression: string, scheduleName: string) => void;
|
||||
defaultScheduleName?: string;
|
||||
};
|
||||
|
||||
export function CronSchedulerDialog({
|
||||
open,
|
||||
setOpen,
|
||||
afterCronCreation,
|
||||
defaultScheduleName = "",
|
||||
}: CronSchedulerDialogProps) {
|
||||
const { toast } = useToast();
|
||||
const [cronExpression, setCronExpression] = useState<string>("");
|
||||
const [scheduleName, setScheduleName] = useState<string>(defaultScheduleName);
|
||||
|
||||
// Reset state when dialog opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setScheduleName(defaultScheduleName);
|
||||
setCronExpression("");
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleDone = () => {
|
||||
if (!scheduleName.trim()) {
|
||||
toast({
|
||||
title: "Please enter a schedule name",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
afterCronCreation(cronExpression, scheduleName);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogTitle>Schedule Task</DialogTitle>
|
||||
<div className="p-2">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="text-sm font-medium">Schedule Name</label>
|
||||
<Input
|
||||
value={scheduleName}
|
||||
onChange={(e) => setScheduleName(e.target.value)}
|
||||
placeholder="Enter a name for this schedule"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CronScheduler onCronExpressionChange={setCronExpression} />
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleDone}>Done</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
338
autogpt_platform/frontend/src/components/cron-scheduler.tsx
Normal file
338
autogpt_platform/frontend/src/components/cron-scheduler.tsx
Normal file
@@ -0,0 +1,338 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CronFrequency, makeCronExpression } from "@/lib/cron-expression-utils";
|
||||
|
||||
const weekDays = [
|
||||
{ label: "Su", value: 0 },
|
||||
{ label: "Mo", value: 1 },
|
||||
{ label: "Tu", value: 2 },
|
||||
{ label: "We", value: 3 },
|
||||
{ label: "Th", value: 4 },
|
||||
{ label: "Fr", value: 5 },
|
||||
{ label: "Sa", value: 6 },
|
||||
];
|
||||
|
||||
const months = [
|
||||
{ label: "Jan", value: "January" },
|
||||
{ label: "Feb", value: "February" },
|
||||
{ label: "Mar", value: "March" },
|
||||
{ label: "Apr", value: "April" },
|
||||
{ label: "May", value: "May" },
|
||||
{ label: "Jun", value: "June" },
|
||||
{ label: "Jul", value: "July" },
|
||||
{ label: "Aug", value: "August" },
|
||||
{ label: "Sep", value: "September" },
|
||||
{ label: "Oct", value: "October" },
|
||||
{ label: "Nov", value: "November" },
|
||||
{ label: "Dec", value: "December" },
|
||||
];
|
||||
|
||||
type CronSchedulerProps = {
|
||||
onCronExpressionChange: (cronExpression: string) => void;
|
||||
};
|
||||
|
||||
export function CronScheduler({
|
||||
onCronExpressionChange,
|
||||
}: CronSchedulerProps): React.ReactElement {
|
||||
const [frequency, setFrequency] = useState<CronFrequency>("daily");
|
||||
const [selectedMinute, setSelectedMinute] = useState<string>("0");
|
||||
const [selectedTime, setSelectedTime] = useState<string>("09:00");
|
||||
const [selectedWeekDays, setSelectedWeekDays] = useState<number[]>([]);
|
||||
const [selectedMonthDays, setSelectedMonthDays] = useState<number[]>([]);
|
||||
const [selectedMonths, setSelectedMonths] = useState<number[]>([]);
|
||||
const [customInterval, setCustomInterval] = useState<{
|
||||
value: number;
|
||||
unit: "minutes" | "hours" | "days";
|
||||
}>({ value: 1, unit: "minutes" });
|
||||
const [showCustomDays, setShowCustomDays] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const cronExpr = makeCronExpression({
|
||||
frequency,
|
||||
minute:
|
||||
frequency === "hourly"
|
||||
? parseInt(selectedMinute)
|
||||
: parseInt(selectedTime.split(":")[1]),
|
||||
hour: parseInt(selectedTime.split(":")[0]),
|
||||
days:
|
||||
frequency === "weekly"
|
||||
? selectedWeekDays
|
||||
: frequency === "monthly"
|
||||
? selectedMonthDays
|
||||
: [],
|
||||
months: frequency === "yearly" ? selectedMonths : [],
|
||||
customInterval:
|
||||
frequency === "custom" ? customInterval : { unit: "minutes", value: 1 },
|
||||
});
|
||||
onCronExpressionChange(cronExpr);
|
||||
}, [
|
||||
frequency,
|
||||
selectedMinute,
|
||||
selectedTime,
|
||||
selectedWeekDays,
|
||||
selectedMonthDays,
|
||||
selectedMonths,
|
||||
customInterval,
|
||||
onCronExpressionChange,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="max-w-md space-y-6">
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base font-medium">Repeat</Label>
|
||||
|
||||
<Select
|
||||
value={frequency}
|
||||
onValueChange={(value: CronFrequency) => setFrequency(value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select frequency" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="every minute">Every Minute</SelectItem>
|
||||
<SelectItem value="hourly">Every Hour</SelectItem>
|
||||
<SelectItem value="daily">Daily</SelectItem>
|
||||
<SelectItem value="weekly">Weekly</SelectItem>
|
||||
<SelectItem value="monthly">Monthly</SelectItem>
|
||||
<SelectItem value="yearly">Yearly</SelectItem>
|
||||
<SelectItem value="custom">Custom</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{frequency === "hourly" && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>At minute</Label>
|
||||
<Select value={selectedMinute} onValueChange={setSelectedMinute}>
|
||||
<SelectTrigger className="w-24">
|
||||
<SelectValue placeholder="Select minute" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[0, 15, 30, 45].map((min) => (
|
||||
<SelectItem key={min} value={min.toString()}>
|
||||
{min}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{frequency === "custom" && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>Every</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
className="w-20"
|
||||
value={customInterval.value}
|
||||
onChange={(e) =>
|
||||
setCustomInterval({
|
||||
...customInterval,
|
||||
value: parseInt(e.target.value),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
value={customInterval.unit}
|
||||
onValueChange={(value: any) =>
|
||||
setCustomInterval({ ...customInterval, unit: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="minutes">Minutes</SelectItem>
|
||||
<SelectItem value="hours">Hours</SelectItem>
|
||||
<SelectItem value="days">Days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{frequency === "weekly" && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>On</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 px-2 py-1 text-xs"
|
||||
onClick={() => {
|
||||
if (selectedWeekDays.length === weekDays.length) {
|
||||
setSelectedWeekDays([]);
|
||||
} else {
|
||||
setSelectedWeekDays(weekDays.map((day) => day.value));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedWeekDays.length === weekDays.length
|
||||
? "Deselect All"
|
||||
: "Select All"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 px-2 py-1 text-xs"
|
||||
onClick={() => setSelectedWeekDays([1, 2, 3, 4, 5])}
|
||||
>
|
||||
Weekdays
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 px-2 py-1 text-xs"
|
||||
onClick={() => setSelectedWeekDays([0, 6])}
|
||||
>
|
||||
Weekends
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{weekDays.map((day) => (
|
||||
<Button
|
||||
key={day.value}
|
||||
variant={
|
||||
selectedWeekDays.includes(day.value) ? "default" : "outline"
|
||||
}
|
||||
className="h-10 w-10 p-0"
|
||||
onClick={() => {
|
||||
setSelectedWeekDays((prev) =>
|
||||
prev.includes(day.value)
|
||||
? prev.filter((d) => d !== day.value)
|
||||
: [...prev, day.value],
|
||||
);
|
||||
}}
|
||||
>
|
||||
{day.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{frequency === "monthly" && (
|
||||
<div className="space-y-4">
|
||||
<Label>Days of Month</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={!showCustomDays ? "default" : "outline"}
|
||||
onClick={() => {
|
||||
setShowCustomDays(false);
|
||||
setSelectedMonthDays(
|
||||
Array.from({ length: 31 }, (_, i) => i + 1),
|
||||
);
|
||||
}}
|
||||
>
|
||||
All Days
|
||||
</Button>
|
||||
<Button
|
||||
variant={showCustomDays ? "default" : "outline"}
|
||||
onClick={() => {
|
||||
setShowCustomDays(true);
|
||||
setSelectedMonthDays([]);
|
||||
}}
|
||||
>
|
||||
Customize
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setSelectedMonthDays([15])}
|
||||
>
|
||||
15th
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setSelectedMonthDays([31])}
|
||||
>
|
||||
Last Day
|
||||
</Button>
|
||||
</div>
|
||||
{showCustomDays && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Array.from({ length: 31 }, (_, i) => (
|
||||
<Button
|
||||
key={i + 1}
|
||||
variant={
|
||||
selectedMonthDays.includes(i + 1) ? "default" : "outline"
|
||||
}
|
||||
className="h-10 w-10 p-0"
|
||||
onClick={() => {
|
||||
setSelectedMonthDays((prev) =>
|
||||
prev.includes(i + 1)
|
||||
? prev.filter((d) => d !== i + 1)
|
||||
: [...prev, i + 1],
|
||||
);
|
||||
}}
|
||||
>
|
||||
{i + 1}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{frequency === "yearly" && (
|
||||
<div className="space-y-4">
|
||||
<Label>Months</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 px-2 py-1 text-xs"
|
||||
onClick={() => {
|
||||
if (selectedMonths.length === months.length) {
|
||||
setSelectedMonths([]);
|
||||
} else {
|
||||
setSelectedMonths(Array.from({ length: 12 }, (_, i) => i));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedMonths.length === months.length
|
||||
? "Deselect All"
|
||||
: "Select All"}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{months.map((month, i) => {
|
||||
const monthNumber = i + 1;
|
||||
return (
|
||||
<Button
|
||||
key={i}
|
||||
variant={
|
||||
selectedMonths.includes(monthNumber) ? "default" : "outline"
|
||||
}
|
||||
className="px-2 py-1"
|
||||
onClick={() => {
|
||||
setSelectedMonths((prev) =>
|
||||
prev.includes(monthNumber)
|
||||
? prev.filter((m) => m !== monthNumber)
|
||||
: [...prev, monthNumber],
|
||||
);
|
||||
}}
|
||||
>
|
||||
{month.label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{frequency !== "every minute" && frequency !== "hourly" && (
|
||||
<div className="flex items-center gap-4 space-y-2">
|
||||
<Label className="pt-2">At</Label>
|
||||
<Input
|
||||
type="time"
|
||||
value={selectedTime}
|
||||
onChange={(e) => setSelectedTime(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,417 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Separator } from "./ui/separator";
|
||||
import { CronExpressionManager } from "@/lib/monitor/cronExpressionManager";
|
||||
|
||||
interface CronSchedulerProps {
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
open: boolean;
|
||||
afterCronCreation: (cronExpression: string) => void;
|
||||
}
|
||||
|
||||
export function CronScheduler({
|
||||
setOpen,
|
||||
open,
|
||||
afterCronCreation,
|
||||
}: CronSchedulerProps) {
|
||||
const [frequency, setFrequency] = useState<
|
||||
"minute" | "hour" | "daily" | "weekly" | "monthly" | "yearly" | "custom"
|
||||
>("daily");
|
||||
const [selectedDays, setSelectedDays] = useState<number[]>([]);
|
||||
const [selectedTime, setSelectedTime] = useState<string>("09:00");
|
||||
const [showCustomDays, setShowCustomDays] = useState<boolean>(false);
|
||||
const [selectedMinute, setSelectedMinute] = useState<string>("0");
|
||||
const [customInterval, setCustomInterval] = useState<{
|
||||
value: number;
|
||||
unit: "minutes" | "hours" | "days";
|
||||
}>({ value: 1, unit: "minutes" });
|
||||
|
||||
// const [endType, setEndType] = useState<"never" | "on" | "after">("never");
|
||||
// const [endDate, setEndDate] = useState<Date | undefined>();
|
||||
// const [occurrences, setOccurrences] = useState<number>(1);
|
||||
|
||||
const weekDays = [
|
||||
{ label: "Su", value: 0 },
|
||||
{ label: "Mo", value: 1 },
|
||||
{ label: "Tu", value: 2 },
|
||||
{ label: "We", value: 3 },
|
||||
{ label: "Th", value: 4 },
|
||||
{ label: "Fr", value: 5 },
|
||||
{ label: "Sa", value: 6 },
|
||||
];
|
||||
|
||||
const months = [
|
||||
{ label: "Jan", value: "January" },
|
||||
{ label: "Feb", value: "February" },
|
||||
{ label: "Mar", value: "March" },
|
||||
{ label: "Apr", value: "April" },
|
||||
{ label: "May", value: "May" },
|
||||
{ label: "Jun", value: "June" },
|
||||
{ label: "Jul", value: "July" },
|
||||
{ label: "Aug", value: "August" },
|
||||
{ label: "Sep", value: "September" },
|
||||
{ label: "Oct", value: "October" },
|
||||
{ label: "Nov", value: "November" },
|
||||
{ label: "Dec", value: "December" },
|
||||
];
|
||||
|
||||
const cron_manager = new CronExpressionManager();
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogTitle>Schedule Task</DialogTitle>
|
||||
<div className="max-w-md space-y-6 p-2">
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base font-medium">Repeat</Label>
|
||||
|
||||
<Select
|
||||
onValueChange={(value: any) => setFrequency(value)}
|
||||
defaultValue="daily"
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select frequency" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="minute">Every Minute</SelectItem>
|
||||
<SelectItem value="hour">Every Hour</SelectItem>
|
||||
<SelectItem value="daily">Daily</SelectItem>
|
||||
<SelectItem value="weekly">Weekly</SelectItem>
|
||||
<SelectItem value="monthly">Monthly</SelectItem>
|
||||
<SelectItem value="yearly">Yearly</SelectItem>
|
||||
<SelectItem value="custom">Custom</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{frequency === "hour" && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>At minute</Label>
|
||||
<Select
|
||||
value={selectedMinute}
|
||||
onValueChange={setSelectedMinute}
|
||||
>
|
||||
<SelectTrigger className="w-24">
|
||||
<SelectValue placeholder="Select minute" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[0, 15, 30, 45].map((min) => (
|
||||
<SelectItem key={min} value={min.toString()}>
|
||||
{min}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{frequency === "custom" && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>Every</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
className="w-20"
|
||||
value={customInterval.value}
|
||||
onChange={(e) =>
|
||||
setCustomInterval({
|
||||
...customInterval,
|
||||
value: parseInt(e.target.value),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
value={customInterval.unit}
|
||||
onValueChange={(value: any) =>
|
||||
setCustomInterval({ ...customInterval, unit: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="minutes">Minutes</SelectItem>
|
||||
<SelectItem value="hours">Hours</SelectItem>
|
||||
<SelectItem value="days">Days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{frequency === "weekly" && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>On</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 px-2 py-1 text-xs"
|
||||
onClick={() => {
|
||||
if (selectedDays.length === weekDays.length) {
|
||||
setSelectedDays([]);
|
||||
} else {
|
||||
setSelectedDays(weekDays.map((day) => day.value));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedDays.length === weekDays.length
|
||||
? "Deselect All"
|
||||
: "Select All"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 px-2 py-1 text-xs"
|
||||
onClick={() => setSelectedDays([1, 2, 3, 4, 5])}
|
||||
>
|
||||
Weekdays
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 px-2 py-1 text-xs"
|
||||
onClick={() => setSelectedDays([0, 6])}
|
||||
>
|
||||
Weekends
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{weekDays.map((day) => (
|
||||
<Button
|
||||
key={day.value}
|
||||
variant={
|
||||
selectedDays.includes(day.value) ? "default" : "outline"
|
||||
}
|
||||
className="h-10 w-10 p-0"
|
||||
onClick={() => {
|
||||
setSelectedDays((prev) =>
|
||||
prev.includes(day.value)
|
||||
? prev.filter((d) => d !== day.value)
|
||||
: [...prev, day.value],
|
||||
);
|
||||
}}
|
||||
>
|
||||
{day.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{frequency === "monthly" && (
|
||||
<div className="space-y-4">
|
||||
<Label>Days of Month</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={!showCustomDays ? "default" : "outline"}
|
||||
onClick={() => {
|
||||
setShowCustomDays(false);
|
||||
setSelectedDays(
|
||||
Array.from({ length: 31 }, (_, i) => i + 1),
|
||||
);
|
||||
}}
|
||||
>
|
||||
All Days
|
||||
</Button>
|
||||
<Button
|
||||
variant={showCustomDays ? "default" : "outline"}
|
||||
onClick={() => {
|
||||
setShowCustomDays(true);
|
||||
setSelectedDays([]);
|
||||
}}
|
||||
>
|
||||
Customize
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setSelectedDays([15])}>
|
||||
15th
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setSelectedDays([31])}>
|
||||
Last Day
|
||||
</Button>
|
||||
</div>
|
||||
{showCustomDays && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Array.from({ length: 31 }, (_, i) => (
|
||||
<Button
|
||||
key={i + 1}
|
||||
variant={
|
||||
selectedDays.includes(i + 1) ? "default" : "outline"
|
||||
}
|
||||
className="h-10 w-10 p-0"
|
||||
onClick={() => {
|
||||
setSelectedDays((prev) =>
|
||||
prev.includes(i + 1)
|
||||
? prev.filter((d) => d !== i + 1)
|
||||
: [...prev, i + 1],
|
||||
);
|
||||
}}
|
||||
>
|
||||
{i + 1}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{frequency === "yearly" && (
|
||||
<div className="space-y-4">
|
||||
<Label>Months</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 px-2 py-1 text-xs"
|
||||
onClick={() => {
|
||||
if (selectedDays.length === months.length) {
|
||||
setSelectedDays([]);
|
||||
} else {
|
||||
setSelectedDays(Array.from({ length: 12 }, (_, i) => i));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedDays.length === months.length
|
||||
? "Deselect All"
|
||||
: "Select All"}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{months.map((month, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant={
|
||||
selectedDays.includes(index) ? "default" : "outline"
|
||||
}
|
||||
className="px-2 py-1"
|
||||
onClick={() => {
|
||||
setSelectedDays((prev) =>
|
||||
prev.includes(index)
|
||||
? prev.filter((m) => m !== index)
|
||||
: [...prev, index],
|
||||
);
|
||||
}}
|
||||
>
|
||||
{month.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{frequency !== "minute" && frequency !== "hour" && (
|
||||
<div className="flex items-center gap-4 space-y-2">
|
||||
<Label className="pt-2">At</Label>
|
||||
<Input
|
||||
type="time"
|
||||
value={selectedTime}
|
||||
onChange={(e) => setSelectedTime(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
{/*
|
||||
|
||||
On the backend, we are using standard cron expressions,
|
||||
which makes it challenging to add an end date or stop execution
|
||||
after a certain time using only cron expressions.
|
||||
(since standard cron expressions have limitations, like the lack of a year field or more...).
|
||||
|
||||
We could also use ranges in cron expression for end dates but It doesm't cover all cases (sometimes break)
|
||||
|
||||
To automatically end the scheduler, we need to store the end date and time occurrence in the database
|
||||
and modify scheduler.add_job. Currently, we can only stop the scheduler manually from the monitor tab.
|
||||
|
||||
*/}
|
||||
|
||||
{/* <div className="space-y-6">
|
||||
<Label className="text-lg font-medium">Ends</Label>
|
||||
<RadioGroup
|
||||
value={endType}
|
||||
onValueChange={(value: "never" | "on" | "after") =>
|
||||
setEndType(value)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="never" id="never" />
|
||||
<Label htmlFor="never">Never</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="on" id="on" />
|
||||
<Label htmlFor="on" className="w-[50px]">
|
||||
On
|
||||
</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
disabled={endType !== "on"}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{endDate ? format(endDate, "PPP") : "Pick a date"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={endDate}
|
||||
onSelect={setEndDate}
|
||||
disabled={(date) => date < new Date()}
|
||||
fromDate={new Date()}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="after" id="after" />
|
||||
<Label htmlFor="after" className="ml-2 w-[50px]">
|
||||
After
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
className="ml-2 w-[100px]"
|
||||
value={occurrences}
|
||||
onChange={(e) => setOccurrences(Number(e.target.value))}
|
||||
/>
|
||||
<span>times</span>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div> */}
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const cronExpr = cron_manager.generateCronExpression(
|
||||
frequency,
|
||||
selectedTime,
|
||||
selectedDays,
|
||||
selectedMinute,
|
||||
customInterval,
|
||||
);
|
||||
setFrequency("minute");
|
||||
setSelectedDays([]);
|
||||
setSelectedTime("00:00");
|
||||
setShowCustomDays(false);
|
||||
setSelectedMinute("0");
|
||||
setCustomInterval({ value: 1, unit: "minutes" });
|
||||
setOpen(false);
|
||||
afterCronCreation(cronExpr);
|
||||
}}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { ClockIcon, Loader2 } from "lucide-react";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { CronExpressionManager } from "@/lib/monitor/cronExpressionManager";
|
||||
import { humanizeCronExpression } from "@/lib/cron-expression-utils";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -52,7 +52,6 @@ export const SchedulesTable = ({
|
||||
}: SchedulesTableProps) => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const cron_manager = new CronExpressionManager();
|
||||
const [selectedAgent, setSelectedAgent] = useState<string>(""); // Library Agent ID
|
||||
const [selectedVersion, setSelectedVersion] = useState<number>(0); // Graph version
|
||||
const [maxVersion, setMaxVersion] = useState<number>(0);
|
||||
@@ -246,7 +245,7 @@ export const SchedulesTable = ({
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">
|
||||
{cron_manager.generateDescription(schedule.cron || "")}
|
||||
{humanizeCronExpression(schedule.cron)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
|
||||
|
||||
@@ -1073,16 +1073,21 @@ export default function useAgentGraph(
|
||||
|
||||
// runs after saving cron expression and inputs (if exists)
|
||||
const scheduleRunner = useCallback(
|
||||
async (cronExpression: string, inputs: InputItem[]) => {
|
||||
async (
|
||||
cronExpression: string,
|
||||
inputs: InputItem[],
|
||||
scheduleName: string,
|
||||
) => {
|
||||
await saveAgent();
|
||||
try {
|
||||
if (flowID) {
|
||||
await api.createSchedule({
|
||||
await api.createGraphExecutionSchedule({
|
||||
graph_id: flowID,
|
||||
// flowVersion is always defined here because scheduling is opened for a specific version
|
||||
graph_version: flowVersion!,
|
||||
name: scheduleName,
|
||||
cron: cronExpression,
|
||||
input_data: inputs.reduce(
|
||||
inputs: inputs.reduce(
|
||||
(acc, input) => ({
|
||||
...acc,
|
||||
[input.hardcodedValues.name]: input.hardcodedValues.value,
|
||||
|
||||
@@ -742,22 +742,35 @@ export default class BackendAPI {
|
||||
/////////// SCHEDULES ////////////
|
||||
//////////////////////////////////
|
||||
|
||||
async createSchedule(schedule: ScheduleCreatable): Promise<Schedule> {
|
||||
return this._request("POST", `/schedules`, schedule).then(
|
||||
parseScheduleTimestamp,
|
||||
async createGraphExecutionSchedule(
|
||||
params: ScheduleCreatable,
|
||||
): Promise<Schedule> {
|
||||
return this._request(
|
||||
"POST",
|
||||
`/graphs/${params.graph_id}/schedules`,
|
||||
params,
|
||||
).then(parseScheduleTimestamp);
|
||||
}
|
||||
|
||||
async listGraphExecutionSchedules(graphID: GraphID): Promise<Schedule[]> {
|
||||
return this._get(`/graphs/${graphID}/schedules`).then((schedules) =>
|
||||
schedules.map(parseScheduleTimestamp),
|
||||
);
|
||||
}
|
||||
|
||||
async deleteSchedule(scheduleId: ScheduleID): Promise<{ id: string }> {
|
||||
return this._request("DELETE", `/schedules/${scheduleId}`);
|
||||
}
|
||||
|
||||
async listSchedules(): Promise<Schedule[]> {
|
||||
/** @deprecated only used in legacy `Monitor` */
|
||||
async listAllGraphsExecutionSchedules(): Promise<Schedule[]> {
|
||||
return this._get(`/schedules`).then((schedules) =>
|
||||
schedules.map(parseScheduleTimestamp),
|
||||
);
|
||||
}
|
||||
|
||||
async deleteGraphExecutionSchedule(
|
||||
scheduleID: ScheduleID,
|
||||
): Promise<{ id: ScheduleID }> {
|
||||
return this._request("DELETE", `/schedules/${scheduleID}`);
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
////////////// OTTO //////////////
|
||||
//////////////////////////////////
|
||||
|
||||
@@ -771,6 +771,7 @@ export type ProfileDetails = {
|
||||
avatar_url: string;
|
||||
};
|
||||
|
||||
/* Mirror of backend/executor/scheduler.py:GraphExecutionJobInfo */
|
||||
export type Schedule = {
|
||||
id: ScheduleID;
|
||||
name: string;
|
||||
@@ -778,17 +779,21 @@ export type Schedule = {
|
||||
user_id: UserID;
|
||||
graph_id: GraphID;
|
||||
graph_version: number;
|
||||
input_data: { [key: string]: any };
|
||||
input_data: Record<string, any>;
|
||||
input_credentials: Record<string, CredentialsMetaInput>;
|
||||
next_run_time: Date;
|
||||
};
|
||||
|
||||
export type ScheduleID = Brand<string, "ScheduleID">;
|
||||
|
||||
/* Mirror of backend/server/routers/v1.py:ScheduleCreationRequest */
|
||||
export type ScheduleCreatable = {
|
||||
cron: string;
|
||||
graph_id: GraphID;
|
||||
graph_version: number;
|
||||
input_data: { [key: string]: any };
|
||||
name: string;
|
||||
cron: string;
|
||||
inputs: Record<string, any>;
|
||||
credentials?: Record<string, CredentialsMetaInput>;
|
||||
};
|
||||
|
||||
export type MyAgent = {
|
||||
|
||||
265
autogpt_platform/frontend/src/lib/cron-expression-utils.ts
Normal file
265
autogpt_platform/frontend/src/lib/cron-expression-utils.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
export type CronFrequency =
|
||||
| "every minute"
|
||||
| "hourly"
|
||||
| "daily"
|
||||
| "weekly"
|
||||
| "monthly"
|
||||
| "yearly"
|
||||
| "custom";
|
||||
|
||||
export type CronExpressionParams =
|
||||
| { frequency: "every minute" }
|
||||
| {
|
||||
frequency: "hourly";
|
||||
minute: number;
|
||||
}
|
||||
| ((
|
||||
| {
|
||||
frequency: "daily";
|
||||
}
|
||||
| {
|
||||
frequency: "weekly";
|
||||
/** 0-based list of weekdays: 0 = Monday ... 6 = Sunday */
|
||||
days: number[];
|
||||
}
|
||||
| {
|
||||
frequency: "monthly";
|
||||
/** 1-based list of month days */
|
||||
days: number[];
|
||||
}
|
||||
| {
|
||||
frequency: "yearly";
|
||||
/** 1-based list of months (1-12) */
|
||||
months: number[];
|
||||
}
|
||||
| {
|
||||
frequency: "custom";
|
||||
customInterval: { unit: string; value: number };
|
||||
}
|
||||
) & {
|
||||
minute: number;
|
||||
hour: number;
|
||||
});
|
||||
|
||||
export function makeCronExpression(params: CronExpressionParams): string {
|
||||
const frequency = params.frequency;
|
||||
|
||||
if (frequency === "every minute") return "* * * * *";
|
||||
if (frequency === "hourly") return `${params.minute} * * * *`;
|
||||
if (frequency === "daily") return `${params.minute} ${params.hour} * * *`;
|
||||
if (frequency === "weekly") {
|
||||
const { minute, hour, days } = params;
|
||||
const weekDaysExpr = days.sort((a, b) => a - b).join(",");
|
||||
return `${minute} ${hour} * * ${weekDaysExpr}`;
|
||||
}
|
||||
if (frequency === "monthly") {
|
||||
const { minute, hour, days } = params;
|
||||
const monthDaysExpr = days.sort((a, b) => a - b).join(",");
|
||||
return `${minute} ${hour} ${monthDaysExpr} * *`;
|
||||
}
|
||||
if (frequency === "yearly") {
|
||||
const { minute, hour, months } = params;
|
||||
const monthList = months.sort((a, b) => a - b).join(",");
|
||||
return `${minute} ${hour} 1 ${monthList} *`;
|
||||
}
|
||||
if (frequency === "custom") {
|
||||
const { minute, hour, customInterval } = params;
|
||||
if (customInterval.unit === "minutes") {
|
||||
return `*/${customInterval.value} * * * *`;
|
||||
} else if (customInterval.unit === "hours") {
|
||||
return `0 */${customInterval.value} * * *`;
|
||||
} else {
|
||||
return `${minute} ${hour} */${customInterval.value} * *`;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
export function humanizeCronExpression(cronExpression: string): string {
|
||||
const parts = cronExpression.trim().split(/\s+/);
|
||||
if (parts.length !== 5) {
|
||||
throw new Error("Invalid cron expression format.");
|
||||
}
|
||||
|
||||
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
||||
|
||||
// Handle every minute
|
||||
if (cronExpression === "* * * * *") {
|
||||
return "Every minute";
|
||||
}
|
||||
|
||||
// Handle minute intervals (e.g., */5 * * * *)
|
||||
if (
|
||||
minute.startsWith("*/") &&
|
||||
hour === "*" &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*"
|
||||
) {
|
||||
const interval = minute.substring(2);
|
||||
return `Every ${interval} minutes`;
|
||||
}
|
||||
|
||||
// Handle hour intervals (e.g., 30 * * * *)
|
||||
if (
|
||||
hour === "*" &&
|
||||
!minute.includes("/") &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*"
|
||||
) {
|
||||
return `Every hour at minute ${minute}`;
|
||||
}
|
||||
|
||||
// Handle every N hours (e.g., 0 */2 * * *)
|
||||
if (
|
||||
hour.startsWith("*/") &&
|
||||
minute === "0" &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*"
|
||||
) {
|
||||
const interval = hour.substring(2);
|
||||
return `Every ${interval} hours`;
|
||||
}
|
||||
|
||||
// Handle daily (e.g., 30 14 * * *)
|
||||
if (
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*" &&
|
||||
!minute.includes("/") &&
|
||||
!hour.includes("/")
|
||||
) {
|
||||
return `Every day at ${formatTime(hour, minute)}`;
|
||||
}
|
||||
|
||||
// Handle weekly (e.g., 30 14 * * 1,3,5)
|
||||
if (
|
||||
dayOfWeek !== "*" &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
!minute.includes("/") &&
|
||||
!hour.includes("/")
|
||||
) {
|
||||
const days = getDayNames(dayOfWeek);
|
||||
return `Every ${days} at ${formatTime(hour, minute)}`;
|
||||
}
|
||||
|
||||
// Handle monthly (e.g., 30 14 1,15 * *)
|
||||
if (
|
||||
dayOfMonth !== "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*" &&
|
||||
!minute.includes("/") &&
|
||||
!hour.includes("/")
|
||||
) {
|
||||
const days = dayOfMonth.split(",").map(Number);
|
||||
const dayList = days.join(", ");
|
||||
return `On day ${dayList} of every month at ${formatTime(hour, minute)}`;
|
||||
}
|
||||
|
||||
// Handle yearly (e.g., 30 14 1 1,6,12 *)
|
||||
if (
|
||||
dayOfMonth !== "*" &&
|
||||
month !== "*" &&
|
||||
dayOfWeek === "*" &&
|
||||
!minute.includes("/") &&
|
||||
!hour.includes("/")
|
||||
) {
|
||||
const months = getMonthNames(month);
|
||||
return `Every year on the 1st day of ${months} at ${formatTime(hour, minute)}`;
|
||||
}
|
||||
|
||||
// Handle custom minute intervals with other fields as * (e.g., every N minutes)
|
||||
if (
|
||||
minute.includes("/") &&
|
||||
hour === "*" &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*"
|
||||
) {
|
||||
const interval = minute.split("/")[1];
|
||||
return `Every ${interval} minutes`;
|
||||
}
|
||||
|
||||
// Handle custom hour intervals with other fields as * (e.g., every N hours)
|
||||
if (
|
||||
hour.includes("/") &&
|
||||
minute === "0" &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*"
|
||||
) {
|
||||
const interval = hour.split("/")[1];
|
||||
return `Every ${interval} hours`;
|
||||
}
|
||||
|
||||
// Handle specific days with custom intervals (e.g., every N days)
|
||||
if (
|
||||
dayOfMonth.startsWith("*/") &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*" &&
|
||||
!minute.includes("/") &&
|
||||
!hour.includes("/")
|
||||
) {
|
||||
const interval = dayOfMonth.substring(2);
|
||||
return `Every ${interval} days at ${formatTime(hour, minute)}`;
|
||||
}
|
||||
|
||||
return `Cron Expression: ${cronExpression}`;
|
||||
}
|
||||
|
||||
function formatTime(hour: string, minute: string): string {
|
||||
const formattedHour = padZero(hour);
|
||||
const formattedMinute = padZero(minute);
|
||||
return `${formattedHour}:${formattedMinute}`;
|
||||
}
|
||||
|
||||
function padZero(value: string): string {
|
||||
return value.padStart(2, "0");
|
||||
}
|
||||
|
||||
function getDayNames(dayOfWeek: string): string {
|
||||
const days = dayOfWeek.split(",").map(Number);
|
||||
const dayNames = days
|
||||
.map((d) => {
|
||||
const names = [
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
];
|
||||
return names[d] || `Unknown(${d})`;
|
||||
})
|
||||
.join(", ");
|
||||
return dayNames;
|
||||
}
|
||||
|
||||
function getMonthNames(month: string): string {
|
||||
const months = month.split(",").map(Number);
|
||||
const monthNames = months
|
||||
.map((m) => {
|
||||
const names = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
return names[m - 1] || `Unknown(${m})`;
|
||||
})
|
||||
.join(", ");
|
||||
return monthNames;
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
export class CronExpressionManager {
|
||||
generateCronExpression(
|
||||
frequency: string,
|
||||
selectedTime: string,
|
||||
selectedDays: number[],
|
||||
selectedMinute: string,
|
||||
customInterval: { unit: string; value: number },
|
||||
): string {
|
||||
const [hours, minutes] = selectedTime.split(":").map(Number);
|
||||
let expression = "";
|
||||
|
||||
switch (frequency) {
|
||||
case "minute":
|
||||
expression = "* * * * *";
|
||||
break;
|
||||
case "hour":
|
||||
expression = `${selectedMinute} * * * *`;
|
||||
break;
|
||||
case "daily":
|
||||
expression = `${minutes} ${hours} * * *`;
|
||||
break;
|
||||
case "weekly":
|
||||
const days = selectedDays.join(",");
|
||||
expression = `${minutes} ${hours} * * ${days}`;
|
||||
break;
|
||||
case "monthly":
|
||||
const monthDays = selectedDays.sort((a, b) => a - b).join(",");
|
||||
expression = `${minutes} ${hours} ${monthDays} * *`;
|
||||
break;
|
||||
case "yearly":
|
||||
const monthList = selectedDays
|
||||
.map((d) => d + 1)
|
||||
.sort((a, b) => a - b)
|
||||
.join(",");
|
||||
expression = `${minutes} ${hours} 1 ${monthList} *`;
|
||||
break;
|
||||
case "custom":
|
||||
if (customInterval.unit === "minutes") {
|
||||
expression = `*/${customInterval.value} * * * *`;
|
||||
} else if (customInterval.unit === "hours") {
|
||||
expression = `0 */${customInterval.value} * * *`;
|
||||
} else {
|
||||
expression = `${minutes} ${hours} */${customInterval.value} * *`;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
expression = "";
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
|
||||
generateDescription(cronExpression: string): string {
|
||||
const parts = cronExpression.trim().split(/\s+/);
|
||||
if (parts.length !== 5) {
|
||||
throw new Error("Invalid cron expression format.");
|
||||
}
|
||||
|
||||
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
||||
|
||||
// Handle every minute
|
||||
if (cronExpression === "* * * * *") {
|
||||
return "Every minute";
|
||||
}
|
||||
|
||||
// Handle minute intervals (e.g., */5 * * * *)
|
||||
if (
|
||||
minute.startsWith("*/") &&
|
||||
hour === "*" &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*"
|
||||
) {
|
||||
const interval = minute.substring(2);
|
||||
return `Every ${interval} minutes`;
|
||||
}
|
||||
|
||||
// Handle hour intervals (e.g., 30 * * * *)
|
||||
if (
|
||||
hour === "*" &&
|
||||
!minute.includes("/") &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*"
|
||||
) {
|
||||
return `Every hour at minute ${minute}`;
|
||||
}
|
||||
|
||||
// Handle every N hours (e.g., 0 */2 * * *)
|
||||
if (
|
||||
hour.startsWith("*/") &&
|
||||
minute === "0" &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*"
|
||||
) {
|
||||
const interval = hour.substring(2);
|
||||
return `Every ${interval} hours`;
|
||||
}
|
||||
|
||||
// Handle daily (e.g., 30 14 * * *)
|
||||
if (
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*" &&
|
||||
!minute.includes("/") &&
|
||||
!hour.includes("/")
|
||||
) {
|
||||
return `Every day at ${this.formatTime(hour, minute)}`;
|
||||
}
|
||||
|
||||
// Handle weekly (e.g., 30 14 * * 1,3,5)
|
||||
if (
|
||||
dayOfWeek !== "*" &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
!minute.includes("/") &&
|
||||
!hour.includes("/")
|
||||
) {
|
||||
const days = this.getDayNames(dayOfWeek);
|
||||
return `Every ${days} at ${this.formatTime(hour, minute)}`;
|
||||
}
|
||||
|
||||
// Handle monthly (e.g., 30 14 1,15 * *)
|
||||
if (
|
||||
dayOfMonth !== "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*" &&
|
||||
!minute.includes("/") &&
|
||||
!hour.includes("/")
|
||||
) {
|
||||
const days = dayOfMonth.split(",").map(Number);
|
||||
const dayList = days.join(", ");
|
||||
return `On day ${dayList} of every month at ${this.formatTime(hour, minute)}`;
|
||||
}
|
||||
|
||||
// Handle yearly (e.g., 30 14 1 1,6,12 *)
|
||||
if (
|
||||
dayOfMonth !== "*" &&
|
||||
month !== "*" &&
|
||||
dayOfWeek === "*" &&
|
||||
!minute.includes("/") &&
|
||||
!hour.includes("/")
|
||||
) {
|
||||
const months = this.getMonthNames(month);
|
||||
return `Every year on the 1st day of ${months} at ${this.formatTime(hour, minute)}`;
|
||||
}
|
||||
|
||||
// Handle custom minute intervals with other fields as * (e.g., every N minutes)
|
||||
if (
|
||||
minute.includes("/") &&
|
||||
hour === "*" &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*"
|
||||
) {
|
||||
const interval = minute.split("/")[1];
|
||||
return `Every ${interval} minutes`;
|
||||
}
|
||||
|
||||
// Handle custom hour intervals with other fields as * (e.g., every N hours)
|
||||
if (
|
||||
hour.includes("/") &&
|
||||
minute === "0" &&
|
||||
dayOfMonth === "*" &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*"
|
||||
) {
|
||||
const interval = hour.split("/")[1];
|
||||
return `Every ${interval} hours`;
|
||||
}
|
||||
|
||||
// Handle specific days with custom intervals (e.g., every N days)
|
||||
if (
|
||||
dayOfMonth.startsWith("*/") &&
|
||||
month === "*" &&
|
||||
dayOfWeek === "*" &&
|
||||
!minute.includes("/") &&
|
||||
!hour.includes("/")
|
||||
) {
|
||||
const interval = dayOfMonth.substring(2);
|
||||
return `Every ${interval} days at ${this.formatTime(hour, minute)}`;
|
||||
}
|
||||
|
||||
return `Cron Expression: ${cronExpression}`;
|
||||
}
|
||||
|
||||
private formatTime(hour: string, minute: string): string {
|
||||
const formattedHour = this.padZero(hour);
|
||||
const formattedMinute = this.padZero(minute);
|
||||
return `${formattedHour}:${formattedMinute}`;
|
||||
}
|
||||
|
||||
private padZero(value: string): string {
|
||||
return value.padStart(2, "0");
|
||||
}
|
||||
|
||||
private getDayNames(dayOfWeek: string): string {
|
||||
const days = dayOfWeek.split(",").map(Number);
|
||||
const dayNames = days
|
||||
.map((d) => {
|
||||
const names = [
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
];
|
||||
return names[d] || `Unknown(${d})`;
|
||||
})
|
||||
.join(", ");
|
||||
return dayNames;
|
||||
}
|
||||
|
||||
private getMonthNames(month: string): string {
|
||||
const months = month.split(",").map(Number);
|
||||
const monthNames = months
|
||||
.map((m) => {
|
||||
const names = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
return names[m - 1] || `Unknown(${m})`;
|
||||
})
|
||||
.join(", ");
|
||||
return monthNames;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user