mirror of
https://github.com/freedbygrace/ActiveDirectoryManager.git
synced 2026-08-26 10:37:08 +00:00
Add dynamic group rule management API endpoints and UI components.
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 705f2157-ef97-4fbd-89e4-8c7f2ecaea90 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7ed01c5f-a82d-405a-b728-b2e3d127c60c/4e0af531-eaa1-4a7b-a243-4f9e551aef27.jpg
This commit is contained in:
@@ -0,0 +1,375 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Trash, Plus, Calendar, Clock, RotateCcw } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
ScheduleFrequency,
|
||||||
|
Weekday,
|
||||||
|
scheduleFrequencies,
|
||||||
|
weekdays,
|
||||||
|
InsertScheduleRule
|
||||||
|
} from '@shared/schema';
|
||||||
|
|
||||||
|
interface CronJobBuilderProps {
|
||||||
|
schedules: ScheduleItem[];
|
||||||
|
onSchedulesChange: (schedules: ScheduleItem[]) => void;
|
||||||
|
ruleId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScheduleItem {
|
||||||
|
id?: number;
|
||||||
|
ruleId?: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
frequency: ScheduleFrequency;
|
||||||
|
cronExpression: string;
|
||||||
|
dayOfWeek?: Weekday | null;
|
||||||
|
dayOfMonth?: number | null;
|
||||||
|
hour: number;
|
||||||
|
minute: number;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialSchedule: ScheduleItem = {
|
||||||
|
name: 'Default Schedule',
|
||||||
|
description: 'Runs daily at midnight',
|
||||||
|
frequency: 'daily',
|
||||||
|
cronExpression: '0 0 * * *',
|
||||||
|
hour: 0,
|
||||||
|
minute: 0,
|
||||||
|
enabled: true
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CronJobBuilder: React.FC<CronJobBuilderProps> = ({
|
||||||
|
schedules = [],
|
||||||
|
onSchedulesChange,
|
||||||
|
ruleId
|
||||||
|
}) => {
|
||||||
|
const [activeSchedules, setActiveSchedules] = useState<ScheduleItem[]>(
|
||||||
|
schedules.length > 0 ? schedules : [initialSchedule]
|
||||||
|
);
|
||||||
|
const [activeTab, setActiveTab] = useState<string>('0');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (schedules.length > 0 && activeSchedules.length === 0) {
|
||||||
|
setActiveSchedules(schedules);
|
||||||
|
}
|
||||||
|
}, [schedules]);
|
||||||
|
|
||||||
|
const updateSchedule = (index: number, updatedSchedule: Partial<ScheduleItem>) => {
|
||||||
|
const newSchedules = [...activeSchedules];
|
||||||
|
newSchedules[index] = { ...newSchedules[index], ...updatedSchedule };
|
||||||
|
|
||||||
|
// Update cron expression
|
||||||
|
const schedule = newSchedules[index];
|
||||||
|
let cronExpression = '';
|
||||||
|
|
||||||
|
switch (schedule.frequency) {
|
||||||
|
case 'minutely':
|
||||||
|
cronExpression = '* * * * *';
|
||||||
|
break;
|
||||||
|
case 'hourly':
|
||||||
|
cronExpression = `${schedule.minute} * * * *`;
|
||||||
|
break;
|
||||||
|
case 'daily':
|
||||||
|
cronExpression = `${schedule.minute} ${schedule.hour} * * *`;
|
||||||
|
break;
|
||||||
|
case 'weekly':
|
||||||
|
const dayNumber = schedule.dayOfWeek ? weekdays.indexOf(schedule.dayOfWeek) : 0;
|
||||||
|
cronExpression = `${schedule.minute} ${schedule.hour} * * ${dayNumber}`;
|
||||||
|
break;
|
||||||
|
case 'monthly':
|
||||||
|
const day = schedule.dayOfMonth || 1;
|
||||||
|
cronExpression = `${schedule.minute} ${schedule.hour} ${day} * *`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
newSchedules[index].cronExpression = cronExpression;
|
||||||
|
|
||||||
|
setActiveSchedules(newSchedules);
|
||||||
|
onSchedulesChange(newSchedules);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addNewSchedule = () => {
|
||||||
|
const newSchedule: ScheduleItem = {
|
||||||
|
...initialSchedule,
|
||||||
|
name: `Schedule ${activeSchedules.length + 1}`,
|
||||||
|
ruleId
|
||||||
|
};
|
||||||
|
|
||||||
|
const newSchedules = [...activeSchedules, newSchedule];
|
||||||
|
setActiveSchedules(newSchedules);
|
||||||
|
setActiveTab(String(newSchedules.length - 1));
|
||||||
|
onSchedulesChange(newSchedules);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSchedule = (index: number) => {
|
||||||
|
if (activeSchedules.length <= 1) {
|
||||||
|
return; // Don't remove the last schedule
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSchedules = activeSchedules.filter((_, i) => i !== index);
|
||||||
|
setActiveSchedules(newSchedules);
|
||||||
|
|
||||||
|
// Set active tab to the previous one or the first one
|
||||||
|
if (Number(activeTab) >= newSchedules.length) {
|
||||||
|
setActiveTab(String(Math.max(0, newSchedules.length - 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
onSchedulesChange(newSchedules);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleScheduleStatus = (index: number) => {
|
||||||
|
const newSchedules = [...activeSchedules];
|
||||||
|
newSchedules[index].enabled = !newSchedules[index].enabled;
|
||||||
|
setActiveSchedules(newSchedules);
|
||||||
|
onSchedulesChange(newSchedules);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to generate a human-readable description of the schedule
|
||||||
|
const getScheduleDescription = (schedule: ScheduleItem): string => {
|
||||||
|
switch (schedule.frequency) {
|
||||||
|
case 'minutely':
|
||||||
|
return 'Runs every minute';
|
||||||
|
case 'hourly':
|
||||||
|
return `Runs every hour at ${schedule.minute} minutes past the hour`;
|
||||||
|
case 'daily':
|
||||||
|
return `Runs daily at ${formatTime(schedule.hour, schedule.minute)}`;
|
||||||
|
case 'weekly':
|
||||||
|
return `Runs weekly on ${schedule.dayOfWeek || 'Monday'} at ${formatTime(schedule.hour, schedule.minute)}`;
|
||||||
|
case 'monthly':
|
||||||
|
return `Runs monthly on day ${schedule.dayOfMonth || 1} at ${formatTime(schedule.hour, schedule.minute)}`;
|
||||||
|
default:
|
||||||
|
return 'Custom schedule';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to format time in 12-hour format
|
||||||
|
const formatTime = (hour: number, minute: number): string => {
|
||||||
|
const period = hour >= 12 ? 'PM' : 'AM';
|
||||||
|
const displayHour = hour % 12 || 12;
|
||||||
|
const displayMinute = minute.toString().padStart(2, '0');
|
||||||
|
return `${displayHour}:${displayMinute} ${period}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<CardTitle>Schedule Configuration</CardTitle>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={addNewSchedule}
|
||||||
|
>
|
||||||
|
<Plus className="mr-1 h-4 w-4" />
|
||||||
|
Add Schedule
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onValueChange={setActiveTab}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2 mb-4 overflow-x-auto pb-2">
|
||||||
|
<TabsList>
|
||||||
|
{activeSchedules.map((schedule, index) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={index}
|
||||||
|
value={String(index)}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{schedule.name}
|
||||||
|
{!schedule.enabled && (
|
||||||
|
<Badge variant="outline" className="ml-1 text-xs">Disabled</Badge>
|
||||||
|
)}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeSchedules.map((schedule, index) => (
|
||||||
|
<TabsContent key={index} value={String(index)} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`schedule-name-${index}`}>Schedule Name</Label>
|
||||||
|
<Input
|
||||||
|
id={`schedule-name-${index}`}
|
||||||
|
value={schedule.name}
|
||||||
|
onChange={(e) => updateSchedule(index, { name: e.target.value })}
|
||||||
|
placeholder="Schedule Name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`schedule-frequency-${index}`}>Frequency</Label>
|
||||||
|
<Select
|
||||||
|
value={schedule.frequency}
|
||||||
|
onValueChange={(value) => updateSchedule(index, { frequency: value as ScheduleFrequency })}
|
||||||
|
>
|
||||||
|
<SelectTrigger id={`schedule-frequency-${index}`}>
|
||||||
|
<SelectValue placeholder="Select frequency" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{scheduleFrequencies.map((freq) => (
|
||||||
|
<SelectItem key={freq} value={freq}>
|
||||||
|
{freq.charAt(0).toUpperCase() + freq.slice(1)}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(schedule.frequency === 'weekly') && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`schedule-day-week-${index}`}>Day of Week</Label>
|
||||||
|
<Select
|
||||||
|
value={schedule.dayOfWeek || 'Monday'}
|
||||||
|
onValueChange={(value) => updateSchedule(index, { dayOfWeek: value as Weekday })}
|
||||||
|
>
|
||||||
|
<SelectTrigger id={`schedule-day-week-${index}`}>
|
||||||
|
<SelectValue placeholder="Select day" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{weekdays.map((day) => (
|
||||||
|
<SelectItem key={day} value={day}>
|
||||||
|
{day}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(schedule.frequency === 'monthly') && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`schedule-day-month-${index}`}>Day of Month</Label>
|
||||||
|
<Select
|
||||||
|
value={String(schedule.dayOfMonth || 1)}
|
||||||
|
onValueChange={(value) => updateSchedule(index, { dayOfMonth: parseInt(value) })}
|
||||||
|
>
|
||||||
|
<SelectTrigger id={`schedule-day-month-${index}`}>
|
||||||
|
<SelectValue placeholder="Select day" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Array.from({ length: 31 }, (_, i) => i + 1).map((day) => (
|
||||||
|
<SelectItem key={day} value={String(day)}>
|
||||||
|
{day}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(schedule.frequency !== 'minutely') && (
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{(schedule.frequency !== 'hourly') && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`schedule-hour-${index}`}>Hour</Label>
|
||||||
|
<Select
|
||||||
|
value={String(schedule.hour)}
|
||||||
|
onValueChange={(value) => updateSchedule(index, { hour: parseInt(value) })}
|
||||||
|
>
|
||||||
|
<SelectTrigger id={`schedule-hour-${index}`}>
|
||||||
|
<SelectValue placeholder="Select hour" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Array.from({ length: 24 }, (_, i) => i).map((hour) => (
|
||||||
|
<SelectItem key={hour} value={String(hour)}>
|
||||||
|
{hour.toString().padStart(2, '0')}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`schedule-minute-${index}`}>Minute</Label>
|
||||||
|
<Select
|
||||||
|
value={String(schedule.minute)}
|
||||||
|
onValueChange={(value) => updateSchedule(index, { minute: parseInt(value) })}
|
||||||
|
>
|
||||||
|
<SelectTrigger id={`schedule-minute-${index}`}>
|
||||||
|
<SelectValue placeholder="Select minute" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Array.from({ length: 60 }, (_, i) => i).map((minute) => (
|
||||||
|
<SelectItem key={minute} value={String(minute)}>
|
||||||
|
{minute.toString().padStart(2, '0')}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`schedule-description-${index}`}>Description</Label>
|
||||||
|
<Input
|
||||||
|
id={`schedule-description-${index}`}
|
||||||
|
value={schedule.description}
|
||||||
|
onChange={(e) => updateSchedule(index, { description: e.target.value })}
|
||||||
|
placeholder="Schedule Description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="block mb-2">Cron Expression</Label>
|
||||||
|
<div className="flex items-center p-2 rounded-md bg-muted/50">
|
||||||
|
<code className="text-sm font-mono">{schedule.cronExpression}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2">
|
||||||
|
<div className="bg-muted/30 p-3 rounded-md">
|
||||||
|
<p className="text-sm font-medium mb-1">Schedule Summary:</p>
|
||||||
|
<p className="text-sm">{getScheduleDescription(schedule)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pt-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Switch
|
||||||
|
id={`schedule-enabled-${index}`}
|
||||||
|
checked={schedule.enabled}
|
||||||
|
onCheckedChange={() => toggleScheduleStatus(index)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={`schedule-enabled-${index}`}>
|
||||||
|
{schedule.enabled ? 'Enabled' : 'Disabled'}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeSchedules.length > 1 && (
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeSchedule(index)}
|
||||||
|
>
|
||||||
|
<Trash className="h-4 w-4 mr-1" />
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CronJobBuilder;
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import { ScheduleFrequency, Weekday, weekdays } from '@shared/schema';
|
||||||
|
import { ScheduleItem } from '@/components/dynamic-groups/cron-job-builder';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a cron expression into its components
|
||||||
|
* @param cronExpression - The cron expression to parse
|
||||||
|
* @returns An object with the cron expression components
|
||||||
|
*/
|
||||||
|
export const parseCronExpression = (cronExpression: string): {
|
||||||
|
minute: number;
|
||||||
|
hour: number;
|
||||||
|
dayOfMonth: number | null;
|
||||||
|
month: number | null;
|
||||||
|
dayOfWeek: number | null;
|
||||||
|
} => {
|
||||||
|
const parts = cronExpression.split(' ');
|
||||||
|
|
||||||
|
// Default to all parts being null/0
|
||||||
|
const result = {
|
||||||
|
minute: 0,
|
||||||
|
hour: 0,
|
||||||
|
dayOfMonth: null as number | null,
|
||||||
|
month: null as number | null,
|
||||||
|
dayOfWeek: null as number | null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle minute (first part)
|
||||||
|
if (parts.length > 0 && parts[0] !== '*') {
|
||||||
|
result.minute = parseInt(parts[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle hour (second part)
|
||||||
|
if (parts.length > 1 && parts[1] !== '*') {
|
||||||
|
result.hour = parseInt(parts[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle day of month (third part)
|
||||||
|
if (parts.length > 2 && parts[2] !== '*') {
|
||||||
|
result.dayOfMonth = parseInt(parts[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle month (fourth part)
|
||||||
|
if (parts.length > 3 && parts[3] !== '*') {
|
||||||
|
result.month = parseInt(parts[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle day of week (fifth part)
|
||||||
|
if (parts.length > 4 && parts[4] !== '*') {
|
||||||
|
result.dayOfWeek = parseInt(parts[4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect the frequency of a schedule from its cron expression
|
||||||
|
* @param cronExpression - The cron expression to analyze
|
||||||
|
* @returns The detected frequency
|
||||||
|
*/
|
||||||
|
export const detectFrequency = (cronExpression: string): ScheduleFrequency => {
|
||||||
|
if (!cronExpression || cronExpression.trim() === '') {
|
||||||
|
return 'daily'; // Default to daily
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = cronExpression.split(' ');
|
||||||
|
|
||||||
|
// * * * * * - Every minute
|
||||||
|
if (parts[0] === '*' && parts[1] === '*') {
|
||||||
|
return 'minutely';
|
||||||
|
}
|
||||||
|
|
||||||
|
// MM * * * * - Every hour at minute MM
|
||||||
|
if (parts[0] !== '*' && parts[1] === '*') {
|
||||||
|
return 'hourly';
|
||||||
|
}
|
||||||
|
|
||||||
|
// MM HH * * d - Weekly on day d at HH:MM
|
||||||
|
if (parts[0] !== '*' && parts[1] !== '*' && parts[2] === '*' && parts[3] === '*' && parts[4] !== '*') {
|
||||||
|
return 'weekly';
|
||||||
|
}
|
||||||
|
|
||||||
|
// MM HH DD * * - Monthly on day DD at HH:MM
|
||||||
|
if (parts[0] !== '*' && parts[1] !== '*' && parts[2] !== '*' && parts[3] === '*' && parts[4] === '*') {
|
||||||
|
return 'monthly';
|
||||||
|
}
|
||||||
|
|
||||||
|
// MM HH * * * - Daily at HH:MM
|
||||||
|
if (parts[0] !== '*' && parts[1] !== '*' && parts[2] === '*' && parts[3] === '*' && parts[4] === '*') {
|
||||||
|
return 'daily';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to daily for any other patterns
|
||||||
|
return 'daily';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a cron expression to a schedule item
|
||||||
|
* @param cronExpression - The cron expression to convert
|
||||||
|
* @param name - Optional name for the schedule
|
||||||
|
* @param description - Optional description for the schedule
|
||||||
|
* @returns A schedule item object
|
||||||
|
*/
|
||||||
|
export const cronToScheduleItem = (
|
||||||
|
cronExpression: string,
|
||||||
|
name = 'Schedule',
|
||||||
|
description = '',
|
||||||
|
enabled = true,
|
||||||
|
id?: number,
|
||||||
|
ruleId?: number
|
||||||
|
): ScheduleItem => {
|
||||||
|
const frequency = detectFrequency(cronExpression);
|
||||||
|
const { minute, hour, dayOfMonth, dayOfWeek } = parseCronExpression(cronExpression);
|
||||||
|
|
||||||
|
// Generate description if not provided
|
||||||
|
let autoDescription = description;
|
||||||
|
if (!autoDescription) {
|
||||||
|
autoDescription = getScheduleDescription({
|
||||||
|
frequency,
|
||||||
|
minute,
|
||||||
|
hour,
|
||||||
|
dayOfMonth,
|
||||||
|
dayOfWeek: dayOfWeek !== null ? weekdays[dayOfWeek] : null,
|
||||||
|
cronExpression,
|
||||||
|
name,
|
||||||
|
description: '',
|
||||||
|
enabled
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
ruleId,
|
||||||
|
name,
|
||||||
|
description: autoDescription,
|
||||||
|
frequency,
|
||||||
|
cronExpression,
|
||||||
|
minute,
|
||||||
|
hour,
|
||||||
|
dayOfMonth: dayOfMonth,
|
||||||
|
dayOfWeek: dayOfWeek !== null ? weekdays[dayOfWeek] as Weekday : null,
|
||||||
|
enabled
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a schedule item to a cron expression
|
||||||
|
* @param schedule - The schedule item to convert
|
||||||
|
* @returns A cron expression string
|
||||||
|
*/
|
||||||
|
export const scheduleToCronExpression = (schedule: Partial<ScheduleItem>): string => {
|
||||||
|
let cronExpression = '';
|
||||||
|
|
||||||
|
switch (schedule.frequency) {
|
||||||
|
case 'minutely':
|
||||||
|
cronExpression = '* * * * *';
|
||||||
|
break;
|
||||||
|
case 'hourly':
|
||||||
|
cronExpression = `${schedule.minute || 0} * * * *`;
|
||||||
|
break;
|
||||||
|
case 'daily':
|
||||||
|
cronExpression = `${schedule.minute || 0} ${schedule.hour || 0} * * *`;
|
||||||
|
break;
|
||||||
|
case 'weekly':
|
||||||
|
const dayNumber = schedule.dayOfWeek ? weekdays.indexOf(schedule.dayOfWeek) : 0;
|
||||||
|
cronExpression = `${schedule.minute || 0} ${schedule.hour || 0} * * ${dayNumber}`;
|
||||||
|
break;
|
||||||
|
case 'monthly':
|
||||||
|
const day = schedule.dayOfMonth || 1;
|
||||||
|
cronExpression = `${schedule.minute || 0} ${schedule.hour || 0} ${day} * *`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
cronExpression = '0 0 * * *'; // Default to midnight every day
|
||||||
|
}
|
||||||
|
|
||||||
|
return cronExpression;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a human-readable description of a schedule
|
||||||
|
* @param schedule - The schedule to describe
|
||||||
|
* @returns A human-readable description
|
||||||
|
*/
|
||||||
|
export const getScheduleDescription = (schedule: {
|
||||||
|
frequency: ScheduleFrequency;
|
||||||
|
minute: number;
|
||||||
|
hour: number;
|
||||||
|
dayOfMonth: number | null;
|
||||||
|
dayOfWeek: Weekday | null;
|
||||||
|
cronExpression: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}): string => {
|
||||||
|
// Format time in 12-hour format
|
||||||
|
const formatTime = (hour: number, minute: number): string => {
|
||||||
|
const period = hour >= 12 ? 'PM' : 'AM';
|
||||||
|
const displayHour = hour % 12 || 12;
|
||||||
|
const displayMinute = minute.toString().padStart(2, '0');
|
||||||
|
return `${displayHour}:${displayMinute} ${period}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (schedule.frequency) {
|
||||||
|
case 'minutely':
|
||||||
|
return 'Runs every minute';
|
||||||
|
case 'hourly':
|
||||||
|
return `Runs every hour at ${schedule.minute} minutes past the hour`;
|
||||||
|
case 'daily':
|
||||||
|
return `Runs daily at ${formatTime(schedule.hour, schedule.minute)}`;
|
||||||
|
case 'weekly':
|
||||||
|
return `Runs weekly on ${schedule.dayOfWeek || 'Monday'} at ${formatTime(schedule.hour, schedule.minute)}`;
|
||||||
|
case 'monthly':
|
||||||
|
return `Runs monthly on day ${schedule.dayOfMonth || 1} at ${formatTime(schedule.hour, schedule.minute)}`;
|
||||||
|
default:
|
||||||
|
return 'Custom schedule';
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -5293,6 +5293,215 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Dynamic Group Rules routes
|
||||||
|
app.get("/api/dynamic-group-rules", requirePermission(PERMISSIONS.VIEW_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const rules = await storage.listDynamicGroupRules();
|
||||||
|
res.json(rules);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching dynamic group rules:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch dynamic group rules' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/dynamic-group-rules/:id", requirePermission(PERMISSIONS.VIEW_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
const rule = await storage.getDynamicGroupRule(id);
|
||||||
|
|
||||||
|
if (!rule) {
|
||||||
|
return res.status(404).json({ error: 'Dynamic group rule not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(rule);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching dynamic group rule:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch dynamic group rule' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/dynamic-group-rules", requirePermission(PERMISSIONS.MANAGE_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const rule = await storage.createDynamicGroupRule(req.body);
|
||||||
|
res.status(201).json(rule);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating dynamic group rule:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to create dynamic group rule' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put("/api/dynamic-group-rules/:id", requirePermission(PERMISSIONS.MANAGE_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
const updatedRule = await storage.updateDynamicGroupRule(id, req.body);
|
||||||
|
|
||||||
|
if (!updatedRule) {
|
||||||
|
return res.status(404).json({ error: 'Dynamic group rule not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(updatedRule);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating dynamic group rule:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to update dynamic group rule' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete("/api/dynamic-group-rules/:id", requirePermission(PERMISSIONS.MANAGE_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
const deleted = await storage.deleteDynamicGroupRule(id);
|
||||||
|
|
||||||
|
if (!deleted) {
|
||||||
|
return res.status(404).json({ error: 'Dynamic group rule not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting dynamic group rule:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to delete dynamic group rule' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dynamic Group Conditions routes
|
||||||
|
app.get("/api/dynamic-group-rules/:ruleId/conditions", requirePermission(PERMISSIONS.VIEW_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const ruleId = Number(req.params.ruleId);
|
||||||
|
const conditions = await storage.getConditionsByRuleId(ruleId);
|
||||||
|
res.json(conditions);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching conditions for dynamic group rule:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch conditions' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/dynamic-group-rules/:ruleId/conditions", requirePermission(PERMISSIONS.MANAGE_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const ruleId = Number(req.params.ruleId);
|
||||||
|
const conditionData = { ...req.body, ruleId };
|
||||||
|
const condition = await storage.createDynamicGroupCondition(conditionData);
|
||||||
|
res.status(201).json(condition);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating condition for dynamic group rule:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to create condition' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dynamic Group Rule Connections routes
|
||||||
|
app.get("/api/dynamic-group-rules/:ruleId/connections", requirePermission(PERMISSIONS.VIEW_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const ruleId = Number(req.params.ruleId);
|
||||||
|
const connections = await storage.getRuleConnectionsByRuleId(ruleId);
|
||||||
|
res.json(connections);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching connections for dynamic group rule:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch connections' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/dynamic-group-rules/:ruleId/connections", requirePermission(PERMISSIONS.MANAGE_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const ruleId = Number(req.params.ruleId);
|
||||||
|
const connectionData = { ...req.body, ruleId };
|
||||||
|
const connection = await storage.createDynamicGroupRuleConnection(connectionData);
|
||||||
|
res.status(201).json(connection);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating connection for dynamic group rule:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to create connection' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schedule Rules routes
|
||||||
|
app.get("/api/schedule-rules", requirePermission(PERMISSIONS.VIEW_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const scheduleRules = await storage.getAllScheduleRules();
|
||||||
|
res.json(scheduleRules);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching schedule rules:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch schedule rules' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/schedule-rules/:id", requirePermission(PERMISSIONS.VIEW_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
const scheduleRule = await storage.getScheduleRule(id);
|
||||||
|
|
||||||
|
if (!scheduleRule) {
|
||||||
|
return res.status(404).json({ error: 'Schedule rule not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(scheduleRule);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching schedule rule:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch schedule rule' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/dynamic-group-rules/:ruleId/schedules", requirePermission(PERMISSIONS.VIEW_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const ruleId = Number(req.params.ruleId);
|
||||||
|
const schedules = await storage.getScheduleRulesByRuleId(ruleId);
|
||||||
|
res.json(schedules);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching schedules for dynamic group rule:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch schedules' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/schedule-rules", requirePermission(PERMISSIONS.MANAGE_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const scheduleRule = await storage.createScheduleRule(req.body);
|
||||||
|
res.status(201).json(scheduleRule);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating schedule rule:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to create schedule rule' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/dynamic-group-rules/:ruleId/schedules", requirePermission(PERMISSIONS.MANAGE_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const ruleId = Number(req.params.ruleId);
|
||||||
|
const scheduleData = { ...req.body, ruleId };
|
||||||
|
const schedule = await storage.createScheduleRule(scheduleData);
|
||||||
|
res.status(201).json(schedule);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating schedule for dynamic group rule:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to create schedule' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put("/api/schedule-rules/:id", requirePermission(PERMISSIONS.MANAGE_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
const updatedRule = await storage.updateScheduleRule(id, req.body);
|
||||||
|
|
||||||
|
if (!updatedRule) {
|
||||||
|
return res.status(404).json({ error: 'Schedule rule not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(updatedRule);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating schedule rule:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to update schedule rule' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete("/api/schedule-rules/:id", requirePermission(PERMISSIONS.MANAGE_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
const deleted = await storage.deleteScheduleRule(id);
|
||||||
|
|
||||||
|
if (!deleted) {
|
||||||
|
return res.status(404).json({ error: 'Schedule rule not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting schedule rule:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to delete schedule rule' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const httpServer = createServer(app);
|
const httpServer = createServer(app);
|
||||||
|
|
||||||
return httpServer;
|
return httpServer;
|
||||||
|
|||||||
@@ -507,6 +507,14 @@ export type MoveComputer = z.infer<typeof moveComputerSchema>;
|
|||||||
export type MoveUser = z.infer<typeof moveUserSchema>;
|
export type MoveUser = z.infer<typeof moveUserSchema>;
|
||||||
export type AddToGroup = z.infer<typeof addToGroupSchema>;
|
export type AddToGroup = z.infer<typeof addToGroupSchema>;
|
||||||
export type RemoveFromGroup = z.infer<typeof removeFromGroupSchema>;
|
export type RemoveFromGroup = z.infer<typeof removeFromGroupSchema>;
|
||||||
|
export type DynamicGroupRule = typeof dynamicGroupRules.$inferSelect;
|
||||||
|
export type InsertDynamicGroupRule = z.infer<typeof insertDynamicGroupRuleSchema>;
|
||||||
|
export type DynamicGroupCondition = typeof dynamicGroupConditions.$inferSelect;
|
||||||
|
export type InsertDynamicGroupCondition = z.infer<typeof insertDynamicGroupConditionSchema>;
|
||||||
|
export type ScheduleRule = typeof scheduleRules.$inferSelect;
|
||||||
|
export type InsertScheduleRule = z.infer<typeof insertScheduleRuleSchema>;
|
||||||
|
export type ScheduleFrequency = typeof scheduleFrequencies[number];
|
||||||
|
export type Weekday = typeof weekdays[number];
|
||||||
|
|
||||||
|
|
||||||
// LDAP Query Builder schemas
|
// LDAP Query Builder schemas
|
||||||
|
|||||||
Reference in New Issue
Block a user