diff --git a/wavefront/client/src/api/index.ts b/wavefront/client/src/api/index.ts index c1d653f4..2a1aa169 100644 --- a/wavefront/client/src/api/index.ts +++ b/wavefront/client/src/api/index.ts @@ -13,6 +13,7 @@ import { LLMInferenceService } from './llm-inference-service'; import { MessageProcessorService } from './message-processor-service'; import { ModelInferenceService } from './model-inference-service'; import { NamespaceService } from './namespace-service'; +import { ScheduledJobService } from './scheduled-job-service'; import { SttConfigService } from './stt-config-service'; import { TelephonyConfigService } from './telephony-config-service'; import { ToolService } from './tool-service'; @@ -80,6 +81,10 @@ class FloConsoleService { return new NamespaceService(this.http); } + get scheduledJobService() { + return new ScheduledJobService(this.http); + } + get sttConfigService() { return new SttConfigService(this.http); } diff --git a/wavefront/client/src/api/scheduled-job-service.ts b/wavefront/client/src/api/scheduled-job-service.ts new file mode 100644 index 00000000..e6758775 --- /dev/null +++ b/wavefront/client/src/api/scheduled-job-service.ts @@ -0,0 +1,43 @@ +import { + CreateScheduledJobRequest, + CreateScheduledJobResponse, + DeleteScheduledJobResponse, + ListScheduledJobsResponse, + UpdateScheduledJobResponse, +} from '@app/types/scheduled-job'; +import { AxiosInstance } from 'axios'; + +export class ScheduledJobService { + constructor(private http: AxiosInstance) {} + + async createScheduledJob(request: CreateScheduledJobRequest): Promise { + return this.http.post(`/v1/:appId/floware/v1/scheduled-jobs`, request); + } + + async listScheduledJobs(params: { + limit?: number; + query_id?: string; + datasource_id?: string; + job_type?: string; + job_status?: string; + }): Promise { + return this.http.get(`/v1/:appId/floware/v1/scheduled-jobs`, { params }); + } + + async updateScheduledJob( + jobId: string, + payload: { + cron_expr?: string; + timezone?: string; + payload?: Record; + max_retries?: number; + status?: 'active' | 'paused' | 'running' | 'failed' | 'completed'; + } + ): Promise { + return this.http.patch(`/v1/:appId/floware/v1/scheduled-jobs/${jobId}`, payload); + } + + async deleteScheduledJob(jobId: string): Promise { + return this.http.delete(`/v1/:appId/floware/v1/scheduled-jobs/${jobId}`); + } +} diff --git a/wavefront/client/src/pages/apps/[appId]/datasources/ScheduleEmailAlertDialog.tsx b/wavefront/client/src/pages/apps/[appId]/datasources/ScheduleEmailAlertDialog.tsx new file mode 100644 index 00000000..cf8b0c4c --- /dev/null +++ b/wavefront/client/src/pages/apps/[appId]/datasources/ScheduleEmailAlertDialog.tsx @@ -0,0 +1,385 @@ +import { Button } from '@app/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@app/components/ui/dialog'; +import { Input } from '@app/components/ui/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@app/components/ui/select'; +import { Textarea } from '@app/components/ui/textarea'; +import { useNotifyStore } from '@app/store'; +import { ScheduledJob } from '@app/types/scheduled-job'; +import floConsoleService from '@app/api'; +import { useEffect, useMemo, useState } from 'react'; + +interface ScheduleEmailAlertDialogProps { + isOpen: boolean; + datasourceId: string; + queryId: string; + onOpenChange: (open: boolean) => void; +} + +const ScheduleEmailAlertDialog: React.FC = ({ + isOpen, + datasourceId, + queryId, + onOpenChange, +}) => { + const { notifySuccess, notifyError } = useNotifyStore(); + const [jobs, setJobs] = useState([]); + const [loadingJobs, setLoadingJobs] = useState(false); + const [cronExpr, setCronExpr] = useState('0 9 * * *'); + const [timezone, setTimezone] = useState('Asia/Kolkata'); + const [recipientsText, setRecipientsText] = useState(''); + const [subject, setSubject] = useState(''); + const [queryParamsJson, setQueryParamsJson] = useState(''); + const [dateRange, setDateRange] = useState<'none' | 'last_day' | 'last_7_days' | 'last_30_days'>('none'); + const [startDateParamKey, setStartDateParamKey] = useState('start_date'); + const [endDateParamKey, setEndDateParamKey] = useState('end_date'); + const [maxRetries, setMaxRetries] = useState('3'); + const [editingJobId, setEditingJobId] = useState(null); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(''); + + const recipients = useMemo( + () => + recipientsText + .split(',') + .map((email) => email.trim()) + .filter(Boolean), + [recipientsText] + ); + + const resetForm = () => { + setCronExpr('0 9 * * *'); + setTimezone('Asia/Kolkata'); + setRecipientsText(''); + setSubject(''); + setQueryParamsJson(''); + setDateRange('none'); + setStartDateParamKey('start_date'); + setEndDateParamKey('end_date'); + setMaxRetries('3'); + setEditingJobId(null); + setError(''); + }; + + const fetchJobs = async () => { + if (!datasourceId || !queryId) return; + setLoadingJobs(true); + try { + const response = await floConsoleService.scheduledJobService.listScheduledJobs({ + limit: 100, + query_id: queryId, + datasource_id: datasourceId, + }); + setJobs(response.data.data?.jobs || []); + } catch { + notifyError('Failed to fetch existing schedules'); + } finally { + setLoadingJobs(false); + } + }; + + useEffect(() => { + if (isOpen) { + void fetchJobs(); + } else { + setJobs([]); + resetForm(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpen, datasourceId, queryId]); + + const handleOpenChange = (open: boolean) => { + if (!open && !saving) { + resetForm(); + } + onOpenChange(open); + }; + + const handleSave = async () => { + const retries = Number(maxRetries); + if (!cronExpr.trim()) { + setError('Cron expression is required'); + return; + } + if (!timezone.trim()) { + setError('Timezone is required'); + return; + } + if (recipients.length === 0) { + setError('At least one recipient email is required'); + return; + } + if (!Number.isInteger(retries) || retries < 0 || retries > 10) { + setError('Max retries must be an integer between 0 and 10'); + return; + } + let parsedParams: Record | undefined; + if (queryParamsJson.trim()) { + try { + const value = JSON.parse(queryParamsJson); + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + setError('Query params must be a JSON object'); + return; + } + parsedParams = value as Record; + } catch { + setError('Query params must be valid JSON (object)'); + return; + } + } + + setSaving(true); + setError(''); + try { + if (editingJobId) { + await floConsoleService.scheduledJobService.updateScheduledJob(editingJobId, { + cron_expr: cronExpr.trim(), + timezone: timezone.trim(), + max_retries: retries, + payload: { + datasource_id: datasourceId, + query_id: queryId, + recipients, + subject: subject.trim() || undefined, + date_range: dateRange === 'none' ? undefined : dateRange, + start_date_param: dateRange === 'none' ? undefined : startDateParamKey.trim() || 'start_date', + end_date_param: dateRange === 'none' ? undefined : endDateParamKey.trim() || 'end_date', + params: parsedParams, + }, + }); + notifySuccess('Schedule updated successfully'); + } else { + await floConsoleService.scheduledJobService.createScheduledJob({ + job_type: 'email_dynamic_query', + cron_expr: cronExpr.trim(), + timezone: timezone.trim(), + max_retries: retries, + payload: { + datasource_id: datasourceId, + query_id: queryId, + recipients, + subject: subject.trim() || undefined, + date_range: dateRange === 'none' ? undefined : dateRange, + start_date_param: dateRange === 'none' ? undefined : startDateParamKey.trim() || 'start_date', + end_date_param: dateRange === 'none' ? undefined : endDateParamKey.trim() || 'end_date', + params: parsedParams, + }, + }); + notifySuccess('Email alert scheduled successfully'); + } + resetForm(); + await fetchJobs(); + } catch { + setError('Unable to create schedule. Please verify the details and try again.'); + } finally { + setSaving(false); + } + }; + + const handleEdit = (job: ScheduledJob) => { + setEditingJobId(job.id); + setCronExpr(job.cron_expr || '0 9 * * *'); + setTimezone(job.timezone || 'Asia/Kolkata'); + setMaxRetries(String(job.max_retries ?? 3)); + const payload = (job.payload || {}) as Record; + const recipients = Array.isArray(payload.recipients) ? payload.recipients : []; + setRecipientsText(recipients.map((item) => String(item)).join(', ')); + setSubject(typeof payload.subject === 'string' ? payload.subject : ''); + const paramsValue = payload.params; + const dateRangeValue = payload.date_range; + if (dateRangeValue === 'last_day' || dateRangeValue === 'last_7_days' || dateRangeValue === 'last_30_days') { + setDateRange(dateRangeValue); + } else { + setDateRange('none'); + } + setStartDateParamKey(typeof payload.start_date_param === 'string' ? payload.start_date_param : 'start_date'); + setEndDateParamKey(typeof payload.end_date_param === 'string' ? payload.end_date_param : 'end_date'); + if (paramsValue && typeof paramsValue === 'object' && !Array.isArray(paramsValue)) { + setQueryParamsJson(JSON.stringify(paramsValue, null, 2)); + } else { + setQueryParamsJson(''); + } + }; + + const handleDelete = async (jobId: string) => { + setSaving(true); + try { + await floConsoleService.scheduledJobService.deleteScheduledJob(jobId); + notifySuccess('Schedule deleted successfully'); + if (editingJobId === jobId) { + resetForm(); + } + await fetchJobs(); + } catch { + notifyError('Failed to delete schedule'); + } finally { + setSaving(false); + } + }; + + return ( + + + + Schedule Email Alert + Create a scheduled query email for this dynamic query. + + +
+
+
+

Existing schedules for this query

+ +
+ {loadingJobs ? ( +

Loading schedules...

+ ) : jobs.length === 0 ? ( +

No schedules found for this dynamic query.

+ ) : ( +
+ {jobs.map((job) => ( +
+
+

+ Cron: {job.cron_expr} ({job.timezone}) +

+

+ Status: {job.status} +

+

+ Next Run:{' '} + {job.next_run_at ? new Date(job.next_run_at).toLocaleString() : '-'} +

+
+
+ + +
+
+ ))} +
+ )} +
+ +
+
+

Datasource ID

+ +
+
+

Query ID

+ +
+
+ +
+
+

Cron expression

+ setCronExpr(e.target.value)} placeholder="0 9 * * *" /> +
+
+

Timezone

+ setTimezone(e.target.value)} placeholder="Asia/Kolkata" /> +
+
+ +
+
+

Max retries

+ setMaxRetries(e.target.value)} placeholder="3" /> +
+
+

Subject (optional)

+ setSubject(e.target.value)} placeholder="Daily report" /> +
+
+ +
+

Recipients (comma-separated emails)

+