From 5a3744669b0f7ec283c2d9ace0fcd7e56d6136ae Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Fri, 21 Nov 2025 22:33:43 -0700 Subject: [PATCH] Redesigned Job Scheduler GUI --- .../src/Scheduling/Create_Job.jsx | 2154 ++++++++++++----- 1 file changed, 1528 insertions(+), 626 deletions(-) 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 ? ( + + ) : null} + {row.has_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) => ( - - ))} - - - + 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. + + + + - + + {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={ + - )} + } /> {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={ + - )} + } /> - - - - 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 }} + > + Immediately + At selected date and time + Every 5 Minutes + Every 10 Minutes + Every 15 Minutes + Every 30 Minutes + Every Hour + Daily + Weekly + Monthly + Yearly + + {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 }} + > + Does not Expire + 30 Minutes + 1 Hour + 2 Hours + 6 Hours + 12 Hours + 1 Day + 2 Days + 3 Days + + )} {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) => ( + + {cred.name} + + ))} + - {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 - - - Showing the last 30 days of runs. + {editing && tab === historyTabIndex && ( + + + + + 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 ? ( - - ) : null} - {d.has_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) => ( + + ))} + + + params.data?.id || params.rowIndex} + onGridReady={handleJobHistoryGridReady} + theme={gridTheme} + style={{ + width: "100%", + height: "100%", + fontFamily: gridFontFamily, + "--ag-icon-font-family": iconFontFamily, + }} + /> + {renderFilterControl()} - - -
+ - - 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 }} + /> - + )}
- setOutputOpen(false)} fullWidth maxWidth="md" - PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }} + setOutputOpen(false)} + fullWidth + maxWidth="md" + PaperProps={{ + sx: { + background: MAGIC_UI.panelBg, + color: MAGIC_UI.textBright, + border: `1px solid ${MAGIC_UI.panelBorder}`, + boxShadow: MAGIC_UI.glow, + }, + }} > {outputTitle} - + {outputLoading ? ( - Loading output… + + Loading output… + ) : null} {!outputLoading && outputError ? ( - {outputError} + + {outputError} + ) : null} - {!outputLoading && !outputError ? ( - outputSections.map((section) => ( - - {section.title} - {section.path ? ( - {section.path} - ) : null} - - {}} - highlight={(code) => highlightCode(code, section.lang)} - padding={12} - style={{ - fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', - fontSize: 12, - color: "#e6edf3", - minHeight: 160 + {!outputLoading && !outputError + ? outputSections.map((section) => ( + + + {section.title} + + {section.path ? ( + + {section.path} + + ) : null} + + > + {}} + highlight={(code) => highlightCode(code, section.lang)} + padding={12} + style={{ + fontFamily: + 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', + fontSize: 12, + color: "#e6edf3", + minHeight: 160, + }} + textareaProps={{ readOnly: true }} + /> + - - )) - ) : null} + )) + : null} - + {/* Bottom actions removed per design; actions live next to tabs. */} {/* Add Component Dialog */} - setAddCompOpen(false)} fullWidth maxWidth="md" - PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }} + setAddCompOpen(false)} + fullWidth + maxWidth="md" + PaperProps={{ + sx: { + background: MAGIC_UI.panelBg, + color: MAGIC_UI.textBright, + border: `1px solid ${MAGIC_UI.panelBorder}`, + boxShadow: MAGIC_UI.glow, + }, + }} > Select an Assembly - - - - - - {assembliesError ? ( - {assembliesError} - ) : null} - {compTab === "scripts" && ( - - { - const n = scriptTreeData.map[id]; - if (n && !n.isFolder) setSelectedNodeId(id); - }}> - {assembliesLoading ? ( - - - Loading assemblies… - - ) : Array.isArray(scriptTreeData.root) && scriptTreeData.root.length ? ( - scriptTreeData.root.map((n) => ( - - {n.children && n.children.length ? renderTreeNodes(n.children, scriptTreeData.map) : null} - - )) - ) : ( - No scripts found. - )} - - - )} - {compTab === "workflows" && ( - - { - const n = workflowTreeData.map[id]; - if (n && !n.isFolder) setSelectedNodeId(id); - }}> - {assembliesLoading ? ( - - - Loading assemblies… - - ) : Array.isArray(workflowTreeData.root) && workflowTreeData.root.length ? ( - workflowTreeData.root.map((n) => ( - - {n.children && n.children.length ? renderTreeNodes(n.children, workflowTreeData.map) : null} - - )) - ) : ( - No workflows found. - )} - - - )} - {compTab === "ansible" && ( - - { - const n = ansibleTreeData.map[id]; - if (n && !n.isFolder) setSelectedNodeId(id); - }}> - {assembliesLoading ? ( - - - Loading assemblies… - - ) : Array.isArray(ansibleTreeData.root) && ansibleTreeData.root.length ? ( - ansibleTreeData.root.map((n) => ( - - {n.children && n.children.length ? renderTreeNodes(n.children, ansibleTreeData.map) : null} - - )) - ) : ( - No playbooks found. - )} - - + + + {["scripts", "ansible", "workflows"].map((key) => ( + + ))} + + + setAssemblyFilterText(e.target.value)} + sx={{ minWidth: 220, ...INPUT_FIELD_SX }} + /> + loadAssemblies()} + sx={{ + color: MAGIC_UI.accentA, + border: `1px solid ${MAGIC_UI.panelBorder}`, + borderRadius: 2, + width: 38, + height: 38, + }} + > + + + + + {assembliesError ? ( + + {assembliesError} + + ) : null} + {assembliesLoading && ( + + + Loading assemblies… + )} + + params.data?.id || params.rowIndex} + onGridReady={handleAssemblyGridReady} + onRowClicked={handleAssemblyRowClick} + onRowDoubleClicked={handleAssemblyRowDoubleClick} + onSelectionChanged={handleAssemblySelectionChanged} + /> + - - + + {/* Add Targets Dialog */} - setAddTargetOpen(false)} fullWidth maxWidth="md" - PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }} + setAddTargetOpen(false)} + fullWidth + maxWidth="md" + PaperProps={{ + sx: { + background: MAGIC_UI.panelBg, + color: MAGIC_UI.textBright, + border: `1px solid ${MAGIC_UI.panelBorder}`, + boxShadow: MAGIC_UI.glow, + }, + }} > Select Targets setTargetPickerTab(value)} - sx={{ mb: 2 }} + sx={{ + mb: 2, + borderBottom: `1px solid ${MAGIC_UI.panelBorder}`, + "& .MuiTab-root": { textTransform: "none", color: MAGIC_UI.textMuted }, + "& .Mui-selected": { color: MAGIC_UI.textBright }, + }} textColor="inherit" indicatorColor="primary" > @@ -2418,10 +3278,10 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic placeholder="Search devices..." value={deviceSearch} onChange={(e) => setDeviceSearch(e.target.value)} - sx={{ flex: 1, "& .MuiOutlinedInput-root": { bgcolor: "#1b1b1b" }, "& .MuiInputBase-input": { color: "#e6edf3" } }} + sx={{ flex: 1, ...INPUT_FIELD_SX }} />
- +
@@ -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 )} - + @@ -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?"} + - - + -
+
); }