mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
added scheduler and unified back buttons everywhere
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user