merlyn/frontend/src/pages/GeneralSettings/ScheduledJobs/components/JobRow.jsx
Marcello Fitton 41495cdabe
feat: Scheduled Jobs (#5322)
* initialize

* expand tool result text limit | add syntax highlighting and json formatting to tool result rendering

* fix onError jsdoc

* lint

* fix unread icon

* route protection

* improve form handling for NewJobModal

* safeJsonParse

* remove unneeded comments

* remove trycatch

* add truncateText helper

* add explicit fallback value tos safeJsonParse

* add shared cron constant and helpers

* reduce frontend indirection

* use isLight to compute syntax highlighting theme

* remove dead code

* remove forJob and make job limit to 50

* create recomputeNextRunAt helper method

* add comment about nextRunAt recomputation

* add job queue and concurrency control to scheduled jobs

* use p-queue

* change default max concurrent value to 1

* add comment explaining internal scheduling system

* add recomputeNextRunAt on boot

* add generated documents to run details

* Modify toolsOverride functionality where no tools selected means no tools are given to the agent

add a select all/deselect all toggle button for easily selecting all
tools in the cerate job form

* create usePolling hook

* add polling to scheduled jobs and scheduled job runs pages

* add cron generation feature in job form

* remove cron generation feature | add cron builder feature | add max active scheduled jobs limit

* set MAX_ACTIVE to null

* replace hour and minute input fields with input with type time

* simplify

* organize components

* move components to bottom of page component

* change Generated Documents to Generated Files

* add i18n to cronstrue

* add i18n

* add type="button" to button elements

* refactor fileSource retrieval logic

* one scheduled job run can have status "running"

* add protection of file retrieveal from scheduled job in multiuser mode

* fix comments

* make job status default to queued

* add queued status

* fix bug with result trace rendering

* store timeout ref and clearTimeout once race settles

* remove unneeded handlerPromise tracking

* move imports to top level

* refactor hardcoded paths to path resolve functions

* implement new job form design

* simplify

* fix button styles

* fix runJob bug

* implement styles for scheduled jobs page

* apply dark mode figma styles

* delete unused translation key

* implement light mode for new new job modal, run history, and run details

* lint

* fix light mode scroll bar in tool call card

* adjust table header contrast

* fix type in subtitle

* kill workers when job is in-flight before deleting job

* add border-none to buttons

* change locale time to iso string

* import BackgroundService module level | instatiate backgroundService singltone once and reuse across handlers

* add p-queue, @breejs/later and cron-validate as core deps

* parse cron expression to a builder state once

* add theme to day buttons in cron builder

* fix stale tools selection caption

* flip popover when popover clips screen height

* make ScheduleJob.trigger() await the run insertion | disable run now button if job is in flight

* regen table

* refactor generated file card

* refactor frontend

* remove logs

* major refactor for tool picking, fix bree/later bug

* combine action endpoints, move contine to method

* fix unoptimized query with include + take + order

* fix dangerous use, refactor job to utils

* add copy content to text response

* improve notification system subscription for browser

* remove unused translations

* prevent gen-file cleanup job from deleting active job file generated references

* rich text copy

* Scheduled Jobs: Translations (#5482)

* add locales for scheduled jobs

* i18n

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>

* add config flag with UI notice

* update README

* telemetry datapoints

* Always use UTC on backend, convert to local in frontend

* fix tz render

* Add job killing

* cleanup thinking text in job notifications and break out reasoning in response text.
Also hide zero metrics since that is useless

* Port generatedFile schema to the normalized workspace chat `outputs` file format so porting to thread is simple and implem between chats <> jobs is 1:1

* what the fuck

* compiled bug

* fixed thinking oddity in complied frontend

* supress multi-toast

* fix duration call

* Revert "fix duration call"

This reverts commit 0491bc71f4223e65ea4046561b15b268fefb8da2.

* revert and reapply fix

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
2026-04-29 12:05:46 -07:00

108 lines
4.4 KiB
JavaScript

import { useNavigate } from "react-router-dom";
import { Play, PencilSimple, X } from "@phosphor-icons/react";
import paths from "@/utils/paths";
import { humanizeCron } from "../utils/cron";
import { useTranslation } from "react-i18next";
// One row of the scheduled-jobs list. Clicking the name navigates to the
// run history; CRUD callbacks come from the parent.
export default function JobRow({ job, onTrigger, onToggle, onEdit, onDelete }) {
const navigate = useNavigate();
const { t, i18n } = useTranslation();
// A job has at most one in-flight run; disable "Run now" while it's queued
// or running so users get visible feedback that their click registered and
// so the backend dedup never has to drop a manual trigger silently.
const inFlight =
job.latestRun?.status === "running" || job.latestRun?.status === "queued";
const statusText = job.latestRun
? t(`scheduledJobs.status.${job.latestRun.status}`, job.latestRun.status)
: t("scheduledJobs.row.neverRun");
const stop = (handler) => (e) => {
e.stopPropagation();
handler();
};
return (
<div
role="button"
tabIndex={0}
onClick={() => navigate(paths.settings.scheduledJobRuns(job.id))}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
navigate(paths.settings.scheduledJobRuns(job.id));
}
}}
className="flex items-center justify-between px-4 h-14 hover:bg-white/5 light:hover:bg-slate-200 transition-colors cursor-pointer"
title={t("scheduledJobs.row.viewRuns")}
>
<span className="w-[150px] text-sm font-medium text-white light:text-slate-950 truncate">
{job.name}
</span>
<span className="w-[180px] text-sm text-zinc-400 light:text-slate-600 truncate">
{humanizeCron(job.schedule, i18n.language)}
</span>
<span className="w-[120px] text-sm text-zinc-400 light:text-slate-600 truncate">
{statusText}
</span>
<span className="w-[180px] text-sm text-zinc-400 light:text-slate-600 truncate">
{job.lastRunAt ? new Date(job.lastRunAt).toLocaleString() : "—"}
</span>
<span className="w-[180px] text-sm text-zinc-400 light:text-slate-600 truncate">
{job.enabled && job.nextRunAt
? new Date(job.nextRunAt).toLocaleString()
: "—"}
</span>
<div className="w-[140px] flex items-center justify-end gap-1">
<button
type="button"
onClick={stop(() => onDelete(job.id))}
className="border-none p-2 rounded-full text-zinc-400 light:text-slate-950 hover:text-red-400 light:hover:text-red-600 hover:bg-white/10 light:hover:bg-slate-300/50 transition-colors"
title={t("scheduledJobs.row.delete")}
>
<X className="h-4 w-4 shrink-0" />
</button>
<button
type="button"
onClick={stop(() => onEdit(job))}
className="border-none p-2 rounded-full text-zinc-400 light:text-slate-950 hover:text-white light:hover:text-slate-700 hover:bg-white/10 light:hover:bg-slate-300/50 transition-colors"
title={t("scheduledJobs.row.edit")}
>
<PencilSimple className="h-4 w-4 shrink-0" />
</button>
<button
type="button"
onClick={stop(() => onTrigger(job.id))}
disabled={inFlight}
className="border-none p-2 rounded-full text-zinc-400 light:text-slate-950 hover:text-white light:hover:text-slate-700 hover:bg-white/10 light:hover:bg-slate-300/50 transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent"
title={t("scheduledJobs.row.runNow")}
>
<Play className="h-4 w-4 shrink-0" />
</button>
<button
type="button"
role="switch"
aria-checked={job.enabled}
onClick={stop(() => onToggle(job.id))}
title={
job.enabled
? t("scheduledJobs.row.disable")
: t("scheduledJobs.row.enable")
}
className={`border-none relative h-[15px] w-7 rounded-full p-0.5 transition-colors ${
job.enabled ? "bg-green-400" : "bg-zinc-600 light:bg-slate-300"
}`}
>
<span
className={`block h-3 w-3 rounded-full bg-white shadow transition-transform ${
job.enabled ? "translate-x-[13px]" : "translate-x-0"
}`}
/>
</button>
</div>
</div>
);
}