diff --git a/Data/Engine/web-interface/src/Scheduling/Create_Job.jsx b/Data/Engine/web-interface/src/Scheduling/Create_Job.jsx
index 373068a0..ecc00bd9 100644
--- a/Data/Engine/web-interface/src/Scheduling/Create_Job.jsx
+++ b/Data/Engine/web-interface/src/Scheduling/Create_Job.jsx
@@ -1,6 +1,5 @@
import React, { useEffect, useMemo, useState, useCallback, useRef } from "react";
import {
- Paper,
Box,
Typography,
Tabs,
@@ -9,10 +8,8 @@ import {
Button,
IconButton,
Checkbox,
- FormControl,
FormControlLabel,
Select,
- InputLabel,
Menu,
MenuItem,
Divider,
@@ -25,7 +22,6 @@ import {
TableRow,
TableCell,
TableBody,
- TableSortLabel,
GlobalStyles,
CircularProgress
} from "@mui/material";
@@ -40,7 +36,6 @@ import {
Error as ErrorIcon,
Refresh as RefreshIcon
} from "@mui/icons-material";
-import { SimpleTreeView, TreeItem } from "@mui/x-tree-view";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
@@ -54,15 +49,325 @@ import "prismjs/themes/prism-okaidia.css";
import Editor from "react-simple-code-editor";
import ReactFlow, { Handle, Position } from "reactflow";
import "reactflow/dist/style.css";
+import { AgGridReact } from "ag-grid-react";
+import { ModuleRegistry, AllCommunityModule, themeQuartz } from "ag-grid-community";
import { DomainBadge } from "../Assemblies/Assembly_Badges";
import {
buildAssemblyIndex,
- buildAssemblyTree,
normalizeAssemblyPath,
parseAssemblyExport,
resolveAssemblyForComponent
} from "../Assemblies/assemblyUtils";
+ModuleRegistry.registerModules([AllCommunityModule]);
+
+const MAGIC_UI = {
+ shellBg:
+ "radial-gradient(120% 120% at 0% 0%, rgba(76, 186, 255, 0.16), transparent 55%), " +
+ "radial-gradient(120% 120% at 100% 0%, rgba(214, 130, 255, 0.18), transparent 60%), #040711",
+ panelBg:
+ "linear-gradient(145deg, rgba(7,10,24,0.96), rgba(6,10,28,0.92) 45%, rgba(14,8,30,0.95))",
+ panelBorder: "rgba(148, 163, 184, 0.32)",
+ textMuted: "#94a3b8",
+ textBright: "#e2e8f0",
+ accentA: "#7dd3fc",
+ accentB: "#c084fc",
+ accentC: "#34d399",
+ glow: "0 30px 70px rgba(2,6,23,0.85)",
+};
+
+const gridTheme = themeQuartz.withParams({
+ accentColor: "#8b5cf6",
+ backgroundColor: "#070b1a",
+ browserColorScheme: "dark",
+ fontFamily: { googleFont: "IBM Plex Sans" },
+ foregroundColor: "#f4f7ff",
+ headerFontSize: 13,
+});
+const gridThemeClass = gridTheme.themeName || "ag-theme-quartz";
+const gridFontFamily = '"IBM Plex Sans","Helvetica Neue",Arial,sans-serif';
+const iconFontFamily = '"Quartz Regular"';
+
+const GRID_WRAPPER_SX = {
+ width: "100%",
+ borderRadius: 3,
+ border: `1px solid ${MAGIC_UI.panelBorder}`,
+ background: "linear-gradient(170deg, rgba(5,8,20,0.92), rgba(8,13,32,0.9))",
+ boxShadow: "0 22px 60px rgba(2,6,23,0.75)",
+ position: "relative",
+ overflow: "hidden",
+ "& .ag-root-wrapper": {
+ borderRadius: 3,
+ minHeight: "100%",
+ },
+ "& .ag-root, & .ag-header, & .ag-center-cols-container": {
+ fontFamily: gridFontFamily,
+ background: "transparent",
+ },
+ "& .ag-header": {
+ backgroundColor: "rgba(3,7,18,0.9)",
+ borderBottom: "1px solid rgba(148,163,184,0.25)",
+ },
+ "& .ag-header-cell-label": {
+ color: "#e2e8f0",
+ fontWeight: 600,
+ letterSpacing: 0.3,
+ },
+ "& .ag-row": {
+ borderColor: "rgba(255,255,255,0.04)",
+ transition: "background 0.2s ease",
+ },
+ "& .ag-row:nth-of-type(even)": {
+ backgroundColor: "rgba(15,23,42,0.32)",
+ },
+ "& .ag-row-hover": {
+ backgroundColor: "rgba(125,183,255,0.08) !important",
+ },
+ "& .ag-row-selected": {
+ backgroundColor: "rgba(56,189,248,0.14) !important",
+ boxShadow: "inset 0 0 0 1px rgba(56,189,248,0.3)",
+ },
+ "& .ag-icon": {
+ fontFamily: iconFontFamily,
+ },
+ "& .ag-checkbox-input-wrapper": {
+ borderRadius: "3px",
+ },
+ "& .ag-cell.auto-col-tight": {
+ paddingLeft: 8,
+ paddingRight: 6,
+ },
+ "& .status-pill-cell": {
+ display: "flex",
+ alignItems: "center",
+ },
+ "& .status-pill-cell .ag-cell-wrapper": {
+ width: "100%",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ height: "100%",
+ paddingTop: 0,
+ paddingBottom: 0,
+ lineHeight: "normal",
+ },
+ "& .status-pill-cell .ag-cell-value": {
+ width: "100%",
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ height: "100%",
+ },
+};
+
+const DEVICE_STATUS_THEME = {
+ online: {
+ label: "Online",
+ text: "#00d18c",
+ background: "rgba(0,209,140,0.16)",
+ border: "1px solid rgba(0,209,140,0.35)",
+ dot: "#00d18c",
+ },
+ offline: {
+ label: "Offline",
+ text: "#b0b8c8",
+ background: "rgba(176,184,200,0.14)",
+ border: "1px solid rgba(176,184,200,0.35)",
+ dot: "#c3cada",
+ },
+};
+
+const JOB_RESULT_THEME = {
+ success: {
+ label: "Success",
+ text: "#34d399",
+ background: "linear-gradient(120deg, rgba(52,211,153,0.22), rgba(30,64,175,0.12))",
+ border: "1px solid rgba(52,211,153,0.45)",
+ dot: "#34d399",
+ },
+ running: {
+ label: "Running",
+ text: "#7dd3fc",
+ background: "linear-gradient(120deg, rgba(125,211,252,0.25), rgba(14,165,233,0.18))",
+ border: "1px solid rgba(125,211,252,0.45)",
+ dot: "#38bdf8",
+ },
+ failed: {
+ label: "Failed",
+ text: "#fb7185",
+ background: "rgba(251,113,133,0.18)",
+ border: "1px solid rgba(251,113,133,0.45)",
+ dot: "#fb7185",
+ },
+ pending: {
+ label: "Pending",
+ text: "#fbbf24",
+ background: "rgba(251,191,36,0.18)",
+ border: "1px solid rgba(251,191,36,0.35)",
+ dot: "#f59e0b",
+ },
+ expired: {
+ label: "Expired",
+ text: "#e5e7eb",
+ background: "rgba(226,232,240,0.14)",
+ border: "1px solid rgba(226,232,240,0.32)",
+ dot: "#cbd5f5",
+ },
+ default: {
+ label: "Status",
+ text: "#e2e8f0",
+ background: "rgba(226,232,240,0.12)",
+ border: "1px solid rgba(226,232,240,0.2)",
+ dot: "#94a3b8",
+ },
+};
+
+const StatusPill = ({ label, theme }) => {
+ if (!label) return null;
+ const pillTheme = theme || JOB_RESULT_THEME.default;
+ return (
+
+ {pillTheme.dot ? (
+
+ ) : null}
+ {label}
+
+ );
+};
+
+const GLASS_PANEL_BASE_SX = {
+ background: MAGIC_UI.panelBg,
+ borderRadius: 3,
+ border: `1px solid ${MAGIC_UI.panelBorder}`,
+ boxShadow: MAGIC_UI.glow,
+ p: { xs: 2, md: 3 },
+};
+
+const PRIMARY_CTA_SX = {
+ borderRadius: 999,
+ px: 3,
+ py: 1,
+ fontWeight: 600,
+ textTransform: "none",
+ color: "#041317",
+ backgroundImage: "linear-gradient(120deg,#34d399,#22d3ee)",
+ "&:hover": {
+ backgroundImage: "linear-gradient(120deg,#22d3ee,#34d399)",
+ },
+};
+
+const OUTLINE_BUTTON_SX = {
+ borderRadius: 999,
+ px: 2.5,
+ textTransform: "none",
+ borderColor: "rgba(148,163,184,0.45)",
+ color: MAGIC_UI.textBright,
+ "&:hover": {
+ borderColor: MAGIC_UI.accentA,
+ },
+};
+
+const INPUT_FIELD_SX = {
+ "& .MuiOutlinedInput-root": {
+ borderRadius: 2,
+ bgcolor: "rgba(5,9,18,0.85)",
+ color: MAGIC_UI.textBright,
+ "& fieldset": {
+ borderColor: "rgba(148,163,184,0.35)",
+ },
+ "&:hover fieldset": {
+ borderColor: MAGIC_UI.accentA,
+ },
+ "&.Mui-focused fieldset": {
+ borderColor: MAGIC_UI.accentB,
+ boxShadow: "0 0 0 1px rgba(192,132,252,0.3)",
+ },
+ },
+ "& .MuiInputLabel-root": {
+ color: MAGIC_UI.textMuted,
+ },
+ "& .MuiFormHelperText-root": {
+ color: "#fda4af",
+ },
+};
+
+const HERO_CARD_SX = {
+ display: "flex",
+ flexDirection: "column",
+ gap: 0.2,
+ px: 0,
+ py: 0,
+ minWidth: 160,
+};
+const GlassPanel = ({ children, sx }) => (
+ {children}
+);
+
+const EXEC_CONTEXT_COPY = {
+ system: { title: "Windows (System)", detail: "Runs on device as SYSTEM" },
+ current_user: { title: "Windows (Logged-In User)", detail: "Runs on device as user session" },
+ ssh: { title: "Remote SSH", detail: "Executes from engine host" },
+ winrm: { title: "Remote WinRM", detail: "Executes from engine host" },
+};
+
+const SCHEDULE_LABELS = {
+ immediately: "Immediate",
+ once: "Single run",
+ every_5_minutes: "Every 5 minutes",
+ every_10_minutes: "Every 10 minutes",
+ every_15_minutes: "Every 15 minutes",
+ every_30_minutes: "Every 30 minutes",
+ every_15: "Every 15 minutes",
+ every_hour: "Hourly cadence",
+ daily: "Daily cadence",
+ weekly: "Weekly cadence",
+ monthly: "Monthly cadence",
+ yearly: "Yearly cadence",
+};
+
+const TABLE_BASE_SX = {
+ "& .MuiTableCell-root": {
+ borderColor: "rgba(148,163,184,0.18)",
+ color: MAGIC_UI.textBright,
+ },
+ "& .MuiTableHead-root .MuiTableCell-root": {
+ color: MAGIC_UI.textMuted,
+ fontWeight: 600,
+ backgroundColor: "rgba(8,12,24,0.7)",
+ },
+ "& .MuiTableBody-root .MuiTableRow-root:hover": {
+ backgroundColor: "rgba(56,189,248,0.08)",
+ },
+};
+
const hiddenHandleStyle = {
width: 12,
height: 12,
@@ -118,6 +423,9 @@ function StatusNode({ data }) {
const displayCount = Number.isFinite(count) ? count : Number(count) || 0;
const borderColor = color || "#333";
const activeGlow = color ? `${color}55` : "rgba(88,166,255,0.35)";
+ const gradientLayer = color
+ ? `linear-gradient(140deg, rgba(8,12,24,0.92), ${color}1f)`
+ : "linear-gradient(140deg, rgba(8,12,24,0.92), rgba(14,20,38,0.85))";
const handleClick = useCallback((event) => {
event?.preventDefault();
event?.stopPropagation();
@@ -129,10 +437,9 @@ function StatusNode({ data }) {
sx={{
px: 5.4,
py: 3.8,
- backgroundColor: "#1f1f1f",
- borderRadius: 1.5,
+ borderRadius: 2,
border: `1px solid ${borderColor}`,
- boxShadow: isActive ? `0 0 0 2px ${activeGlow}` : "none",
+ boxShadow: isActive ? `0 0 25px ${activeGlow}` : "0 20px 40px rgba(2,6,23,0.65)",
cursor: "pointer",
minWidth: 324,
textAlign: "left",
@@ -140,14 +447,39 @@ function StatusNode({ data }) {
transform: isActive ? "translateY(-2px)" : "none",
display: "flex",
alignItems: "flex-start",
- justifyContent: "flex-start"
+ justifyContent: "flex-start",
+ position: "relative",
+ overflow: "hidden",
+ "&::before": {
+ content: '""',
+ position: "absolute",
+ inset: 0,
+ background: gradientLayer,
+ borderRadius: "inherit",
+ opacity: 0.95,
+ transition: "opacity 0.2s ease",
+ },
+ "&::after": {
+ content: '""',
+ position: "absolute",
+ inset: "-25% -40%",
+ background: color
+ ? `radial-gradient(circle at 30% 20%, ${color}30, transparent 55%)`
+ : "radial-gradient(circle at 30% 20%, rgba(125,183,255,0.3), transparent 55%)",
+ borderRadius: "inherit",
+ opacity: 0.65,
+ filter: "blur(0px)",
+ transition: "opacity 0.2s ease",
+ },
+ "&:hover::before": { opacity: 1 },
+ "&:hover::after": { opacity: 0.85 },
}}
>
-
+
{Icon ? : null}
{`${displayCount} ${label || ""}`}
@@ -159,28 +491,33 @@ function StatusNode({ data }) {
function SectionHeader({ title, action }) {
return (
-
- {title}
+
+
+ {title}
+
{action || null}
);
}
-// Recursive renderer for both Scripts and Workflows trees
-function renderTreeNodes(nodes = [], map = {}) {
- return nodes.map((n) => (
-
- {n.children && n.children.length ? renderTreeNodes(n.children, map) : null}
-
- ));
-}
-
function normalizeVariableDefinitions(vars = []) {
return (Array.isArray(vars) ? vars : [])
.map((raw) => {
@@ -302,22 +639,31 @@ function ComponentCard({ comp, onRemove, onVariableChange, errors = {} }) {
: [];
const description = comp.description || comp.path || "";
return (
-
-
-
-
-
+
+
+
+
+
{comp.name}
{comp.domain ? : null}
-
+
{description}
-
-
- Variables
+
+
+
+ Variables
+
{variables.length ? (
{variables.map((variable) => (
@@ -325,22 +671,30 @@ function ComponentCard({ comp, onRemove, onVariableChange, errors = {} }) {
{variable.type === "boolean" ? (
<>
onVariableChange(comp.localId, variable.name, e.target.checked)}
+ sx={{
+ color: MAGIC_UI.accentA,
+ "&.Mui-checked": { color: MAGIC_UI.accentB },
+ }}
/>
- )}
+ }
label={
-
+ <>
{variable.label}
{variable.required ? " *" : ""}
-
+ >
}
/>
{variable.description ? (
-
+
{variable.description}
) : null}
@@ -354,10 +708,7 @@ function ComponentCard({ comp, onRemove, onVariableChange, errors = {} }) {
value={variable.value ?? ""}
onChange={(e) => onVariableChange(comp.localId, variable.name, e.target.value)}
InputLabelProps={{ shrink: true }}
- sx={{
- "& .MuiOutlinedInput-root": { bgcolor: "#1b1b1b", color: "#e6edf3" },
- "& .MuiInputBase-input": { color: "#e6edf3" }
- }}
+ sx={{ ...INPUT_FIELD_SX }}
error={Boolean(errors[variable.name])}
helperText={errors[variable.name] || variable.description || ""}
/>
@@ -366,16 +717,27 @@ function ComponentCard({ comp, onRemove, onVariableChange, errors = {} }) {
))}
) : (
- No variables defined for this assembly.
+
+ No variables defined for this assembly.
+
)}
-
- onRemove(comp.localId)} size="small" sx={{ color: "#ff6666" }}>
+
+ onRemove(comp.localId)}
+ size="small"
+ sx={{
+ color: "#f87171",
+ border: "1px solid rgba(248,113,113,0.4)",
+ borderRadius: 1.5,
+ "&:hover": { borderColor: "#fb7185", color: "#fb7185" },
+ }}
+ >
-
+
);
}
@@ -478,18 +840,23 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
() => buildAssemblyIndex(assembliesPayload.items, assembliesPayload.queue),
[assembliesPayload.items, assembliesPayload.queue]
);
- const scriptTreeData = useMemo(
- () => buildAssemblyTree(assemblyIndex.grouped?.scripts || [], { rootLabel: "Scripts" }),
- [assemblyIndex]
- );
- const ansibleTreeData = useMemo(
- () => buildAssemblyTree(assemblyIndex.grouped?.ansible || [], { rootLabel: "Ansible Playbooks" }),
- [assemblyIndex]
- );
- const workflowTreeData = useMemo(
- () => buildAssemblyTree(assemblyIndex.grouped?.workflows || [], { rootLabel: "Workflows" }),
- [assemblyIndex]
- );
+ const assemblyGridRows = useMemo(() => {
+ const toRow = (record) => ({
+ id: record.assemblyGuid || record.pathLower || record.displayName,
+ name: record.displayName || record.path || record.assemblyGuid,
+ domain: record.domainLabel || record.domain || "General",
+ path: record.path || "",
+ summary: record.summary || "",
+ kind: record.kind || "script",
+ record
+ });
+ const grouped = assemblyIndex.grouped || {};
+ return {
+ scripts: (grouped.scripts || []).map(toRow),
+ ansible: (grouped.ansible || []).map(toRow),
+ workflows: (grouped.workflows || []).map(toRow)
+ };
+ }, [assemblyIndex]);
const loadAssemblyExport = useCallback(
async (assemblyGuid) => {
@@ -519,10 +886,64 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
const [addCompOpen, setAddCompOpen] = useState(false);
const [compTab, setCompTab] = useState("scripts");
const [selectedNodeId, setSelectedNodeId] = useState("");
+ const [assemblyFilterText, setAssemblyFilterText] = useState("");
useEffect(() => {
setSelectedNodeId("");
}, [compTab]);
+ const selectedAssemblyRecord = useMemo(() => {
+ if (!selectedNodeId) return null;
+ const key = String(selectedNodeId).toLowerCase();
+ return assemblyIndex.byGuid?.get(key) || null;
+ }, [selectedNodeId, assemblyIndex]);
+ const assemblyRowData = useMemo(() => assemblyGridRows[compTab] || [], [assemblyGridRows, compTab]);
+ const filteredAssemblyRows = useMemo(() => {
+ const query = assemblyFilterText.trim().toLowerCase();
+ if (!query) return assemblyRowData;
+ return assemblyRowData.filter((row) => {
+ const fields = [row.name, row.domain, row.path, row.summary];
+ return fields.some((value) => typeof value === "string" && value.toLowerCase().includes(query));
+ });
+ }, [assemblyRowData, assemblyFilterText]);
+ const assemblyColumnDefs = useMemo(
+ () => [
+ { field: "name", headerName: "Name", minWidth: 200, flex: 1.1 },
+ { field: "domain", headerName: "Domain", minWidth: 140 },
+ { field: "path", headerName: "Path", minWidth: 220, flex: 1.2 },
+ { field: "summary", headerName: "Summary", minWidth: 260, flex: 1.4 }
+ ],
+ []
+ );
+ const assemblyDefaultColDef = useMemo(
+ () => ({
+ sortable: true,
+ resizable: false,
+ flex: 1,
+ suppressMenu: false,
+ filter: true,
+ floatingFilter: false,
+ cellClass: "auto-col-tight"
+ }),
+ []
+ );
+ const ASSEMBLY_AUTO_COLUMNS = useRef(["name", "domain", "path", "summary"]);
+ const assemblyGridApiRef = useRef(null);
+ const handleAssemblyGridReady = useCallback((params) => {
+ assemblyGridApiRef.current = params.api;
+ requestAnimationFrame(() => {
+ try {
+ params.api.autoSizeColumns(ASSEMBLY_AUTO_COLUMNS.current, true);
+ } catch {}
+ });
+ }, []);
+ useEffect(() => {
+ if (!assemblyGridApiRef.current) return;
+ requestAnimationFrame(() => {
+ try {
+ assemblyGridApiRef.current.autoSizeColumns(ASSEMBLY_AUTO_COLUMNS.current, true);
+ } catch {}
+ });
+ }, [assemblyRowData, compTab]);
const remoteExec = useMemo(() => execContext === "ssh" || execContext === "winrm", [execContext]);
const handleExecContextChange = useCallback((value) => {
@@ -587,8 +1008,6 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
}, [components]);
const [deviceRows, setDeviceRows] = useState([]);
const [deviceStatusFilter, setDeviceStatusFilter] = useState(null);
- const [deviceOrderBy, setDeviceOrderBy] = useState("hostname");
- const [deviceOrder, setDeviceOrder] = useState("asc");
const [deviceFilters, setDeviceFilters] = useState({});
const [filterAnchorEl, setFilterAnchorEl] = useState(null);
const [activeFilterColumn, setActiveFilterColumn] = useState(null);
@@ -711,6 +1130,94 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
},
[targetKey]
);
+ const targetGridRows = useMemo(() => {
+ return targets.map((target) => {
+ const key = targetKey(target) || `${target?.kind || "target"}-${Math.random().toString(36).slice(2, 8)}`;
+ const isFilter = target?.kind === "filter";
+ const deviceCount =
+ typeof target?.deviceCount === "number" && Number.isFinite(target.deviceCount) ? target.deviceCount : null;
+ const detailText = isFilter
+ ? `${deviceCount != null ? deviceCount.toLocaleString() : "—"} device${deviceCount === 1 ? "" : "s"}${
+ target?.site_scope === "scoped" ? ` • ${target?.site || "Specific site"}` : ""
+ }`
+ : "—";
+ return {
+ id: key,
+ typeLabel: isFilter ? "Filter" : "Device",
+ targetLabel: isFilter ? target?.name || `Filter #${target?.filter_id}` : target?.hostname,
+ detailText,
+ rawTarget: target,
+ };
+ });
+ }, [targets, targetKey]);
+ const targetGridColumnDefs = useMemo(
+ () => [
+ { field: "typeLabel", headerName: "Type", minWidth: 120, filter: "agTextColumnFilter" },
+ { field: "targetLabel", headerName: "Target", minWidth: 200, flex: 1.1, filter: "agTextColumnFilter" },
+ { field: "detailText", headerName: "Details", minWidth: 200, flex: 1.4, filter: "agTextColumnFilter" },
+ {
+ field: "actions",
+ headerName: "",
+ minWidth: 80,
+ maxWidth: 100,
+ cellRenderer: "TargetActionsRenderer",
+ sortable: false,
+ suppressMenu: true,
+ filter: false,
+ },
+ ],
+ []
+ );
+ const targetGridComponents = useMemo(
+ () => ({
+ TargetActionsRenderer: (params) => (
+ {
+ e.stopPropagation();
+ params.context?.removeTarget?.(params.data?.rawTarget);
+ }}
+ sx={{
+ color: "#fb7185",
+ "&:hover": { color: "#fecdd3" },
+ }}
+ >
+
+
+ ),
+ }),
+ []
+ );
+ const targetGridDefaultColDef = useMemo(
+ () => ({
+ sortable: true,
+ resizable: false,
+ flex: 1,
+ suppressMenu: false,
+ filter: true,
+ floatingFilter: false,
+ cellClass: "auto-col-tight",
+ }),
+ []
+ );
+ const targetGridApiRef = useRef(null);
+ const TARGET_AUTO_COLS = useRef(["typeLabel", "targetLabel", "detailText"]);
+ const handleTargetGridReady = useCallback((params) => {
+ targetGridApiRef.current = params.api;
+ requestAnimationFrame(() => {
+ try {
+ params.api.autoSizeColumns(TARGET_AUTO_COLS.current, true);
+ } catch {}
+ });
+ }, []);
+ useEffect(() => {
+ if (!targetGridApiRef.current) return;
+ requestAnimationFrame(() => {
+ try {
+ targetGridApiRef.current.autoSizeColumns(TARGET_AUTO_COLS.current, true);
+ } catch {}
+ });
+ }, [targetGridRows]);
useEffect(() => {
setTargets((prev) => {
@@ -867,17 +1374,6 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
);
};
- const handleDeviceSort = useCallback((key) => {
- setDeviceOrderBy((prevKey) => {
- if (prevKey === key) {
- setDeviceOrder((prevDir) => (prevDir === "asc" ? "desc" : "asc"));
- return prevKey;
- }
- setDeviceOrder(key === "ran_on" ? "desc" : "asc");
- return key;
- });
- }, []);
-
const fmtTs = useCallback((ts) => {
if (!ts) return "";
try {
@@ -944,42 +1440,21 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
});
}, [deviceRows, deviceStatusFilter, deviceFilters, fmtTs]);
- const deviceSorted = useMemo(() => {
- const arr = [...deviceFiltered];
- const dir = deviceOrder === "asc" ? 1 : -1;
- arr.sort((a, b) => {
- let delta = 0;
- switch (deviceOrderBy) {
- case "hostname":
- delta = String(a?.hostname || "").localeCompare(String(b?.hostname || ""));
- break;
- case "online":
- delta = Number(a?.online ? 1 : 0) - Number(b?.online ? 1 : 0);
- break;
- case "site":
- delta = String(a?.site || "").localeCompare(String(b?.site || ""));
- break;
- case "ran_on":
- delta = Number(a?.ran_on || 0) - Number(b?.ran_on || 0);
- break;
- case "job_status":
- delta = String(a?.job_status || "").localeCompare(String(b?.job_status || ""));
- break;
- case "output": {
- const score = (row) => (row?.has_stdout ? 2 : 0) + (row?.has_stderr ? 1 : 0);
- delta = score(a) - score(b);
- break;
- }
- default:
- delta = 0;
- }
- if (delta === 0) {
- delta = String(a?.hostname || "").localeCompare(String(b?.hostname || ""));
- }
- return delta * dir;
- });
- return arr;
- }, [deviceFiltered, deviceOrder, deviceOrderBy]);
+ const jobHistoryGridRows = useMemo(
+ () =>
+ deviceFiltered.map((row, index) => ({
+ id: `${row.hostname || "device"}-${index}`,
+ hostname: row.hostname || "",
+ online: Boolean(row.online),
+ site: row.site || "",
+ ranOn: row.ran_on,
+ jobStatus: row.job_status || "",
+ hasStdOut: Boolean(row.has_stdout),
+ hasStdErr: Boolean(row.has_stderr),
+ raw: row,
+ })),
+ [deviceFiltered]
+ );
const hydrateExistingComponents = useCallback(async (rawComponents = []) => {
const results = [];
for (const raw of rawComponents) {
@@ -1156,8 +1631,6 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
// --- Job History (only when editing) ---
const [historyRows, setHistoryRows] = useState([]);
- const [historyOrderBy, setHistoryOrderBy] = useState("started_ts");
- const [historyOrder, setHistoryOrder] = useState("desc");
const activityCacheRef = useRef(new Map());
const [outputOpen, setOutputOpen] = useState(false);
const [outputTitle, setOutputTitle] = useState("");
@@ -1201,22 +1674,12 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
return () => { if (t) clearInterval(t); };
}, [editing, loadHistory]);
- const resultChip = (status) => {
- const map = {
- Success: { bg: '#00d18c', fg: '#000' },
- Running: { bg: '#58a6ff', fg: '#000' },
- Scheduled: { bg: '#999999', fg: '#fff' },
- Expired: { bg: '#777777', fg: '#fff' },
- Failed: { bg: '#ff4f4f', fg: '#fff' },
- Warning: { bg: '#ff8c00', fg: '#000' }
- };
- const c = map[status] || { bg: '#aaa', fg: '#000' };
- return (
-
- {status || ''}
-
- );
- };
+ const resultChip = useCallback((status) => {
+ const key = String(status || "").toLowerCase();
+ const theme = JOB_RESULT_THEME[key] || JOB_RESULT_THEME.default;
+ const label = JOB_RESULT_THEME[key]?.label || status || "Status";
+ return ;
+ }, []);
const aggregatedHistory = useMemo(() => {
if (!Array.isArray(historyRows) || historyRows.length === 0) return [];
@@ -1263,70 +1726,76 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
}, [historyRows]);
const sortedHistory = useMemo(() => {
- const dir = historyOrder === 'asc' ? 1 : -1;
- const key = historyOrderBy;
- return [...aggregatedHistory].sort((a, b) => {
- const getVal = (row) => {
- if (key === 'scheduled_ts' || key === 'started_ts' || key === 'finished_ts') {
- return Number(row?.[key] || 0);
- }
- return String(row?.[key] || '');
- };
- const A = getVal(a);
- const B = getVal(b);
- if (typeof A === 'number' && typeof B === 'number') {
- return (A - B) * dir;
- }
- return String(A).localeCompare(String(B)) * dir;
- });
- }, [aggregatedHistory, historyOrderBy, historyOrder]);
+ return [...aggregatedHistory].sort(
+ (a, b) => Number(b?.finished_ts || 0) - Number(a?.finished_ts || 0)
+ );
+ }, [aggregatedHistory]);
- const handleHistorySort = (col) => {
- if (historyOrderBy === col) setHistoryOrder(historyOrder === 'asc' ? 'desc' : 'asc');
- else { setHistoryOrderBy(col); setHistoryOrder('asc'); }
- };
-
- const renderHistory = () => (
-
-
-
-
-
- handleHistorySort('scheduled_ts')}>
- Scheduled
-
-
-
- handleHistorySort('started_ts')}>
- Started
-
-
-
- handleHistorySort('finished_ts')}>
- Finished
-
-
- Status
-
-
-
- {sortedHistory.map((r) => (
-
- {fmtTs(r.scheduled_ts)}
- {fmtTs(r.started_ts)}
- {fmtTs(r.finished_ts)}
- {resultChip(r.status)}
-
- ))}
- {sortedHistory.length === 0 && (
-
- No runs in the last 30 days.
-
- )}
-
-
-
+ const historySummaryComponents = useMemo(
+ () => ({
+ HistoryStatusRenderer: (params) => resultChip(params.value || ""),
+ }),
+ [resultChip]
);
+ const historySummaryColumnDefs = useMemo(
+ () => [
+ {
+ field: "scheduled_ts",
+ headerName: "Scheduled",
+ minWidth: 180,
+ valueFormatter: (params) => (params.value ? fmtTs(params.value) : ""),
+ },
+ {
+ field: "started_ts",
+ headerName: "Started",
+ minWidth: 180,
+ valueFormatter: (params) => (params.value ? fmtTs(params.value) : ""),
+ },
+ {
+ field: "finished_ts",
+ headerName: "Finished",
+ minWidth: 180,
+ valueFormatter: (params) => (params.value ? fmtTs(params.value) : ""),
+ },
+ {
+ field: "status",
+ headerName: "Status",
+ minWidth: 140,
+ cellRenderer: "HistoryStatusRenderer",
+ cellClass: "status-pill-cell",
+ sortable: false,
+ suppressMenu: true,
+ },
+ ],
+ [fmtTs]
+ );
+ const historySummaryDefaultColDef = useMemo(
+ () => ({
+ sortable: true,
+ resizable: false,
+ flex: 1,
+ cellClass: "auto-col-tight",
+ }),
+ []
+ );
+ const historySummaryGridApiRef = useRef(null);
+ const HISTORY_SUMMARY_AUTO_COLS = useRef(["scheduled_ts", "started_ts", "finished_ts", "status"]);
+ const handleHistorySummaryGridReady = useCallback((params) => {
+ historySummaryGridApiRef.current = params.api;
+ requestAnimationFrame(() => {
+ try {
+ params.api.autoSizeColumns(HISTORY_SUMMARY_AUTO_COLS.current, true);
+ } catch {}
+ });
+ }, []);
+ useEffect(() => {
+ if (!historySummaryGridApiRef.current) return;
+ requestAnimationFrame(() => {
+ try {
+ historySummaryGridApiRef.current.autoSizeColumns(HISTORY_SUMMARY_AUTO_COLS.current, true);
+ } catch {}
+ });
+ }, [sortedHistory]);
// --- Job Progress (summary) ---
const [jobSummary, setJobSummary] = useState({});
@@ -1491,20 +1960,22 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
], []);
const JobStatusFlow = () => (
-
-
-
+
+
+
{deviceStatusFilter ? (
-
-
+
+
Showing devices with {STATUS_META[deviceStatusFilter]?.label || deviceStatusFilter} results
-
) : null}
-
+
);
const inferLanguage = useCallback((path = "") => {
const lower = String(path || "").toLowerCase();
@@ -1612,6 +2083,122 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
setOutputLoading(false);
}, [inferLanguage, loadActivity]);
+ const jobHistoryGridComponents = useMemo(
+ () => ({
+ DeviceStatusRenderer: (params) => {
+ const online = Boolean(params.value);
+ const theme = online ? DEVICE_STATUS_THEME.online : DEVICE_STATUS_THEME.offline;
+ return (
+
+ );
+ },
+ JobStatusRenderer: (params) => resultChip(params.value || ""),
+ OutputActionsRenderer: (params) => {
+ const row = params.data?.raw;
+ if (!row) return null;
+ return (
+
+ {row.has_stdout ? (
+ {
+ e.stopPropagation();
+ params.context?.viewOutput?.(row, "stdout");
+ }}
+ >
+ StdOut
+
+ ) : null}
+ {row.has_stderr ? (
+ {
+ e.stopPropagation();
+ params.context?.viewOutput?.(row, "stderr");
+ }}
+ >
+ StdErr
+
+ ) : null}
+
+ );
+ },
+ }),
+ [resultChip]
+ );
+ const jobHistoryGridColumnDefs = useMemo(
+ () => [
+ { field: "hostname", headerName: "Hostname", minWidth: 180 },
+ {
+ field: "online",
+ headerName: "Status",
+ minWidth: 140,
+ cellRenderer: "DeviceStatusRenderer",
+ cellClass: "status-pill-cell",
+ sortable: false,
+ suppressMenu: true,
+ },
+ { field: "site", headerName: "Site", minWidth: 160 },
+ {
+ field: "ranOn",
+ headerName: "Ran On",
+ minWidth: 200,
+ valueFormatter: (params) => (params.value ? fmtTs(params.value) : ""),
+ comparator: (a, b) => Number(a || 0) - Number(b || 0),
+ },
+ {
+ field: "jobStatus",
+ headerName: "Job Status",
+ minWidth: 150,
+ cellRenderer: "JobStatusRenderer",
+ cellClass: "status-pill-cell",
+ sortable: false,
+ suppressMenu: true,
+ },
+ {
+ field: "output",
+ headerName: "StdOut / StdErr",
+ minWidth: 210,
+ cellRenderer: "OutputActionsRenderer",
+ sortable: false,
+ suppressMenu: true,
+ },
+ ],
+ [fmtTs]
+ );
+ const jobHistoryGridDefaultColDef = useMemo(
+ () => ({
+ sortable: true,
+ resizable: false,
+ flex: 1,
+ cellClass: "auto-col-tight",
+ }),
+ []
+ );
+ const jobHistoryGridApiRef = useRef(null);
+ const JOB_HISTORY_AUTO_COLS = useRef(["hostname", "online", "site", "ranOn", "jobStatus"]);
+ const handleJobHistoryGridReady = useCallback((params) => {
+ jobHistoryGridApiRef.current = params.api;
+ requestAnimationFrame(() => {
+ try {
+ params.api.autoSizeColumns(JOB_HISTORY_AUTO_COLS.current, true);
+ } catch {}
+ });
+ }, []);
+ useEffect(() => {
+ if (!jobHistoryGridApiRef.current) return;
+ requestAnimationFrame(() => {
+ try {
+ jobHistoryGridApiRef.current.autoSizeColumns(JOB_HISTORY_AUTO_COLS.current, true);
+ } catch {}
+ });
+ }, [jobHistoryGridRows]);
+
useEffect(() => {
let canceled = false;
const hydrate = async () => {
@@ -1658,23 +2245,19 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
}
};
- const addSelectedComponent = useCallback(async () => {
- const treeData =
- compTab === "ansible" ? ansibleTreeData : compTab === "workflows" ? workflowTreeData : scriptTreeData;
- const node = treeData.map[selectedNodeId];
- if (!node || node.isFolder) return false;
- if (compTab === "workflows") {
+ const addSelectedComponent = useCallback(async (recordOverride = null) => {
+ const record = recordOverride || selectedAssemblyRecord;
+ if (!record || !record.assemblyGuid) return false;
+ if (record.kind === "workflow") {
alert("Workflows within Scheduled Jobs are not supported yet");
return false;
}
- const record = node.assembly;
- if (!record || !record.assemblyGuid) return false;
try {
const exportDoc = await loadAssemblyExport(record.assemblyGuid);
const parsed = parseAssemblyExport(exportDoc);
const docVars = Array.isArray(parsed.rawVariables) ? parsed.rawVariables : [];
const mergedVariables = mergeComponentVariables(docVars, [], {});
- const type = compTab === "ansible" ? "ansible" : "script";
+ const type = record.kind === "ansible" || record.type === "ansible" || compTab === "ansible" ? "ansible" : "script";
const normalizedPath = normalizeAssemblyPath(type, record.path || "", record.displayName);
setComponents((prev) => [
...prev,
@@ -1697,7 +2280,42 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
alert(err?.message || "Failed to load assembly details.");
return false;
}
- }, [compTab, selectedNodeId, ansibleTreeData, workflowTreeData, scriptTreeData, loadAssemblyExport, mergeComponentVariables, generateLocalId, normalizeAssemblyPath]);
+ }, [selectedAssemblyRecord, compTab, loadAssemblyExport, mergeComponentVariables, normalizeAssemblyPath, generateLocalId]);
+
+ const handleAssemblyRowClick = useCallback((event) => {
+ const record = event?.data?.record;
+ if (!record?.assemblyGuid) return;
+ setSelectedNodeId((record.assemblyGuid || "").toLowerCase());
+ }, []);
+
+ const handleAssemblyRowDoubleClick = useCallback(
+ async (event) => {
+ const record = event?.data?.record;
+ if (!record) return;
+ setSelectedNodeId((record.assemblyGuid || "").toLowerCase());
+ await addSelectedComponent(record);
+ },
+ [addSelectedComponent]
+ );
+ const handleAssemblySelectionChanged = useCallback((event) => {
+ const selectedNode = event.api.getSelectedNodes()[0];
+ if (selectedNode?.data?.record?.assemblyGuid) {
+ setSelectedNodeId(selectedNode.data.record.assemblyGuid.toLowerCase());
+ } else {
+ setSelectedNodeId("");
+ }
+ }, []);
+ const syncAssemblySelection = useCallback(() => {
+ if (!assemblyGridApiRef.current) return;
+ const targetId = String(selectedNodeId || "").toLowerCase();
+ assemblyGridApiRef.current.forEachNode((node) => {
+ const guid = String(node.data?.record?.assemblyGuid || "").toLowerCase();
+ node.setSelected(Boolean(targetId) && guid === targetId);
+ });
+ }, [selectedNodeId]);
+ useEffect(() => {
+ syncAssemblySelection();
+ }, [syncAssemblySelection, filteredAssemblyRows]);
const openAddTargets = async () => {
setAddTargetOpen(true);
@@ -1786,6 +2404,59 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
if (editing) base.push({ key: 'history', label: 'Job History' });
return base;
}, [editing]);
+ const historyTabIndex = useMemo(() => tabDefs.findIndex((t) => t.key === "history"), [tabDefs]);
+
+ const scheduleSummary = useMemo(() => {
+ const base = SCHEDULE_LABELS[scheduleType] || "Scheduled run";
+ if (scheduleType === "immediately") {
+ return "Runs as soon as the job is created";
+ }
+ const dt = startDateTime ? dayjs(startDateTime) : null;
+ if (dt && dt.isValid()) {
+ return `${base} • ${dt.format("MMM D, YYYY h:mm A")}`;
+ }
+ return base;
+ }, [scheduleType, startDateTime]);
+
+ const targetSummary = useMemo(() => {
+ if (!targets.length) return "No targets selected";
+ let deviceCount = 0;
+ let filterCount = 0;
+ targets.forEach((target) => {
+ if (target?.kind === "filter") filterCount += 1;
+ else deviceCount += 1;
+ });
+ const segments = [];
+ if (deviceCount) segments.push(`${deviceCount} device${deviceCount === 1 ? "" : "s"}`);
+ if (filterCount) segments.push(`${filterCount} filter${filterCount === 1 ? "" : "s"}`);
+ return segments.join(" • ") || `${targets.length} target${targets.length === 1 ? "" : "s"}`;
+ }, [targets]);
+
+const heroTiles = useMemo(() => {
+ const execMeta = EXEC_CONTEXT_COPY[execContext] || EXEC_CONTEXT_COPY.system;
+ return [
+ {
+ key: "assemblies",
+ label: "Assemblies",
+ value: components.length ? components.length.toString() : "0",
+ },
+ {
+ key: "targets",
+ label: "Targets",
+ value: targets.length ? targets.length.toString() : "0",
+ },
+ {
+ key: "schedule",
+ label: "Schedule",
+ value: SCHEDULE_LABELS[scheduleType] || "Schedule",
+ },
+ {
+ key: "context",
+ label: "Execution",
+ value: execMeta.title,
+ },
+ ];
+ }, [components.length, targets.length, scheduleType, scheduleSummary, targetSummary, execContext]);
useEffect(() => {
if (editing) return;
@@ -1838,57 +2509,151 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
}, [primaryComponentName, quickJobMeta, jobName]);
return (
-
-
-
- Create a Scheduled Job
- {pageTitleJobName && (
-
- {`: "${pageTitleJobName}"`}
-
- )}
-
-
- Configure advanced schedulable automation jobs for one or more devices.
-
-
-
-
- setTab(v)} sx={{ minHeight: 36 }}>
- {tabDefs.map((t, i) => (
-
- ))}
-
-
- Cancel
+ flexDirection: "column",
+ gap: 3,
+ borderRadius: 0,
+ background: MAGIC_UI.shellBg,
+ border: `1px solid ${MAGIC_UI.panelBorder}`,
+ boxShadow: MAGIC_UI.glow,
+ }}
+ >
+
+
+
+
+
+ Scheduled Job
+ {pageTitleJobName ? (
+
+ {`: "${pageTitleJobName}"`}
+
+ ) : null}
+
+
+
+ Configure advanced scheduled jobs against one or several targeted devices or device filters.
+
+
+
+
+ Cancel
+
(isValid ? setConfirmOpen(true) : null)}
+ variant="contained"
startIcon={}
+ onClick={() => (isValid ? setConfirmOpen(true) : null)}
disabled={!isValid}
- sx={{ color: isValid ? "#58a6ff" : "#666", borderColor: isValid ? "#58a6ff" : "#444", textTransform: "none" }}
+ sx={{
+ ...PRIMARY_CTA_SX,
+ color: isValid ? "#041317" : "#ffffff",
+ backgroundImage: isValid
+ ? PRIMARY_CTA_SX.backgroundImage
+ : "linear-gradient(135deg, rgba(148,163,184,0.35), rgba(51,65,85,0.45))",
+ boxShadow: isValid ? PRIMARY_CTA_SX.boxShadow : "none",
+ opacity: 1,
+ }}
>
{initialJob && initialJob.id ? "Save Changes" : "Create Job"}
-
+
+ {heroTiles.map((tile) => {
+ let mainValue = tile.value || "";
+ let qualifier = "";
+ if (tile.key === "context") {
+ const match = mainValue.match(/^(.*?)\s*\((.+)\)$/);
+ if (match) {
+ mainValue = match[1].trim();
+ qualifier = match[2];
+ }
+ }
+ return (
+
+
+ {tile.label}
+
+
+ {mainValue}
+ {qualifier ? (
+
+ ({qualifier})
+
+ ) : null}
+
+
+ );
+ })}
+
+
+ setTab(v)}
+ variant="scrollable"
+ scrollButtons="auto"
+ TabIndicatorProps={{
+ style: {
+ height: 3,
+ borderRadius: 3,
+ background: "linear-gradient(90deg, #7dd3fc, #c084fc)",
+ },
+ }}
+ sx={{
+ borderBottom: `1px solid ${MAGIC_UI.panelBorder}`,
+ "& .MuiTab-root": {
+ color: MAGIC_UI.textMuted,
+ textTransform: "none",
+ fontWeight: 600,
+ transition: "background 0.2s ease, color 0.2s ease",
+ "&:hover": {
+ color: MAGIC_UI.accentA,
+ backgroundColor: "rgba(30,64,175,0.35)",
+ },
+ },
+ "& .Mui-selected": {
+ color: MAGIC_UI.textBright,
+ },
+ }}
+ >
+ {tabDefs.map((t) => (
+
+ ))}
+
+
+
{tab === 0 && (
-
-
+
+
handleJobNameInputChange(e.target.value)}
@@ -1897,22 +2662,29 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
error={jobName.trim().length === 0}
helperText={jobName.trim().length === 0 ? "Job name is required" : ""}
/>
-
+
)}
{tab === 1 && (
-
+
} onClick={openAddComponent}
- sx={{ color: "#58a6ff", borderColor: "#58a6ff" }} variant="outlined">
+ action={
+ }
+ onClick={openAddComponent}
+ variant="outlined"
+ sx={{ ...OUTLINE_BUTTON_SX, borderColor: MAGIC_UI.accentB, color: MAGIC_UI.accentB }}
+ >
Add Assembly
- )}
+ }
/>
{components.length === 0 && (
- No assemblies added yet.
+
+ No assemblies added yet.
+
)}
{components.map((c) => (
))}
{components.length === 0 && (
- At least one assembly is required.
+
+ At least one assembly is required.
+
)}
-
+
)}
{tab === 2 && (
-
+
} onClick={openAddTargets}
- sx={{ color: "#58a6ff", borderColor: "#58a6ff" }} variant="outlined">
+ action={
+ }
+ onClick={openAddTargets}
+ variant="outlined"
+ sx={{ ...OUTLINE_BUTTON_SX, borderColor: MAGIC_UI.accentA, color: MAGIC_UI.accentA }}
+ >
Add Target
- )}
+ }
/>
-
-
-
- Type
- Target
- Details
- Actions
-
-
-
- {targets.map((target) => {
- const key = targetKey(target) || target.hostname || target.filter_id || Math.random().toString(36);
- const isFilter = target?.kind === "filter";
- const deviceCount = typeof target?.deviceCount === "number" && Number.isFinite(target.deviceCount) ? target.deviceCount : null;
- const detailText = isFilter
- ? `${deviceCount != null ? deviceCount.toLocaleString() : "—"} device${deviceCount === 1 ? "" : "s"}${
- target?.site_scope === "scoped" ? ` • ${target?.site || "Specific site"}` : ""
- }`
- : "—";
- return (
-
- {isFilter ? "Filter" : "Device"}
- {isFilter ? (target?.name || `Filter #${target?.filter_id}`) : target?.hostname}
- {detailText}
-
- removeTarget(target)} sx={{ color: "#ff6666" }}>
-
-
-
-
- );
- })}
- {targets.length === 0 && (
-
- No targets selected.
-
- )}
-
-
+
+ params.data?.id || params.rowIndex}
+ onGridReady={handleTargetGridReady}
+ theme={gridTheme}
+ style={{ width: "100%", height: "100%", fontFamily: gridFontFamily, "--ag-icon-font-family": iconFontFamily }}
+ />
+
{targets.length === 0 && (
- At least one target is required.
+ At least one target is required.
)}
)}
{tab === 3 && (
-
-
-
-
- Recurrence
-
-
- {(scheduleType !== "immediately") && (
-
- Start date and execution time
-
- setStartDateTime(val?.second ? val.second(0) : val)}
- views={['year','month','day','hours','minutes']}
- format="YYYY-MM-DD hh:mm A"
- slotProps={{ textField: { size: "small" } }}
- />
-
-
+
+
+
+ setScheduleType(e.target.value)}
+ sx={{ minWidth: 240, flex: "1 1 260px", ...INPUT_FIELD_SX }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ {scheduleType !== "immediately" && (
+
+ setStartDateTime(val?.second ? val.second(0) : val)}
+ views={["year", "month", "day", "hours", "minutes"]}
+ format="YYYY-MM-DD hh:mm A"
+ slotProps={{
+ textField: {
+ size: "small",
+ sx: { minWidth: 260, flex: "1 1 280px", ...INPUT_FIELD_SX },
+ },
+ }}
+ />
+
)}
-
+
setStopAfterEnabled(e.target.checked)} />}
- label={Stop running this job after}
+ sx={{
+ color: MAGIC_UI.textBright,
+ alignItems: "center",
+ "& .MuiTypography-root": { color: MAGIC_UI.textBright, fontSize: 13 },
+ }}
+ control={
+ setStopAfterEnabled(e.target.checked)}
+ sx={{
+ color: MAGIC_UI.accentA,
+ "&.Mui-checked": { color: MAGIC_UI.accentB },
+ }}
+ />
+ }
+ label="Stop running this job after"
/>
-
- Expiration
-
-
-
+ setExpiration(e.target.value)}
+ sx={{ mt: 1, maxWidth: 260, ...INPUT_FIELD_SX }}
+ >
+
+
+
+
+
+
+
+
+
+
+
)}
{tab === 4 && (
-
+
-
+
{remoteExec && (
{execContext === "winrm" && (
}
label="Use Configured svcBorealis Account"
/>
)}
- setSelectedCredentialId(e.target.value)}
+ sx={{ minWidth: 280, ...INPUT_FIELD_SX }}
disabled={credentialLoading || !filteredCredentials.length || (execContext === "winrm" && useSvcAccount)}
>
- Credential
-
-
+ {filteredCredentials.map((cred) => (
+
+ ))}
+
}
onClick={loadCredentials}
disabled={credentialLoading}
- sx={{ color: "#58a6ff", borderColor: "#58a6ff" }}
+ sx={{ ...OUTLINE_BUTTON_SX, borderColor: MAGIC_UI.accentA, color: MAGIC_UI.accentA }}
>
Refresh
- {credentialLoading && }
+ {credentialLoading && }
{!credentialLoading && credentialError && (
-
+
{credentialError}
)}
{execContext === "winrm" && useSvcAccount && (
-
+
Runs with the agent's svcBorealis account.
)}
- {!credentialLoading && !credentialError && !filteredCredentials.length && (!(execContext === "winrm" && useSvcAccount)) && (
-
- No {execContext === "winrm" ? "WinRM" : "SSH"} credentials available. Create one under Access Management > Credentials.
-
- )}
+ {!credentialLoading &&
+ !credentialError &&
+ !filteredCredentials.length &&
+ !(execContext === "winrm" && useSvcAccount) && (
+
+ No {execContext === "winrm" ? "WinRM" : "SSH"} credentials available. Create one under Access Management > Credentials.
+
+ )}
)}
-
+
)}
- {/* Job History tab (only when editing) */}
- {editing && tab === tabDefs.findIndex(t => t.key === 'history') && (
-
-
- Job History
- { try { await fetch(`/api/scheduled_jobs/${initialJob.id}/runs`, { method: 'DELETE' }); await loadHistory(); } catch {} }}
- >
- Clear Job History
-
-
- Showing the last 30 days of runs.
+ {editing && tab === historyTabIndex && (
+
+
+
+
+ Job History
+
+ {
+ try {
+ await fetch(`/api/scheduled_jobs/${initialJob.id}/runs`, { method: "DELETE" });
+ await loadHistory();
+ } catch {}
+ }}
+ >
+ Clear Job History
+
+
+
+ Showing the last 30 days of runs.
+
+
-
-
-
+
-
- Devices
- Devices targeted by this scheduled job. Individual job history is listed here.
-
-
-
- {DEVICE_COLUMNS.map((col) => (
-
-
- handleDeviceSort(col.key)}
- >
- {col.label}
-
- openFilterMenu(event, col.key)}
- sx={{ color: isColumnFiltered(col.key) ? "#58a6ff" : "#666" }}
- >
-
-
-
-
- ))}
-
-
-
- {deviceSorted.map((d, i) => (
-
- {d.hostname}
-
-
- {d.online ? 'Online' : 'Offline'}
-
- {d.site || ''}
- {fmtTs(d.ran_on)}
- {resultChip(d.job_status)}
-
-
- {d.has_stdout ? (
- { e.stopPropagation(); handleViewDeviceOutput(d, 'stdout'); }}
- >
- StdOut
-
- ) : null}
- {d.has_stderr ? (
- { e.stopPropagation(); handleViewDeviceOutput(d, 'stderr'); }}
- >
- StdErr
-
- ) : null}
-
-
-
- ))}
- {deviceSorted.length === 0 && (
-
- No targets found for this job.
-
- )}
-
-
+
+
+ Devices
+
+
+ Devices targeted by this scheduled job. Individual job history is listed here.
+
+
+ {DEVICE_COLUMNS.map((col) => (
+ }
+ onClick={(event) => openFilterMenu(event, col.key)}
+ sx={{
+ textTransform: "none",
+ borderRadius: 999,
+ borderColor: isColumnFiltered(col.key) ? MAGIC_UI.accentA : "rgba(148,163,184,0.3)",
+ color: isColumnFiltered(col.key) ? MAGIC_UI.accentA : MAGIC_UI.textMuted,
+ }}
+ variant="outlined"
+ >
+ {col.label}
+
+ ))}
+
+
+ params.data?.id || params.rowIndex}
+ onGridReady={handleJobHistoryGridReady}
+ theme={gridTheme}
+ style={{
+ width: "100%",
+ height: "100%",
+ fontFamily: gridFontFamily,
+ "--ag-icon-font-family": iconFontFamily,
+ }}
+ />
+
-
+
-
- Past Job History
- Historical job history summaries. Detailed job history is not recorded.
-
- {renderHistory()}
+
+
+ Past Job History
+
+
+ Historical job history summaries. Detailed job history is not recorded.
+
+
+ params.data?.key || params.rowIndex}
+ onGridReady={handleHistorySummaryGridReady}
+ theme={gridTheme}
+ style={{ width: "100%", height: "100%", fontFamily: gridFontFamily, "--ag-icon-font-family": iconFontFamily }}
+ />
-
+
)}
-
-
+
@@ -2442,6 +3302,10 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
e.stopPropagation();
setSelectedDeviceTargets((prev) => ({ ...prev, [d.hostname]: e.target.checked }));
}}
+ sx={{
+ color: MAGIC_UI.accentA,
+ "&.Mui-checked": { color: MAGIC_UI.accentB },
+ }}
/>
{d.display}
@@ -2452,7 +3316,11 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
))}
{availableDevices.length === 0 && (
- No devices available.
+
+
+ No devices available.
+
+
)}
@@ -2465,10 +3333,10 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
placeholder="Search filters..."
value={filterSearch}
onChange={(e) => setFilterSearch(e.target.value)}
- sx={{ flex: 1, "& .MuiOutlinedInput-root": { bgcolor: "#1b1b1b" }, "& .MuiInputBase-input": { color: "#e6edf3" } }}
+ sx={{ flex: 1, ...INPUT_FIELD_SX }}
/>
-
+
@@ -2490,6 +3358,10 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
e.stopPropagation();
setSelectedFilterTargets((prev) => ({ ...prev, [f.id]: e.target.checked }));
}}
+ sx={{
+ color: MAGIC_UI.accentA,
+ "&.Mui-checked": { color: MAGIC_UI.accentB },
+ }}
/>
{f.name}
@@ -2498,10 +3370,18 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
))}
{!loadingFilterCatalog && (!filterCatalog || filterCatalog.length === 0) && (
- No filters available.
+
+
+ No filters available.
+
+
)}
{loadingFilterCatalog && (
- Loading filters…
+
+
+ Loading filters…
+
+
)}
@@ -2509,7 +3389,9 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
)}
- setAddTargetOpen(false)} sx={{ color: "#58a6ff" }}>Cancel
+ setAddTargetOpen(false)} sx={{ color: MAGIC_UI.textMuted, textTransform: "none" }}>
+ Cancel
+
{
if (targetPickerTab === "filters") {
@@ -2530,7 +3412,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
}
setAddTargetOpen(false);
}}
- sx={{ color: "#58a6ff" }}
+ sx={{ color: MAGIC_UI.accentA, textTransform: "none" }}
>
Add Selected
@@ -2538,17 +3420,37 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
{/* Confirm Create Dialog */}
- setConfirmOpen(false)}
- PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}>
- {initialJob && initialJob.id ? "Are you sure you wish to save changes?" : "Are you sure you wish to create this Job?"}
+ setConfirmOpen(false)}
+ PaperProps={{
+ sx: {
+ background: MAGIC_UI.panelBg,
+ color: MAGIC_UI.textBright,
+ border: `1px solid ${MAGIC_UI.panelBorder}`,
+ boxShadow: MAGIC_UI.glow,
+ },
+ }}
+ >
+
+ {initialJob && initialJob.id ? "Are you sure you wish to save changes?" : "Are you sure you wish to create this Job?"}
+
- setConfirmOpen(false)} sx={{ color: "#58a6ff" }}>Cancel
- { setConfirmOpen(false); handleCreate(); }}
- variant="outlined" sx={{ color: "#58a6ff", borderColor: "#58a6ff" }}>
+ setConfirmOpen(false)} sx={{ color: MAGIC_UI.textMuted, textTransform: "none" }}>
+ Cancel
+
+ {
+ setConfirmOpen(false);
+ handleCreate();
+ }}
+ variant="outlined"
+ sx={{ ...OUTLINE_BUTTON_SX, borderColor: MAGIC_UI.accentA, color: MAGIC_UI.accentA }}
+ >
Confirm
-
+
);
}