added scheduler and unified back buttons everywhere

This commit is contained in:
Zane Staggs
2025-07-09 18:14:06 -07:00
parent f892fa8265
commit 8d482af0e7
6 changed files with 277 additions and 284 deletions
@@ -169,25 +169,23 @@ const AppSidebar: React.FC<SidebarProps> = ({ currentPath }) => {
</SidebarMenuItem>
</motion.div>
{process.env.ALPHA && (
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ type: 'spring', stiffness: 300, damping: 25, delay: 0.3 }}
>
<SidebarMenuItem>
<SidebarMenuButton
onClick={() => navigate('/schedules')}
isActive={isActivePath('/schedules')}
tooltip="Manage scheduled runs"
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-background-medium/50 transition-all duration-200 data-[active=true]:bg-background-medium"
>
<Clock className="w-4 h-4" />
<span>Scheduler</span>
</SidebarMenuButton>
</SidebarMenuItem>
</motion.div>
)}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ type: 'spring', stiffness: 300, damping: 25, delay: 0.5 }}
>
<SidebarMenuItem>
<SidebarMenuButton
onClick={() => navigate('/schedules')}
isActive={isActivePath('/schedules')}
tooltip="Manage scheduled runs"
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-background-medium/50 transition-all duration-200 data-[active=true]:bg-background-medium"
>
<Clock className="w-4 h-4" />
<span>Scheduler</span>
</SidebarMenuButton>
</SidebarMenuItem>
</motion.div>
</SidebarGroupContent>
</SidebarGroup>
@@ -96,12 +96,12 @@ const daysOfWeekOptions: { value: string; label: string }[] = [
{ value: '0', label: 'Sun' },
];
const modalLabelClassName = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1';
const cronPreviewTextColor = 'text-xs text-gray-500 dark:text-gray-400 mt-1';
const cronPreviewSpecialNoteColor = 'text-xs text-yellow-600 dark:text-yellow-500 mt-1';
const checkboxLabelClassName = 'flex items-center text-sm text-textStandard dark:text-gray-300';
const modalLabelClassName = 'block text-sm font-medium text-text-prominent mb-1';
const cronPreviewTextColor = 'text-xs text-text-subtle mt-1';
const cronPreviewSpecialNoteColor = 'text-xs text-text-warning mt-1';
const checkboxLabelClassName = 'flex items-center text-sm text-text-default';
const checkboxInputClassName =
'h-4 w-4 text-indigo-600 border-gray-300 dark:border-gray-600 rounded focus:ring-indigo-500 mr-2';
'h-4 w-4 text-accent-default border-border-subtle rounded focus:ring-accent-default mr-2';
type SourceType = 'file' | 'deeplink';
type ExecutionMode = 'background' | 'foreground';
@@ -548,10 +548,8 @@ export const CreateScheduleModal: React.FC<CreateScheduleModalProps> = ({
<div className="px-8 pt-8 pb-4 flex-shrink-0 text-center">
<div className="flex flex-col items-center">
<img src={ClockIcon} alt="Clock" className="w-11 h-11 mb-2" />
<h2 className="text-base font-semibold text-gray-900 dark:text-white">
Create New Schedule
</h2>
<p className="text-base text-gray-500 dark:text-gray-400 mt-2 max-w-sm">
<h2 className="text-base font-semibold text-text-prominent">Create New Schedule</h2>
<p className="text-base text-text-subtle mt-2 max-w-sm">
Create a new schedule using the settings below to do things like automatically run
tasks or create files
</p>
@@ -564,12 +562,12 @@ export const CreateScheduleModal: React.FC<CreateScheduleModalProps> = ({
className="px-8 py-4 space-y-4 flex-grow overflow-y-auto"
>
{apiErrorExternally && (
<p className="text-red-500 text-sm mb-3 p-2 bg-red-100 dark:bg-red-900/30 rounded-md border border-red-500/50">
<p className="text-text-error text-sm mb-3 p-2 bg-background-error border border-border-error rounded-md">
{apiErrorExternally}
</p>
)}
{internalValidationError && (
<p className="text-red-500 text-sm mb-3 p-2 bg-red-100 dark:bg-red-900/30 rounded-md border border-red-500/50">
<p className="text-text-error text-sm mb-3 p-2 bg-background-error border border-border-error rounded-md">
{internalValidationError}
</p>
)}
@@ -67,12 +67,10 @@ const ScheduleInfoCard = React.memo<{
}, [scheduleDetails.process_start_time]);
return (
<Card className="p-4 bg-white dark:bg-gray-800 shadow mb-6">
<Card className="p-4 bg-background-card shadow mb-6">
<div className="space-y-2">
<div className="flex flex-col md:flex-row md:items-center justify-between">
<h3 className="text-base font-semibold text-gray-900 dark:text-white">
{scheduleDetails.id}
</h3>
<h3 className="text-base font-semibold text-text-prominent">{scheduleDetails.id}</h3>
<div className="mt-2 md:mt-0 flex items-center gap-2">
{scheduleDetails.currently_running && (
<div className="text-sm text-green-500 dark:text-green-400 font-semibold flex items-center">
@@ -88,20 +86,20 @@ const ScheduleInfoCard = React.memo<{
)}
</div>
</div>
<p className="text-sm text-gray-600 dark:text-gray-300">
<p className="text-sm text-text-default">
<span className="font-semibold">Schedule:</span> {readableCron}
</p>
<p className="text-sm text-gray-600 dark:text-gray-300">
<p className="text-sm text-text-default">
<span className="font-semibold">Cron Expression:</span> {scheduleDetails.cron}
</p>
<p className="text-sm text-gray-600 dark:text-gray-300">
<p className="text-sm text-text-default">
<span className="font-semibold">Recipe Source:</span> {scheduleDetails.source}
</p>
<p className="text-sm text-gray-600 dark:text-gray-300">
<p className="text-sm text-text-default">
<span className="font-semibold">Last Run:</span> {formattedLastRun}
</p>
{scheduleDetails.execution_mode && (
<p className="text-sm text-gray-600 dark:text-gray-300">
<p className="text-sm text-text-default">
<span className="font-semibold">Execution Mode:</span>{' '}
<span
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
@@ -115,13 +113,13 @@ const ScheduleInfoCard = React.memo<{
</p>
)}
{scheduleDetails.currently_running && scheduleDetails.current_session_id && (
<p className="text-sm text-gray-600 dark:text-gray-300">
<p className="text-sm text-text-default">
<span className="font-semibold">Current Session:</span>{' '}
{scheduleDetails.current_session_id}
</p>
)}
{scheduleDetails.currently_running && formattedProcessStartTime && (
<p className="text-sm text-gray-600 dark:text-gray-300">
<p className="text-sm text-text-default">
<span className="font-semibold">Process Started:</span> {formattedProcessStartTime}
</p>
)}
@@ -486,13 +484,11 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
if (!scheduleId) {
return (
<div className="h-screen w-full flex flex-col items-center justify-center bg-app text-textStandard p-8">
<div className="h-screen w-full flex flex-col items-center justify-center bg-white dark:bg-gray-900 text-text-default p-8">
<MoreMenuLayout showMenu={false} />
<BackButton onClick={onNavigateBack} />
<h1 className="text-2xl font-medium text-gray-900 dark:text-white mt-4">
Schedule Not Found
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-2">
<h1 className="text-2xl font-medium text-text-prominent mt-4">Schedule Not Found</h1>
<p className="text-text-subtle mt-2">
No schedule ID was provided. Please return to the schedules list and select a schedule.
</p>
</div>
@@ -500,31 +496,25 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
}
return (
<div className="h-screen w-full flex flex-col bg-app text-textStandard">
<div className="h-screen w-full flex flex-col bg-background-default text-text-default">
<MoreMenuLayout showMenu={false} />
<div className="px-8 pt-6 pb-4 border-b border-borderSubtle flex-shrink-0">
<div className="px-8 pt-6 pb-4 border-b border-border-subtle flex-shrink-0">
<BackButton onClick={onNavigateBack} />
<h1 className="text-3xl font-medium text-gray-900 dark:text-white mt-1">
Schedule Details
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Viewing Schedule ID: {scheduleId}
</p>
<h1 className="text-3xl font-medium text-text-prominent mt-1">Schedule Details</h1>
<p className="text-sm text-text-subtle mt-1">Viewing Schedule ID: {scheduleId}</p>
</div>
<ScrollArea className="flex-grow">
<div className="p-8 space-y-6">
<section>
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
Schedule Information
</h2>
<h2 className="text-xl font-semibold text-text-prominent mb-3">Schedule Information</h2>
{isLoadingSchedule && (
<div className="flex items-center text-gray-500 dark:text-gray-400">
<div className="flex items-center text-text-subtle">
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Loading schedule details...
</div>
)}
{scheduleError && (
<p className="text-red-500 dark:text-red-400 text-sm p-3 bg-red-100 dark:bg-red-900/30 border border-red-500 dark:border-red-700 rounded-md">
<p className="text-text-error text-sm p-3 bg-background-error border border-border-error rounded-md">
Error: {scheduleError}
</p>
)}
@@ -534,7 +524,7 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
</section>
<section>
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">Actions</h2>
<h2 className="text-xl font-semibold text-text-prominent mb-3">Actions</h2>
<div className="flex flex-col md:flex-row gap-2">
<Button
onClick={handleRunNow}
@@ -619,19 +609,17 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
</section>
<section>
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
<h2 className="text-xl font-semibold text-text-prominent mb-4">
Recent Sessions for this Schedule
</h2>
{isLoadingSessions && (
<p className="text-gray-500 dark:text-gray-400">Loading sessions...</p>
)}
{isLoadingSessions && <p className="text-text-subtle">Loading sessions...</p>}
{sessionsError && (
<p className="text-red-500 dark:text-red-400 text-sm p-3 bg-red-100 dark:bg-red-900/30 border border-red-500 dark:border-red-700 rounded-md">
<p className="text-text-error text-sm p-3 bg-background-error border border-border-error rounded-md">
Error: {sessionsError}
</p>
)}
{!isLoadingSessions && !sessionsError && sessions.length === 0 && (
<p className="text-gray-500 dark:text-gray-400 text-center py-4">
<p className="text-text-subtle text-center py-4">
No sessions found for this schedule.
</p>
)}
@@ -641,7 +629,7 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
{sessions.map((session) => (
<Card
key={session.id}
className="p-4 bg-white dark:bg-gray-800 shadow cursor-pointer hover:shadow-lg transition-shadow duration-200"
className="p-4 bg-background-card shadow cursor-pointer hover:shadow-lg transition-shadow duration-200"
onClick={() => handleSessionCardClick(session.id)}
role="button"
tabIndex={0}
@@ -652,23 +640,23 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
}}
>
<h3
className="text-sm font-semibold text-gray-900 dark:text-white truncate"
className="text-sm font-semibold text-text-prominent truncate"
title={session.name || session.id}
>
{session.name || `Session ID: ${session.id}`}{' '}
</h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
<p className="text-xs text-text-subtle mt-1">
Created:{' '}
{session.createdAt ? new Date(session.createdAt).toLocaleString() : 'N/A'}
</p>
{session.messageCount !== undefined && (
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
<p className="text-xs text-text-subtle mt-1">
Messages: {session.messageCount}
</p>
)}
{session.workingDir && (
<p
className="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate"
className="text-xs text-text-subtle mt-1 truncate"
title={session.workingDir}
>
Dir: {session.workingDir}
@@ -676,11 +664,11 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
)}
{session.accumulatedTotalTokens !== undefined &&
session.accumulatedTotalTokens !== null && (
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
<p className="text-xs text-text-subtle mt-1">
Tokens: {session.accumulatedTotalTokens}
</p>
)}
<p className="text-xs text-gray-600 dark:text-gray-500 mt-1">
<p className="text-xs text-text-muted mt-1">
ID: <span className="font-mono">{session.id}</span>
</p>
</Card>
@@ -10,23 +10,21 @@ import {
inspectRunningJob,
ScheduledJob,
} from '../../schedule';
import BackButton from '../ui/BackButton';
import { ScrollArea } from '../ui/scroll-area';
import MoreMenuLayout from '../more_menu/MoreMenuLayout';
import { Card } from '../ui/card';
import { Button } from '../ui/button';
import { TrashIcon } from '../icons/TrashIcon';
import { Plus, RefreshCw, Pause, Play, Edit, Square, Eye, MoreHorizontal } from 'lucide-react';
import { Plus, RefreshCw, Pause, Play, Edit, Square, Eye } from 'lucide-react';
import { CreateScheduleModal, NewSchedulePayload } from './CreateScheduleModal';
import { EditScheduleModal } from './EditScheduleModal';
import ScheduleDetailView from './ScheduleDetailView';
import { toastError, toastSuccess } from '../../toasts';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import cronstrue from 'cronstrue';
import { formatToLocalDateWithTimezone } from '../../utils/date';
import { MainPanelLayout } from '../Layout/MainPanelLayout';
interface SchedulesViewProps {
onClose: () => void;
onClose?: () => void;
}
// Memoized ScheduleCard component to prevent unnecessary re-renders
@@ -75,146 +73,133 @@ const ScheduleCard = React.memo<{
return (
<Card
className="p-4 bg-white dark:bg-gray-800 shadow cursor-pointer hover:shadow-lg transition-shadow duration-200"
className="py-2 px-4 mb-2 bg-background-default border-none hover:bg-background-muted cursor-pointer transition-all duration-150"
onClick={() => onNavigateToDetail(job.id)}
>
<div className="flex justify-between items-start">
<div className="flex-grow mr-2 overflow-hidden">
<h3
className="text-base font-semibold text-gray-900 dark:text-white truncate"
title={job.id}
>
{job.id}
</h3>
<p
className="text-xs text-gray-500 dark:text-gray-400 mt-1 break-all"
title={job.source}
>
Source: {job.source}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1" title={readableCron}>
Schedule: {readableCron}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Last Run: {formattedLastRun}
</p>
{job.execution_mode && (
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Mode:{' '}
<div className="flex justify-between items-start gap-4">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 mb-1">
<h3 className="text-base truncate max-w-[50vw]" title={job.id}>
{job.id}
</h3>
{job.execution_mode && (
<span
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
job.execution_mode === 'foreground'
? 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300'
: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300'
? 'bg-background-accent text-text-on-accent'
: 'bg-background-medium text-text-default'
}`}
>
{job.execution_mode === 'foreground' ? '🖥️ Foreground' : '⚡ Background'}
{job.execution_mode === 'foreground' ? '🖥️' : '⚡'}
</span>
</p>
)}
{job.currently_running && (
<p className="text-xs text-green-500 dark:text-green-400 mt-1 font-semibold flex items-center">
<span className="inline-block w-2 h-2 bg-green-500 dark:bg-green-400 rounded-full mr-1 animate-pulse"></span>
Currently Running
</p>
)}
{job.paused && (
<p className="text-xs text-orange-500 dark:text-orange-400 mt-1 font-semibold flex items-center">
<Pause className="w-3 h-3 mr-1" />
Paused
</p>
)}
)}
{job.currently_running && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">
<span className="inline-block w-2 h-2 bg-green-500 rounded-full mr-1 animate-pulse"></span>
Running
</span>
)}
{job.paused && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300">
<Pause className="w-3 h-3 mr-1" />
Paused
</span>
)}
</div>
<p className="text-text-muted text-sm mb-2 line-clamp-2" title={readableCron}>
{readableCron}
</p>
<div className="flex items-center text-xs text-text-muted">
<span>Last run: {formattedLastRun}</span>
</div>
</div>
<div className="flex-shrink-0">
<Popover>
<PopoverTrigger asChild>
<div className="flex items-center gap-2 shrink-0">
{!job.currently_running && (
<>
<Button
variant="ghost"
size="xs"
onClick={(e) => {
e.stopPropagation();
onEdit(job);
}}
className="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100/50 dark:hover:bg-gray-800/50"
disabled={isPausing || isDeleting || isSubmitting}
variant="outline"
size="sm"
className="h-8"
>
<MoreHorizontal className="w-4 h-4" />
<Edit className="w-4 h-4 mr-1" />
Edit
</Button>
</PopoverTrigger>
<PopoverContent
className="w-48 p-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 shadow-lg"
align="end"
>
<div className="space-y-1">
{!job.currently_running && (
<Button
onClick={(e) => {
e.stopPropagation();
if (job.paused) {
onUnpause(job.id);
} else {
onPause(job.id);
}
}}
disabled={isPausing || isDeleting}
variant="outline"
size="sm"
className="h-8"
>
{job.paused ? (
<>
<button
onClick={(e) => {
e.stopPropagation();
onEdit(job);
}}
disabled={isPausing || isDeleting || isSubmitting}
className="w-full flex items-center justify-between px-3 py-2 text-sm text-gray-900 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
>
<span>Edit</span>
<Edit className="w-4 h-4" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
if (job.paused) {
onUnpause(job.id);
} else {
onPause(job.id);
}
}}
disabled={isPausing || isDeleting}
className="w-full flex items-center justify-between px-3 py-2 text-sm text-gray-900 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
>
<span>{job.paused ? 'Resume schedule' : 'Stop schedule'}</span>
{job.paused ? <Play className="w-4 h-4" /> : <Pause className="w-4 h-4" />}
</button>
<Play className="w-4 h-4 mr-1" />
Resume
</>
) : (
<>
<Pause className="w-4 h-4 mr-1" />
Pause
</>
)}
{job.currently_running && (
<>
<button
onClick={(e) => {
e.stopPropagation();
onInspect(job.id);
}}
disabled={isInspecting || isKilling}
className="w-full flex items-center justify-between px-3 py-2 text-sm text-gray-900 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
>
<span>Inspect</span>
<Eye className="w-4 h-4" />
</button>
<button
onClick={(e) => {
e.stopPropagation();
onKill(job.id);
}}
disabled={isKilling || isInspecting}
className="w-full flex items-center justify-between px-3 py-2 text-sm text-gray-900 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
>
<span>Kill job</span>
<Square className="w-4 h-4" />
</button>
</>
)}
<hr className="border-gray-200 dark:border-gray-600 my-1" />
<button
onClick={(e) => {
e.stopPropagation();
onDelete(job.id);
}}
disabled={isPausing || isDeleting || isKilling || isInspecting}
className="w-full flex items-center justify-between px-3 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
>
<span>Delete</span>
<TrashIcon className="w-4 h-4" />
</button>
</div>
</PopoverContent>
</Popover>
</Button>
</>
)}
{job.currently_running && (
<>
<Button
onClick={(e) => {
e.stopPropagation();
onInspect(job.id);
}}
disabled={isInspecting || isKilling}
variant="outline"
size="sm"
className="h-8"
>
<Eye className="w-4 h-4 mr-1" />
Inspect
</Button>
<Button
onClick={(e) => {
e.stopPropagation();
onKill(job.id);
}}
disabled={isKilling || isInspecting}
variant="outline"
size="sm"
className="h-8"
>
<Square className="w-4 h-4 mr-1" />
Kill
</Button>
</>
)}
<Button
onClick={(e) => {
e.stopPropagation();
onDelete(job.id);
}}
disabled={isPausing || isDeleting || isKilling || isInspecting}
variant="ghost"
size="sm"
className="h-8 text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20"
>
<TrashIcon className="w-4 h-4" />
</Button>
</div>
</div>
</Card>
@@ -224,7 +209,7 @@ const ScheduleCard = React.memo<{
ScheduleCard.displayName = 'ScheduleCard';
const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose }) => {
const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose: _onClose }) => {
const [schedules, setSchedules] = useState<ScheduledJob[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -573,80 +558,96 @@ const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose }) => {
}
return (
<div className="h-screen w-full flex flex-col bg-app text-textStandard">
<MoreMenuLayout showMenu={false} />
<div className="px-8 pt-6 pb-4 border-b border-borderSubtle flex-shrink-0">
<BackButton onClick={onClose} />
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white mt-2">
Schedules Management
</h1>
</div>
<ScrollArea className="flex-grow">
<div className="p-8">
<div className="flex flex-col md:flex-row gap-2 mb-8">
<Button
onClick={handleOpenCreateModal}
className="w-full md:w-auto flex items-center gap-2 justify-center text-white dark:text-black bg-background-defaultInverse hover:bg-bgStandardInverse [&>svg]:!size-4"
>
<Plus className="h-4 w-4" /> Create New Schedule
</Button>
<Button
onClick={handleRefresh}
disabled={isRefreshing || isLoading}
variant="outline"
className="w-full md:w-auto flex items-center gap-2 justify-center rounded-full [&>svg]:!size-4"
>
<RefreshCw className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} />
{isRefreshing ? 'Refreshing...' : 'Refresh'}
</Button>
<>
<MainPanelLayout>
<div className="flex-1 flex flex-col min-h-0">
<div className="bg-background-default px-8 pb-8 pt-16">
<div className="flex flex-col animate-in fade-in duration-500">
<div className="flex justify-between items-center mb-1">
<h1 className="text-4xl font-light">Scheduler</h1>
<div className="flex gap-2">
<Button
onClick={handleRefresh}
disabled={isRefreshing || isLoading}
variant="outline"
size="sm"
className="flex items-center gap-2"
>
<RefreshCw className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} />
{isRefreshing ? 'Refreshing...' : 'Refresh'}
</Button>
<Button
onClick={handleOpenCreateModal}
size="sm"
className="flex items-center gap-2"
>
<Plus className="h-4 w-4" />
Create Schedule
</Button>
</div>
</div>
<p className="text-sm text-text-muted mb-1">
Create and manage scheduled tasks to run recipes automatically at specified times.
</p>
</div>
</div>
{apiError && (
<p className="text-red-500 dark:text-red-400 text-sm p-4 bg-red-100 dark:bg-red-900/30 border border-red-500 dark:border-red-700 rounded-md">
Error: {apiError}
</p>
)}
<div className="flex-1 min-h-0 relative px-8">
<ScrollArea className="h-full">
<div className="h-full relative">
{apiError && (
<div className="mb-4 p-4 bg-background-error border border-border-error rounded-md">
<p className="text-text-error text-sm">Error: {apiError}</p>
</div>
)}
<section>
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
Existing Schedules
</h2>
{isLoading && schedules.length === 0 && (
<p className="text-gray-500 dark:text-gray-400">Loading schedules...</p>
)}
{!isLoading && !apiError && schedules.length === 0 && (
<p className="text-gray-500 dark:text-gray-400 text-center py-4">
No schedules found. Create one to get started!
</p>
)}
{isLoading && schedules.length === 0 && (
<div className="flex justify-center items-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-text-default"></div>
</div>
)}
{!isLoading && schedules.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{schedules.map((job) => (
<ScheduleCard
key={job.id}
job={job}
onNavigateToDetail={handleNavigateToScheduleDetail}
onEdit={handleOpenEditModal}
onPause={handlePauseSchedule}
onUnpause={handleUnpauseSchedule}
onKill={handleKillRunningJob}
onInspect={handleInspectRunningJob}
onDelete={handleDeleteSchedule}
isPausing={pausingScheduleIds.has(job.id)}
isDeleting={deletingScheduleIds.has(job.id)}
isKilling={killingScheduleIds.has(job.id)}
isInspecting={inspectingScheduleIds.has(job.id)}
isSubmitting={isSubmitting}
/>
))}
{!isLoading && !apiError && schedules.length === 0 && (
<div className="flex flex-col justify-center items-center py-12">
<p className="text-lg mb-2">No schedules found</p>
<p className="text-sm text-text-muted mb-4">
Create your first schedule to get started with automated tasks.
</p>
<Button onClick={handleOpenCreateModal} className="flex items-center gap-2">
<Plus className="h-4 w-4" />
Create Schedule
</Button>
</div>
)}
{!isLoading && schedules.length > 0 && (
<div className="space-y-2 pb-8">
{schedules.map((job) => (
<ScheduleCard
key={job.id}
job={job}
onNavigateToDetail={handleNavigateToScheduleDetail}
onEdit={handleOpenEditModal}
onPause={handlePauseSchedule}
onUnpause={handleUnpauseSchedule}
onKill={handleKillRunningJob}
onInspect={handleInspectRunningJob}
onDelete={handleDeleteSchedule}
isPausing={pausingScheduleIds.has(job.id)}
isDeleting={deletingScheduleIds.has(job.id)}
isKilling={killingScheduleIds.has(job.id)}
isInspecting={inspectingScheduleIds.has(job.id)}
isSubmitting={isSubmitting}
/>
))}
</div>
)}
</div>
)}
</section>
</ScrollArea>
</div>
</div>
</ScrollArea>
</MainPanelLayout>
<CreateScheduleModal
isOpen={isCreateModalOpen}
onClose={handleCloseCreateModal}
@@ -662,7 +663,7 @@ const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose }) => {
isLoadingExternally={isSubmitting}
apiErrorExternally={submitApiError}
/>
</div>
</>
);
};
@@ -61,7 +61,7 @@ export const SessionHeaderCard: React.FC<SessionHeaderCardProps> = ({ onBack, ch
<BackButton
showText={false}
onClick={onBack}
iconSize="w-7 h-7"
size="lg"
className="!text-textProminentInverse dark:!text-textStandardInverse"
/>
{children}
+21 -13
View File
@@ -1,20 +1,24 @@
import React from 'react';
import Back from '../icons/Back';
import { ChevronLeft } from 'lucide-react';
import { Button } from './button';
import type { VariantProps } from 'class-variance-authority';
import { buttonVariants } from './button';
interface BackButtonProps {
onClick?: () => void; // Mark onClick as optional
interface BackButtonProps extends VariantProps<typeof buttonVariants> {
onClick?: () => void;
className?: string;
textSize?: 'sm' | 'base' | 'md' | 'lg';
iconSize?: 'w-3 h-3' | 'w-4 h-4' | 'w-5 h-5' | 'w-6 h-6' | 'w-7 h-7';
showText?: boolean; // Add new prop
showText?: boolean;
shape?: 'pill' | 'round';
}
const BackButton: React.FC<BackButtonProps> = ({
onClick,
className = '',
textSize = 'sm',
iconSize = 'w-3 h-3',
variant = 'outline',
size = 'xs',
shape = 'pill',
showText = true,
...props
}) => {
const handleExit = () => {
if (onClick) {
@@ -27,13 +31,17 @@ const BackButton: React.FC<BackButtonProps> = ({
};
return (
<button
<Button
onClick={handleExit}
className={`flex items-center text-${textSize} text-textSubtle group hover:text-textStandard ${className}`}
variant={variant}
size={size}
shape={shape}
className={className}
{...props}
>
<Back className={`${iconSize} group-hover:-translate-x-1 transition-all mr-1`} />
{showText && <span>Back</span>}
</button>
<ChevronLeft />
{showText && 'Back'}
</Button>
);
};