diff --git a/wavefront/client/src/api/app-user-service.ts b/wavefront/client/src/api/app-user-service.ts index 0bd3f471..87ae8cbc 100644 --- a/wavefront/client/src/api/app-user-service.ts +++ b/wavefront/client/src/api/app-user-service.ts @@ -22,8 +22,8 @@ export class AppUserService { /** * List users with access to an app (owners only) */ - async listAppUsers(appId: string): Promise> { - return this.http.get(`/v1/apps/${appId}/users`); + async listAppUsers(): Promise> { + return this.http.get(`/v1/:appId/floware/v1/users`); } /** diff --git a/wavefront/client/src/api/scheduled-job-service.ts b/wavefront/client/src/api/scheduled-job-service.ts index e6758775..dcd26c97 100644 --- a/wavefront/client/src/api/scheduled-job-service.ts +++ b/wavefront/client/src/api/scheduled-job-service.ts @@ -3,6 +3,7 @@ import { CreateScheduledJobResponse, DeleteScheduledJobResponse, ListScheduledJobsResponse, + UpdateScheduledJobRequest, UpdateScheduledJobResponse, } from '@app/types/scheduled-job'; import { AxiosInstance } from 'axios'; @@ -16,6 +17,7 @@ export class ScheduledJobService { async listScheduledJobs(params: { limit?: number; + offset?: number; query_id?: string; datasource_id?: string; job_type?: string; @@ -24,20 +26,19 @@ export class ScheduledJobService { 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 updateScheduledJob(jobId: string, request: UpdateScheduledJobRequest): Promise { + return this.http.patch(`/v1/:appId/floware/v1/scheduled-jobs/${jobId}`, request); } async deleteScheduledJob(jobId: string): Promise { return this.http.delete(`/v1/:appId/floware/v1/scheduled-jobs/${jobId}`); } + + async pauseScheduledJob(jobId: string): Promise { + return this.http.post(`/v1/:appId/floware/v1/scheduled-jobs/${jobId}/pause`); + } + + async resumeScheduledJob(jobId: string): Promise { + return this.http.post(`/v1/:appId/floware/v1/scheduled-jobs/${jobId}/resume`); + } } diff --git a/wavefront/client/src/assets/icons/index.ts b/wavefront/client/src/assets/icons/index.ts index 90218e74..629ae24f 100644 --- a/wavefront/client/src/assets/icons/index.ts +++ b/wavefront/client/src/assets/icons/index.ts @@ -6,5 +6,6 @@ export { default as ModelRepositoryIcon } from './model-repository-icon'; export { default as PermissionIcon } from './permission-icon'; export { PhoneActiveIcon, PhoneIcon } from './phone-icon'; export { default as RagIcon } from './rag-icon'; +export { default as ScheduledJobsIcon } from './scheduled-jobs-icon'; export { default as RootfloIcon } from './rootflo-icon'; export { default as WorkflowIcon } from './workflow-icon'; diff --git a/wavefront/client/src/assets/icons/scheduled-jobs-icon.tsx b/wavefront/client/src/assets/icons/scheduled-jobs-icon.tsx new file mode 100644 index 00000000..b884c881 --- /dev/null +++ b/wavefront/client/src/assets/icons/scheduled-jobs-icon.tsx @@ -0,0 +1,13 @@ +const ScheduledJobsIcon = ({ ...props }: React.SVGProps) => ( + + + + +); + +export default ScheduledJobsIcon; diff --git a/wavefront/client/src/hooks/data/fetch-hooks.ts b/wavefront/client/src/hooks/data/fetch-hooks.ts index c49feb3d..bf415cc1 100644 --- a/wavefront/client/src/hooks/data/fetch-hooks.ts +++ b/wavefront/client/src/hooks/data/fetch-hooks.ts @@ -15,6 +15,7 @@ import { TelephonyConfig } from '@app/types/telephony-config'; import { ToolDetails, VoiceAgentTool, VoiceAgentToolWithAssociation } from '@app/types/tool'; import { TtsConfig } from '@app/types/tts-config'; import { VoiceAgent } from '@app/types/voice-agent'; +import { ScheduledJob } from '@app/types/scheduled-job'; import { WorkflowListItem, WorkflowPipelineListItem, WorkflowRunListData } from '@app/types/workflow'; import { UseQueryResult } from '@tanstack/react-query'; @@ -28,6 +29,7 @@ import { getApiServiceQueryFn, getApiServicesQueryFn, getAppByIdFn, + getAppUsersQueryFn, getAuthenticatorQueryFn, getAuthenticatorsQueryFn, getCurrentUserQueryFn, @@ -54,7 +56,7 @@ import { getToolsQueryFn, getTtsConfigQueryFn, getTtsConfigsQueryFn, - getUsersQueryFn, + getConsoleUsersQueryFn, getVoiceAgentQueryFn, getVoiceAgentToolQueryFn, getVoiceAgentToolsQueryFn, @@ -64,6 +66,7 @@ import { getWorkflowRunsQueryFn, getWorkflowsQueryFn, readYamlQueryFn, + getScheduledJobsQueryFn, } from './query-functions'; import { getAgentKey, @@ -74,6 +77,7 @@ import { getApiServiceKey, getApiServicesKey, getAppByIdKey, + getAppUsersKey, getAuthenticatorKey, getAuthenticatorsKey, getCurrentUserKey, @@ -100,7 +104,7 @@ import { getToolsKey, getTtsConfigKey, getTtsConfigsKey, - getUsersKey, + getConsoleUsersKey, getVoiceAgentKey, getVoiceAgentToolKey, getVoiceAgentToolsKey, @@ -110,6 +114,7 @@ import { getWorkflowRunsKey, getWorkflowsKey, readYamlKey, + getScheduledJobsKey, } from './query-keys'; export const useGetAllApps = (enabled: boolean): UseQueryResult => { @@ -417,8 +422,16 @@ export const useGetAppById = (appId: string, enabled: boolean = true): UseQueryR return useQueryInit(getAppByIdKey(appId), () => getAppByIdFn(appId), enabled); }; -export const useGetUsers = (): UseQueryResult => { - return useQueryInit(getUsersKey(), getUsersQueryFn, true); +export const useGetAppUsers = (appId: string | undefined): UseQueryResult => { + return useQueryInit(getAppUsersKey(appId || ''), () => getAppUsersQueryFn(), !!appId); +}; + +export const useGetConsoleUsers = (): UseQueryResult => { + return useQueryInit(getConsoleUsersKey(), getConsoleUsersQueryFn, true); +}; + +export const useGetScheduledJobs = (appId: string | undefined): UseQueryResult => { + return useQueryInit(getScheduledJobsKey(appId || ''), getScheduledJobsQueryFn, !!appId); }; // Voice Agent Tools Hooks diff --git a/wavefront/client/src/hooks/data/mutation-hooks.ts b/wavefront/client/src/hooks/data/mutation-hooks.ts index 287bec1f..1482f0e2 100644 --- a/wavefront/client/src/hooks/data/mutation-hooks.ts +++ b/wavefront/client/src/hooks/data/mutation-hooks.ts @@ -1,5 +1,5 @@ import { QueryClient, useMutation, useQueryClient } from '@tanstack/react-query'; -import { getAgentKey, getAgentsKey, getAppByIdKey, getUserKey, getUsersKey } from './query-keys'; +import { getAgentKey, getAgentsKey, getAppByIdKey, getConsoleUsersKey, getUserKey } from './query-keys'; import { createUserMutationFn, deleteAgentMutationFn, @@ -90,7 +90,7 @@ export const useCreateUser = () => { mutationFn: createUserMutationFn, onSuccess: () => { notifySuccess('User created successfully'); - queryClient.invalidateQueries({ queryKey: getUsersKey() }); + queryClient.invalidateQueries({ queryKey: getConsoleUsersKey() }); }, onError: (error) => { console.error('Error creating user:', error); @@ -108,7 +108,7 @@ export const useUpdateUser = (userId: string | undefined) => { mutationFn: updateUserMutationFn, onSuccess: () => { notifySuccess('User updated successfully'); - queryClient.invalidateQueries({ queryKey: getUsersKey() }); + queryClient.invalidateQueries({ queryKey: getConsoleUsersKey() }); if (userId) { queryClient.invalidateQueries({ queryKey: getUserKey(userId) }); } @@ -129,7 +129,7 @@ export const useDeleteUser = () => { mutationFn: deleteUserMutationFn, onSuccess: () => { notifySuccess('User deleted successfully'); - queryClient.invalidateQueries({ queryKey: getUsersKey() }); + queryClient.invalidateQueries({ queryKey: getConsoleUsersKey() }); }, onError: (error) => { console.error('Error deleting user:', error); diff --git a/wavefront/client/src/hooks/data/query-functions.ts b/wavefront/client/src/hooks/data/query-functions.ts index 4b463c6f..5e3c23c3 100644 --- a/wavefront/client/src/hooks/data/query-functions.ts +++ b/wavefront/client/src/hooks/data/query-functions.ts @@ -15,8 +15,12 @@ import { ToolDetails, VoiceAgentTool, VoiceAgentToolWithAssociation } from '@app import { TtsConfig } from '@app/types/tts-config'; import { IUser } from '@app/types/user'; import { VoiceAgent } from '@app/types/voice-agent'; +import { ScheduledJob } from '@app/types/scheduled-job'; import { WorkflowListItem, WorkflowPipelineListItem, WorkflowRunListData } from '@app/types/workflow'; +const SCHEDULED_JOBS_PAGE_SIZE = 20; +const MAX_SCHEDULED_JOBS = 1000; + const getAllAppsQueryFn = async () => { const { data: { data = { apps: [] } }, @@ -396,7 +400,15 @@ const getAgentToolsQueryFn = async (agentId: string): Promise => { +const getAppUsersQueryFn = async (): Promise => { + const response = await floConsoleService.appUserService.listAppUsers(); + if (response.data?.data?.users && Array.isArray(response.data.data.users)) { + return response.data.data.users; + } + return []; +}; + +const getConsoleUsersQueryFn = async (): Promise => { const response = await floConsoleService.userService.listUsers(); if (response.data?.data?.users && Array.isArray(response.data.data.users)) { return response.data.data.users; @@ -404,6 +416,26 @@ const getUsersQueryFn = async (): Promise => { return []; }; +const getScheduledJobsQueryFn = async (): Promise => { + const jobs: ScheduledJob[] = []; + let offset = 0; + + while (jobs.length < MAX_SCHEDULED_JOBS) { + const response = await floConsoleService.scheduledJobService.listScheduledJobs({ + limit: SCHEDULED_JOBS_PAGE_SIZE, + offset, + }); + const page = response.data.data?.jobs ?? []; + jobs.push(...page); + if (page.length < SCHEDULED_JOBS_PAGE_SIZE) { + break; + } + offset += SCHEDULED_JOBS_PAGE_SIZE; + } + + return jobs.slice(0, MAX_SCHEDULED_JOBS); +}; + export { getAgentQueryFn, getAgentsQueryFn, @@ -414,6 +446,7 @@ export { getApiServiceQueryFn, getApiServicesQueryFn, getAppByIdFn, + getAppUsersQueryFn, getAuthenticatorQueryFn, getAuthenticatorsQueryFn, getCurrentUserQueryFn, @@ -440,7 +473,7 @@ export { getToolsQueryFn, getTtsConfigQueryFn, getTtsConfigsQueryFn, - getUsersQueryFn, + getConsoleUsersQueryFn, getVoiceAgentQueryFn, getVoiceAgentToolQueryFn, getVoiceAgentToolsQueryFn, @@ -449,4 +482,5 @@ export { getWorkflowRunsQueryFn, getWorkflowsQueryFn, readYamlQueryFn, + getScheduledJobsQueryFn, }; diff --git a/wavefront/client/src/hooks/data/query-keys.ts b/wavefront/client/src/hooks/data/query-keys.ts index b7cddee6..db29f6fb 100644 --- a/wavefront/client/src/hooks/data/query-keys.ts +++ b/wavefront/client/src/hooks/data/query-keys.ts @@ -62,11 +62,13 @@ const getPipelinesKey = (appId: string, statusFilter?: string) => { const getPipelineKey = (appId: string, pipelineId: string) => ['pipeline', appId, pipelineId]; const getPipelineFilesKey = (appId: string, pipelineId: string) => ['pipeline-files', appId, pipelineId]; const getAppByIdKey = (appId: string) => ['app-by-id', appId]; -const getUsersKey = () => ['users']; +const getAppUsersKey = (appId: string) => ['app-users', appId]; +const getConsoleUsersKey = () => ['console-users']; const getUserKey = (userId: string) => ['user', userId]; const getVoiceAgentToolsKey = (appId: string) => ['voice-agent-tools', appId]; const getVoiceAgentToolKey = (appId: string, toolId: string) => ['voice-agent-tool', appId, toolId]; const getAgentToolsKey = (appId: string, agentId: string) => ['agent-tools', appId, agentId]; +const getScheduledJobsKey = (appId: string) => ['scheduled-jobs', appId]; export { getAgentKey, @@ -105,7 +107,7 @@ export { getTtsConfigKey, getTtsConfigsKey, getUserKey, - getUsersKey, + getConsoleUsersKey, getVoiceAgentKey, getVoiceAgentToolKey, getVoiceAgentToolsKey, @@ -114,4 +116,6 @@ export { getWorkflowRunsKey, getWorkflowsKey, getAppByIdKey, + getAppUsersKey, + getScheduledJobsKey, }; diff --git a/wavefront/client/src/pages/apps/[appId]/datasources/ScheduleEmailAlertDialog.tsx b/wavefront/client/src/pages/apps/[appId]/datasources/ScheduleEmailAlertDialog.tsx deleted file mode 100644 index aefb1cb8..00000000 --- a/wavefront/client/src/pages/apps/[appId]/datasources/ScheduleEmailAlertDialog.tsx +++ /dev/null @@ -1,393 +0,0 @@ -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_hour' | '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_hour' || - 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)

-